diff --git a/.changeset/config.json b/.changeset/config.json index dbc6b9042c1..942e7e703f5 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,37 +7,22 @@ "commit": false, "linked": [], "access": "public", - "baseBranch": "master", + "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [ - "firebase-namespace-integration-test", "firebase-firestore-integration-test", "firebase-messaging-integration-test", - "@firebase/app-exp", - "@firebase/app-types-exp", - "@firebase/auth-exp", - "@firebase/auth-compat", - "@firebase/auth-types-exp", - "@firebase/functions-compat", - "@firebase/functions-exp", - "@firebase/functions-types-exp", - "@firebase/installations-exp", - "@firebase/installations-types-exp", - "@firebase/installations-compat", - "@firebase/messaging-exp", - "@firebase/messaging-types-exp", - "@firebase/performance-exp", - "@firebase/performance-types-exp", - "@firebase/remote-config-exp", - "@firebase/remote-config-types-exp", - "@firebase/remote-config-compat", - "firebase-exp", - "@firebase/app-compat", + "firebase-compat-interop-test", + "firebase-compat-typings-test", "@firebase/changelog-generator", - "firebase-size-analysis" + "firebase-size-analysis", + "@firebase/api-documenter", + "firebase-repo-scripts-prune-dts" ], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { - "onlyUpdatePeerDependentsWhenOutOfRange": true, - "useCalculatedVersionForSnapshots": true + "onlyUpdatePeerDependentsWhenOutOfRange": true + }, + "snapshot": { + "useCalculatedVersion": true } } diff --git a/.changeset/hip-glasses-grin.md b/.changeset/hip-glasses-grin.md deleted file mode 100644 index b3583102f9f..00000000000 --- a/.changeset/hip-glasses-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@firebase/database': minor ---- - -Add `startAfter` and `endBefore` filters for paginating RTDB queries. diff --git a/.changeset/spotty-shirts-design.md b/.changeset/spotty-shirts-design.md new file mode 100644 index 00000000000..77c6a8c8307 --- /dev/null +++ b/.changeset/spotty-shirts-design.md @@ -0,0 +1,5 @@ +--- +"@firebase/data-connect": patch +--- + +Fixed issue where onComplete wasn't triggering when the user calls `unsubscribe` on a subscription. diff --git a/.changeset/wet-badgers-nail.md b/.changeset/wet-badgers-nail.md deleted file mode 100644 index 47182420bb9..00000000000 --- a/.changeset/wet-badgers-nail.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@firebase/auth": patch -"firebase": patch ---- - -Add the `useEmulator()` function and `emulatorConfig` to the `firebase` package externs diff --git a/.editorconfig b/.editorconfig index f4d80702c64..83abd4e12b5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,7 @@ root = true end_of_line = lf insert_final_newline = true -# Javascript and Typescript look like Google-style +# JavaScript and TypeScript look like Google-style [*.{js,json,ts}] charset = utf-8 indent_style = space diff --git a/.gitattributes b/.gitattributes index df3ddcba602..22d47224d43 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,5 @@ +# Source files +# ============ +*.md text eol=lf + *.json linguist-language=JSON-with-Comments \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8a7db086816..1ae328951f4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,101 +7,74 @@ # ####################################################################################################### -# =========================================================== -# @firebase/jssdk-global-approvers -# =========================================================== -# Used for approving minor changes, large-scale refactorings, and emergency situations. -# (secret team to avoid review requests) -# -# - @Feiyang1 -# - @hiranya911 -# - @bojeil-google -# - @depoll -# - @hsubox76 - - -# =========================================================== -# @firebase/firestore-js-team -# =========================================================== -# Used for approving firestore changes. -# (secret team to avoid review requests) -# -# - @schmidt-sebastian -# - @wilhuff -# - @var-const -# - @zxu123 - - # These owners will be the default owners for everything in the repo. -* @Feiyang1 @hiranya911 @hsubox76 @firebase/jssdk-global-approvers +* @firebase/jssdk-global-approvers # Database Code -packages/database @schmidt-sebastian @jsdt @firebase/jssdk-global-approvers -packages/database-types @schmidt-sebastian @jsdt @firebase/jssdk-global-approvers +packages/database @maneesht @aashishpatil-g @firebase/jssdk-global-approvers +packages/database-compat @maneesht @aashishpatil-g @firebase/jssdk-global-approvers +packages/database-types @maneesht @aashishpatil-g @firebase/jssdk-global-approvers + +# Data Connect Code +packages/data-connect @maneesht @aashishpatil-g @firebase/jssdk-global-approvers # Firestore Code packages/firestore @firebase/firestore-js-team @firebase/jssdk-global-approvers +packages/firestore-compat @firebase/firestore-js-team @firebase/jssdk-global-approvers packages/webchannel-wrapper @firebase/firestore-js-team @firebase/jssdk-global-approvers packages/firestore-types @firebase/firestore-js-team @firebase/jssdk-global-approvers integration/firestore @firebase/firestore-js-team @firebase/jssdk-global-approvers # Storage Code -packages/storage @schmidt-sebastian @firebase/jssdk-global-approvers -packages/storage-types @schmidt-sebastian @firebase/jssdk-global-approvers +packages/storage @maneesht @tonyjhuang @firebase/jssdk-global-approvers +packages/storage-compat @maneesht @tonyjhuang @firebase/jssdk-global-approvers +packages/storage-types @maneesht @tonyjhuang @firebase/jssdk-global-approvers # Messaging Code packages/messaging @zwu52 @firebase/jssdk-global-approvers -packages/messaging-types @zwu52 @firebase/jssdk-global-approvers +packages/messaging-compat @zwu52 @firebase/jssdk-global-approvers +packages/messaging-interop-types @zwu52 @firebase/jssdk-global-approvers integration/messaging @zwu52 @firebase/jssdk-global-approvers # Auth Code -packages/auth @bojeil-google @avolkovi @samhorlbeck @yuchenshi @firebase/jssdk-global-approvers -packages/auth-types @bojeil-google @avolkovi @samhorlbeck @yuchenshi @firebase/jssdk-global-approvers +packages/auth @lisajian @Xiaoshouzi-gh @sam-gc @pashanka @mansisampat @nhienlam @firebase/jssdk-global-approvers +packages/auth-compat @lisajian @Xiaoshouzi-gh @sam-gc @pashanka @mansisampat @nhienlam @firebase/jssdk-global-approvers +packages/auth-types @lisajian @Xiaoshouzi-gh @sam-gc @pashanka @mansisampat @nhienlam @firebase/jssdk-global-approvers +packages/auth-interop-types @lisajian @Xiaoshouzi-gh @sam-gc @pashanka @mansisampat @nhienlam @firebase/jssdk-global-approvers # Testing Code -packages/testing @avolkovi @samhorlbeck @yuchenshi @firebase/jssdk-global-approvers -packages/rules-unit-testing @avolkovi @samhorlbeck @yuchenshi @firebase/jssdk-global-approvers - -# RxFire Code -packages/rxfire @davideast @jamesdaniels @firebase/jssdk-global-approvers +packages/rules-unit-testing @avolkovi @sam-gc @yuchenshi @firebase/jssdk-global-approvers # Installations -packages/installations @andirayo @ChaoqunCHEN @firebase/jssdk-global-approvers -packages/installations-types @andirayo @ChaoqunCHEN @firebase/jssdk-global-approvers +packages/installations @avolkovi @yoyomyo @firebase/jssdk-global-approvers +packages/installations-compat @avolkovi @yoyomyo @firebase/jssdk-global-approvers +packages/installations-types @avolkovi @yoyomyo @firebase/jssdk-global-approvers # Performance Code -packages/performance @alikn @zijianjoy @firebase/jssdk-global-approvers -packages/performance-types @alikn @zijianjoy @firebase/jssdk-global-approvers +packages/performance @visumickey @firebase/jssdk-global-approvers +packages/performance-compat @visumickey @firebase/jssdk-global-approvers +packages/performance-types @visumickey @firebase/jssdk-global-approvers # Analytics Code -packages/analytics @hsubox76 @Feiyang1 @firebase/jssdk-global-approvers -packages/analytics-types @hsubox76 @Feiyang1 @firebase/jssdk-global-approvers +packages/analytics @hsubox76 @firebase/jssdk-global-approvers +packages/analytics-compat @hsubox76 @firebase/jssdk-global-approvers +packages/analytics-types @hsubox76 @firebase/jssdk-global-approvers # Remote Config Code packages/remote-config @erikeldridge @firebase/jssdk-global-approvers +packages/remote-config-compat @erikeldridge @firebase/jssdk-global-approvers packages/remote-config-types @erikeldridge @firebase/jssdk-global-approvers +# App Check Code +packages/app-check @hsubox76 @firebase/jssdk-global-approvers +packages/app-check-compat @hsubox76 @firebase/jssdk-global-approvers +packages/app-check-types @hsubox76 @firebase/jssdk-global-approvers +packages/app-check-interop-types @hsubox76 @firebase/jssdk-global-approvers + # Documentation Changes -packages/firebase/index.d.ts @firebase/firebase-techwriters @firebase/jssdk-global-approvers -scripts/docgen/content-sources/ @firebase/firebase-techwriters @firebase/jssdk-global-approvers +packages/firebase/compat/index.d.ts @egilmorez @firebase/jssdk-global-approvers +scripts/docgen/content-sources/ @egilmorez @firebase/jssdk-global-approvers +docs-devsite/ @firebase/firebase-techwriters # Changeset -.changeset @firebase/firebase-techwriters @firebase/jssdk-changeset-approvers @firebase/firestore-js-team @firebase/jssdk-global-approvers - -# Auth-Exp Code -packages-exp/auth-exp @avolkovi @samhorlbeck @yuchenshi @firebase/jssdk-global-approvers -packages-exp/auth-types-exp @avolkovi @samhorlbeck @yuchenshi @firebase/jssdk-global-approvers -packages-exp/auth-compat-exp @avolkovi @samhorlbeck @yuchenshi @firebase/jssdk-global-approvers - -# Installations-Exp Code -packages/installations-exp @andirayo @ChaoqunCHEN @firebase/jssdk-global-approvers -packages/installations-types-exp @andirayo @ChaoqunCHEN @firebase/jssdk-global-approvers - -# Perf-Exp Code -packages/performance-exp @alikn @zijianjoy @firebase/jssdk-global-approvers -packages/performance-types-exp @alikn @zijianjoy @firebase/jssdk-global-approvers - -# RC-Exp Code -packages/remote-config-exp @erikeldridge @firebase/jssdk-global-approvers -packages/remote-config-types-exp @erikeldridge @firebase/jssdk-global-approvers -packages/remote-config-compat @erikeldridge @firebase/jssdk-global-approvers \ No newline at end of file +.changeset @firebase/jssdk-changeset-approvers @firebase/firestore-js-team @firebase/jssdk-global-approvers diff --git a/.github/ISSUE_TEMPLATE/alpha_bug_report.md b/.github/ISSUE_TEMPLATE/alpha_bug_report.md deleted file mode 100644 index dfe2b874103..00000000000 --- a/.github/ISSUE_TEMPLATE/alpha_bug_report.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: 🧪 Alpha SDK bug report -about: Have you found a bug in one of our pre-release or alpha SDKs? File it here. -title: '' -labels: alpha -assignees: '' - ---- - - - - - - -### [REQUIRED] Describe your environment - - * Operating System version: _____ - * Browser version: _____ - * Firebase SDK version: _____ - * Firebase Product: _____ (auth, database, storage, etc) - - - -### [REQUIRED] Describe the problem - -#### Steps to reproduce: - -#### Relevant Code: - -```javascript -// TODO(you): code here to reproduce the problem -``` diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 847e993c4dd..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: 🐞 Bug report -about: Found a bug in the JS SDK? File it here. -title: '' -labels: '' -assignees: '' - ---- - - - - - - - -### [REQUIRED] Describe your environment - - * Operating System version: _____ - * Browser version: _____ - * Firebase SDK version: _____ - * Firebase Product: _____ (auth, database, storage, etc) - - - -### [REQUIRED] Describe the problem - -#### Steps to reproduce: - -#### Relevant Code: - -```javascript -// TODO(you): code here to reproduce the problem -``` diff --git a/.github/ISSUE_TEMPLATE/bug_report_v2.yaml b/.github/ISSUE_TEMPLATE/bug_report_v2.yaml new file mode 100644 index 00000000000..b466840862d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_v2.yaml @@ -0,0 +1,108 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: 🐞 Bug Report +description: File a bug report +title: 'Title for the bug' +labels: 'question, new' +body: + - type: markdown + id: before-you-start + attributes: + value: | + *[READ THIS]:* Are you in the right place? + - For issues or feature requests related to __the code in this repository__, file a GitHub issue. + - If this is a __feature request__, make sure the issue title starts with "FR:". + - For general technical questions, post a question on [StackOverflow](http://stackoverflow.com/) with the firebase tag. + - For general Firebase discussion, use the [firebase-talk](https://groups.google.com/forum/#!forum/firebase-talk) google group. + - For help troubleshooting your application that does not fall under one of the above categories, reach out to the personalized [Firebase support channel](https://firebase.google.com/support/). + - type: input + id: operating-system + attributes: + label: Operating System + description: Describe the operating system(s) where you are experiencing the issue. + placeholder: ex. iOS 16.4, macOS Ventura 13.4, Windows 11 + validations: + required: true + - type: input + id: environment + attributes: + label: Environment (if applicable) + description: Describe the environment where you are experiencing the issue. This could include the browser and its version, Node.js version, or any other relevant environment details. + placeholder: ex. Chrome 115, Node.js v18.16.0, React Native + validations: + required: true + - type: input + id: firebase-sdk-version + attributes: + label: Firebase SDK Version + description: The Firebase JS SDK version you're using. + placeholder: ex. 9.16.0 + validations: + required: true + - type: dropdown + id: firebase-sdk-products + attributes: + label: Firebase SDK Product(s) + description: Select the Firebase product(s) relevant to your issue. You can select multiple options in the dropdown. + multiple: true + options: + - AI + - Analytics + - AppCheck + - Auth + - Component + - Database + - DataConnect + - Firestore + - Functions + - Installations + - Logger + - Messaging + - Performance + - Remote-Config + - Storage + validations: + required: true + - type: textarea + id: project-tooling + attributes: + label: Project Tooling + description: Describe the tooling your app is built with + placeholder: React app with Webpack and Jest + validations: + required: true + - type: textarea + id: describe-your-problem + attributes: + label: Detailed Problem Description + description: | + Please provide a clear and concise description of the problem. Include: + - What you were trying to achieve. + - What actually happened. + - Any error messages or unexpected behavior you observed. + - Relevant log snippets or console output (if available). + placeholder: | + What were you trying to accomplish? What happened? This should include a background description, log/console output, etc. + validations: + required: true + - type: textarea + id: reproduce-code + attributes: + label: Steps and code to reproduce issue + description: | + If possible, provide a minimal, self-contained code snippet or steps that consistently reproduce the issue. + This will significantly aid in debugging. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 82a99ad6b41..626f38c164d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,17 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + blank_issues_enabled: false contact_links: - name: 🔥 Firebase Support diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 00000000000..da3305fb60c --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,40 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Set to true to add reviewers to pull requests +addReviewers: false + +# Set to true to add assignees to pull requests +addAssignees: true + +# A list of reviewers to be added to pull requests (GitHub user name) +# reviewers: +# - egilmorez + +# A number of reviewers added to the pull request +# Set 0 to add all the reviewers (default: 0) +# numberOfReviewers: 0 + +# A list of assignees, overrides reviewers if set +assignees: + - egilmorez + +# A number of assignees to add to the pull request +# Set to 0 to add all of the assignees. +# Uses numberOfReviewers if unset. +numberOfAssignees: 0 + +# A list of keywords to be skipped the process that add reviewers if pull requests include it +# skipKeywords: +# - wip \ No newline at end of file diff --git a/.github/workflows/canary-deploy.yml b/.github/workflows/canary-deploy.yml index c4e45416e11..0d93ceefad8 100644 --- a/.github/workflows/canary-deploy.yml +++ b/.github/workflows/canary-deploy.yml @@ -1,9 +1,24 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Canary Deploy on: push: branches: - - master + - main + workflow_dispatch: jobs: deploy: @@ -12,14 +27,14 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@master + uses: actions/checkout@v4 with: # Canary release script requires git history and tags. fetch-depth: 0 - - name: Set up Node (10) - uses: actions/setup-node@v2 + - name: Set up Node (22) + uses: actions/setup-node@v4 with: - node-version: 10.x + node-version: 22.10.0 - name: Yarn install run: yarn - name: Deploy canary @@ -30,10 +45,14 @@ jobs: NPM_TOKEN_ANALYTICS_TYPES: ${{secrets.NPM_TOKEN_ANALYTICS_TYPES}} NPM_TOKEN_APP: ${{secrets.NPM_TOKEN_APP}} NPM_TOKEN_APP_TYPES: ${{secrets.NPM_TOKEN_APP_TYPES}} + NPM_TOKEN_APP_CHECK: ${{secrets.NPM_TOKEN_APP_CHECK}} + NPM_TOKEN_APP_CHECK_INTEROP_TYPES: ${{secrets.NPM_TOKEN_APP_CHECK_INTEROP_TYPES}} + NPM_TOKEN_APP_CHECK_TYPES: ${{secrets.NPM_TOKEN_APP_CHECK_TYPES}} NPM_TOKEN_AUTH: ${{secrets.NPM_TOKEN_AUTH}} NPM_TOKEN_AUTH_INTEROP_TYPES: ${{secrets.NPM_TOKEN_AUTH_INTEROP_TYPES}} NPM_TOKEN_AUTH_TYPES: ${{secrets.NPM_TOKEN_AUTH_TYPES}} NPM_TOKEN_COMPONENT: ${{secrets.NPM_TOKEN_COMPONENT}} + NPM_TOKEN_DATA_CONNECT: ${{secrets.NPM_TOKEN_DATA_CONNECT}} NPM_TOKEN_DATABASE: ${{secrets.NPM_TOKEN_DATABASE}} NPM_TOKEN_DATABASE_TYPES: ${{secrets.NPM_TOKEN_DATABASE_TYPES}} NPM_TOKEN_FIRESTORE: ${{secrets.NPM_TOKEN_FIRESTORE}} @@ -44,18 +63,42 @@ jobs: NPM_TOKEN_INSTALLATIONS_TYPES: ${{secrets.NPM_TOKEN_INSTALLATIONS_TYPES}} NPM_TOKEN_LOGGER: ${{secrets.NPM_TOKEN_LOGGER}} NPM_TOKEN_MESSAGING: ${{secrets.NPM_TOKEN_MESSAGING}} - NPM_TOKEN_MESSAGING_TYPES: ${{secrets.NPM_TOKEN_MESSAGING_TYPES}} NPM_TOKEN_PERFORMANCE: ${{secrets.NPM_TOKEN_PERFORMANCE}} NPM_TOKEN_PERFORMANCE_TYPES: ${{secrets.NPM_TOKEN_PERFORMANCE_TYPES}} - NPM_TOKEN_POLYFILL: ${{secrets.NPM_TOKEN_POLYFILL}} NPM_TOKEN_REMOTE_CONFIG: ${{secrets.NPM_TOKEN_REMOTE_CONFIG}} NPM_TOKEN_REMOTE_CONFIG_TYPES: ${{secrets.NPM_TOKEN_REMOTE_CONFIG_TYPES}} NPM_TOKEN_RULES_UNIT_TESTING: ${{secrets.NPM_TOKEN_RULES_UNIT_TESTING}} NPM_TOKEN_STORAGE: ${{secrets.NPM_TOKEN_STORAGE}} NPM_TOKEN_STORAGE_TYPES: ${{secrets.NPM_TOKEN_STORAGE_TYPES}} - NPM_TOKEN_TESTING: ${{secrets.NPM_TOKEN_TESTING}} NPM_TOKEN_UTIL: ${{secrets.NPM_TOKEN_UTIL}} + NPM_TOKEN_AI: ${{secrets.NPM_TOKEN_AI}} + NPM_TOKEN_VERTEXAI: ${{secrets.NPM_TOKEN_VERTEXAI}} NPM_TOKEN_WEBCHANNEL_WRAPPER: ${{secrets.NPM_TOKEN_WEBCHANNEL_WRAPPER}} NPM_TOKEN_FIREBASE: ${{secrets.NPM_TOKEN_FIREBASE}} - NPM_TOKEN_RXFIRE: ${{secrets.NPM_TOKEN_RXFIRE}} + NPM_TOKEN_APP_COMPAT: ${{ secrets.NPM_TOKEN_APP_COMPAT }} + NPM_TOKEN_INSTALLATIONS_COMPAT: ${{ secrets.NPM_TOKEN_INSTALLATIONS_COMPAT }} + NPM_TOKEN_ANALYTICS_COMPAT: ${{ secrets.NPM_TOKEN_ANALYTICS_COMPAT }} + NPM_TOKEN_AUTH_COMPAT: ${{ secrets.NPM_TOKEN_AUTH_COMPAT }} + NPM_TOKEN_MESSAGING_INTEROP_TYPES: ${{ secrets.NPM_TOKEN_MESSAGING_INTEROP_TYPES }} + NPM_TOKEN_FUNCTIONS_COMPAT: ${{ secrets.NPM_TOKEN_FUNCTIONS_COMPAT }} + NPM_TOKEN_MESSAGING_COMPAT: ${{ secrets.NPM_TOKEN_MESSAGING_COMPAT }} + NPM_TOKEN_PERFORMANCE_COMPAT: ${{ secrets.NPM_TOKEN_PERFORMANCE_COMPAT }} + NPM_TOKEN_REMOTE_CONFIG_COMPAT: ${{ secrets.NPM_TOKEN_REMOTE_CONFIG_COMPAT }} + NPM_TOKEN_DATABASE_COMPAT: ${{ secrets.NPM_TOKEN_DATABASE_COMPAT }} + NPM_TOKEN_FIRESTORE_COMPAT: ${{ secrets.NPM_TOKEN_FIRESTORE_COMPAT }} + NPM_TOKEN_STORAGE_COMPAT: ${{ secrets.NPM_TOKEN_STORAGE_COMPAT }} + NPM_TOKEN_APP_CHECK_COMPAT: ${{ secrets.NPM_TOKEN_APP_CHECK_COMPAT }} + NPM_TOKEN_API_DOCUMENTER: ${{ secrets.NPM_TOKEN_API_DOCUMENTER }} CI: true + - name: Launch E2E tests workflow + # Trigger e2e-test.yml + run: | + VERSION_SCRIPT="const pkg = require('./packages/firebase/package.json'); console.log(pkg.version);" + VERSION_OR_TAG=`node -e "${VERSION_SCRIPT}"` + OSS_BOT_GITHUB_TOKEN=${{ secrets.OSS_BOT_GITHUB_TOKEN }} + curl -X POST \ + -H "Content-Type:application/json" \ + -H "Accept:application/vnd.github.v3+json" \ + -H "Authorization:Bearer $OSS_BOT_GITHUB_TOKEN" \ + -d "{\"event_type\":\"canary-tests\", \"client_payload\":{\"versionOrTag\":\"$VERSION_OR_TAG\"}}" \ + https://api.github.com/repos/firebase/firebase-js-sdk/dispatches \ No newline at end of file diff --git a/.github/workflows/check-changeset.yml b/.github/workflows/check-changeset.yml index 2f0be1762d7..26eb962887f 100644 --- a/.github/workflows/check-changeset.yml +++ b/.github/workflows/check-changeset.yml @@ -1,57 +1,91 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Check Changeset -on: pull_request +on: + pull_request: + branches: + - main + +env: + GITHUB_PULL_REQUEST_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + GITHUB_PULL_REQUEST_BASE_SHA: ${{ github.event.pull_request.base.sha }} jobs: check_changeset: name: Check changeset vs changed files runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: - name: Checkout Repo - uses: actions/checkout@master + uses: actions/checkout@v4 with: # This makes Actions fetch all Git history so check_changeset script can diff properly. fetch-depth: 0 - - name: Set up Node (10) - uses: actions/setup-node@v2 + - name: Set up Node (22) + uses: actions/setup-node@v4 with: - node-version: 10.x + node-version: 22.10.0 - name: Yarn install run: yarn - name: Run changeset script - run: yarn ts-node-script scripts/check_changeset.ts + # pull main so changeset can diff against it + run: | + git pull -f --no-rebase origin main:main + yarn ts-node-script scripts/ci/check_changeset.ts id: check-changeset - name: Print changeset checker output - run: echo "${{steps.check-changeset.outputs.CHANGESET_ERROR_MESSAGE}}" + run: | + cat << 'eof_delimiter_that_will_never_occur_in_CHANGESET_ERROR_MESSAGE' + ${{steps.check-changeset.outputs.CHANGESET_ERROR_MESSAGE}} + eof_delimiter_that_will_never_occur_in_CHANGESET_ERROR_MESSAGE - name: Print blocking failure status run: echo "${{steps.check-changeset.outputs.BLOCKING_FAILURE}}" - name: Find Comment - uses: peter-evans/find-comment@v1 + # This commit represents v3.1.0 + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e id: fc with: issue-number: ${{github.event.number}} body-includes: Changeset File Check - name: Create comment (missing packages) if: ${{!steps.fc.outputs.comment-id && steps.check-changeset.outputs.CHANGESET_ERROR_MESSAGE}} - uses: peter-evans/create-or-update-comment@v1 + # This commit represents v4.0.0 + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 with: issue-number: ${{github.event.number}} body: | ### Changeset File Check :warning: ${{steps.check-changeset.outputs.CHANGESET_ERROR_MESSAGE}} - name: Update comment (missing packages) - if: ${{steps.fc.outputs.comment-id}} - uses: peter-evans/create-or-update-comment@v1 + if: ${{steps.fc.outputs.comment-id && steps.check-changeset.outputs.CHANGESET_ERROR_MESSAGE}} + # This commit represents v4.0.0 + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 with: - comment-id: ${{steps.fc.outputs.comment-id}} && steps.check-changeset.outputs.CHANGESET_ERROR_MESSAGE}} + comment-id: ${{steps.fc.outputs.comment-id}} edit-mode: replace body: | ### Changeset File Check :warning: ${{steps.check-changeset.outputs.CHANGESET_ERROR_MESSAGE}} - name: Update comment (no missing packages) if: ${{steps.fc.outputs.comment-id && !steps.check-changeset.outputs.CHANGESET_ERROR_MESSAGE}} - uses: peter-evans/create-or-update-comment@v1 + # This commit represents v4.0.0 + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 with: comment-id: ${{steps.fc.outputs.comment-id}} edit-mode: replace @@ -62,4 +96,4 @@ jobs: # Don't want it to throw before editing the comment. - name: Fail if checker script logged a blocking failure if: ${{steps.check-changeset.outputs.BLOCKING_FAILURE == 'true'}} - run: exit 1 \ No newline at end of file + run: exit 1 diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml new file mode 100644 index 00000000000..2e57efd0adf --- /dev/null +++ b/.github/workflows/check-docs.yml @@ -0,0 +1,55 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Doc Change Check (Run "yarn docgen:all" if this fails) + +on: pull_request + +jobs: + doc-check: + name: Check if reference docs have changed + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # get all history for the diff + fetch-depth: 0 + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Yarn install + run: yarn + - name: Run doc generation + run: yarn docgen:all + # Fail first if there are docs-devsite changes since running yarn docgen:all + # will also regenerate any API report changes. + - name: Check for changes in docs-devsite dir (fail if so) + run: | + if [[ -n "$(git status docs-devsite --porcelain)" ]]; then + echo "Unstaged changes detected in docs-devsite/:" + git status -s + echo "Changes in this PR affect the reference docs or API reports. Run \`yarn docgen:all\` locally to regenerate the changed files and add them to this PR." + exit 1 + fi + - name: Check for changes in common/api-review dir (fail if so) + run: | + if [[ -n "$(git status common/api-review --porcelain)" ]]; then + echo "Unstaged changes detected in api-report(s):" + git status -s + echo "Changes in this PR affect the API reports. Run \`yarn build\` locally to regenerate the API reports and add them to this PR." + exit 1 + fi diff --git a/.github/workflows/check-pkg-paths.yml b/.github/workflows/check-pkg-paths.yml new file mode 100644 index 00000000000..c7ae3c0c133 --- /dev/null +++ b/.github/workflows/check-pkg-paths.yml @@ -0,0 +1,41 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Test Package Paths + +on: pull_request + +jobs: + test: + name: Test Package Paths + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # This makes Actions fetch all Git history so run-changed script can diff properly. + fetch-depth: 0 + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Yarn install + run: yarn + - name: Yarn build + run: yarn build + - name: Swap in public typings + run: yarn release:prepare + - name: Check paths + run: yarn ts-node scripts/ci-test/check-paths.ts \ No newline at end of file diff --git a/.github/workflows/check-vertexai-responses.yml b/.github/workflows/check-vertexai-responses.yml new file mode 100644 index 00000000000..4eceacd61b1 --- /dev/null +++ b/.github/workflows/check-vertexai-responses.yml @@ -0,0 +1,65 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Check Vertex AI Responses + +on: pull_request + +jobs: + check-version: + runs-on: ubuntu-latest + # Allow GITHUB_TOKEN to have write permissions + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Clone mock responses + run: scripts/update_vertexai_responses.sh + - name: Find cloned and latest versions + run: | + CLONED=$(git describe --tags) + LATEST=$(git tag --sort=v:refname | tail -n1) + echo "cloned_tag=$CLONED" >> $GITHUB_ENV + echo "latest_tag=$LATEST" >> $GITHUB_ENV + working-directory: packages/ai/test-utils/vertexai-sdk-test-data + - name: Find comment from previous run if exists + # This commit represents v3.1.0 + uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e + id: fc + with: + issue-number: ${{github.event.number}} + body-includes: Vertex AI Mock Responses Check + - name: Comment on PR if newer version is available + if: ${{env.cloned_tag != env.latest_tag && !steps.fc.outputs.comment-id}} + # This commit represents v4.0.0 + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 + with: + issue-number: ${{github.event.number}} + body: > + ### Vertex AI Mock Responses Check :warning: + + A newer major version of the mock responses for Vertex AI unit tests is available. + [update_vertexai_responses.sh](https://github.com/firebase/firebase-js-sdk/blob/main/scripts/update_vertexai_responses.sh) + should be updated to clone the latest version of the responses: `${{env.latest_tag}}` + - name: Delete comment when version gets updated + if: ${{env.cloned_tag == env.latest_tag && steps.fc.outputs.comment-id}} + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: ${{ steps.fc.outputs.comment-id }}, + }) diff --git a/.github/workflows/cross-browser-test.yml b/.github/workflows/cross-browser-test.yml deleted file mode 100644 index 0ec49a405bc..00000000000 --- a/.github/workflows/cross-browser-test.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Cross-Browser Test Flow - -on: - push: - branches: - - master - -jobs: - cross-browser-test: - name: Cross-Browser (Saucelabs) Tests - runs-on: ubuntu-latest - env: - SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js 10.x - uses: actions/setup-node@v2 - with: - node-version: 10.x - - name: Test setup and yarn install - run: | - cp config/ci.config.json config/project.json - yarn - - name: yarn build - run: yarn build - - name: Run Saucelabs tests - run: xvfb-run yarn test:saucelabs - diff --git a/.github/workflows/deploy-config.yml b/.github/workflows/deploy-config.yml new file mode 100644 index 00000000000..c6e32689e1d --- /dev/null +++ b/.github/workflows/deploy-config.yml @@ -0,0 +1,47 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Deploy Project Config + +on: pull_request + +# Detects if any project config files (rules or functions) have changed, +# and deploys them to the test project used for CI if so. +# Run this in its own workflow instead of as a step before each test +# workflow to avoid too many deploys, possibly causing race conditions. +# Since the build step of each test workflow takes a long time, this +# this should finish before the tests begin running. + +jobs: + test: + name: Deploy Firebase Project Rules and Functions + runs-on: ubuntu-latest + if: (github.actor != 'dependabot[bot]') + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # This makes Actions fetch all Git history so run-changed script can diff properly. + fetch-depth: 0 + - name: Set up node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Yarn install + run: yarn + - name: Deploy project config if needed + run: yarn ts-node scripts/ci-test/deploy-if-needed.ts + env: + FIREBASE_CLI_TOKEN: ${{secrets.FIREBASE_CLI_TOKEN}} diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 00000000000..f9ac06ab9c0 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,106 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: E2E Smoke Tests + +# Allows REST trigger. Currently triggered by release-cli script during a staging run. +on: + repository_dispatch: + types: [staging-tests,canary-tests] + +env: + # Bump Node memory limit + NODE_OPTIONS: "--max_old_space_size=4096" + +jobs: + test: + name: Run E2E Smoke Tests + runs-on: ubuntu-latest + + defaults: + run: + # Run any command steps in the /e2e/smoke-tests subdir + working-directory: './e2e/smoke-tests' + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Set up Node (22) + uses: actions/setup-node@master + with: + node-version: 22.10.0 + - name: install Chrome stable + run: | + sudo apt-get update + sudo apt-get install google-chrome-stable + - name: Write project config + env: + PROJECT_CONFIG: ${{ secrets.TEST_PROJECT_CONFIG }} + TEST_ACCOUNT: ${{ secrets.TEST_ACCOUNT }} + run: | + echo "export const config = $PROJECT_CONFIG; export const testAccount = $TEST_ACCOUNT" > firebase-config.js + - name: Poll npm until version to test is available for install + run: | + echo "Polling npm for firebase@${{ github.event.client_payload.versionOrTag }}" + node ./scripts/release/poll-npm-publish.js + # run in root + working-directory: '.' + env: + VERSION: ${{ github.event.client_payload.versionOrTag }} + - name: Yarn install + run: | + echo "Installing firebase@${{ github.event.client_payload.versionOrTag }}" + yarn add firebase@${{ github.event.client_payload.versionOrTag }} + yarn + - name: Deploy "callTest" cloud function + run: | + pushd functions + npm install + popd + npx firebase-tools@13.0.2 deploy --only functions:callTest --project jscore-sandbox-141b5 --token $FIREBASE_CLI_TOKEN + working-directory: ./config + env: + FIREBASE_CLI_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }} + - name: Do modular build + run: yarn build:modular + - name: Do compat build + run: yarn build:compat + - name: Run modular tests + env: + APP_CHECK_DEBUG_TOKEN: ${{ secrets.APP_CHECK_DEBUG_TOKEN }} + run: xvfb-run yarn test:modular + - name: Run compat tests + env: + APP_CHECK_DEBUG_TOKEN: ${{ secrets.APP_CHECK_DEBUG_TOKEN }} + run: xvfb-run yarn test:compat + - name: Tests succeeded + if: success() + run: node scripts/ci/notify-test-result.js success + # we don't want THIS step erroring to trigger the failure notification + continue-on-error: true + env: + WEBHOOK_URL: ${{ secrets.JSCORE_CHAT_WEBHOOK_URL }} + RELEASE_TRACKER_URL: ${{ secrets.RELEASE_TRACKER_URL }} + VERSION_OR_TAG: ${{ github.event.client_payload.versionOrTag }} + # run in root + working-directory: '.' + - name: Tests failed + if: failure() + run: node scripts/ci/notify-test-result.js fail + env: + WEBHOOK_URL: ${{ secrets.JSCORE_CHAT_WEBHOOK_URL }} + RELEASE_TRACKER_URL: ${{ secrets.RELEASE_TRACKER_URL }} + VERSION_OR_TAG: ${{ github.event.client_payload.versionOrTag }} + # run in root + working-directory: '.' diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 00000000000..ef232d9ddf5 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,46 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Formatting Check (Run yarn format locally if this fails) + +on: pull_request + +env: + GITHUB_PULL_REQUEST_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + GITHUB_PULL_REQUEST_BASE_SHA: ${{ github.event.pull_request.base.sha }} + +jobs: + format: + name: Run license and prettier formatting tasks + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # get all history for the diff + fetch-depth: 0 + - name: Set up node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Yarn install + run: yarn + - name: Run formatting script + run: yarn format + - name: Check for changes (fail if so) + run: git diff --exit-code + - name: Formatting needs to be updated. See message below. + if: ${{ failure() }} + run: echo "Something was changed by formatting. Run \`yarn format\` locally to do a prettier/license pass. Use \`yarn format --help\` to see options." \ No newline at end of file diff --git a/.github/workflows/health-metrics-pull-request.yml b/.github/workflows/health-metrics-pull-request.yml new file mode 100644 index 00000000000..bc28a0841c6 --- /dev/null +++ b/.github/workflows/health-metrics-pull-request.yml @@ -0,0 +1,74 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Health Metrics + +on: + push: + branches: ['**'] + pull_request: + +env: + GITHUB_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + # TODO(yifany): parse from git commit history directly + # Reason: actions/checkout@v2 does not always honor ${{ github.event.pull_request.base.sha }}, + # therefore "base.sha" sometimes is not the commit that actually gets merged with the + # pull request head commit for CI test. + # See: + # - https://github.com/actions/checkout/issues/27 + # - https://github.com/actions/checkout/issues/237 + GITHUB_PULL_REQUEST_BASE_SHA: ${{ github.event.pull_request.base.sha }} + # Bump Node memory limit + NODE_OPTIONS: "--max-old-space-size=4096" + +jobs: + binary-size: + name: Binary Size + if: (github.event_name == 'push' || !(github.event.pull_request.head.repo.fork)) && (github.actor != 'dependabot[bot]') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + # This commit represents v0.8.3 + - uses: 'google-github-actions/auth@c4799db9111fba4461e9f9da8732e5057b394f72' + with: + credentials_json: '${{ secrets.GCP_SA_KEY }}' + # This commit represents v2.1.4 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a + - run: yarn install + - run: yarn build + - name: Run health-metrics/binary-size test + run: yarn size-report + modular-export-size: + name: Binary Size For Modular Exports + if: (github.event_name == 'push' || !(github.event.pull_request.head.repo.fork)) && (github.actor != 'dependabot[bot]') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + # This commit represents v0.8.3 + - uses: 'google-github-actions/auth@c4799db9111fba4461e9f9da8732e5057b394f72' + with: + credentials_json: '${{ secrets.GCP_SA_KEY }}' + # This commit represents v2.1.4 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a + - run: yarn install + - run: yarn build + - name: Run health-metrics/modular-exports-binary-size test + run: yarn modular-export-size-report + # TODO(yifany): Enable startup times testing on CI. diff --git a/.github/workflows/health-metrics-release.yml b/.github/workflows/health-metrics-release.yml new file mode 100644 index 00000000000..79aef4b3dad --- /dev/null +++ b/.github/workflows/health-metrics-release.yml @@ -0,0 +1,37 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Health Metrics + +on: + push: + tags: ['**'] + +jobs: + release-diffing: + name: Release Diffing + runs-on: ubuntu-latest + steps: + # This commit represents v0.8.3 + - uses: 'google-github-actions/auth@c4799db9111fba4461e9f9da8732e5057b394f72' + with: + credentials_json: '${{ secrets.GCP_SA_KEY }}' + # This commit represents v2.1.4 + - uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a + # This commit represents v1.4 + - uses: FirebaseExtended/github-actions/health-metrics/release-diffing@41c787c37157e4c5932b951e531c041efa5bb7a4 + with: + repo: ${{ github.repository }} + ref: ${{ github.ref }} + commit: ${{ github.sha }} diff --git a/.github/workflows/health-metrics-test.yml b/.github/workflows/health-metrics-test.yml deleted file mode 100644 index 40b587c4667..00000000000 --- a/.github/workflows/health-metrics-test.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Health Metrics - -on: [push, pull_request] - -env: - METRICS_SERVICE_URL: ${{ secrets.METRICS_SERVICE_URL }} - GITHUB_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GITHUB_PULL_REQUEST_BASE_SHA: ${{ github.event.pull_request.base.sha }} - -jobs: - binary-size: - name: Binary Size - if: github.event_name == 'push' || !(github.event.pull_request.head.repo.fork) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: 10.x - - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master - with: - service_account_key: ${{ secrets.GCP_SA_KEY }} - - run: yarn install - - run: yarn build - - name: Run health-metrics/binary-size test - run: yarn size-report - modular-export-size: - name: Binary Size For Modular Exports - if: github.event_name == 'push' || !(github.event.pull_request.head.repo.fork) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: 10.x - - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master - with: - service_account_key: ${{ secrets.GCP_SA_KEY }} - - run: yarn install - - run: yarn build - - name: Run health-metrics/modular-exports-binary-size test - run: yarn modular-export-size-report - # TODO(yifany): Enable startup times testing on CI. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f4af8a6c470..c922f9c6a67 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,20 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Lint All Packages -on: push +on: pull_request jobs: test: @@ -8,11 +22,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Node (10) - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - name: Set up node (22) + uses: actions/setup-node@v4 with: - node-version: 10.x + node-version: 22.10.0 - name: yarn install run: yarn - name: yarn lint diff --git a/.github/workflows/prerelease-manual-deploy.yml b/.github/workflows/prerelease-manual-deploy.yml index 683964177dc..cf85836d997 100644 --- a/.github/workflows/prerelease-manual-deploy.yml +++ b/.github/workflows/prerelease-manual-deploy.yml @@ -1,3 +1,17 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Prerelease manual deploy on: @@ -16,14 +30,14 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@master + uses: actions/checkout@v4 with: # Canary release script requires git history and tags. fetch-depth: 0 - - name: Set up Node (10) - uses: actions/setup-node@v2 + - name: Set up node (22) + uses: actions/setup-node@v4 with: - node-version: 10.x + node-version: 22.10.0 - name: Yarn install run: yarn - name: Deploy prerelease @@ -34,10 +48,14 @@ jobs: NPM_TOKEN_ANALYTICS_TYPES: ${{secrets.NPM_TOKEN_ANALYTICS_TYPES}} NPM_TOKEN_APP: ${{secrets.NPM_TOKEN_APP}} NPM_TOKEN_APP_TYPES: ${{secrets.NPM_TOKEN_APP_TYPES}} + NPM_TOKEN_APP_CHECK: ${{secrets.NPM_TOKEN_APP_CHECK}} + NPM_TOKEN_APP_CHECK_INTEROP_TYPES: ${{secrets.NPM_TOKEN_APP_CHECK_INTEROP_TYPES}} + NPM_TOKEN_APP_CHECK_TYPES: ${{secrets.NPM_TOKEN_APP_CHECK_TYPES}} NPM_TOKEN_AUTH: ${{secrets.NPM_TOKEN_AUTH}} NPM_TOKEN_AUTH_INTEROP_TYPES: ${{secrets.NPM_TOKEN_AUTH_INTEROP_TYPES}} NPM_TOKEN_AUTH_TYPES: ${{secrets.NPM_TOKEN_AUTH_TYPES}} NPM_TOKEN_COMPONENT: ${{secrets.NPM_TOKEN_COMPONENT}} + NPM_TOKEN_DATA_CONNECT: ${{secrets.NPM_TOKEN_DATA_CONNECT}} NPM_TOKEN_DATABASE: ${{secrets.NPM_TOKEN_DATABASE}} NPM_TOKEN_DATABASE_TYPES: ${{secrets.NPM_TOKEN_DATABASE_TYPES}} NPM_TOKEN_FIRESTORE: ${{secrets.NPM_TOKEN_FIRESTORE}} @@ -48,19 +66,31 @@ jobs: NPM_TOKEN_INSTALLATIONS_TYPES: ${{secrets.NPM_TOKEN_INSTALLATIONS_TYPES}} NPM_TOKEN_LOGGER: ${{secrets.NPM_TOKEN_LOGGER}} NPM_TOKEN_MESSAGING: ${{secrets.NPM_TOKEN_MESSAGING}} - NPM_TOKEN_MESSAGING_TYPES: ${{secrets.NPM_TOKEN_MESSAGING_TYPES}} NPM_TOKEN_PERFORMANCE: ${{secrets.NPM_TOKEN_PERFORMANCE}} NPM_TOKEN_PERFORMANCE_TYPES: ${{secrets.NPM_TOKEN_PERFORMANCE_TYPES}} - NPM_TOKEN_POLYFILL: ${{secrets.NPM_TOKEN_POLYFILL}} NPM_TOKEN_REMOTE_CONFIG: ${{secrets.NPM_TOKEN_REMOTE_CONFIG}} NPM_TOKEN_REMOTE_CONFIG_TYPES: ${{secrets.NPM_TOKEN_REMOTE_CONFIG_TYPES}} NPM_TOKEN_RULES_UNIT_TESTING: ${{secrets.NPM_TOKEN_RULES_UNIT_TESTING}} NPM_TOKEN_STORAGE: ${{secrets.NPM_TOKEN_STORAGE}} NPM_TOKEN_STORAGE_TYPES: ${{secrets.NPM_TOKEN_STORAGE_TYPES}} - NPM_TOKEN_TESTING: ${{secrets.NPM_TOKEN_TESTING}} NPM_TOKEN_UTIL: ${{secrets.NPM_TOKEN_UTIL}} + NPM_TOKEN_AI: ${{secrets.NPM_TOKEN_AI}} + NPM_TOKEN_VERTEXAI: ${{secrets.NPM_TOKEN_VERTEXAI}} NPM_TOKEN_WEBCHANNEL_WRAPPER: ${{secrets.NPM_TOKEN_WEBCHANNEL_WRAPPER}} NPM_TOKEN_FIREBASE: ${{secrets.NPM_TOKEN_FIREBASE}} - NPM_TOKEN_RXFIRE: ${{secrets.NPM_TOKEN_RXFIRE}} + NPM_TOKEN_APP_COMPAT: ${{ secrets.NPM_TOKEN_APP_COMPAT }} + NPM_TOKEN_INSTALLATIONS_COMPAT: ${{ secrets.NPM_TOKEN_INSTALLATIONS_COMPAT }} + NPM_TOKEN_ANALYTICS_COMPAT: ${{ secrets.NPM_TOKEN_ANALYTICS_COMPAT }} + NPM_TOKEN_AUTH_COMPAT: ${{ secrets.NPM_TOKEN_AUTH_COMPAT }} + NPM_TOKEN_MESSAGING_INTEROP_TYPES: ${{ secrets.NPM_TOKEN_MESSAGING_INTEROP_TYPES }} + NPM_TOKEN_FUNCTIONS_COMPAT: ${{ secrets.NPM_TOKEN_FUNCTIONS_COMPAT }} + NPM_TOKEN_MESSAGING_COMPAT: ${{ secrets.NPM_TOKEN_MESSAGING_COMPAT }} + NPM_TOKEN_PERFORMANCE_COMPAT: ${{ secrets.NPM_TOKEN_PERFORMANCE_COMPAT }} + NPM_TOKEN_REMOTE_CONFIG_COMPAT: ${{ secrets.NPM_TOKEN_REMOTE_CONFIG_COMPAT }} + NPM_TOKEN_DATABASE_COMPAT: ${{ secrets.NPM_TOKEN_DATABASE_COMPAT }} + NPM_TOKEN_FIRESTORE_COMPAT: ${{ secrets.NPM_TOKEN_FIRESTORE_COMPAT }} + NPM_TOKEN_STORAGE_COMPAT: ${{ secrets.NPM_TOKEN_STORAGE_COMPAT }} + NPM_TOKEN_APP_CHECK_COMPAT: ${{ secrets.NPM_TOKEN_APP_CHECK_COMPAT }} + NPM_TOKEN_API_DOCUMENTER: ${{ secrets.NPM_TOKEN_API_DOCUMENTER }} CI: true \ No newline at end of file diff --git a/.github/workflows/release-log.yml b/.github/workflows/release-log.yml new file mode 100644 index 00000000000..eaee66ce962 --- /dev/null +++ b/.github/workflows/release-log.yml @@ -0,0 +1,39 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Log Release PR + +on: + pull_request: + branches: + - release + - '*-releasebranch' + +jobs: + release: + name: Send PR number to tracker endpoint + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup Node.js 20.x + uses: actions/setup-node@master + with: + node-version: 22.10.0 + + - name: Get PR number and send to tracker. + run: node scripts/ci/log-changesets.js + env: + RELEASE_TRACKER_URL: ${{ secrets.RELEASE_TRACKER_URL }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 00000000000..a999258a882 --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,57 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Create Release PR + +on: + push: + branches: + - release + - '*-releasebranch' + +jobs: + release: + name: Create Release PR + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + if: ${{ !startsWith(github.event.head_commit.message, 'Version Packages (#') }} + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits + fetch-depth: 0 + + - name: Setup Node.js 20.x + uses: actions/setup-node@master + with: + node-version: 22.10.0 + + - name: Install Dependencies + run: yarn + + # Ensures a new @firebase/app is published with every release. + # This keeps the SDK_VERSION variable up to date. + - name: Add a changeset for @firebase/app + # pull main so changeset can diff against it + run: | + git pull -f --no-rebase origin main:main + yarn ts-node-script scripts/ci/add_changeset.ts + + - name: Create Release Pull Request + uses: changesets/action@v1 + env: + GITHUB_TOKEN: ${{ secrets.OSS_BOT_GITHUB_TOKEN }} diff --git a/.github/workflows/release-prod.yml b/.github/workflows/release-prod.yml new file mode 100644 index 00000000000..253ae95120f --- /dev/null +++ b/.github/workflows/release-prod.yml @@ -0,0 +1,156 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Production Release + +on: + workflow_dispatch: + inputs: + release-branch: + description: 'Release branch' + type: string + default: 'release' + required: true + +jobs: + deploy: + name: Production Release + runs-on: ubuntu-latest + # Allow GITHUB_TOKEN to have write permissions + permissions: + contents: write + + steps: + - name: Set up node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Checkout release branch (with history) + uses: actions/checkout@v4 + with: + # Release script requires git history and tags. + fetch-depth: 0 + ref: ${{ github.event.inputs.release-branch }} + token: ${{ secrets.OSS_BOT_GITHUB_TOKEN }} + - name: Yarn install + run: yarn + - name: Publish to NPM + # --skipTests No need to run tests + # --skipReinstall Yarn install has already been run + # --ignoreUnstaged Adding the @firebase/app changeset file means + # there's unstaged changes. Ignore. + # TODO: Make these flags defaults in the release script. + run: yarn release --releaseType Production --ci --skipTests --skipReinstall --ignoreUnstaged + env: + NPM_TOKEN_ANALYTICS: ${{secrets.NPM_TOKEN_ANALYTICS}} + NPM_TOKEN_ANALYTICS_INTEROP_TYPES: ${{secrets.NPM_TOKEN_ANALYTICS_INTEROP_TYPES}} + NPM_TOKEN_ANALYTICS_TYPES: ${{secrets.NPM_TOKEN_ANALYTICS_TYPES}} + NPM_TOKEN_APP: ${{secrets.NPM_TOKEN_APP}} + NPM_TOKEN_APP_TYPES: ${{secrets.NPM_TOKEN_APP_TYPES}} + NPM_TOKEN_APP_CHECK: ${{secrets.NPM_TOKEN_APP_CHECK}} + NPM_TOKEN_APP_CHECK_INTEROP_TYPES: ${{secrets.NPM_TOKEN_APP_CHECK_INTEROP_TYPES}} + NPM_TOKEN_APP_CHECK_TYPES: ${{secrets.NPM_TOKEN_APP_CHECK_TYPES}} + NPM_TOKEN_AUTH: ${{secrets.NPM_TOKEN_AUTH}} + NPM_TOKEN_AUTH_INTEROP_TYPES: ${{secrets.NPM_TOKEN_AUTH_INTEROP_TYPES}} + NPM_TOKEN_AUTH_TYPES: ${{secrets.NPM_TOKEN_AUTH_TYPES}} + NPM_TOKEN_COMPONENT: ${{secrets.NPM_TOKEN_COMPONENT}} + NPM_TOKEN_DATA_CONNECT: ${{secrets.NPM_TOKEN_DATA_CONNECT}} + NPM_TOKEN_DATABASE: ${{secrets.NPM_TOKEN_DATABASE}} + NPM_TOKEN_DATABASE_TYPES: ${{secrets.NPM_TOKEN_DATABASE_TYPES}} + NPM_TOKEN_FIRESTORE: ${{secrets.NPM_TOKEN_FIRESTORE}} + NPM_TOKEN_FIRESTORE_TYPES: ${{secrets.NPM_TOKEN_FIRESTORE_TYPES}} + NPM_TOKEN_FUNCTIONS: ${{secrets.NPM_TOKEN_FUNCTIONS}} + NPM_TOKEN_FUNCTIONS_TYPES: ${{secrets.NPM_TOKEN_FUNCTIONS_TYPES}} + NPM_TOKEN_INSTALLATIONS: ${{secrets.NPM_TOKEN_INSTALLATIONS}} + NPM_TOKEN_INSTALLATIONS_TYPES: ${{secrets.NPM_TOKEN_INSTALLATIONS_TYPES}} + NPM_TOKEN_LOGGER: ${{secrets.NPM_TOKEN_LOGGER}} + NPM_TOKEN_MESSAGING: ${{secrets.NPM_TOKEN_MESSAGING}} + NPM_TOKEN_PERFORMANCE: ${{secrets.NPM_TOKEN_PERFORMANCE}} + NPM_TOKEN_PERFORMANCE_TYPES: ${{secrets.NPM_TOKEN_PERFORMANCE_TYPES}} + NPM_TOKEN_REMOTE_CONFIG: ${{secrets.NPM_TOKEN_REMOTE_CONFIG}} + NPM_TOKEN_REMOTE_CONFIG_TYPES: ${{secrets.NPM_TOKEN_REMOTE_CONFIG_TYPES}} + NPM_TOKEN_RULES_UNIT_TESTING: ${{secrets.NPM_TOKEN_RULES_UNIT_TESTING}} + NPM_TOKEN_STORAGE: ${{secrets.NPM_TOKEN_STORAGE}} + NPM_TOKEN_STORAGE_TYPES: ${{secrets.NPM_TOKEN_STORAGE_TYPES}} + NPM_TOKEN_UTIL: ${{secrets.NPM_TOKEN_UTIL}} + NPM_TOKEN_AI: ${{secrets.NPM_TOKEN_AI}} + NPM_TOKEN_VERTEXAI: ${{secrets.NPM_TOKEN_VERTEXAI}} + NPM_TOKEN_WEBCHANNEL_WRAPPER: ${{secrets.NPM_TOKEN_WEBCHANNEL_WRAPPER}} + NPM_TOKEN_FIREBASE: ${{secrets.NPM_TOKEN_FIREBASE}} + NPM_TOKEN_APP_COMPAT: ${{ secrets.NPM_TOKEN_APP_COMPAT }} + NPM_TOKEN_INSTALLATIONS_COMPAT: ${{ secrets.NPM_TOKEN_INSTALLATIONS_COMPAT }} + NPM_TOKEN_ANALYTICS_COMPAT: ${{ secrets.NPM_TOKEN_ANALYTICS_COMPAT }} + NPM_TOKEN_AUTH_COMPAT: ${{ secrets.NPM_TOKEN_AUTH_COMPAT }} + NPM_TOKEN_MESSAGING_INTEROP_TYPES: ${{ secrets.NPM_TOKEN_MESSAGING_INTEROP_TYPES }} + NPM_TOKEN_FUNCTIONS_COMPAT: ${{ secrets.NPM_TOKEN_FUNCTIONS_COMPAT }} + NPM_TOKEN_MESSAGING_COMPAT: ${{ secrets.NPM_TOKEN_MESSAGING_COMPAT }} + NPM_TOKEN_PERFORMANCE_COMPAT: ${{ secrets.NPM_TOKEN_PERFORMANCE_COMPAT }} + NPM_TOKEN_REMOTE_CONFIG_COMPAT: ${{ secrets.NPM_TOKEN_REMOTE_CONFIG_COMPAT }} + NPM_TOKEN_DATABASE_COMPAT: ${{ secrets.NPM_TOKEN_DATABASE_COMPAT }} + NPM_TOKEN_FIRESTORE_COMPAT: ${{ secrets.NPM_TOKEN_FIRESTORE_COMPAT }} + NPM_TOKEN_STORAGE_COMPAT: ${{ secrets.NPM_TOKEN_STORAGE_COMPAT }} + NPM_TOKEN_APP_CHECK_COMPAT: ${{ secrets.NPM_TOKEN_APP_CHECK_COMPAT }} + NPM_TOKEN_API_DOCUMENTER: ${{ secrets.NPM_TOKEN_API_DOCUMENTER }} + CI: true + - name: Get release version + id: get-version + # In production, there is only one version number + run: | + VERSION_SCRIPT="const pkg = require('./packages/firebase/package.json'); console.log(pkg.version);" + VERSION=`node -e "${VERSION_SCRIPT}"` + echo "BASE_VERSION=$VERSION" >> $GITHUB_OUTPUT + - name: Echo version in shell + run: | + echo "Base version: ${{ steps.get-version.outputs.BASE_VERSION }}" + - name: Log to release tracker + # Sends release information to cloud functions endpoint of release tracker. + run: | + DATE=$(date +'%m/%d/%Y') + BASE_VERSION=${{ steps.get-version.outputs.BASE_VERSION }} + RELEASE_TRACKER_URL=${{ secrets.RELEASE_TRACKER_URL }} + curl -X POST -H "Content-Type:application/json" \ + -d "{\"version\":\"$BASE_VERSION\",\"date\":\"$DATE\"}" \ + $RELEASE_TRACKER_URL/logProduction + - name: Create GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get the newest release tag for the firebase package (e.g. firebase@10.12.0) + NEWEST_TAG=$(git describe --tags --match "firebase@[0-9]*.[0-9]*.[0-9]*" --abbrev=0) + + # Get the release notes from the description of the most recent merged PR into the "release" branch + # See: https://github.com/firebase/firebase-js-sdk/pull/8236 for an example description + JSON_RELEASE_NOTES=$(gh pr list \ + --repo "$GITHUB_REPOSITORY" \ + --state "merged" \ + --base "release" \ + --limit 1 \ + --json "body" \ + | jq '.[].body | split("\n# Releases\n")[-1]' # Remove the generated changesets header + ) + + # Prepend the new release header + # We have to be careful to insert the new release header after a " character, since we're + # modifying the JSON string + JSON_RELEASE_NOTES="\"For more detailed release notes, see [Firebase JavaScript SDK Release Notes](https://firebase.google.com/support/release-notes/js).\n\n# What's Changed\n\n${JSON_RELEASE_NOTES:1}" + + # Format the JSON string into a readable markdown string + RELEASE_NOTES=$(echo -E $JSON_RELEASE_NOTES | jq -r .) + + # Create the GitHub release + gh release create "$NEWEST_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$NEWEST_TAG" \ + --notes "$RELEASE_NOTES" \ + --verify-tag diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml new file mode 100644 index 00000000000..52aafa9273f --- /dev/null +++ b/.github/workflows/release-staging.yml @@ -0,0 +1,183 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Staging Release + +on: + workflow_dispatch: + inputs: + release-branch: + description: 'Release branch' + type: string + default: 'release' + required: true + source-branch: + description: 'Branch to release from' + type: choice + default: 'main' + required: true + options: + - main + - v8 + - at-11-7-1 + verbose: + description: 'Enable verbose logging' + type: boolean + default: false + +env: + # Bump Node memory limit + NODE_OPTIONS: "--max_old_space_size=4096" + +jobs: + deploy: + name: Staging Release + runs-on: ubuntu-latest + # Block this workflow if run on a non-release branch. + if: github.event.inputs.release-branch == 'release' || endsWith(github.event.inputs.release-branch, '-releasebranch') + steps: + - name: Set up node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Merge main into release + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.OSS_BOT_GITHUB_TOKEN }} + script: | + const result = await github.rest.repos.merge({ + owner: context.repo.owner, + repo: context.repo.repo, + base: '${{ github.event.inputs.release-branch }}', + head: '${{ github.event.inputs.source-branch }}' + }) + console.log(result) + - name: Checkout current branch (with history) + uses: actions/checkout@v4 + with: + # Release script requires git history and tags. + fetch-depth: 0 + ref: ${{ github.event.inputs.release-branch }} + - name: Yarn install + run: yarn + - name: Publish to NPM + # --skipTests No need to run tests + # --skipReinstall Yarn install has already been run + # --ignoreUnstaged Adding the @firebase/app changeset file means + # there's unstaged changes. Ignore. + # TODO: Make these flags defaults in the release script. + run: yarn release --releaseType Staging --ci --skipTests --skipReinstall --ignoreUnstaged + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN_ANALYTICS: ${{secrets.NPM_TOKEN_ANALYTICS}} + NPM_TOKEN_ANALYTICS_INTEROP_TYPES: ${{secrets.NPM_TOKEN_ANALYTICS_INTEROP_TYPES}} + NPM_TOKEN_ANALYTICS_TYPES: ${{secrets.NPM_TOKEN_ANALYTICS_TYPES}} + NPM_TOKEN_APP: ${{secrets.NPM_TOKEN_APP}} + NPM_TOKEN_APP_TYPES: ${{secrets.NPM_TOKEN_APP_TYPES}} + NPM_TOKEN_APP_CHECK: ${{secrets.NPM_TOKEN_APP_CHECK}} + NPM_TOKEN_APP_CHECK_INTEROP_TYPES: ${{secrets.NPM_TOKEN_APP_CHECK_INTEROP_TYPES}} + NPM_TOKEN_APP_CHECK_TYPES: ${{secrets.NPM_TOKEN_APP_CHECK_TYPES}} + NPM_TOKEN_AUTH: ${{secrets.NPM_TOKEN_AUTH}} + NPM_TOKEN_AUTH_INTEROP_TYPES: ${{secrets.NPM_TOKEN_AUTH_INTEROP_TYPES}} + NPM_TOKEN_AUTH_TYPES: ${{secrets.NPM_TOKEN_AUTH_TYPES}} + NPM_TOKEN_COMPONENT: ${{secrets.NPM_TOKEN_COMPONENT}} + NPM_TOKEN_DATA_CONNECT: ${{secrets.NPM_TOKEN_DATA_CONNECT}} + NPM_TOKEN_DATABASE: ${{secrets.NPM_TOKEN_DATABASE}} + NPM_TOKEN_DATABASE_TYPES: ${{secrets.NPM_TOKEN_DATABASE_TYPES}} + NPM_TOKEN_FIRESTORE: ${{secrets.NPM_TOKEN_FIRESTORE}} + NPM_TOKEN_FIRESTORE_TYPES: ${{secrets.NPM_TOKEN_FIRESTORE_TYPES}} + NPM_TOKEN_FUNCTIONS: ${{secrets.NPM_TOKEN_FUNCTIONS}} + NPM_TOKEN_FUNCTIONS_TYPES: ${{secrets.NPM_TOKEN_FUNCTIONS_TYPES}} + NPM_TOKEN_INSTALLATIONS: ${{secrets.NPM_TOKEN_INSTALLATIONS}} + NPM_TOKEN_INSTALLATIONS_TYPES: ${{secrets.NPM_TOKEN_INSTALLATIONS_TYPES}} + NPM_TOKEN_LOGGER: ${{secrets.NPM_TOKEN_LOGGER}} + NPM_TOKEN_MESSAGING: ${{secrets.NPM_TOKEN_MESSAGING}} + NPM_TOKEN_PERFORMANCE: ${{secrets.NPM_TOKEN_PERFORMANCE}} + NPM_TOKEN_PERFORMANCE_TYPES: ${{secrets.NPM_TOKEN_PERFORMANCE_TYPES}} + NPM_TOKEN_REMOTE_CONFIG: ${{secrets.NPM_TOKEN_REMOTE_CONFIG}} + NPM_TOKEN_REMOTE_CONFIG_TYPES: ${{secrets.NPM_TOKEN_REMOTE_CONFIG_TYPES}} + NPM_TOKEN_RULES_UNIT_TESTING: ${{secrets.NPM_TOKEN_RULES_UNIT_TESTING}} + NPM_TOKEN_STORAGE: ${{secrets.NPM_TOKEN_STORAGE}} + NPM_TOKEN_STORAGE_TYPES: ${{secrets.NPM_TOKEN_STORAGE_TYPES}} + NPM_TOKEN_UTIL: ${{secrets.NPM_TOKEN_UTIL}} + NPM_TOKEN_AI: ${{secrets.NPM_TOKEN_AI}} + NPM_TOKEN_VERTEXAI: ${{secrets.NPM_TOKEN_VERTEXAI}} + NPM_TOKEN_WEBCHANNEL_WRAPPER: ${{secrets.NPM_TOKEN_WEBCHANNEL_WRAPPER}} + NPM_TOKEN_FIREBASE: ${{secrets.NPM_TOKEN_FIREBASE}} + NPM_TOKEN_APP_COMPAT: ${{ secrets.NPM_TOKEN_APP_COMPAT }} + NPM_TOKEN_INSTALLATIONS_COMPAT: ${{ secrets.NPM_TOKEN_INSTALLATIONS_COMPAT }} + NPM_TOKEN_ANALYTICS_COMPAT: ${{ secrets.NPM_TOKEN_ANALYTICS_COMPAT }} + NPM_TOKEN_AUTH_COMPAT: ${{ secrets.NPM_TOKEN_AUTH_COMPAT }} + NPM_TOKEN_MESSAGING_INTEROP_TYPES: ${{ secrets.NPM_TOKEN_MESSAGING_INTEROP_TYPES }} + NPM_TOKEN_FUNCTIONS_COMPAT: ${{ secrets.NPM_TOKEN_FUNCTIONS_COMPAT }} + NPM_TOKEN_MESSAGING_COMPAT: ${{ secrets.NPM_TOKEN_MESSAGING_COMPAT }} + NPM_TOKEN_PERFORMANCE_COMPAT: ${{ secrets.NPM_TOKEN_PERFORMANCE_COMPAT }} + NPM_TOKEN_REMOTE_CONFIG_COMPAT: ${{ secrets.NPM_TOKEN_REMOTE_CONFIG_COMPAT }} + NPM_TOKEN_DATABASE_COMPAT: ${{ secrets.NPM_TOKEN_DATABASE_COMPAT }} + NPM_TOKEN_FIRESTORE_COMPAT: ${{ secrets.NPM_TOKEN_FIRESTORE_COMPAT }} + NPM_TOKEN_STORAGE_COMPAT: ${{ secrets.NPM_TOKEN_STORAGE_COMPAT }} + NPM_TOKEN_APP_CHECK_COMPAT: ${{ secrets.NPM_TOKEN_APP_CHECK_COMPAT }} + NPM_TOKEN_API_DOCUMENTER: ${{ secrets.NPM_TOKEN_API_DOCUMENTER }} + CI: true + VERBOSE_NPM_LOGGING: ${{github.event.inputs.verbose}} + - name: Get release version + id: get-version + # STAGING_VERSION = version with staging hash, e.g. 1.2.3-20430523 + # BASE_VERSION = version without staging hash, e.g. 1.2.3 + run: | + VERSION_SCRIPT="const pkg = require('./packages/firebase/package.json'); console.log(pkg.version);" + VERSION=`node -e "${VERSION_SCRIPT}"` + echo "STAGING_VERSION=$VERSION" >> $GITHUB_OUTPUT + BASE_VERSION=$(echo $VERSION | cut -d "-" -f 1) + echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_OUTPUT + - name: Echo versions in shell + run: | + echo "Staging release ${{ steps.get-version.outputs.STAGING_VERSION }}" + echo "Base version: ${{ steps.get-version.outputs.BASE_VERSION }}" + - name: Launch E2E tests workflow + # Trigger e2e-test.yml + run: | + OSS_BOT_GITHUB_TOKEN=${{ secrets.OSS_BOT_GITHUB_TOKEN }} + VERSION_OR_TAG=${{ steps.get-version.outputs.STAGING_VERSION }} + curl -X POST \ + -H "Content-Type:application/json" \ + -H "Accept:application/vnd.github.v3+json" \ + -H "Authorization:Bearer $OSS_BOT_GITHUB_TOKEN" \ + -d "{\"event_type\":\"staging-tests\", \"client_payload\":{\"versionOrTag\":\"$VERSION_OR_TAG\"}}" \ + https://api.github.com/repos/firebase/firebase-js-sdk/dispatches + - name: Check for changes requiring a reference doc publish + id: docs-check + # If a diff is found (length of DIFF_CONTENTS > 0) it will write DOCS_NEEDED=true + run: | + LAST_PUBLISHED_VERSION=$(npm info firebase version) + DIFF_CONTENTS=$(git diff firebase@$LAST_PUBLISHED_VERSION HEAD docs-devsite) + if [ -n "$DIFF_CONTENTS" ] + then + echo "DOCS_NEEDED=true" >> $GITHUB_OUTPUT + else + echo "DOCS_NEEDED=false" >> $GITHUB_OUTPUT + fi + - name: Log to release tracker + # Sends release information to cloud functions endpoint of release tracker. + if: ${{ always() }} + run: | + DATE=$(date +'%m/%d/%Y') + BASE_VERSION=${{ steps.get-version.outputs.BASE_VERSION }} + STAGING_VERSION=${{ steps.get-version.outputs.STAGING_VERSION }} + OPERATOR=${{ github.actor }} + RELEASE_TRACKER_URL=${{ secrets.RELEASE_TRACKER_URL }} + DOCS_NEEDED=${{ steps.docs-check.outputs.DOCS_NEEDED }} + curl -X POST -H "Content-Type:application/json" \ + -d "{\"version\":\"$BASE_VERSION\",\"tag\":\"$STAGING_VERSION\",\"date\":\"$DATE\",\"operator\":\"$OPERATOR\",\"docs_needed\":\"$DOCS_NEEDED\"}" \ + $RELEASE_TRACKER_URL/logStaging diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 1f566be4da1..00000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Release - -on: - push: - branches: - - release - -jobs: - release: - name: Release - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@master - with: - # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits - fetch-depth: 0 - - - name: Setup Node.js 12.x - uses: actions/setup-node@master - with: - node-version: 12.x - - - name: Install Dependencies - run: yarn - - - name: Create Release Pull Request - uses: changesets/action@master - env: - GITHUB_TOKEN: ${{ secrets.OSS_BOT_GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index 34deb6a3ae9..02c3ab0326f 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -1,36 +1,147 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Test All Packages -on: push +on: pull_request + +env: + # make chromedriver detect installed Chrome version and download the corresponding driver + DETECT_CHROMEDRIVER_VERSION: true + # The default behavior of chromedriver uses the older Chrome download URLs. We need to override + # the behavior to use the new URLs. + CHROMEDRIVER_CDNURL: https://googlechromelabs.github.io/ + CHROMEDRIVER_CDNBINARIESURL: https://storage.googleapis.com/chrome-for-testing-public + CHROME_VALIDATED_VERSION: linux-132.0.6834.110 + CHROME_VERSION_MISMATCH_MESSAGE: "The Chrome version doesn't match the previously validated version. Consider updating CHROME_VALIDATED_VERSION in the GitHub workflow if tests pass, or rollback the installed Chrome version if tests fail." + artifactRetentionDays: 14 + # Bump Node memory limit + NODE_OPTIONS: "--max_old_space_size=4096" jobs: - test: - name: Node.js and Browser (Chrome) Tests + build: + name: Build the SDK runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Node (10) - uses: actions/setup-node@v2 - with: - node-version: 10.x + # Install Chrome so the correct version of webdriver can be installed by chromedriver when + # setting up the repo. This must be done to build and execute Auth properly. - name: install Chrome stable run: | - sudo apt-get update - sudo apt-get install google-chrome-stable - - name: Bump Node memory limit - run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV + npx @puppeteer/browsers install chrome@stable + - uses: actions/checkout@v4 + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json yarn - name: yarn build run: yarn build + - name: Archive build + if: ${{ !cancelled() }} + run: | + tar -cf build.tar --exclude=.git . + gzip build.tar + - name: Upload build archive + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: build.tar.gz + path: build.tar.gz + retention-days: ${{ env.artifactRetentionDays }} + + # Auth and Firestore are built and executed in their own jobs in an attempt to reduce flakiness. + test-the-rest: + name: (bulk) Node.js and Browser (Chrome) Tests + needs: build + runs-on: ubuntu-latest + steps: + # install Chrome first, so the correct version of webdriver can be installed by chromedriver when setting up the repo + - name: install Chrome stable + run: | + npx @puppeteer/browsers install chrome@stable + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Test setup and yarn install + run: | + cp config/ci.config.json config/project.json + yarn + - name: Set start timestamp env var + run: echo "FIREBASE_CI_TEST_START_TIME=$(date +%s)" >> $GITHUB_ENV + - name: Run unit tests + # Ignore auth and firestore since they're handled in their own separate jobs. + run: | + xvfb-run yarn lerna run --ignore '{firebase-messaging-integration-test,@firebase/auth*,@firebase/firestore*,firebase-firestore-integration-test}' test:ci + node scripts/print_test_logs.js + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }} + - name: Generate coverage file + run: yarn ci:coverage + - name: Run coverage + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: ./lcov-all.info + continue-on-error: true + + test-auth: + name: (Auth) Node.js and Browser (Chrome) Tests + needs: build + runs-on: ubuntu-latest + steps: + # install Chrome first, so the correct version of webdriver can be installed by chromedriver + # when setting up the repo + - name: install Chrome stable + run: | + npx @puppeteer/browsers install chrome@stable + chromeVersionString=$(ls chrome) + if [ "$CHROME_VALIDATED_VERSION" != "$chromeVersionString" ]; then + echo "::warning ::${CHROME_VERSION_MISMATCH_MESSAGE}" + echo "::warning ::Previously validated version: ${CHROME_VALIDATED_VERSION} vs. Installed version: $chromeVersionString" + fi + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Test setup and yarn install + run: | + cp config/ci.config.json config/project.json + yarn - name: Set start timestamp env var run: echo "FIREBASE_CI_TEST_START_TIME=$(date +%s)" >> $GITHUB_ENV - name: Run unit tests run: | - xvfb-run yarn test:ci + xvfb-run yarn lerna run test:ci --scope '@firebase/auth*' node scripts/print_test_logs.js + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }} - name: Generate coverage file run: yarn ci:coverage - name: Run coverage @@ -39,3 +150,74 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: ./lcov-all.info continue-on-error: true + + test-firestore: + name: (Firestore) Node.js and Browser (Chrome) Tests + needs: build + runs-on: ubuntu-latest + steps: + # install Chrome so the correct version of webdriver can be installed by chromedriver when setting up the repo + - name: install Chrome stable + run: | + npx @puppeteer/browsers install chrome@stable + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Test setup and yarn install + run: | + cp config/ci.config.json config/project.json + yarn + - name: Set start timestamp env var + run: echo "FIREBASE_CI_TEST_START_TIME=$(date +%s)" >> $GITHUB_ENV + - name: Run unit tests + run: | + yarn lerna run test:all:ci --scope '@firebase/firestore*' --stream --concurrency 1 + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }} + EXPERIMENTAL_MODE: true + - name: Generate coverage file + run: yarn ci:coverage + - name: Run coverage + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: ./lcov-all.info + continue-on-error: true + test-firestore-integration: + strategy: + fail-fast: false + matrix: + persistence: ['memory', 'persistence'] + name: Firestore Integration Tests (${{ matrix.persistence }}) + needs: build + runs-on: ubuntu-latest + steps: + # install Chrome so the correct version of webdriver can be installed by chromedriver when setting up the repo + - name: install Chrome stable + run: | + npx @puppeteer/browsers install chrome@stable + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - run: cp config/ci.config.json config/project.json + - run: yarn + - run: yarn build:${{ matrix.persistence }} + working-directory: integration/firestore + - run: xvfb-run yarn karma:singlerun + working-directory: integration/firestore + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }} diff --git a/.github/workflows/test-changed-auth.yml b/.github/workflows/test-changed-auth.yml index c04b8fcb8f2..2ae77916492 100644 --- a/.github/workflows/test-changed-auth.yml +++ b/.github/workflows/test-changed-auth.yml @@ -1,33 +1,122 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Test Auth on: pull_request +env: + # make chromedriver detect installed Chrome version and download the corresponding driver + DETECT_CHROMEDRIVER_VERSION: true + # The default behavior of chromedriver uses the older Chrome download URLs. We need to override + # the behavior to use the new URLs. + CHROMEDRIVER_CDNURL: https://googlechromelabs.github.io/ + CHROMEDRIVER_CDNBINARIESURL: https://storage.googleapis.com/chrome-for-testing-public + CHROME_VALIDATED_VERSION: linux-120.0.6099.71 + # Bump Node memory limit + NODE_OPTIONS: "--max_old_space_size=4096" + jobs: - test: - name: Test Auth If Changed + test-chrome: + name: Test Auth on Chrome and Node If Changed + runs-on: ubuntu-latest + steps: + # install Chrome first, so the correct version of webdriver can be installed by chromedriver + # when setting up the repo + # + # Note: we only need to check the chrome version change in one job as the warning annotation + # is appended to the entire workflow results, not just this job's results. + - name: install Chrome stable + env: + CHROME_VERSION_MISMATCH_MESSAGE: "The Chrome version doesn't match the previously validated version. Consider updating CHROME_VALIDATED_VERSION in the GitHub workflow if tests pass." + run: | + npx @puppeteer/browsers install chrome@stable + chromeVersionString=$(ls chrome) + if [ "$CHROME_VALIDATED_VERSION" != "$chromeVersionString" ]; then + echo "::warning ::The Chrome version doesn't match the previously validated version. Consider updating CHROME_VALIDATED_VERSION in the GitHub workflow if tests pass." + echo "::warning ::Previously validated version: ${CHROME_VALIDATED_VERSION} vs. Installed version: $chromeVersionString" + echo "CHROME_VERSION_NOTES=$CHROME_VERSION_MISMATCH_MESSAGE" >> "$GITHUB_ENV" + fi + - name: Test Evn TEMP + run: | + echo $CHROME_VERSION_NOTES=$CHROME_VERSION_MISMATCH_MESSAGE + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # This makes Actions fetch all Git history so run-changed script can diff properly. + fetch-depth: 0 + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Test setup and yarn install + run: | + cp config/ci.config.json config/project.json + yarn + - name: build + run: yarn build:changed auth + - name: Run tests on changed packages + run: xvfb-run yarn test:changed auth + test-firefox: + name: Test Auth on Firefox If Changed + runs-on: ubuntu-latest steps: - - name: Checkout Repo - uses: actions/checkout@master - with: - # This makes Actions fetch all Git history so run-changed script can diff properly. - fetch-depth: 0 - - name: Set up Node (10) - uses: actions/setup-node@v2 - with: - node-version: 10.x - - name: install Chrome stable - run: | - sudo apt-get update - sudo apt-get install google-chrome-stable - - name: Bump Node memory limit - run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - - name: Test setup and yarn install - run: | - cp config/ci.config.json config/project.json - yarn - - name: build - run: yarn build - - name: Run tests on changed packages - run: xvfb-run yarn test:changed auth \ No newline at end of file + - name: install Firefox stable + run: npx @puppeteer/browsers install firefox@stable + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # This makes Actions fetch all Git history so run-changed script can diff properly. + fetch-depth: 0 + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Test setup and yarn install + run: | + cp config/ci.config.json config/project.json + yarn + - name: build + run: yarn build:changed auth + - name: Run tests on auth changed packages + run: xvfb-run yarn test:changed auth + env: + BROWSERS: 'Firefox' + + test-webkit: + name: Test Auth on Webkit if Changed + runs-on: macos-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Test setup and yarn install + run: | + cp config/ci.config.json config/project.json + yarn + npx playwright install webkit + - name: build + run: yarn build:changed auth + - name: Run tests on changed packages + run: yarn test:changed auth + env: + BROWSERS: 'WebkitHeadless' \ No newline at end of file diff --git a/.github/workflows/test-changed-fcm-integration.yml b/.github/workflows/test-changed-fcm-integration.yml index 94242247400..2fb6f9b890b 100644 --- a/.github/workflows/test-changed-fcm-integration.yml +++ b/.github/workflows/test-changed-fcm-integration.yml @@ -1,33 +1,52 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Test FCM integration on: pull_request +env: + # make chromedriver detect installed Chrome version and download the corresponding driver + DETECT_CHROMEDRIVER_VERSION: true + # Bump Node memory limit + NODE_OPTIONS: "--max_old_space_size=4096" + jobs: test: name: Test FCM integration If Changed runs-on: ubuntu-latest steps: + # install Chrome first, so the correct version of webdriver can be installed by chromedriver when setting up the repo + - name: install Chrome stable + run: | + sudo apt-get update + sudo apt-get install google-chrome-stable - name: Checkout Repo - uses: actions/checkout@master + uses: actions/checkout@v4 with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up Node (10) - uses: actions/setup-node@v2 + - name: Set up Node (22) + uses: actions/setup-node@v4 with: - node-version: 10.x - - name: install Chrome stable - run: | - sudo apt-get update - sudo apt-get install google-chrome-stable - - name: Bump Node memory limit - run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV + node-version: 22.10.0 - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json yarn - name: build - run: yarn build:changed fcm-integration --buildAppExp + run: yarn build:changed fcm-integration - name: Run tests if FCM or its dependencies has changed run: xvfb-run yarn test:changed fcm-integration diff --git a/.github/workflows/test-changed-firestore-integration.yml b/.github/workflows/test-changed-firestore-integration.yml index 663781cb045..b894e70e1ec 100644 --- a/.github/workflows/test-changed-firestore-integration.yml +++ b/.github/workflows/test-changed-firestore-integration.yml @@ -1,33 +1,86 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Test Firestore Integration on: pull_request +env: + # Bump Node memory limit + NODE_OPTIONS: "--max_old_space_size=4096" + jobs: test: name: Test Firestore Integration If Changed runs-on: ubuntu-latest + env: + run_terraform_steps: ${{ secrets.JSSDK_ACTIONS_SA_KEY != '' }} steps: - name: Checkout Repo - uses: actions/checkout@master + uses: actions/checkout@v4 with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up Node (10) - uses: actions/setup-node@v2 + # This commit represents v0.8.3 + - uses: 'google-github-actions/auth@c4799db9111fba4461e9f9da8732e5057b394f72' + if: ${{ fromJSON(env.run_terraform_steps) }} with: - node-version: 10.x + credentials_json: '${{ secrets.JSSDK_ACTIONS_SA_KEY }}' + + # create composite indexes with Terraform + - name: Setup Terraform + if: ${{ fromJSON(env.run_terraform_steps) }} + # This commit represents v3.1.2 + uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd + - name: Terraform Init + if: ${{ fromJSON(env.run_terraform_steps) }} + run: | + cp config/ci.config.json config/project.json + cd packages/firestore + terraform init + continue-on-error: true + - name: Terraform Apply + if: github.event_name == 'pull_request' && fromJSON(env.run_terraform_steps) + run: | + cd packages/firestore + + # Define a temporary file, redirect both stdout and stderr to it + output_file=$(mktemp) + if ! terraform apply -var-file=../../config/project.json -auto-approve > "$output_file" 2>&1 ; then + cat "$output_file" + if cat "$output_file" | grep -q "index already exists"; then + echo "===================================================================================" + echo -e "\e[93m\e[1mTerraform apply failed due to index already exists; We can safely ignore this error.\e[0m" + echo "===================================================================================" + fi + exit 1 + fi + rm -f "$output_file" + continue-on-error: true + + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 - name: install Chrome stable run: | sudo apt-get update sudo apt-get install google-chrome-stable - - name: Bump Node memory limit - run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - name: Test setup and yarn install - run: | - cp config/ci.config.json config/project.json - yarn + run: yarn - name: build - run: yarn build:changed firestore-integration --buildAppExp --buildAppCompat + run: yarn build:changed firestore-integration - name: Run tests if firestore or its dependencies has changed run: yarn test:changed firestore-integration diff --git a/.github/workflows/test-changed-firestore.yml b/.github/workflows/test-changed-firestore.yml index faceaa1668b..feb5eb0f672 100644 --- a/.github/workflows/test-changed-firestore.yml +++ b/.github/workflows/test-changed-firestore.yml @@ -1,33 +1,290 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Test Firestore -on: pull_request +on: + workflow_dispatch: + pull_request: + +env: + artifactRetentionDays: 14 + # Bump Node memory limit + NODE_OPTIONS: "--max_old_space_size=4096" jobs: - test: - name: Test Firestore If Changed + build: + name: Build Firestore + runs-on: ubuntu-latest + outputs: + changed: ${{ steps.set-output.outputs.CHANGED }} steps: - - name: Checkout Repo - uses: actions/checkout@master - with: - # This makes Actions fetch all Git history so run-changed script can diff properly. - fetch-depth: 0 - - name: Set up Node (10) - uses: actions/setup-node@v2 - with: - node-version: 10.x - - name: install Chrome stable - run: | - sudo apt-get update - sudo apt-get install google-chrome-stable - - name: Bump Node memory limit - run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - - name: Test setup and yarn install - run: | - cp config/ci.config.json config/project.json - yarn - - name: build - run: yarn build:changed firestore --buildAppExp --buildAppCompat - - name: Run tests if firestore or its dependencies has changed - run: yarn test:changed firestore + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # This makes Actions fetch all Git history so run-changed script can diff properly. + fetch-depth: 0 + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: install Chrome stable + run: | + sudo apt-get update + sudo apt-get install google-chrome-stable + - name: Test setup and yarn install + run: | + cp config/ci.config.json config/project.json + yarn + - name: build + id: build + run: | + set -o pipefail + yarn build:changed firestore | tee ${{ runner.temp }}/yarn.log.txt + continue-on-error: false + - name: Check if Firestore is changed + id: check-changed + run: egrep "Skipping all" ${{ runner.temp }}/yarn.log.txt + # Continue when "Skipping all" is not found + continue-on-error: true + - name: set output + # This means "Skipping all" was not found + if: steps.check-changed.outcome != 'success' + id: set-output + run: echo "CHANGED=true" >> "$GITHUB_OUTPUT"; + - name: Archive build + if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.check-changed.outcome != 'success' }} + run: | + tar -cf build.tar --exclude=.git . + gzip build.tar + - name: Upload build archive + if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.check-changed.outcome != 'success' }} + uses: actions/upload-artifact@v4 + with: + name: build.tar.gz + path: build.tar.gz + retention-days: ${{ env.artifactRetentionDays }} + + compat-test-chrome: + name: Test Firestore Compatible + runs-on: ubuntu-latest + needs: build + if: ${{ needs.build.outputs.changed == 'true'}} + steps: + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: install Chrome stable + run: | + sudo apt-get update + sudo apt-get install google-chrome-stable + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Test setup and yarn install + run: cp config/ci.config.json config/project.json + - name: Run compat tests + run: cd packages/firestore-compat && yarn run test:ci + + test-chrome: + name: Test Firestore + strategy: + matrix: + test-name: ["test:browser", "test:travis", "test:lite:browser", "test:browser:prod:nameddb", "test:lite:browser:nameddb"] + runs-on: ubuntu-latest + needs: build + if: ${{ needs.build.outputs.changed == 'true'}} + steps: + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: install Chrome stable + run: | + sudo apt-get update + sudo apt-get install google-chrome-stable + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Test setup and yarn install + run: cp config/ci.config.json config/project.json + - name: Run tests + run: cd packages/firestore && yarn run ${{ matrix.test-name }} + env: + EXPERIMENTAL_MODE: true + + test-firestore-nightly-with-chrome: + name: Test Nightly Firestore with Chrome + strategy: + matrix: + test-name: ["test:browser:nightly"] + runs-on: ubuntu-latest + needs: build + if: ${{ github.event_name != 'pull_request' }} + steps: + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: install Chrome stable + run: | + sudo apt-get update + sudo apt-get install google-chrome-stable + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Test setup against nightly Firestore + env: + INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.FIRESTORE_SDK_NIGHTLY_PROJECT_JSON }} + run: | + echo $INTEG_TESTS_GOOGLE_SERVICES > config/project.json + - name: Run tests + run: cd packages/firestore && yarn run ${{ matrix.test-name }} + env: + EXPERIMENTAL_MODE: true + + + compat-test-firefox: + name: Test Firestore Compatible on Firefox + runs-on: ubuntu-latest + needs: build + if: ${{ needs.build.outputs.changed == 'true'}} + steps: + - name: install Firefox stable + run: npx @puppeteer/browsers install firefox@stable + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Test setup and yarn install + run: cp config/ci.config.json config/project.json + - name: Run compat tests + run: cd packages/firestore-compat && xvfb-run yarn run test:ci + env: + BROWSERS: 'Firefox' + + test-firefox: + name: Test Firestore on Firefox + strategy: + matrix: + test-name: ["test:browser", "test:travis", "test:lite:browser", "test:browser:prod:nameddb", "test:lite:browser:nameddb"] + runs-on: ubuntu-latest + needs: build + if: ${{ needs.build.outputs.changed == 'true'}} + steps: + - name: install Firefox stable + run: npx @puppeteer/browsers install firefox@stable + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Test setup and yarn install + run: cp config/ci.config.json config/project.json + - name: Run tests + run: cd packages/firestore && xvfb-run yarn run ${{ matrix.test-name }} + env: + BROWSERS: 'Firefox' + EXPERIMENTAL_MODE: true + + compat-test-webkit: + name: Test Firestore Compatible on Webkit + runs-on: macos-latest + needs: build + if: ${{ needs.build.outputs.changed == 'true'}} + steps: + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Test setup + run: | + cp config/ci.config.json config/project.json + npx playwright install webkit + - name: Run compat tests + run: cd packages/firestore-compat && yarn run test:ci + env: + BROWSERS: 'WebkitHeadless' + + test-webkit: + name: Test Firestore on Webkit + strategy: + matrix: + # TODO (dlarocque): Add test:travis once the browser tests are isolated + # Exclude test:travis for now, since it includes node tests, which are failing for + # some reason. + test-name: ["test:browser", "test:lite:browser", "test:browser:prod:nameddb", "test:lite:browser:nameddb"] + runs-on: macos-latest + needs: build + if: ${{ needs.build.outputs.changed == 'true'}} + steps: + - name: Download build archive + uses: actions/download-artifact@v4 + with: + name: build.tar.gz + - name: Unzip build artifact + run: tar xf build.tar.gz + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Test setup + run: | + cp config/ci.config.json config/project.json + npx playwright install webkit + - name: Run tests + run: cd packages/firestore && yarn run ${{ matrix.test-name }} + env: + BROWSERS: 'WebkitHeadless' + EXPERIMENTAL_MODE: true + # A job that fails if any required job in the test matrix fails, + # to be used as a required check for merging. + check-required-tests: + runs-on: ubuntu-latest + if: always() + name: Check all required tests results + needs: [build, test-chrome, compat-test-chrome] + steps: + - name: Check test matrix + if: needs.build.result == 'failure' || needs.test-chrome.result == 'failure' || needs.compat-test-chrome.result == 'failure' + run: exit 1 \ No newline at end of file diff --git a/.github/workflows/test-changed-misc.yml b/.github/workflows/test-changed-misc.yml index e22f9b94c24..52abe5ddddb 100644 --- a/.github/workflows/test-changed-misc.yml +++ b/.github/workflows/test-changed-misc.yml @@ -1,7 +1,25 @@ -name: Test rxFire and @firebase/rules-unit-testing +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Test @firebase/rules-unit-testing on: pull_request +env: + # Bump Node memory limit + NODE_OPTIONS: "--max_old_space_size=4096" + jobs: test: name: Test Misc Packages If Changed @@ -9,25 +27,25 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@master + uses: actions/checkout@v4 with: # This makes Actions fetch all Git history so run-changed script can diff properly. fetch-depth: 0 - - name: Set up Node (10) - uses: actions/setup-node@v2 + - name: Set up Node (22) + uses: actions/setup-node@v4 with: - node-version: 10.x + node-version: 22.10.0 - name: install Chrome stable run: | sudo apt-get update sudo apt-get install google-chrome-stable - - name: Bump Node memory limit - run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - name: Test setup and yarn install run: | cp config/ci.config.json config/project.json yarn - name: build - run: yarn build:changed misc --buildAppExp + run: yarn build:changed misc - name: Run tests - run: yarn test:changed misc \ No newline at end of file + run: yarn test:changed misc + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test-changed.yml b/.github/workflows/test-changed.yml index 0923a17dd7c..5a0f18600f0 100644 --- a/.github/workflows/test-changed.yml +++ b/.github/workflows/test-changed.yml @@ -1,33 +1,101 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: Test Modified Packages on: pull_request +env: + # Bump Node memory limit + NODE_OPTIONS: "--max_old_space_size=4096" + jobs: - test: - name: Test Packages With Changed Files + test-chrome: + name: Test Packages With Changed Files in Chrome and Node + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # This makes Actions fetch all Git history so run-changed script can diff properly. + fetch-depth: 0 + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: install Chrome stable + run: | + sudo apt-get update + sudo apt-get install google-chrome-stable + - name: Test setup and yarn install + run: | + cp config/ci.config.json config/project.json + yarn + - name: build + run: yarn build:changed core + - name: Run tests on changed packages + run: xvfb-run yarn test:changed core + + test-firefox: + name: Test Packages With Changed Files in Firefox runs-on: ubuntu-latest steps: - - name: Checkout Repo - uses: actions/checkout@master - with: - # This makes Actions fetch all Git history so run-changed script can diff properly. - fetch-depth: 0 - - name: Set up Node (10) - uses: actions/setup-node@v2 - with: - node-version: 10.x - - name: install Chrome stable - run: | - sudo apt-get update - sudo apt-get install google-chrome-stable - - name: Bump Node memory limit - run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - - name: Test setup and yarn install - run: | - cp config/ci.config.json config/project.json - yarn - - name: build - run: yarn build:changed core - - name: Run tests on changed packages - run: xvfb-run yarn test:changed core \ No newline at end of file + - name: Checkout Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: install Firefox stable + run: npx @puppeteer/browsers install firefox@stable + - name: Test setup and yarn install + run: | + cp config/ci.config.json config/project.json + yarn + - name: build + run: yarn build:changed core + - name: Run tests on changed packages + run: xvfb-run yarn test:changed core + env: + BROWSERS: 'Firefox' + + + test-webkit: + name: Test Packages With Changed Files in Webkit + runs-on: macos-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Node (22) + uses: actions/setup-node@v4 + with: + node-version: 22.10.0 + - name: Test setup and yarn install + run: | + cp config/ci.config.json config/project.json + yarn + npx playwright install webkit + - name: build + run: yarn build:changed core + - name: Run tests on changed packages + run: yarn test:changed core + env: + BROWSERS: 'WebkitHeadless' \ No newline at end of file diff --git a/.github/workflows/test-firebase-integration.yml b/.github/workflows/test-firebase-integration.yml deleted file mode 100644 index a36aa65f2b0..00000000000 --- a/.github/workflows/test-firebase-integration.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Test Firebase Namespace - -on: pull_request - -jobs: - test: - name: Test Firebase Namespace - runs-on: ubuntu-latest - - steps: - - name: Checkout Repo - uses: actions/checkout@master - with: - # This makes Actions fetch all Git history so run-changed script can diff properly. - fetch-depth: 0 - - name: Set up Node (10) - uses: actions/setup-node@v2 - with: - node-version: 10.x - - name: install Chrome stable - run: | - sudo apt-get update - sudo apt-get install google-chrome-stable - - name: Bump Node memory limit - run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV - - name: Test setup and yarn install - run: | - cp config/ci.config.json config/project.json - yarn - - name: build - run: yarn build:changed firebase-integration --buildAppExp - - name: Run tests on changed packages - run: yarn test:changed firebase-integration \ No newline at end of file diff --git a/.gitignore b/.gitignore index dc9fe690372..a989576ccf3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ dist .awcache .cache /config/project.json -scripts/docgen/html +scripts/docgen-compat/html # OS Specific Files .DS_Store @@ -39,9 +39,6 @@ coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt -# Bower dependency directory (https://bower.io/) -bower_components - # node-waf configuration .lock-wscript @@ -84,11 +81,26 @@ package-lock.json # temp folder used by api-extractor temp -packages-exp/**/temp # temp markdowns generated for individual SDKs -packages-exp/**/docs packages/**/docs # files generated by api-extractor that should not be tracked -tsdoc-metadata.json \ No newline at end of file +tsdoc-metadata.json + +# generated html docs +docs-rut/ +docs/ + +# generated Terraform docs +.terraform/* +.terraform.lock.hcl +*.tfstate +*.tfstate.* + +# vertexai test data +vertexai-sdk-test-data +mocks-lookup.ts + +# temp changeset output +changeset-temp.json diff --git a/packages-exp/auth-compat-exp/demo/.eslintignore b/.gitmodules similarity index 100% rename from packages-exp/auth-compat-exp/demo/.eslintignore rename to .gitmodules diff --git a/.prettierignore b/.prettierignore index ca9f916676d..38c05295e08 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,4 @@ # This file is pre-built and need not be formatted -packages/auth packages/firebase/firebase* packages/firestore/scripts dist diff --git a/.vscode/launch.json b/.vscode/launch.json index 4a6c01dc886..8f132cbe5c6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,23 +4,83 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "AI Unit Tests (node)", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/_mocha", + "cwd": "${workspaceRoot}/packages/ai", + "args": [ + "--require", + "ts-node/register", + "--require", + "src/index.node.ts", + "--timeout", + "5000", + "'src/**/!(*-browser)*.test.ts" + ], + "env": { + "TS_NODE_COMPILER_OPTIONS": "{\"module\":\"commonjs\"}" + }, + "sourceMaps": true + }, + { + "name": "AI Integration Tests (node)", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/_mocha", + "cwd": "${workspaceRoot}/packages/ai", + "args": [ + "--require", + "ts-node/register", + "--require", + "src/index.node.ts", + "--timeout", + "5000", + "integration/**/*.test.ts" + ], + "env": { + "TS_NODE_COMPILER_OPTIONS": "{\"module\":\"commonjs\"}" + }, + "sourceMaps": true + }, + { + "type": "node", + "request": "launch", + "name": "Prune .d.ts", + "program": "${workspaceRoot}/repo-scripts/prune-dts/node_modules/.bin/_mocha", + "cwd": "${workspaceRoot}/repo-scripts/prune-dts", + "args": [ + "--require", + "ts-node/register", + "--timeout", + "5000", + "${workspaceFolder}/repo-scripts/prune-dts/*.test.ts" + ], + "env": { + "TS_NODE_COMPILER_OPTIONS": "{\"module\":\"commonjs\"}" + }, + "sourceMaps": true + }, { "type": "node", "request": "launch", "name": "RTDB Unit Tests (Node)", - "program": "${workspaceRoot}/packages/firebase/node_modules/.bin/_mocha", + "program": "${workspaceRoot}/node_modules/.bin/_mocha", "cwd": "${workspaceRoot}/packages/database", "args": [ "test/{,!(browser)/**/}*.test.ts", - "--file", "index.node.ts", - "--config", "../../config/mocharc.node.js" + "--file", + "src/index.node.ts", + "--config", + "../../config/mocharc.node.js" ], "env": { + "TS_NODE_FILES": true, "TS_NODE_CACHE": "NO", - "TS_NODE_COMPILER_OPTIONS" : "{\"module\":\"commonjs\"}" + "TS_NODE_COMPILER_OPTIONS": "{\"module\":\"commonjs\"}" }, - "sourceMaps": true, - "protocol": "inspector" + "sourceMaps": true }, { "type": "node", @@ -29,18 +89,16 @@ "program": "${workspaceRoot}/node_modules/.bin/_mocha", "cwd": "${workspaceRoot}/packages/firestore", "args": [ - "--require", "ts-node/register/type-check", - "--require", "index.node.ts", - "--timeout", "5000", + "--require", + "babel-register.js", + "--require", + "src/index.node.ts", + "--timeout", + "5000", "test/{,!(browser|integration)/**/}*.test.ts", "--exit" ], - "env": { - "TS_NODE_CACHE": "NO", - "TS_NODE_COMPILER_OPTIONS" : "{\"module\":\"commonjs\"}" - }, - "sourceMaps": true, - "protocol": "inspector" + "sourceMaps": true }, { "type": "node", @@ -49,20 +107,21 @@ "program": "${workspaceRoot}/node_modules/.bin/_mocha", "cwd": "${workspaceRoot}/packages/firestore", "args": [ - "--require", "ts-node/register/type-check", - "--require", "index.node.ts", - "--require", "test/util/node_persistence.ts", - "--timeout", "5000", + "--require", + "babel-register.js", + "--require", + "index.node.ts", + "--require", + "test/util/node_persistence.ts", + "--timeout", + "5000", "test/{,!(browser|integration)/**/}*.test.ts", "--exit" ], "env": { - "USE_MOCK_PERSISTENCE": "YES", - "TS_NODE_CACHE": "NO", - "TS_NODE_COMPILER_OPTIONS" : "{\"module\":\"commonjs\"}" + "USE_MOCK_PERSISTENCE": "YES" }, - "sourceMaps": true, - "protocol": "inspector" + "sourceMaps": true }, { "type": "node", @@ -71,20 +130,19 @@ "program": "${workspaceRoot}/node_modules/.bin/_mocha", "cwd": "${workspaceRoot}/packages/firestore", "args": [ - "--require", "ts-node/register/type-check", - "--require", "index.node.ts", - "--timeout", "5000", + "--require", + "babel-register.js", + "--require", + "index.node.ts", + "--timeout", + "5000", "test/{,!(browser|unit)/**/}*.test.ts", "--exit" ], "env": { - "TS_NODE_CACHE": "NO", - "TS_NODE_COMPILER_OPTIONS" : "{\"module\":\"commonjs\"}", - "FIRESTORE_EMULATOR_PORT" : "8080", - "FIRESTORE_EMULATOR_PROJECT_ID" : "test-emulator" + "FIRESTORE_TARGET_BACKEND": "emulator" }, - "sourceMaps": true, - "protocol": "inspector" + "sourceMaps": true }, { "type": "node", @@ -93,22 +151,22 @@ "program": "${workspaceRoot}/node_modules/.bin/_mocha", "cwd": "${workspaceRoot}/packages/firestore", "args": [ - "--require", "ts-node/register/type-check", - "--require", "index.node.ts", - "--require", "test/util/node_persistence.ts", - "--timeout", "5000", + "--require", + "babel-register.js", + "--require", + "index.node.ts", + "--require", + "test/util/node_persistence.ts", + "--timeout", + "5000", "test/{,!(browser|unit)/**/}*.test.ts", "--exit" ], "env": { "USE_MOCK_PERSISTENCE": "YES", - "TS_NODE_CACHE": "NO", - "TS_NODE_COMPILER_OPTIONS" : "{\"module\":\"commonjs\"}", - "FIRESTORE_EMULATOR_PORT" : "8080", - "FIRESTORE_EMULATOR_PROJECT_ID" : "test-emulator" + "FIRESTORE_TARGET_BACKEND": "emulator" }, - "sourceMaps": true, - "protocol": "inspector" + "sourceMaps": true }, { "type": "node", @@ -116,12 +174,7 @@ "name": "Firestore Unit Tests (Browser)", "program": "${workspaceRoot}/node_modules/.bin/karma", "cwd": "${workspaceRoot}/packages/firestore", - "args": [ - "start", - "--auto-watch", - "--unit", - "--browsers", "Chrome" - ] + "args": ["start", "--auto-watch", "--unit", "--browsers", "Chrome"] }, { "type": "node", @@ -129,12 +182,7 @@ "name": "Firestore Integration Tests (Browser)", "program": "${workspaceRoot}/node_modules/.bin/karma", "cwd": "${workspaceRoot}/packages/firestore", - "args": [ - "start", - "--auto-watch", - "--integration", - "--browsers", "Chrome" - ] + "args": ["start", "--auto-watch", "--integration", "--browsers", "Chrome"] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 338dd7801dc..706e611f5d5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,6 @@ "**/node_modules": true }, "typescript.tsdk": "node_modules/typescript/lib", - "files.associations": { "*.json": "jsonc" } -} \ No newline at end of file + "files.associations": { "*.json": "jsonc" }, + "eslint.workingDirectories": [{ "mode": "auto" }] +} diff --git a/.yarn/releases/yarn-1.22.11.cjs b/.yarn/releases/yarn-1.22.11.cjs new file mode 100755 index 00000000000..41236bd841f --- /dev/null +++ b/.yarn/releases/yarn-1.22.11.cjs @@ -0,0 +1,147406 @@ +#!/usr/bin/env node +module.exports = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 549); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + +module.exports = require("path"); + +/***/ }), +/* 1 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (immutable) */ __webpack_exports__["a"] = __extends; +/* unused harmony export __assign */ +/* unused harmony export __rest */ +/* unused harmony export __decorate */ +/* unused harmony export __param */ +/* unused harmony export __metadata */ +/* unused harmony export __awaiter */ +/* unused harmony export __generator */ +/* unused harmony export __exportStar */ +/* unused harmony export __values */ +/* unused harmony export __read */ +/* unused harmony export __spread */ +/* unused harmony export __await */ +/* unused harmony export __asyncGenerator */ +/* unused harmony export __asyncDelegator */ +/* unused harmony export __asyncValues */ +/* unused harmony export __makeTemplateObject */ +/* unused harmony export __importStar */ +/* unused harmony export __importDefault */ +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ +/* global Reflect, Promise */ + +var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); +}; + +function __extends(d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); +} + +var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + } + return __assign.apply(this, arguments); +} + +function __rest(s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0) + t[p[i]] = s[p[i]]; + return t; +} + +function __decorate(decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +} + +function __param(paramIndex, decorator) { + return function (target, key) { decorator(target, key, paramIndex); } +} + +function __metadata(metadataKey, metadataValue) { + if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue); +} + +function __awaiter(thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +} + +function __generator(thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +} + +function __exportStar(m, exports) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} + +function __values(o) { + var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0; + if (m) return m.call(o); + return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; +} + +function __read(o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +} + +function __spread() { + for (var ar = [], i = 0; i < arguments.length; i++) + ar = ar.concat(__read(arguments[i])); + return ar; +} + +function __await(v) { + return this instanceof __await ? (this.v = v, this) : new __await(v); +} + +function __asyncGenerator(thisArg, _arguments, generator) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var g = generator.apply(thisArg, _arguments || []), i, q = []; + return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i; + function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; } + function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } + function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } + function fulfill(value) { resume("next", value); } + function reject(value) { resume("throw", value); } + function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } +} + +function __asyncDelegator(o) { + var i, p; + return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i; + function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === "return" } : f ? f(v) : v; } : f; } +} + +function __asyncValues(o) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var m = o[Symbol.asyncIterator], i; + return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); + function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } + function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } +} + +function __makeTemplateObject(cooked, raw) { + if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } + return cooked; +}; + +function __importStar(mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result.default = mod; + return result; +} + +function __importDefault(mod) { + return (mod && mod.__esModule) ? mod : { default: mod }; +} + + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +exports.__esModule = true; + +var _promise = __webpack_require__(227); + +var _promise2 = _interopRequireDefault(_promise); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.default = function (fn) { + return function () { + var gen = fn.apply(this, arguments); + return new _promise2.default(function (resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + return _promise2.default.resolve(value).then(function (value) { + step("next", value); + }, function (err) { + step("throw", err); + }); + } + } + + return step("next"); + }); + }; +}; + +/***/ }), +/* 3 */ +/***/ (function(module, exports) { + +module.exports = require("util"); + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.getFirstSuitableFolder = exports.readFirstAvailableStream = exports.makeTempDir = exports.hardlinksWork = exports.writeFilePreservingEol = exports.getFileSizeOnDisk = exports.walk = exports.symlink = exports.find = exports.readJsonAndFile = exports.readJson = exports.readFileAny = exports.hardlinkBulk = exports.copyBulk = exports.unlink = exports.glob = exports.link = exports.chmod = exports.lstat = exports.exists = exports.mkdirp = exports.stat = exports.access = exports.rename = exports.readdir = exports.realpath = exports.readlink = exports.writeFile = exports.open = exports.readFileBuffer = exports.lockQueue = exports.constants = undefined; + +var _asyncToGenerator2; + +function _load_asyncToGenerator() { + return _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(2)); +} + +let buildActionsForCopy = (() => { + var _ref = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (queue, events, possibleExtraneous, reporter) { + + // + let build = (() => { + var _ref5 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (data) { + const src = data.src, + dest = data.dest, + type = data.type; + + const onFresh = data.onFresh || noop; + const onDone = data.onDone || noop; + + // TODO https://github.com/yarnpkg/yarn/issues/3751 + // related to bundled dependencies handling + if (files.has(dest.toLowerCase())) { + reporter.verbose(`The case-insensitive file ${dest} shouldn't be copied twice in one bulk copy`); + } else { + files.add(dest.toLowerCase()); + } + + if (type === 'symlink') { + yield mkdirp((_path || _load_path()).default.dirname(dest)); + onFresh(); + actions.symlink.push({ + dest, + linkname: src + }); + onDone(); + return; + } + + if (events.ignoreBasenames.indexOf((_path || _load_path()).default.basename(src)) >= 0) { + // ignored file + return; + } + + const srcStat = yield lstat(src); + let srcFiles; + + if (srcStat.isDirectory()) { + srcFiles = yield readdir(src); + } + + let destStat; + try { + // try accessing the destination + destStat = yield lstat(dest); + } catch (e) { + // proceed if destination doesn't exist, otherwise error + if (e.code !== 'ENOENT') { + throw e; + } + } + + // if destination exists + if (destStat) { + const bothSymlinks = srcStat.isSymbolicLink() && destStat.isSymbolicLink(); + const bothFolders = srcStat.isDirectory() && destStat.isDirectory(); + const bothFiles = srcStat.isFile() && destStat.isFile(); + + // EINVAL access errors sometimes happen which shouldn't because node shouldn't be giving + // us modes that aren't valid. investigate this, it's generally safe to proceed. + + /* if (srcStat.mode !== destStat.mode) { + try { + await access(dest, srcStat.mode); + } catch (err) {} + } */ + + if (bothFiles && artifactFiles.has(dest)) { + // this file gets changed during build, likely by a custom install script. Don't bother checking it. + onDone(); + reporter.verbose(reporter.lang('verboseFileSkipArtifact', src)); + return; + } + + if (bothFiles && srcStat.size === destStat.size && (0, (_fsNormalized || _load_fsNormalized()).fileDatesEqual)(srcStat.mtime, destStat.mtime)) { + // we can safely assume this is the same file + onDone(); + reporter.verbose(reporter.lang('verboseFileSkip', src, dest, srcStat.size, +srcStat.mtime)); + return; + } + + if (bothSymlinks) { + const srcReallink = yield readlink(src); + if (srcReallink === (yield readlink(dest))) { + // if both symlinks are the same then we can continue on + onDone(); + reporter.verbose(reporter.lang('verboseFileSkipSymlink', src, dest, srcReallink)); + return; + } + } + + if (bothFolders) { + // mark files that aren't in this folder as possibly extraneous + const destFiles = yield readdir(dest); + invariant(srcFiles, 'src files not initialised'); + + for (var _iterator4 = destFiles, _isArray4 = Array.isArray(_iterator4), _i4 = 0, _iterator4 = _isArray4 ? _iterator4 : _iterator4[Symbol.iterator]();;) { + var _ref6; + + if (_isArray4) { + if (_i4 >= _iterator4.length) break; + _ref6 = _iterator4[_i4++]; + } else { + _i4 = _iterator4.next(); + if (_i4.done) break; + _ref6 = _i4.value; + } + + const file = _ref6; + + if (srcFiles.indexOf(file) < 0) { + const loc = (_path || _load_path()).default.join(dest, file); + possibleExtraneous.add(loc); + + if ((yield lstat(loc)).isDirectory()) { + for (var _iterator5 = yield readdir(loc), _isArray5 = Array.isArray(_iterator5), _i5 = 0, _iterator5 = _isArray5 ? _iterator5 : _iterator5[Symbol.iterator]();;) { + var _ref7; + + if (_isArray5) { + if (_i5 >= _iterator5.length) break; + _ref7 = _iterator5[_i5++]; + } else { + _i5 = _iterator5.next(); + if (_i5.done) break; + _ref7 = _i5.value; + } + + const file = _ref7; + + possibleExtraneous.add((_path || _load_path()).default.join(loc, file)); + } + } + } + } + } + } + + if (destStat && destStat.isSymbolicLink()) { + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(dest); + destStat = null; + } + + if (srcStat.isSymbolicLink()) { + onFresh(); + const linkname = yield readlink(src); + actions.symlink.push({ + dest, + linkname + }); + onDone(); + } else if (srcStat.isDirectory()) { + if (!destStat) { + reporter.verbose(reporter.lang('verboseFileFolder', dest)); + yield mkdirp(dest); + } + + const destParts = dest.split((_path || _load_path()).default.sep); + while (destParts.length) { + files.add(destParts.join((_path || _load_path()).default.sep).toLowerCase()); + destParts.pop(); + } + + // push all files to queue + invariant(srcFiles, 'src files not initialised'); + let remaining = srcFiles.length; + if (!remaining) { + onDone(); + } + for (var _iterator6 = srcFiles, _isArray6 = Array.isArray(_iterator6), _i6 = 0, _iterator6 = _isArray6 ? _iterator6 : _iterator6[Symbol.iterator]();;) { + var _ref8; + + if (_isArray6) { + if (_i6 >= _iterator6.length) break; + _ref8 = _iterator6[_i6++]; + } else { + _i6 = _iterator6.next(); + if (_i6.done) break; + _ref8 = _i6.value; + } + + const file = _ref8; + + queue.push({ + dest: (_path || _load_path()).default.join(dest, file), + onFresh, + onDone: function (_onDone) { + function onDone() { + return _onDone.apply(this, arguments); + } + + onDone.toString = function () { + return _onDone.toString(); + }; + + return onDone; + }(function () { + if (--remaining === 0) { + onDone(); + } + }), + src: (_path || _load_path()).default.join(src, file) + }); + } + } else if (srcStat.isFile()) { + onFresh(); + actions.file.push({ + src, + dest, + atime: srcStat.atime, + mtime: srcStat.mtime, + mode: srcStat.mode + }); + onDone(); + } else { + throw new Error(`unsure how to copy this: ${src}`); + } + }); + + return function build(_x5) { + return _ref5.apply(this, arguments); + }; + })(); + + const artifactFiles = new Set(events.artifactFiles || []); + const files = new Set(); + + // initialise events + for (var _iterator = queue, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { + var _ref2; + + if (_isArray) { + if (_i >= _iterator.length) break; + _ref2 = _iterator[_i++]; + } else { + _i = _iterator.next(); + if (_i.done) break; + _ref2 = _i.value; + } + + const item = _ref2; + + const onDone = item.onDone; + item.onDone = function () { + events.onProgress(item.dest); + if (onDone) { + onDone(); + } + }; + } + events.onStart(queue.length); + + // start building actions + const actions = { + file: [], + symlink: [], + link: [] + }; + + // custom concurrency logic as we're always executing stacks of CONCURRENT_QUEUE_ITEMS queue items + // at a time due to the requirement to push items onto the queue + while (queue.length) { + const items = queue.splice(0, CONCURRENT_QUEUE_ITEMS); + yield Promise.all(items.map(build)); + } + + // simulate the existence of some files to prevent considering them extraneous + for (var _iterator2 = artifactFiles, _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { + var _ref3; + + if (_isArray2) { + if (_i2 >= _iterator2.length) break; + _ref3 = _iterator2[_i2++]; + } else { + _i2 = _iterator2.next(); + if (_i2.done) break; + _ref3 = _i2.value; + } + + const file = _ref3; + + if (possibleExtraneous.has(file)) { + reporter.verbose(reporter.lang('verboseFilePhantomExtraneous', file)); + possibleExtraneous.delete(file); + } + } + + for (var _iterator3 = possibleExtraneous, _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) { + var _ref4; + + if (_isArray3) { + if (_i3 >= _iterator3.length) break; + _ref4 = _iterator3[_i3++]; + } else { + _i3 = _iterator3.next(); + if (_i3.done) break; + _ref4 = _i3.value; + } + + const loc = _ref4; + + if (files.has(loc.toLowerCase())) { + possibleExtraneous.delete(loc); + } + } + + return actions; + }); + + return function buildActionsForCopy(_x, _x2, _x3, _x4) { + return _ref.apply(this, arguments); + }; +})(); + +let buildActionsForHardlink = (() => { + var _ref9 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (queue, events, possibleExtraneous, reporter) { + + // + let build = (() => { + var _ref13 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (data) { + const src = data.src, + dest = data.dest; + + const onFresh = data.onFresh || noop; + const onDone = data.onDone || noop; + if (files.has(dest.toLowerCase())) { + // Fixes issue https://github.com/yarnpkg/yarn/issues/2734 + // When bulk hardlinking we have A -> B structure that we want to hardlink to A1 -> B1, + // package-linker passes that modules A1 and B1 need to be hardlinked, + // the recursive linking algorithm of A1 ends up scheduling files in B1 to be linked twice which will case + // an exception. + onDone(); + return; + } + files.add(dest.toLowerCase()); + + if (events.ignoreBasenames.indexOf((_path || _load_path()).default.basename(src)) >= 0) { + // ignored file + return; + } + + const srcStat = yield lstat(src); + let srcFiles; + + if (srcStat.isDirectory()) { + srcFiles = yield readdir(src); + } + + const destExists = yield exists(dest); + if (destExists) { + const destStat = yield lstat(dest); + + const bothSymlinks = srcStat.isSymbolicLink() && destStat.isSymbolicLink(); + const bothFolders = srcStat.isDirectory() && destStat.isDirectory(); + const bothFiles = srcStat.isFile() && destStat.isFile(); + + if (srcStat.mode !== destStat.mode) { + try { + yield access(dest, srcStat.mode); + } catch (err) { + // EINVAL access errors sometimes happen which shouldn't because node shouldn't be giving + // us modes that aren't valid. investigate this, it's generally safe to proceed. + reporter.verbose(err); + } + } + + if (bothFiles && artifactFiles.has(dest)) { + // this file gets changed during build, likely by a custom install script. Don't bother checking it. + onDone(); + reporter.verbose(reporter.lang('verboseFileSkipArtifact', src)); + return; + } + + // correct hardlink + if (bothFiles && srcStat.ino !== null && srcStat.ino === destStat.ino) { + onDone(); + reporter.verbose(reporter.lang('verboseFileSkip', src, dest, srcStat.ino)); + return; + } + + if (bothSymlinks) { + const srcReallink = yield readlink(src); + if (srcReallink === (yield readlink(dest))) { + // if both symlinks are the same then we can continue on + onDone(); + reporter.verbose(reporter.lang('verboseFileSkipSymlink', src, dest, srcReallink)); + return; + } + } + + if (bothFolders) { + // mark files that aren't in this folder as possibly extraneous + const destFiles = yield readdir(dest); + invariant(srcFiles, 'src files not initialised'); + + for (var _iterator10 = destFiles, _isArray10 = Array.isArray(_iterator10), _i10 = 0, _iterator10 = _isArray10 ? _iterator10 : _iterator10[Symbol.iterator]();;) { + var _ref14; + + if (_isArray10) { + if (_i10 >= _iterator10.length) break; + _ref14 = _iterator10[_i10++]; + } else { + _i10 = _iterator10.next(); + if (_i10.done) break; + _ref14 = _i10.value; + } + + const file = _ref14; + + if (srcFiles.indexOf(file) < 0) { + const loc = (_path || _load_path()).default.join(dest, file); + possibleExtraneous.add(loc); + + if ((yield lstat(loc)).isDirectory()) { + for (var _iterator11 = yield readdir(loc), _isArray11 = Array.isArray(_iterator11), _i11 = 0, _iterator11 = _isArray11 ? _iterator11 : _iterator11[Symbol.iterator]();;) { + var _ref15; + + if (_isArray11) { + if (_i11 >= _iterator11.length) break; + _ref15 = _iterator11[_i11++]; + } else { + _i11 = _iterator11.next(); + if (_i11.done) break; + _ref15 = _i11.value; + } + + const file = _ref15; + + possibleExtraneous.add((_path || _load_path()).default.join(loc, file)); + } + } + } + } + } + } + + if (srcStat.isSymbolicLink()) { + onFresh(); + const linkname = yield readlink(src); + actions.symlink.push({ + dest, + linkname + }); + onDone(); + } else if (srcStat.isDirectory()) { + reporter.verbose(reporter.lang('verboseFileFolder', dest)); + yield mkdirp(dest); + + const destParts = dest.split((_path || _load_path()).default.sep); + while (destParts.length) { + files.add(destParts.join((_path || _load_path()).default.sep).toLowerCase()); + destParts.pop(); + } + + // push all files to queue + invariant(srcFiles, 'src files not initialised'); + let remaining = srcFiles.length; + if (!remaining) { + onDone(); + } + for (var _iterator12 = srcFiles, _isArray12 = Array.isArray(_iterator12), _i12 = 0, _iterator12 = _isArray12 ? _iterator12 : _iterator12[Symbol.iterator]();;) { + var _ref16; + + if (_isArray12) { + if (_i12 >= _iterator12.length) break; + _ref16 = _iterator12[_i12++]; + } else { + _i12 = _iterator12.next(); + if (_i12.done) break; + _ref16 = _i12.value; + } + + const file = _ref16; + + queue.push({ + onFresh, + src: (_path || _load_path()).default.join(src, file), + dest: (_path || _load_path()).default.join(dest, file), + onDone: function (_onDone2) { + function onDone() { + return _onDone2.apply(this, arguments); + } + + onDone.toString = function () { + return _onDone2.toString(); + }; + + return onDone; + }(function () { + if (--remaining === 0) { + onDone(); + } + }) + }); + } + } else if (srcStat.isFile()) { + onFresh(); + actions.link.push({ + src, + dest, + removeDest: destExists + }); + onDone(); + } else { + throw new Error(`unsure how to copy this: ${src}`); + } + }); + + return function build(_x10) { + return _ref13.apply(this, arguments); + }; + })(); + + const artifactFiles = new Set(events.artifactFiles || []); + const files = new Set(); + + // initialise events + for (var _iterator7 = queue, _isArray7 = Array.isArray(_iterator7), _i7 = 0, _iterator7 = _isArray7 ? _iterator7 : _iterator7[Symbol.iterator]();;) { + var _ref10; + + if (_isArray7) { + if (_i7 >= _iterator7.length) break; + _ref10 = _iterator7[_i7++]; + } else { + _i7 = _iterator7.next(); + if (_i7.done) break; + _ref10 = _i7.value; + } + + const item = _ref10; + + const onDone = item.onDone || noop; + item.onDone = function () { + events.onProgress(item.dest); + onDone(); + }; + } + events.onStart(queue.length); + + // start building actions + const actions = { + file: [], + symlink: [], + link: [] + }; + + // custom concurrency logic as we're always executing stacks of CONCURRENT_QUEUE_ITEMS queue items + // at a time due to the requirement to push items onto the queue + while (queue.length) { + const items = queue.splice(0, CONCURRENT_QUEUE_ITEMS); + yield Promise.all(items.map(build)); + } + + // simulate the existence of some files to prevent considering them extraneous + for (var _iterator8 = artifactFiles, _isArray8 = Array.isArray(_iterator8), _i8 = 0, _iterator8 = _isArray8 ? _iterator8 : _iterator8[Symbol.iterator]();;) { + var _ref11; + + if (_isArray8) { + if (_i8 >= _iterator8.length) break; + _ref11 = _iterator8[_i8++]; + } else { + _i8 = _iterator8.next(); + if (_i8.done) break; + _ref11 = _i8.value; + } + + const file = _ref11; + + if (possibleExtraneous.has(file)) { + reporter.verbose(reporter.lang('verboseFilePhantomExtraneous', file)); + possibleExtraneous.delete(file); + } + } + + for (var _iterator9 = possibleExtraneous, _isArray9 = Array.isArray(_iterator9), _i9 = 0, _iterator9 = _isArray9 ? _iterator9 : _iterator9[Symbol.iterator]();;) { + var _ref12; + + if (_isArray9) { + if (_i9 >= _iterator9.length) break; + _ref12 = _iterator9[_i9++]; + } else { + _i9 = _iterator9.next(); + if (_i9.done) break; + _ref12 = _i9.value; + } + + const loc = _ref12; + + if (files.has(loc.toLowerCase())) { + possibleExtraneous.delete(loc); + } + } + + return actions; + }); + + return function buildActionsForHardlink(_x6, _x7, _x8, _x9) { + return _ref9.apply(this, arguments); + }; +})(); + +let copyBulk = exports.copyBulk = (() => { + var _ref17 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (queue, reporter, _events) { + const events = { + onStart: _events && _events.onStart || noop, + onProgress: _events && _events.onProgress || noop, + possibleExtraneous: _events ? _events.possibleExtraneous : new Set(), + ignoreBasenames: _events && _events.ignoreBasenames || [], + artifactFiles: _events && _events.artifactFiles || [] + }; + + const actions = yield buildActionsForCopy(queue, events, events.possibleExtraneous, reporter); + events.onStart(actions.file.length + actions.symlink.length + actions.link.length); + + const fileActions = actions.file; + + const currentlyWriting = new Map(); + + yield (_promise || _load_promise()).queue(fileActions, (() => { + var _ref18 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (data) { + let writePromise; + while (writePromise = currentlyWriting.get(data.dest)) { + yield writePromise; + } + + reporter.verbose(reporter.lang('verboseFileCopy', data.src, data.dest)); + const copier = (0, (_fsNormalized || _load_fsNormalized()).copyFile)(data, function () { + return currentlyWriting.delete(data.dest); + }); + currentlyWriting.set(data.dest, copier); + events.onProgress(data.dest); + return copier; + }); + + return function (_x14) { + return _ref18.apply(this, arguments); + }; + })(), CONCURRENT_QUEUE_ITEMS); + + // we need to copy symlinks last as they could reference files we were copying + const symlinkActions = actions.symlink; + yield (_promise || _load_promise()).queue(symlinkActions, function (data) { + const linkname = (_path || _load_path()).default.resolve((_path || _load_path()).default.dirname(data.dest), data.linkname); + reporter.verbose(reporter.lang('verboseFileSymlink', data.dest, linkname)); + return symlink(linkname, data.dest); + }); + }); + + return function copyBulk(_x11, _x12, _x13) { + return _ref17.apply(this, arguments); + }; +})(); + +let hardlinkBulk = exports.hardlinkBulk = (() => { + var _ref19 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (queue, reporter, _events) { + const events = { + onStart: _events && _events.onStart || noop, + onProgress: _events && _events.onProgress || noop, + possibleExtraneous: _events ? _events.possibleExtraneous : new Set(), + artifactFiles: _events && _events.artifactFiles || [], + ignoreBasenames: [] + }; + + const actions = yield buildActionsForHardlink(queue, events, events.possibleExtraneous, reporter); + events.onStart(actions.file.length + actions.symlink.length + actions.link.length); + + const fileActions = actions.link; + + yield (_promise || _load_promise()).queue(fileActions, (() => { + var _ref20 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (data) { + reporter.verbose(reporter.lang('verboseFileLink', data.src, data.dest)); + if (data.removeDest) { + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(data.dest); + } + yield link(data.src, data.dest); + }); + + return function (_x18) { + return _ref20.apply(this, arguments); + }; + })(), CONCURRENT_QUEUE_ITEMS); + + // we need to copy symlinks last as they could reference files we were copying + const symlinkActions = actions.symlink; + yield (_promise || _load_promise()).queue(symlinkActions, function (data) { + const linkname = (_path || _load_path()).default.resolve((_path || _load_path()).default.dirname(data.dest), data.linkname); + reporter.verbose(reporter.lang('verboseFileSymlink', data.dest, linkname)); + return symlink(linkname, data.dest); + }); + }); + + return function hardlinkBulk(_x15, _x16, _x17) { + return _ref19.apply(this, arguments); + }; +})(); + +let readFileAny = exports.readFileAny = (() => { + var _ref21 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (files) { + for (var _iterator13 = files, _isArray13 = Array.isArray(_iterator13), _i13 = 0, _iterator13 = _isArray13 ? _iterator13 : _iterator13[Symbol.iterator]();;) { + var _ref22; + + if (_isArray13) { + if (_i13 >= _iterator13.length) break; + _ref22 = _iterator13[_i13++]; + } else { + _i13 = _iterator13.next(); + if (_i13.done) break; + _ref22 = _i13.value; + } + + const file = _ref22; + + if (yield exists(file)) { + return readFile(file); + } + } + return null; + }); + + return function readFileAny(_x19) { + return _ref21.apply(this, arguments); + }; +})(); + +let readJson = exports.readJson = (() => { + var _ref23 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (loc) { + return (yield readJsonAndFile(loc)).object; + }); + + return function readJson(_x20) { + return _ref23.apply(this, arguments); + }; +})(); + +let readJsonAndFile = exports.readJsonAndFile = (() => { + var _ref24 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (loc) { + const file = yield readFile(loc); + try { + return { + object: (0, (_map || _load_map()).default)(JSON.parse(stripBOM(file))), + content: file + }; + } catch (err) { + err.message = `${loc}: ${err.message}`; + throw err; + } + }); + + return function readJsonAndFile(_x21) { + return _ref24.apply(this, arguments); + }; +})(); + +let find = exports.find = (() => { + var _ref25 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (filename, dir) { + const parts = dir.split((_path || _load_path()).default.sep); + + while (parts.length) { + const loc = parts.concat(filename).join((_path || _load_path()).default.sep); + + if (yield exists(loc)) { + return loc; + } else { + parts.pop(); + } + } + + return false; + }); + + return function find(_x22, _x23) { + return _ref25.apply(this, arguments); + }; +})(); + +let symlink = exports.symlink = (() => { + var _ref26 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (src, dest) { + if (process.platform !== 'win32') { + // use relative paths otherwise which will be retained if the directory is moved + src = (_path || _load_path()).default.relative((_path || _load_path()).default.dirname(dest), src); + // When path.relative returns an empty string for the current directory, we should instead use + // '.', which is a valid fs.symlink target. + src = src || '.'; + } + + try { + const stats = yield lstat(dest); + if (stats.isSymbolicLink()) { + const resolved = dest; + if (resolved === src) { + return; + } + } + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + + // We use rimraf for unlink which never throws an ENOENT on missing target + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(dest); + + if (process.platform === 'win32') { + // use directory junctions if possible on win32, this requires absolute paths + yield fsSymlink(src, dest, 'junction'); + } else { + yield fsSymlink(src, dest); + } + }); + + return function symlink(_x24, _x25) { + return _ref26.apply(this, arguments); + }; +})(); + +let walk = exports.walk = (() => { + var _ref27 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (dir, relativeDir, ignoreBasenames = new Set()) { + let files = []; + + let filenames = yield readdir(dir); + if (ignoreBasenames.size) { + filenames = filenames.filter(function (name) { + return !ignoreBasenames.has(name); + }); + } + + for (var _iterator14 = filenames, _isArray14 = Array.isArray(_iterator14), _i14 = 0, _iterator14 = _isArray14 ? _iterator14 : _iterator14[Symbol.iterator]();;) { + var _ref28; + + if (_isArray14) { + if (_i14 >= _iterator14.length) break; + _ref28 = _iterator14[_i14++]; + } else { + _i14 = _iterator14.next(); + if (_i14.done) break; + _ref28 = _i14.value; + } + + const name = _ref28; + + const relative = relativeDir ? (_path || _load_path()).default.join(relativeDir, name) : name; + const loc = (_path || _load_path()).default.join(dir, name); + const stat = yield lstat(loc); + + files.push({ + relative, + basename: name, + absolute: loc, + mtime: +stat.mtime + }); + + if (stat.isDirectory()) { + files = files.concat((yield walk(loc, relative, ignoreBasenames))); + } + } + + return files; + }); + + return function walk(_x26, _x27) { + return _ref27.apply(this, arguments); + }; +})(); + +let getFileSizeOnDisk = exports.getFileSizeOnDisk = (() => { + var _ref29 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (loc) { + const stat = yield lstat(loc); + const size = stat.size, + blockSize = stat.blksize; + + + return Math.ceil(size / blockSize) * blockSize; + }); + + return function getFileSizeOnDisk(_x28) { + return _ref29.apply(this, arguments); + }; +})(); + +let getEolFromFile = (() => { + var _ref30 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (path) { + if (!(yield exists(path))) { + return undefined; + } + + const buffer = yield readFileBuffer(path); + + for (let i = 0; i < buffer.length; ++i) { + if (buffer[i] === cr) { + return '\r\n'; + } + if (buffer[i] === lf) { + return '\n'; + } + } + return undefined; + }); + + return function getEolFromFile(_x29) { + return _ref30.apply(this, arguments); + }; +})(); + +let writeFilePreservingEol = exports.writeFilePreservingEol = (() => { + var _ref31 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (path, data) { + const eol = (yield getEolFromFile(path)) || (_os || _load_os()).default.EOL; + if (eol !== '\n') { + data = data.replace(/\n/g, eol); + } + yield writeFile(path, data); + }); + + return function writeFilePreservingEol(_x30, _x31) { + return _ref31.apply(this, arguments); + }; +})(); + +let hardlinksWork = exports.hardlinksWork = (() => { + var _ref32 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (dir) { + const filename = 'test-file' + Math.random(); + const file = (_path || _load_path()).default.join(dir, filename); + const fileLink = (_path || _load_path()).default.join(dir, filename + '-link'); + try { + yield writeFile(file, 'test'); + yield link(file, fileLink); + } catch (err) { + return false; + } finally { + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(file); + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(fileLink); + } + return true; + }); + + return function hardlinksWork(_x32) { + return _ref32.apply(this, arguments); + }; +})(); + +// not a strict polyfill for Node's fs.mkdtemp + + +let makeTempDir = exports.makeTempDir = (() => { + var _ref33 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (prefix) { + const dir = (_path || _load_path()).default.join((_os || _load_os()).default.tmpdir(), `yarn-${prefix || ''}-${Date.now()}-${Math.random()}`); + yield (0, (_fsNormalized || _load_fsNormalized()).unlink)(dir); + yield mkdirp(dir); + return dir; + }); + + return function makeTempDir(_x33) { + return _ref33.apply(this, arguments); + }; +})(); + +let readFirstAvailableStream = exports.readFirstAvailableStream = (() => { + var _ref34 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (paths) { + for (var _iterator15 = paths, _isArray15 = Array.isArray(_iterator15), _i15 = 0, _iterator15 = _isArray15 ? _iterator15 : _iterator15[Symbol.iterator]();;) { + var _ref35; + + if (_isArray15) { + if (_i15 >= _iterator15.length) break; + _ref35 = _iterator15[_i15++]; + } else { + _i15 = _iterator15.next(); + if (_i15.done) break; + _ref35 = _i15.value; + } + + const path = _ref35; + + try { + const fd = yield open(path, 'r'); + return (_fs || _load_fs()).default.createReadStream(path, { fd }); + } catch (err) { + // Try the next one + } + } + return null; + }); + + return function readFirstAvailableStream(_x34) { + return _ref34.apply(this, arguments); + }; +})(); + +let getFirstSuitableFolder = exports.getFirstSuitableFolder = (() => { + var _ref36 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (paths, mode = constants.W_OK | constants.X_OK) { + const result = { + skipped: [], + folder: null + }; + + for (var _iterator16 = paths, _isArray16 = Array.isArray(_iterator16), _i16 = 0, _iterator16 = _isArray16 ? _iterator16 : _iterator16[Symbol.iterator]();;) { + var _ref37; + + if (_isArray16) { + if (_i16 >= _iterator16.length) break; + _ref37 = _iterator16[_i16++]; + } else { + _i16 = _iterator16.next(); + if (_i16.done) break; + _ref37 = _i16.value; + } + + const folder = _ref37; + + try { + yield mkdirp(folder); + yield access(folder, mode); + + result.folder = folder; + + return result; + } catch (error) { + result.skipped.push({ + error, + folder + }); + } + } + return result; + }); + + return function getFirstSuitableFolder(_x35) { + return _ref36.apply(this, arguments); + }; +})(); + +exports.copy = copy; +exports.readFile = readFile; +exports.readFileRaw = readFileRaw; +exports.normalizeOS = normalizeOS; + +var _fs; + +function _load_fs() { + return _fs = _interopRequireDefault(__webpack_require__(5)); +} + +var _glob; + +function _load_glob() { + return _glob = _interopRequireDefault(__webpack_require__(99)); +} + +var _os; + +function _load_os() { + return _os = _interopRequireDefault(__webpack_require__(46)); +} + +var _path; + +function _load_path() { + return _path = _interopRequireDefault(__webpack_require__(0)); +} + +var _blockingQueue; + +function _load_blockingQueue() { + return _blockingQueue = _interopRequireDefault(__webpack_require__(110)); +} + +var _promise; + +function _load_promise() { + return _promise = _interopRequireWildcard(__webpack_require__(50)); +} + +var _promise2; + +function _load_promise2() { + return _promise2 = __webpack_require__(50); +} + +var _map; + +function _load_map() { + return _map = _interopRequireDefault(__webpack_require__(29)); +} + +var _fsNormalized; + +function _load_fsNormalized() { + return _fsNormalized = __webpack_require__(218); +} + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const constants = exports.constants = typeof (_fs || _load_fs()).default.constants !== 'undefined' ? (_fs || _load_fs()).default.constants : { + R_OK: (_fs || _load_fs()).default.R_OK, + W_OK: (_fs || _load_fs()).default.W_OK, + X_OK: (_fs || _load_fs()).default.X_OK +}; + +const lockQueue = exports.lockQueue = new (_blockingQueue || _load_blockingQueue()).default('fs lock'); + +const readFileBuffer = exports.readFileBuffer = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.readFile); +const open = exports.open = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.open); +const writeFile = exports.writeFile = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.writeFile); +const readlink = exports.readlink = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.readlink); +const realpath = exports.realpath = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.realpath); +const readdir = exports.readdir = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.readdir); +const rename = exports.rename = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.rename); +const access = exports.access = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.access); +const stat = exports.stat = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.stat); +const mkdirp = exports.mkdirp = (0, (_promise2 || _load_promise2()).promisify)(__webpack_require__(145)); +const exists = exports.exists = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.exists, true); +const lstat = exports.lstat = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.lstat); +const chmod = exports.chmod = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.chmod); +const link = exports.link = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.link); +const glob = exports.glob = (0, (_promise2 || _load_promise2()).promisify)((_glob || _load_glob()).default); +exports.unlink = (_fsNormalized || _load_fsNormalized()).unlink; + +// fs.copyFile uses the native file copying instructions on the system, performing much better +// than any JS-based solution and consumes fewer resources. Repeated testing to fine tune the +// concurrency level revealed 128 as the sweet spot on a quad-core, 16 CPU Intel system with SSD. + +const CONCURRENT_QUEUE_ITEMS = (_fs || _load_fs()).default.copyFile ? 128 : 4; + +const fsSymlink = (0, (_promise2 || _load_promise2()).promisify)((_fs || _load_fs()).default.symlink); +const invariant = __webpack_require__(9); +const stripBOM = __webpack_require__(160); + +const noop = () => {}; + +function copy(src, dest, reporter) { + return copyBulk([{ src, dest }], reporter); +} + +function _readFile(loc, encoding) { + return new Promise((resolve, reject) => { + (_fs || _load_fs()).default.readFile(loc, encoding, function (err, content) { + if (err) { + reject(err); + } else { + resolve(content); + } + }); + }); +} + +function readFile(loc) { + return _readFile(loc, 'utf8').then(normalizeOS); +} + +function readFileRaw(loc) { + return _readFile(loc, 'binary'); +} + +function normalizeOS(body) { + return body.replace(/\r\n/g, '\n'); +} + +const cr = '\r'.charCodeAt(0); +const lf = '\n'.charCodeAt(0); + +/***/ }), +/* 5 */ +/***/ (function(module, exports) { + +module.exports = require("fs"); + +/***/ }), +/* 6 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +class MessageError extends Error { + constructor(msg, code) { + super(msg); + this.code = code; + } + +} + +exports.MessageError = MessageError; +class ProcessSpawnError extends MessageError { + constructor(msg, code, process) { + super(msg, code); + this.process = process; + } + +} + +exports.ProcessSpawnError = ProcessSpawnError; +class SecurityError extends MessageError {} + +exports.SecurityError = SecurityError; +class ProcessTermError extends MessageError {} + +exports.ProcessTermError = ProcessTermError; +class ResponseError extends Error { + constructor(msg, responseCode) { + super(msg); + this.responseCode = responseCode; + } + +} + +exports.ResponseError = ResponseError; +class OneTimePasswordError extends Error {} +exports.OneTimePasswordError = OneTimePasswordError; + +/***/ }), +/* 7 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Subscriber; }); +/* unused harmony export SafeSubscriber */ +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_tslib__ = __webpack_require__(1); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__util_isFunction__ = __webpack_require__(154); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__Observer__ = __webpack_require__(420); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__Subscription__ = __webpack_require__(25); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__internal_symbol_rxSubscriber__ = __webpack_require__(321); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__config__ = __webpack_require__(185); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_6__util_hostReportError__ = __webpack_require__(323); +/** PURE_IMPORTS_START tslib,_util_isFunction,_Observer,_Subscription,_internal_symbol_rxSubscriber,_config,_util_hostReportError PURE_IMPORTS_END */ + + + + + + + +var Subscriber = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](Subscriber, _super); + function Subscriber(destinationOrNext, error, complete) { + var _this = _super.call(this) || this; + _this.syncErrorValue = null; + _this.syncErrorThrown = false; + _this.syncErrorThrowable = false; + _this.isStopped = false; + _this._parentSubscription = null; + switch (arguments.length) { + case 0: + _this.destination = __WEBPACK_IMPORTED_MODULE_2__Observer__["a" /* empty */]; + break; + case 1: + if (!destinationOrNext) { + _this.destination = __WEBPACK_IMPORTED_MODULE_2__Observer__["a" /* empty */]; + break; + } + if (typeof destinationOrNext === 'object') { + if (destinationOrNext instanceof Subscriber) { + _this.syncErrorThrowable = destinationOrNext.syncErrorThrowable; + _this.destination = destinationOrNext; + destinationOrNext.add(_this); + } + else { + _this.syncErrorThrowable = true; + _this.destination = new SafeSubscriber(_this, destinationOrNext); + } + break; + } + default: + _this.syncErrorThrowable = true; + _this.destination = new SafeSubscriber(_this, destinationOrNext, error, complete); + break; + } + return _this; + } + Subscriber.prototype[__WEBPACK_IMPORTED_MODULE_4__internal_symbol_rxSubscriber__["a" /* rxSubscriber */]] = function () { return this; }; + Subscriber.create = function (next, error, complete) { + var subscriber = new Subscriber(next, error, complete); + subscriber.syncErrorThrowable = false; + return subscriber; + }; + Subscriber.prototype.next = function (value) { + if (!this.isStopped) { + this._next(value); + } + }; + Subscriber.prototype.error = function (err) { + if (!this.isStopped) { + this.isStopped = true; + this._error(err); + } + }; + Subscriber.prototype.complete = function () { + if (!this.isStopped) { + this.isStopped = true; + this._complete(); + } + }; + Subscriber.prototype.unsubscribe = function () { + if (this.closed) { + return; + } + this.isStopped = true; + _super.prototype.unsubscribe.call(this); + }; + Subscriber.prototype._next = function (value) { + this.destination.next(value); + }; + Subscriber.prototype._error = function (err) { + this.destination.error(err); + this.unsubscribe(); + }; + Subscriber.prototype._complete = function () { + this.destination.complete(); + this.unsubscribe(); + }; + Subscriber.prototype._unsubscribeAndRecycle = function () { + var _a = this, _parent = _a._parent, _parents = _a._parents; + this._parent = null; + this._parents = null; + this.unsubscribe(); + this.closed = false; + this.isStopped = false; + this._parent = _parent; + this._parents = _parents; + this._parentSubscription = null; + return this; + }; + return Subscriber; +}(__WEBPACK_IMPORTED_MODULE_3__Subscription__["a" /* Subscription */])); + +var SafeSubscriber = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](SafeSubscriber, _super); + function SafeSubscriber(_parentSubscriber, observerOrNext, error, complete) { + var _this = _super.call(this) || this; + _this._parentSubscriber = _parentSubscriber; + var next; + var context = _this; + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1__util_isFunction__["a" /* isFunction */])(observerOrNext)) { + next = observerOrNext; + } + else if (observerOrNext) { + next = observerOrNext.next; + error = observerOrNext.error; + complete = observerOrNext.complete; + if (observerOrNext !== __WEBPACK_IMPORTED_MODULE_2__Observer__["a" /* empty */]) { + context = Object.create(observerOrNext); + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1__util_isFunction__["a" /* isFunction */])(context.unsubscribe)) { + _this.add(context.unsubscribe.bind(context)); + } + context.unsubscribe = _this.unsubscribe.bind(_this); + } + } + _this._context = context; + _this._next = next; + _this._error = error; + _this._complete = complete; + return _this; + } + SafeSubscriber.prototype.next = function (value) { + if (!this.isStopped && this._next) { + var _parentSubscriber = this._parentSubscriber; + if (!__WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling || !_parentSubscriber.syncErrorThrowable) { + this.__tryOrUnsub(this._next, value); + } + else if (this.__tryOrSetError(_parentSubscriber, this._next, value)) { + this.unsubscribe(); + } + } + }; + SafeSubscriber.prototype.error = function (err) { + if (!this.isStopped) { + var _parentSubscriber = this._parentSubscriber; + var useDeprecatedSynchronousErrorHandling = __WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling; + if (this._error) { + if (!useDeprecatedSynchronousErrorHandling || !_parentSubscriber.syncErrorThrowable) { + this.__tryOrUnsub(this._error, err); + this.unsubscribe(); + } + else { + this.__tryOrSetError(_parentSubscriber, this._error, err); + this.unsubscribe(); + } + } + else if (!_parentSubscriber.syncErrorThrowable) { + this.unsubscribe(); + if (useDeprecatedSynchronousErrorHandling) { + throw err; + } + __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_6__util_hostReportError__["a" /* hostReportError */])(err); + } + else { + if (useDeprecatedSynchronousErrorHandling) { + _parentSubscriber.syncErrorValue = err; + _parentSubscriber.syncErrorThrown = true; + } + else { + __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_6__util_hostReportError__["a" /* hostReportError */])(err); + } + this.unsubscribe(); + } + } + }; + SafeSubscriber.prototype.complete = function () { + var _this = this; + if (!this.isStopped) { + var _parentSubscriber = this._parentSubscriber; + if (this._complete) { + var wrappedComplete = function () { return _this._complete.call(_this._context); }; + if (!__WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling || !_parentSubscriber.syncErrorThrowable) { + this.__tryOrUnsub(wrappedComplete); + this.unsubscribe(); + } + else { + this.__tryOrSetError(_parentSubscriber, wrappedComplete); + this.unsubscribe(); + } + } + else { + this.unsubscribe(); + } + } + }; + SafeSubscriber.prototype.__tryOrUnsub = function (fn, value) { + try { + fn.call(this._context, value); + } + catch (err) { + this.unsubscribe(); + if (__WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling) { + throw err; + } + else { + __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_6__util_hostReportError__["a" /* hostReportError */])(err); + } + } + }; + SafeSubscriber.prototype.__tryOrSetError = function (parent, fn, value) { + if (!__WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling) { + throw new Error('bad call'); + } + try { + fn.call(this._context, value); + } + catch (err) { + if (__WEBPACK_IMPORTED_MODULE_5__config__["a" /* config */].useDeprecatedSynchronousErrorHandling) { + parent.syncErrorValue = err; + parent.syncErrorThrown = true; + return true; + } + else { + __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_6__util_hostReportError__["a" /* hostReportError */])(err); + return true; + } + } + return false; + }; + SafeSubscriber.prototype._unsubscribe = function () { + var _parentSubscriber = this._parentSubscriber; + this._context = null; + this._parentSubscriber = null; + _parentSubscriber.unsubscribe(); + }; + return SafeSubscriber; +}(Subscriber)); + +//# sourceMappingURL=Subscriber.js.map + + +/***/ }), +/* 8 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.getPathKey = getPathKey; +const os = __webpack_require__(46); +const path = __webpack_require__(0); +const userHome = __webpack_require__(67).default; + +var _require = __webpack_require__(225); + +const getCacheDir = _require.getCacheDir, + getConfigDir = _require.getConfigDir, + getDataDir = _require.getDataDir; + +const isWebpackBundle = __webpack_require__(278); + +const DEPENDENCY_TYPES = exports.DEPENDENCY_TYPES = ['devDependencies', 'dependencies', 'optionalDependencies', 'peerDependencies']; +const OWNED_DEPENDENCY_TYPES = exports.OWNED_DEPENDENCY_TYPES = ['devDependencies', 'dependencies', 'optionalDependencies']; + +const RESOLUTIONS = exports.RESOLUTIONS = 'resolutions'; +const MANIFEST_FIELDS = exports.MANIFEST_FIELDS = [RESOLUTIONS, ...DEPENDENCY_TYPES]; + +const SUPPORTED_NODE_VERSIONS = exports.SUPPORTED_NODE_VERSIONS = '^4.8.0 || ^5.7.0 || ^6.2.2 || >=8.0.0'; + +const YARN_REGISTRY = exports.YARN_REGISTRY = 'https://registry.yarnpkg.com'; +const NPM_REGISTRY_RE = exports.NPM_REGISTRY_RE = /https?:\/\/registry\.npmjs\.org/g; + +const YARN_DOCS = exports.YARN_DOCS = 'https://yarnpkg.com/en/docs/cli/'; +const YARN_INSTALLER_SH = exports.YARN_INSTALLER_SH = 'https://yarnpkg.com/install.sh'; +const YARN_INSTALLER_MSI = exports.YARN_INSTALLER_MSI = 'https://yarnpkg.com/latest.msi'; + +const SELF_UPDATE_VERSION_URL = exports.SELF_UPDATE_VERSION_URL = 'https://yarnpkg.com/latest-version'; + +// cache version, bump whenever we make backwards incompatible changes +const CACHE_VERSION = exports.CACHE_VERSION = 6; + +// lockfile version, bump whenever we make backwards incompatible changes +const LOCKFILE_VERSION = exports.LOCKFILE_VERSION = 1; + +// max amount of network requests to perform concurrently +const NETWORK_CONCURRENCY = exports.NETWORK_CONCURRENCY = 8; + +// HTTP timeout used when downloading packages +const NETWORK_TIMEOUT = exports.NETWORK_TIMEOUT = 30 * 1000; // in milliseconds + +// max amount of child processes to execute concurrently +const CHILD_CONCURRENCY = exports.CHILD_CONCURRENCY = 5; + +const REQUIRED_PACKAGE_KEYS = exports.REQUIRED_PACKAGE_KEYS = ['name', 'version', '_uid']; + +function getPreferredCacheDirectories() { + const preferredCacheDirectories = [getCacheDir()]; + + if (process.getuid) { + // $FlowFixMe: process.getuid exists, dammit + preferredCacheDirectories.push(path.join(os.tmpdir(), `.yarn-cache-${process.getuid()}`)); + } + + preferredCacheDirectories.push(path.join(os.tmpdir(), `.yarn-cache`)); + + return preferredCacheDirectories; +} + +const PREFERRED_MODULE_CACHE_DIRECTORIES = exports.PREFERRED_MODULE_CACHE_DIRECTORIES = getPreferredCacheDirectories(); +const CONFIG_DIRECTORY = exports.CONFIG_DIRECTORY = getConfigDir(); +const DATA_DIRECTORY = exports.DATA_DIRECTORY = getDataDir(); +const LINK_REGISTRY_DIRECTORY = exports.LINK_REGISTRY_DIRECTORY = path.join(DATA_DIRECTORY, 'link'); +const GLOBAL_MODULE_DIRECTORY = exports.GLOBAL_MODULE_DIRECTORY = path.join(DATA_DIRECTORY, 'global'); + +const NODE_BIN_PATH = exports.NODE_BIN_PATH = process.execPath; +const YARN_BIN_PATH = exports.YARN_BIN_PATH = getYarnBinPath(); + +// Webpack needs to be configured with node.__dirname/__filename = false +function getYarnBinPath() { + if (isWebpackBundle) { + return __filename; + } else { + return path.join(__dirname, '..', 'bin', 'yarn.js'); + } +} + +const NODE_MODULES_FOLDER = exports.NODE_MODULES_FOLDER = 'node_modules'; +const NODE_PACKAGE_JSON = exports.NODE_PACKAGE_JSON = 'package.json'; + +const PNP_FILENAME = exports.PNP_FILENAME = '.pnp.js'; + +const POSIX_GLOBAL_PREFIX = exports.POSIX_GLOBAL_PREFIX = `${process.env.DESTDIR || ''}/usr/local`; +const FALLBACK_GLOBAL_PREFIX = exports.FALLBACK_GLOBAL_PREFIX = path.join(userHome, '.yarn'); + +const META_FOLDER = exports.META_FOLDER = '.yarn-meta'; +const INTEGRITY_FILENAME = exports.INTEGRITY_FILENAME = '.yarn-integrity'; +const LOCKFILE_FILENAME = exports.LOCKFILE_FILENAME = 'yarn.lock'; +const METADATA_FILENAME = exports.METADATA_FILENAME = '.yarn-metadata.json'; +const TARBALL_FILENAME = exports.TARBALL_FILENAME = '.yarn-tarball.tgz'; +const CLEAN_FILENAME = exports.CLEAN_FILENAME = '.yarnclean'; + +const NPM_LOCK_FILENAME = exports.NPM_LOCK_FILENAME = 'package-lock.json'; +const NPM_SHRINKWRAP_FILENAME = exports.NPM_SHRINKWRAP_FILENAME = 'npm-shrinkwrap.json'; + +const DEFAULT_INDENT = exports.DEFAULT_INDENT = ' '; +const SINGLE_INSTANCE_PORT = exports.SINGLE_INSTANCE_PORT = 31997; +const SINGLE_INSTANCE_FILENAME = exports.SINGLE_INSTANCE_FILENAME = '.yarn-single-instance'; + +const ENV_PATH_KEY = exports.ENV_PATH_KEY = getPathKey(process.platform, process.env); + +function getPathKey(platform, env) { + let pathKey = 'PATH'; + + // windows calls its path "Path" usually, but this is not guaranteed. + if (platform === 'win32') { + pathKey = 'Path'; + + for (const key in env) { + if (key.toLowerCase() === 'path') { + pathKey = key; + } + } + } + + return pathKey; +} + +const VERSION_COLOR_SCHEME = exports.VERSION_COLOR_SCHEME = { + major: 'red', + premajor: 'red', + minor: 'yellow', + preminor: 'yellow', + patch: 'green', + prepatch: 'green', + prerelease: 'red', + unchanged: 'white', + unknown: 'red' +}; + +/***/ }), +/* 9 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + + +/** + * Use invariant() to assert state which your program assumes to be true. + * + * Provide sprintf-style format (only %s is supported) and arguments + * to provide information about what broke and what you were + * expecting. + * + * The invariant message will be stripped in production, but the invariant + * will remain to ensure logic does not differ in production. + */ + +var NODE_ENV = process.env.NODE_ENV; + +var invariant = function(condition, format, a, b, c, d, e, f) { + if (NODE_ENV !== 'production') { + if (format === undefined) { + throw new Error('invariant requires an error message argument'); + } + } + + if (!condition) { + var error; + if (format === undefined) { + error = new Error( + 'Minified exception occurred; use the non-minified dev environment ' + + 'for the full error message and additional helpful warnings.' + ); + } else { + var args = [a, b, c, d, e, f]; + var argIndex = 0; + error = new Error( + format.replace(/%s/g, function() { return args[argIndex++]; }) + ); + error.name = 'Invariant Violation'; + } + + error.framesToPop = 1; // we don't care about invariant's own frame + throw error; + } +}; + +module.exports = invariant; + + +/***/ }), +/* 10 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var YAMLException = __webpack_require__(54); + +var TYPE_CONSTRUCTOR_OPTIONS = [ + 'kind', + 'resolve', + 'construct', + 'instanceOf', + 'predicate', + 'represent', + 'defaultStyle', + 'styleAliases' +]; + +var YAML_NODE_KINDS = [ + 'scalar', + 'sequence', + 'mapping' +]; + +function compileStyleAliases(map) { + var result = {}; + + if (map !== null) { + Object.keys(map).forEach(function (style) { + map[style].forEach(function (alias) { + result[String(alias)] = style; + }); + }); + } + + return result; +} + +function Type(tag, options) { + options = options || {}; + + Object.keys(options).forEach(function (name) { + if (TYPE_CONSTRUCTOR_OPTIONS.indexOf(name) === -1) { + throw new YAMLException('Unknown option "' + name + '" is met in definition of "' + tag + '" YAML type.'); + } + }); + + // TODO: Add tag format check. + this.tag = tag; + this.kind = options['kind'] || null; + this.resolve = options['resolve'] || function () { return true; }; + this.construct = options['construct'] || function (data) { return data; }; + this.instanceOf = options['instanceOf'] || null; + this.predicate = options['predicate'] || null; + this.represent = options['represent'] || null; + this.defaultStyle = options['defaultStyle'] || null; + this.styleAliases = compileStyleAliases(options['styleAliases'] || null); + + if (YAML_NODE_KINDS.indexOf(this.kind) === -1) { + throw new YAMLException('Unknown kind "' + this.kind + '" is specified for "' + tag + '" YAML type.'); + } +} + +module.exports = Type; + + +/***/ }), +/* 11 */ +/***/ (function(module, exports) { + +module.exports = require("crypto"); + +/***/ }), +/* 12 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Observable; }); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__util_canReportError__ = __webpack_require__(322); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__util_toSubscriber__ = __webpack_require__(932); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__internal_symbol_observable__ = __webpack_require__(117); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__util_pipe__ = __webpack_require__(324); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__config__ = __webpack_require__(185); +/** PURE_IMPORTS_START _util_canReportError,_util_toSubscriber,_internal_symbol_observable,_util_pipe,_config PURE_IMPORTS_END */ + + + + + +var Observable = /*@__PURE__*/ (function () { + function Observable(subscribe) { + this._isScalar = false; + if (subscribe) { + this._subscribe = subscribe; + } + } + Observable.prototype.lift = function (operator) { + var observable = new Observable(); + observable.source = this; + observable.operator = operator; + return observable; + }; + Observable.prototype.subscribe = function (observerOrNext, error, complete) { + var operator = this.operator; + var sink = __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1__util_toSubscriber__["a" /* toSubscriber */])(observerOrNext, error, complete); + if (operator) { + operator.call(sink, this.source); + } + else { + sink.add(this.source || (__WEBPACK_IMPORTED_MODULE_4__config__["a" /* config */].useDeprecatedSynchronousErrorHandling && !sink.syncErrorThrowable) ? + this._subscribe(sink) : + this._trySubscribe(sink)); + } + if (__WEBPACK_IMPORTED_MODULE_4__config__["a" /* config */].useDeprecatedSynchronousErrorHandling) { + if (sink.syncErrorThrowable) { + sink.syncErrorThrowable = false; + if (sink.syncErrorThrown) { + throw sink.syncErrorValue; + } + } + } + return sink; + }; + Observable.prototype._trySubscribe = function (sink) { + try { + return this._subscribe(sink); + } + catch (err) { + if (__WEBPACK_IMPORTED_MODULE_4__config__["a" /* config */].useDeprecatedSynchronousErrorHandling) { + sink.syncErrorThrown = true; + sink.syncErrorValue = err; + } + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__util_canReportError__["a" /* canReportError */])(sink)) { + sink.error(err); + } + else { + console.warn(err); + } + } + }; + Observable.prototype.forEach = function (next, promiseCtor) { + var _this = this; + promiseCtor = getPromiseCtor(promiseCtor); + return new promiseCtor(function (resolve, reject) { + var subscription; + subscription = _this.subscribe(function (value) { + try { + next(value); + } + catch (err) { + reject(err); + if (subscription) { + subscription.unsubscribe(); + } + } + }, reject, resolve); + }); + }; + Observable.prototype._subscribe = function (subscriber) { + var source = this.source; + return source && source.subscribe(subscriber); + }; + Observable.prototype[__WEBPACK_IMPORTED_MODULE_2__internal_symbol_observable__["a" /* observable */]] = function () { + return this; + }; + Observable.prototype.pipe = function () { + var operations = []; + for (var _i = 0; _i < arguments.length; _i++) { + operations[_i] = arguments[_i]; + } + if (operations.length === 0) { + return this; + } + return __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_3__util_pipe__["b" /* pipeFromArray */])(operations)(this); + }; + Observable.prototype.toPromise = function (promiseCtor) { + var _this = this; + promiseCtor = getPromiseCtor(promiseCtor); + return new promiseCtor(function (resolve, reject) { + var value; + _this.subscribe(function (x) { return value = x; }, function (err) { return reject(err); }, function () { return resolve(value); }); + }); + }; + Observable.create = function (subscribe) { + return new Observable(subscribe); + }; + return Observable; +}()); + +function getPromiseCtor(promiseCtor) { + if (!promiseCtor) { + promiseCtor = __WEBPACK_IMPORTED_MODULE_4__config__["a" /* config */].Promise || Promise; + } + if (!promiseCtor) { + throw new Error('no Promise impl found'); + } + return promiseCtor; +} +//# sourceMappingURL=Observable.js.map + + +/***/ }), +/* 13 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return OuterSubscriber; }); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_tslib__ = __webpack_require__(1); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__Subscriber__ = __webpack_require__(7); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ + + +var OuterSubscriber = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](OuterSubscriber, _super); + function OuterSubscriber() { + return _super !== null && _super.apply(this, arguments) || this; + } + OuterSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.destination.next(innerValue); + }; + OuterSubscriber.prototype.notifyError = function (error, innerSub) { + this.destination.error(error); + }; + OuterSubscriber.prototype.notifyComplete = function (innerSub) { + this.destination.complete(); + }; + return OuterSubscriber; +}(__WEBPACK_IMPORTED_MODULE_1__Subscriber__["a" /* Subscriber */])); + +//# sourceMappingURL=OuterSubscriber.js.map + + +/***/ }), +/* 14 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (immutable) */ __webpack_exports__["a"] = subscribeToResult; +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__InnerSubscriber__ = __webpack_require__(84); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__subscribeTo__ = __webpack_require__(446); +/** PURE_IMPORTS_START _InnerSubscriber,_subscribeTo PURE_IMPORTS_END */ + + +function subscribeToResult(outerSubscriber, result, outerValue, outerIndex, destination) { + if (destination === void 0) { + destination = new __WEBPACK_IMPORTED_MODULE_0__InnerSubscriber__["a" /* InnerSubscriber */](outerSubscriber, outerValue, outerIndex); + } + if (destination.closed) { + return; + } + return __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1__subscribeTo__["a" /* subscribeTo */])(result)(destination); +} +//# sourceMappingURL=subscribeToResult.js.map + + +/***/ }), +/* 15 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/* eslint-disable node/no-deprecated-api */ + + + +var buffer = __webpack_require__(64) +var Buffer = buffer.Buffer + +var safer = {} + +var key + +for (key in buffer) { + if (!buffer.hasOwnProperty(key)) continue + if (key === 'SlowBuffer' || key === 'Buffer') continue + safer[key] = buffer[key] +} + +var Safer = safer.Buffer = {} +for (key in Buffer) { + if (!Buffer.hasOwnProperty(key)) continue + if (key === 'allocUnsafe' || key === 'allocUnsafeSlow') continue + Safer[key] = Buffer[key] +} + +safer.Buffer.prototype = Buffer.prototype + +if (!Safer.from || Safer.from === Uint8Array.from) { + Safer.from = function (value, encodingOrOffset, length) { + if (typeof value === 'number') { + throw new TypeError('The "value" argument must not be of type number. Received type ' + typeof value) + } + if (value && typeof value.length === 'undefined') { + throw new TypeError('The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type ' + typeof value) + } + return Buffer(value, encodingOrOffset, length) + } +} + +if (!Safer.alloc) { + Safer.alloc = function (size, fill, encoding) { + if (typeof size !== 'number') { + throw new TypeError('The "size" argument must be of type number. Received type ' + typeof size) + } + if (size < 0 || size >= 2 * (1 << 30)) { + throw new RangeError('The value "' + size + '" is invalid for option "size"') + } + var buf = Buffer(size) + if (!fill || fill.length === 0) { + buf.fill(0) + } else if (typeof encoding === 'string') { + buf.fill(fill, encoding) + } else { + buf.fill(fill) + } + return buf + } +} + +if (!safer.kStringMaxLength) { + try { + safer.kStringMaxLength = process.binding('buffer').kStringMaxLength + } catch (e) { + // we can't determine kStringMaxLength in environments where process.binding + // is unsupported, so let's not set it + } +} + +if (!safer.constants) { + safer.constants = { + MAX_LENGTH: safer.kMaxLength + } + if (safer.kStringMaxLength) { + safer.constants.MAX_STRING_LENGTH = safer.kStringMaxLength + } +} + +module.exports = safer + + +/***/ }), +/* 16 */ +/***/ (function(module, exports, __webpack_require__) { + +// Copyright (c) 2012, Mark Cavage. All rights reserved. +// Copyright 2015 Joyent, Inc. + +var assert = __webpack_require__(28); +var Stream = __webpack_require__(23).Stream; +var util = __webpack_require__(3); + + +///--- Globals + +/* JSSTYLED */ +var UUID_REGEXP = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/; + + +///--- Internal + +function _capitalize(str) { + return (str.charAt(0).toUpperCase() + str.slice(1)); +} + +function _toss(name, expected, oper, arg, actual) { + throw new assert.AssertionError({ + message: util.format('%s (%s) is required', name, expected), + actual: (actual === undefined) ? typeof (arg) : actual(arg), + expected: expected, + operator: oper || '===', + stackStartFunction: _toss.caller + }); +} + +function _getClass(arg) { + return (Object.prototype.toString.call(arg).slice(8, -1)); +} + +function noop() { + // Why even bother with asserts? +} + + +///--- Exports + +var types = { + bool: { + check: function (arg) { return typeof (arg) === 'boolean'; } + }, + func: { + check: function (arg) { return typeof (arg) === 'function'; } + }, + string: { + check: function (arg) { return typeof (arg) === 'string'; } + }, + object: { + check: function (arg) { + return typeof (arg) === 'object' && arg !== null; + } + }, + number: { + check: function (arg) { + return typeof (arg) === 'number' && !isNaN(arg); + } + }, + finite: { + check: function (arg) { + return typeof (arg) === 'number' && !isNaN(arg) && isFinite(arg); + } + }, + buffer: { + check: function (arg) { return Buffer.isBuffer(arg); }, + operator: 'Buffer.isBuffer' + }, + array: { + check: function (arg) { return Array.isArray(arg); }, + operator: 'Array.isArray' + }, + stream: { + check: function (arg) { return arg instanceof Stream; }, + operator: 'instanceof', + actual: _getClass + }, + date: { + check: function (arg) { return arg instanceof Date; }, + operator: 'instanceof', + actual: _getClass + }, + regexp: { + check: function (arg) { return arg instanceof RegExp; }, + operator: 'instanceof', + actual: _getClass + }, + uuid: { + check: function (arg) { + return typeof (arg) === 'string' && UUID_REGEXP.test(arg); + }, + operator: 'isUUID' + } +}; + +function _setExports(ndebug) { + var keys = Object.keys(types); + var out; + + /* re-export standard assert */ + if (process.env.NODE_NDEBUG) { + out = noop; + } else { + out = function (arg, msg) { + if (!arg) { + _toss(msg, 'true', arg); + } + }; + } + + /* standard checks */ + keys.forEach(function (k) { + if (ndebug) { + out[k] = noop; + return; + } + var type = types[k]; + out[k] = function (arg, msg) { + if (!type.check(arg)) { + _toss(msg, k, type.operator, arg, type.actual); + } + }; + }); + + /* optional checks */ + keys.forEach(function (k) { + var name = 'optional' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + out[name] = function (arg, msg) { + if (arg === undefined || arg === null) { + return; + } + if (!type.check(arg)) { + _toss(msg, k, type.operator, arg, type.actual); + } + }; + }); + + /* arrayOf checks */ + keys.forEach(function (k) { + var name = 'arrayOf' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + var expected = '[' + k + ']'; + out[name] = function (arg, msg) { + if (!Array.isArray(arg)) { + _toss(msg, expected, type.operator, arg, type.actual); + } + var i; + for (i = 0; i < arg.length; i++) { + if (!type.check(arg[i])) { + _toss(msg, expected, type.operator, arg, type.actual); + } + } + }; + }); + + /* optionalArrayOf checks */ + keys.forEach(function (k) { + var name = 'optionalArrayOf' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + var expected = '[' + k + ']'; + out[name] = function (arg, msg) { + if (arg === undefined || arg === null) { + return; + } + if (!Array.isArray(arg)) { + _toss(msg, expected, type.operator, arg, type.actual); + } + var i; + for (i = 0; i < arg.length; i++) { + if (!type.check(arg[i])) { + _toss(msg, expected, type.operator, arg, type.actual); + } + } + }; + }); + + /* re-export built-in assertions */ + Object.keys(assert).forEach(function (k) { + if (k === 'AssertionError') { + out[k] = assert[k]; + return; + } + if (ndebug) { + out[k] = noop; + return; + } + out[k] = assert[k]; + }); + + /* export ourselves (for unit tests _only_) */ + out._setExports = _setExports; + + return out; +} + +module.exports = _setExports(process.env.NODE_NDEBUG); + + +/***/ }), +/* 17 */ +/***/ (function(module, exports) { + +// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 +var global = module.exports = typeof window != 'undefined' && window.Math == Math + ? window : typeof self != 'undefined' && self.Math == Math ? self + // eslint-disable-next-line no-new-func + : Function('return this')(); +if (typeof __g == 'number') __g = global; // eslint-disable-line no-undef + + +/***/ }), +/* 18 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.sortAlpha = sortAlpha; +exports.sortOptionsByFlags = sortOptionsByFlags; +exports.entries = entries; +exports.removePrefix = removePrefix; +exports.removeSuffix = removeSuffix; +exports.addSuffix = addSuffix; +exports.hyphenate = hyphenate; +exports.camelCase = camelCase; +exports.compareSortedArrays = compareSortedArrays; +exports.sleep = sleep; +const _camelCase = __webpack_require__(230); + +function sortAlpha(a, b) { + // sort alphabetically in a deterministic way + const shortLen = Math.min(a.length, b.length); + for (let i = 0; i < shortLen; i++) { + const aChar = a.charCodeAt(i); + const bChar = b.charCodeAt(i); + if (aChar !== bChar) { + return aChar - bChar; + } + } + return a.length - b.length; +} + +function sortOptionsByFlags(a, b) { + const aOpt = a.flags.replace(/-/g, ''); + const bOpt = b.flags.replace(/-/g, ''); + return sortAlpha(aOpt, bOpt); +} + +function entries(obj) { + const entries = []; + if (obj) { + for (const key in obj) { + entries.push([key, obj[key]]); + } + } + return entries; +} + +function removePrefix(pattern, prefix) { + if (pattern.startsWith(prefix)) { + pattern = pattern.slice(prefix.length); + } + + return pattern; +} + +function removeSuffix(pattern, suffix) { + if (pattern.endsWith(suffix)) { + return pattern.slice(0, -suffix.length); + } + + return pattern; +} + +function addSuffix(pattern, suffix) { + if (!pattern.endsWith(suffix)) { + return pattern + suffix; + } + + return pattern; +} + +function hyphenate(str) { + return str.replace(/[A-Z]/g, match => { + return '-' + match.charAt(0).toLowerCase(); + }); +} + +function camelCase(str) { + if (/[A-Z]/.test(str)) { + return null; + } else { + return _camelCase(str); + } +} + +function compareSortedArrays(array1, array2) { + if (array1.length !== array2.length) { + return false; + } + for (let i = 0, len = array1.length; i < len; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + return true; +} + +function sleep(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + +/***/ }), +/* 19 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.stringify = exports.parse = undefined; + +var _asyncToGenerator2; + +function _load_asyncToGenerator() { + return _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(2)); +} + +var _parse; + +function _load_parse() { + return _parse = __webpack_require__(105); +} + +Object.defineProperty(exports, 'parse', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_parse || _load_parse()).default; + } +}); + +var _stringify; + +function _load_stringify() { + return _stringify = __webpack_require__(199); +} + +Object.defineProperty(exports, 'stringify', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_stringify || _load_stringify()).default; + } +}); +exports.implodeEntry = implodeEntry; +exports.explodeEntry = explodeEntry; + +var _misc; + +function _load_misc() { + return _misc = __webpack_require__(18); +} + +var _normalizePattern; + +function _load_normalizePattern() { + return _normalizePattern = __webpack_require__(37); +} + +var _parse2; + +function _load_parse2() { + return _parse2 = _interopRequireDefault(__webpack_require__(105)); +} + +var _constants; + +function _load_constants() { + return _constants = __webpack_require__(8); +} + +var _fs; + +function _load_fs() { + return _fs = _interopRequireWildcard(__webpack_require__(4)); +} + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const invariant = __webpack_require__(9); + +const path = __webpack_require__(0); +const ssri = __webpack_require__(65); + +function getName(pattern) { + return (0, (_normalizePattern || _load_normalizePattern()).normalizePattern)(pattern).name; +} + +function blankObjectUndefined(obj) { + return obj && Object.keys(obj).length ? obj : undefined; +} + +function keyForRemote(remote) { + return remote.resolved || (remote.reference && remote.hash ? `${remote.reference}#${remote.hash}` : null); +} + +function serializeIntegrity(integrity) { + // We need this because `Integrity.toString()` does not use sorting to ensure a stable string output + // See https://git.io/vx2Hy + return integrity.toString().split(' ').sort().join(' '); +} + +function implodeEntry(pattern, obj) { + const inferredName = getName(pattern); + const integrity = obj.integrity ? serializeIntegrity(obj.integrity) : ''; + const imploded = { + name: inferredName === obj.name ? undefined : obj.name, + version: obj.version, + uid: obj.uid === obj.version ? undefined : obj.uid, + resolved: obj.resolved, + registry: obj.registry === 'npm' ? undefined : obj.registry, + dependencies: blankObjectUndefined(obj.dependencies), + optionalDependencies: blankObjectUndefined(obj.optionalDependencies), + permissions: blankObjectUndefined(obj.permissions), + prebuiltVariants: blankObjectUndefined(obj.prebuiltVariants) + }; + if (integrity) { + imploded.integrity = integrity; + } + return imploded; +} + +function explodeEntry(pattern, obj) { + obj.optionalDependencies = obj.optionalDependencies || {}; + obj.dependencies = obj.dependencies || {}; + obj.uid = obj.uid || obj.version; + obj.permissions = obj.permissions || {}; + obj.registry = obj.registry || 'npm'; + obj.name = obj.name || getName(pattern); + const integrity = obj.integrity; + if (integrity && integrity.isIntegrity) { + obj.integrity = ssri.parse(integrity); + } + return obj; +} + +class Lockfile { + constructor({ cache, source, parseResultType } = {}) { + this.source = source || ''; + this.cache = cache; + this.parseResultType = parseResultType; + } + + // source string if the `cache` was parsed + + + // if true, we're parsing an old yarn file and need to update integrity fields + hasEntriesExistWithoutIntegrity() { + if (!this.cache) { + return false; + } + + for (const key in this.cache) { + // $FlowFixMe - `this.cache` is clearly defined at this point + if (!/^.*@(file:|http)/.test(key) && this.cache[key] && !this.cache[key].integrity) { + return true; + } + } + + return false; + } + + static fromDirectory(dir, reporter) { + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + // read the manifest in this directory + const lockfileLoc = path.join(dir, (_constants || _load_constants()).LOCKFILE_FILENAME); + + let lockfile; + let rawLockfile = ''; + let parseResult; + + if (yield (_fs || _load_fs()).exists(lockfileLoc)) { + rawLockfile = yield (_fs || _load_fs()).readFile(lockfileLoc); + parseResult = (0, (_parse2 || _load_parse2()).default)(rawLockfile, lockfileLoc); + + if (reporter) { + if (parseResult.type === 'merge') { + reporter.info(reporter.lang('lockfileMerged')); + } else if (parseResult.type === 'conflict') { + reporter.warn(reporter.lang('lockfileConflict')); + } + } + + lockfile = parseResult.object; + } else if (reporter) { + reporter.info(reporter.lang('noLockfileFound')); + } + + if (lockfile && lockfile.__metadata) { + const lockfilev2 = lockfile; + lockfile = {}; + } + + return new Lockfile({ cache: lockfile, source: rawLockfile, parseResultType: parseResult && parseResult.type }); + })(); + } + + getLocked(pattern) { + const cache = this.cache; + if (!cache) { + return undefined; + } + + const shrunk = pattern in cache && cache[pattern]; + + if (typeof shrunk === 'string') { + return this.getLocked(shrunk); + } else if (shrunk) { + explodeEntry(pattern, shrunk); + return shrunk; + } + + return undefined; + } + + removePattern(pattern) { + const cache = this.cache; + if (!cache) { + return; + } + delete cache[pattern]; + } + + getLockfile(patterns) { + const lockfile = {}; + const seen = new Map(); + + // order by name so that lockfile manifest is assigned to the first dependency with this manifest + // the others that have the same remoteKey will just refer to the first + // ordering allows for consistency in lockfile when it is serialized + const sortedPatternsKeys = Object.keys(patterns).sort((_misc || _load_misc()).sortAlpha); + + for (var _iterator = sortedPatternsKeys, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { + var _ref; + + if (_isArray) { + if (_i >= _iterator.length) break; + _ref = _iterator[_i++]; + } else { + _i = _iterator.next(); + if (_i.done) break; + _ref = _i.value; + } + + const pattern = _ref; + + const pkg = patterns[pattern]; + const remote = pkg._remote, + ref = pkg._reference; + + invariant(ref, 'Package is missing a reference'); + invariant(remote, 'Package is missing a remote'); + + const remoteKey = keyForRemote(remote); + const seenPattern = remoteKey && seen.get(remoteKey); + if (seenPattern) { + // no point in duplicating it + lockfile[pattern] = seenPattern; + + // if we're relying on our name being inferred and two of the patterns have + // different inferred names then we need to set it + if (!seenPattern.name && getName(pattern) !== pkg.name) { + seenPattern.name = pkg.name; + } + continue; + } + const obj = implodeEntry(pattern, { + name: pkg.name, + version: pkg.version, + uid: pkg._uid, + resolved: remote.resolved, + integrity: remote.integrity, + registry: remote.registry, + dependencies: pkg.dependencies, + peerDependencies: pkg.peerDependencies, + optionalDependencies: pkg.optionalDependencies, + permissions: ref.permissions, + prebuiltVariants: pkg.prebuiltVariants + }); + + lockfile[pattern] = obj; + + if (remoteKey) { + seen.set(remoteKey, obj); + } + } + + return lockfile; + } +} +exports.default = Lockfile; + +/***/ }), +/* 20 */ +/***/ (function(module, exports, __webpack_require__) { + +var store = __webpack_require__(133)('wks'); +var uid = __webpack_require__(137); +var Symbol = __webpack_require__(17).Symbol; +var USE_SYMBOL = typeof Symbol == 'function'; + +var $exports = module.exports = function (name) { + return store[name] || (store[name] = + USE_SYMBOL && Symbol[name] || (USE_SYMBOL ? Symbol : uid)('Symbol.' + name)); +}; + +$exports.store = store; + + +/***/ }), +/* 21 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +exports.__esModule = true; + +var _assign = __webpack_require__(591); + +var _assign2 = _interopRequireDefault(_assign); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +exports.default = _assign2.default || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; +}; + +/***/ }), +/* 22 */ +/***/ (function(module, exports) { + +exports = module.exports = SemVer; + +// The debug function is excluded entirely from the minified version. +/* nomin */ var debug; +/* nomin */ if (typeof process === 'object' && + /* nomin */ process.env && + /* nomin */ process.env.NODE_DEBUG && + /* nomin */ /\bsemver\b/i.test(process.env.NODE_DEBUG)) + /* nomin */ debug = function() { + /* nomin */ var args = Array.prototype.slice.call(arguments, 0); + /* nomin */ args.unshift('SEMVER'); + /* nomin */ console.log.apply(console, args); + /* nomin */ }; +/* nomin */ else + /* nomin */ debug = function() {}; + +// Note: this is the semver.org version of the spec that it implements +// Not necessarily the package version of this code. +exports.SEMVER_SPEC_VERSION = '2.0.0'; + +var MAX_LENGTH = 256; +var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; + +// Max safe segment length for coercion. +var MAX_SAFE_COMPONENT_LENGTH = 16; + +// The actual regexps go on exports.re +var re = exports.re = []; +var src = exports.src = []; +var R = 0; + +// The following Regular Expressions can be used for tokenizing, +// validating, and parsing SemVer version strings. + +// ## Numeric Identifier +// A single `0`, or a non-zero digit followed by zero or more digits. + +var NUMERICIDENTIFIER = R++; +src[NUMERICIDENTIFIER] = '0|[1-9]\\d*'; +var NUMERICIDENTIFIERLOOSE = R++; +src[NUMERICIDENTIFIERLOOSE] = '[0-9]+'; + + +// ## Non-numeric Identifier +// Zero or more digits, followed by a letter or hyphen, and then zero or +// more letters, digits, or hyphens. + +var NONNUMERICIDENTIFIER = R++; +src[NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*'; + + +// ## Main Version +// Three dot-separated numeric identifiers. + +var MAINVERSION = R++; +src[MAINVERSION] = '(' + src[NUMERICIDENTIFIER] + ')\\.' + + '(' + src[NUMERICIDENTIFIER] + ')\\.' + + '(' + src[NUMERICIDENTIFIER] + ')'; + +var MAINVERSIONLOOSE = R++; +src[MAINVERSIONLOOSE] = '(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[NUMERICIDENTIFIERLOOSE] + ')'; + +// ## Pre-release Version Identifier +// A numeric identifier, or a non-numeric identifier. + +var PRERELEASEIDENTIFIER = R++; +src[PRERELEASEIDENTIFIER] = '(?:' + src[NUMERICIDENTIFIER] + + '|' + src[NONNUMERICIDENTIFIER] + ')'; + +var PRERELEASEIDENTIFIERLOOSE = R++; +src[PRERELEASEIDENTIFIERLOOSE] = '(?:' + src[NUMERICIDENTIFIERLOOSE] + + '|' + src[NONNUMERICIDENTIFIER] + ')'; + + +// ## Pre-release Version +// Hyphen, followed by one or more dot-separated pre-release version +// identifiers. + +var PRERELEASE = R++; +src[PRERELEASE] = '(?:-(' + src[PRERELEASEIDENTIFIER] + + '(?:\\.' + src[PRERELEASEIDENTIFIER] + ')*))'; + +var PRERELEASELOOSE = R++; +src[PRERELEASELOOSE] = '(?:-?(' + src[PRERELEASEIDENTIFIERLOOSE] + + '(?:\\.' + src[PRERELEASEIDENTIFIERLOOSE] + ')*))'; + +// ## Build Metadata Identifier +// Any combination of digits, letters, or hyphens. + +var BUILDIDENTIFIER = R++; +src[BUILDIDENTIFIER] = '[0-9A-Za-z-]+'; + +// ## Build Metadata +// Plus sign, followed by one or more period-separated build metadata +// identifiers. + +var BUILD = R++; +src[BUILD] = '(?:\\+(' + src[BUILDIDENTIFIER] + + '(?:\\.' + src[BUILDIDENTIFIER] + ')*))'; + + +// ## Full Version String +// A main version, followed optionally by a pre-release version and +// build metadata. + +// Note that the only major, minor, patch, and pre-release sections of +// the version string are capturing groups. The build metadata is not a +// capturing group, because it should not ever be used in version +// comparison. + +var FULL = R++; +var FULLPLAIN = 'v?' + src[MAINVERSION] + + src[PRERELEASE] + '?' + + src[BUILD] + '?'; + +src[FULL] = '^' + FULLPLAIN + '$'; + +// like full, but allows v1.2.3 and =1.2.3, which people do sometimes. +// also, 1.0.0alpha1 (prerelease without the hyphen) which is pretty +// common in the npm registry. +var LOOSEPLAIN = '[v=\\s]*' + src[MAINVERSIONLOOSE] + + src[PRERELEASELOOSE] + '?' + + src[BUILD] + '?'; + +var LOOSE = R++; +src[LOOSE] = '^' + LOOSEPLAIN + '$'; + +var GTLT = R++; +src[GTLT] = '((?:<|>)?=?)'; + +// Something like "2.*" or "1.2.x". +// Note that "x.x" is a valid xRange identifer, meaning "any version" +// Only the first item is strictly required. +var XRANGEIDENTIFIERLOOSE = R++; +src[XRANGEIDENTIFIERLOOSE] = src[NUMERICIDENTIFIERLOOSE] + '|x|X|\\*'; +var XRANGEIDENTIFIER = R++; +src[XRANGEIDENTIFIER] = src[NUMERICIDENTIFIER] + '|x|X|\\*'; + +var XRANGEPLAIN = R++; +src[XRANGEPLAIN] = '[v=\\s]*(' + src[XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIER] + ')' + + '(?:' + src[PRERELEASE] + ')?' + + src[BUILD] + '?' + + ')?)?'; + +var XRANGEPLAINLOOSE = R++; +src[XRANGEPLAINLOOSE] = '[v=\\s]*(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:' + src[PRERELEASELOOSE] + ')?' + + src[BUILD] + '?' + + ')?)?'; + +var XRANGE = R++; +src[XRANGE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAIN] + '$'; +var XRANGELOOSE = R++; +src[XRANGELOOSE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAINLOOSE] + '$'; + +// Coercion. +// Extract anything that could conceivably be a part of a valid semver +var COERCE = R++; +src[COERCE] = '(?:^|[^\\d])' + + '(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '})' + + '(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' + + '(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' + + '(?:$|[^\\d])'; + +// Tilde ranges. +// Meaning is "reasonably at or greater than" +var LONETILDE = R++; +src[LONETILDE] = '(?:~>?)'; + +var TILDETRIM = R++; +src[TILDETRIM] = '(\\s*)' + src[LONETILDE] + '\\s+'; +re[TILDETRIM] = new RegExp(src[TILDETRIM], 'g'); +var tildeTrimReplace = '$1~'; + +var TILDE = R++; +src[TILDE] = '^' + src[LONETILDE] + src[XRANGEPLAIN] + '$'; +var TILDELOOSE = R++; +src[TILDELOOSE] = '^' + src[LONETILDE] + src[XRANGEPLAINLOOSE] + '$'; + +// Caret ranges. +// Meaning is "at least and backwards compatible with" +var LONECARET = R++; +src[LONECARET] = '(?:\\^)'; + +var CARETTRIM = R++; +src[CARETTRIM] = '(\\s*)' + src[LONECARET] + '\\s+'; +re[CARETTRIM] = new RegExp(src[CARETTRIM], 'g'); +var caretTrimReplace = '$1^'; + +var CARET = R++; +src[CARET] = '^' + src[LONECARET] + src[XRANGEPLAIN] + '$'; +var CARETLOOSE = R++; +src[CARETLOOSE] = '^' + src[LONECARET] + src[XRANGEPLAINLOOSE] + '$'; + +// A simple gt/lt/eq thing, or just "" to indicate "any version" +var COMPARATORLOOSE = R++; +src[COMPARATORLOOSE] = '^' + src[GTLT] + '\\s*(' + LOOSEPLAIN + ')$|^$'; +var COMPARATOR = R++; +src[COMPARATOR] = '^' + src[GTLT] + '\\s*(' + FULLPLAIN + ')$|^$'; + + +// An expression to strip any whitespace between the gtlt and the thing +// it modifies, so that `> 1.2.3` ==> `>1.2.3` +var COMPARATORTRIM = R++; +src[COMPARATORTRIM] = '(\\s*)' + src[GTLT] + + '\\s*(' + LOOSEPLAIN + '|' + src[XRANGEPLAIN] + ')'; + +// this one has to use the /g flag +re[COMPARATORTRIM] = new RegExp(src[COMPARATORTRIM], 'g'); +var comparatorTrimReplace = '$1$2$3'; + + +// Something like `1.2.3 - 1.2.4` +// Note that these all use the loose form, because they'll be +// checked against either the strict or loose comparator form +// later. +var HYPHENRANGE = R++; +src[HYPHENRANGE] = '^\\s*(' + src[XRANGEPLAIN] + ')' + + '\\s+-\\s+' + + '(' + src[XRANGEPLAIN] + ')' + + '\\s*$'; + +var HYPHENRANGELOOSE = R++; +src[HYPHENRANGELOOSE] = '^\\s*(' + src[XRANGEPLAINLOOSE] + ')' + + '\\s+-\\s+' + + '(' + src[XRANGEPLAINLOOSE] + ')' + + '\\s*$'; + +// Star ranges basically just allow anything at all. +var STAR = R++; +src[STAR] = '(<|>)?=?\\s*\\*'; + +// Compile to actual regexp objects. +// All are flag-free, unless they were created above with a flag. +for (var i = 0; i < R; i++) { + debug(i, src[i]); + if (!re[i]) + re[i] = new RegExp(src[i]); +} + +exports.parse = parse; +function parse(version, loose) { + if (version instanceof SemVer) + return version; + + if (typeof version !== 'string') + return null; + + if (version.length > MAX_LENGTH) + return null; + + var r = loose ? re[LOOSE] : re[FULL]; + if (!r.test(version)) + return null; + + try { + return new SemVer(version, loose); + } catch (er) { + return null; + } +} + +exports.valid = valid; +function valid(version, loose) { + var v = parse(version, loose); + return v ? v.version : null; +} + + +exports.clean = clean; +function clean(version, loose) { + var s = parse(version.trim().replace(/^[=v]+/, ''), loose); + return s ? s.version : null; +} + +exports.SemVer = SemVer; + +function SemVer(version, loose) { + if (version instanceof SemVer) { + if (version.loose === loose) + return version; + else + version = version.version; + } else if (typeof version !== 'string') { + throw new TypeError('Invalid Version: ' + version); + } + + if (version.length > MAX_LENGTH) + throw new TypeError('version is longer than ' + MAX_LENGTH + ' characters') + + if (!(this instanceof SemVer)) + return new SemVer(version, loose); + + debug('SemVer', version, loose); + this.loose = loose; + var m = version.trim().match(loose ? re[LOOSE] : re[FULL]); + + if (!m) + throw new TypeError('Invalid Version: ' + version); + + this.raw = version; + + // these are actually numbers + this.major = +m[1]; + this.minor = +m[2]; + this.patch = +m[3]; + + if (this.major > MAX_SAFE_INTEGER || this.major < 0) + throw new TypeError('Invalid major version') + + if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) + throw new TypeError('Invalid minor version') + + if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) + throw new TypeError('Invalid patch version') + + // numberify any prerelease numeric ids + if (!m[4]) + this.prerelease = []; + else + this.prerelease = m[4].split('.').map(function(id) { + if (/^[0-9]+$/.test(id)) { + var num = +id; + if (num >= 0 && num < MAX_SAFE_INTEGER) + return num; + } + return id; + }); + + this.build = m[5] ? m[5].split('.') : []; + this.format(); +} + +SemVer.prototype.format = function() { + this.version = this.major + '.' + this.minor + '.' + this.patch; + if (this.prerelease.length) + this.version += '-' + this.prerelease.join('.'); + return this.version; +}; + +SemVer.prototype.toString = function() { + return this.version; +}; + +SemVer.prototype.compare = function(other) { + debug('SemVer.compare', this.version, this.loose, other); + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + return this.compareMain(other) || this.comparePre(other); +}; + +SemVer.prototype.compareMain = function(other) { + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + return compareIdentifiers(this.major, other.major) || + compareIdentifiers(this.minor, other.minor) || + compareIdentifiers(this.patch, other.patch); +}; + +SemVer.prototype.comparePre = function(other) { + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + // NOT having a prerelease is > having one + if (this.prerelease.length && !other.prerelease.length) + return -1; + else if (!this.prerelease.length && other.prerelease.length) + return 1; + else if (!this.prerelease.length && !other.prerelease.length) + return 0; + + var i = 0; + do { + var a = this.prerelease[i]; + var b = other.prerelease[i]; + debug('prerelease compare', i, a, b); + if (a === undefined && b === undefined) + return 0; + else if (b === undefined) + return 1; + else if (a === undefined) + return -1; + else if (a === b) + continue; + else + return compareIdentifiers(a, b); + } while (++i); +}; + +// preminor will bump the version up to the next minor release, and immediately +// down to pre-release. premajor and prepatch work the same way. +SemVer.prototype.inc = function(release, identifier) { + switch (release) { + case 'premajor': + this.prerelease.length = 0; + this.patch = 0; + this.minor = 0; + this.major++; + this.inc('pre', identifier); + break; + case 'preminor': + this.prerelease.length = 0; + this.patch = 0; + this.minor++; + this.inc('pre', identifier); + break; + case 'prepatch': + // If this is already a prerelease, it will bump to the next version + // drop any prereleases that might already exist, since they are not + // relevant at this point. + this.prerelease.length = 0; + this.inc('patch', identifier); + this.inc('pre', identifier); + break; + // If the input is a non-prerelease version, this acts the same as + // prepatch. + case 'prerelease': + if (this.prerelease.length === 0) + this.inc('patch', identifier); + this.inc('pre', identifier); + break; + + case 'major': + // If this is a pre-major version, bump up to the same major version. + // Otherwise increment major. + // 1.0.0-5 bumps to 1.0.0 + // 1.1.0 bumps to 2.0.0 + if (this.minor !== 0 || this.patch !== 0 || this.prerelease.length === 0) + this.major++; + this.minor = 0; + this.patch = 0; + this.prerelease = []; + break; + case 'minor': + // If this is a pre-minor version, bump up to the same minor version. + // Otherwise increment minor. + // 1.2.0-5 bumps to 1.2.0 + // 1.2.1 bumps to 1.3.0 + if (this.patch !== 0 || this.prerelease.length === 0) + this.minor++; + this.patch = 0; + this.prerelease = []; + break; + case 'patch': + // If this is not a pre-release version, it will increment the patch. + // If it is a pre-release it will bump up to the same patch version. + // 1.2.0-5 patches to 1.2.0 + // 1.2.0 patches to 1.2.1 + if (this.prerelease.length === 0) + this.patch++; + this.prerelease = []; + break; + // This probably shouldn't be used publicly. + // 1.0.0 "pre" would become 1.0.0-0 which is the wrong direction. + case 'pre': + if (this.prerelease.length === 0) + this.prerelease = [0]; + else { + var i = this.prerelease.length; + while (--i >= 0) { + if (typeof this.prerelease[i] === 'number') { + this.prerelease[i]++; + i = -2; + } + } + if (i === -1) // didn't increment anything + this.prerelease.push(0); + } + if (identifier) { + // 1.2.0-beta.1 bumps to 1.2.0-beta.2, + // 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0 + if (this.prerelease[0] === identifier) { + if (isNaN(this.prerelease[1])) + this.prerelease = [identifier, 0]; + } else + this.prerelease = [identifier, 0]; + } + break; + + default: + throw new Error('invalid increment argument: ' + release); + } + this.format(); + this.raw = this.version; + return this; +}; + +exports.inc = inc; +function inc(version, release, loose, identifier) { + if (typeof(loose) === 'string') { + identifier = loose; + loose = undefined; + } + + try { + return new SemVer(version, loose).inc(release, identifier).version; + } catch (er) { + return null; + } +} + +exports.diff = diff; +function diff(version1, version2) { + if (eq(version1, version2)) { + return null; + } else { + var v1 = parse(version1); + var v2 = parse(version2); + if (v1.prerelease.length || v2.prerelease.length) { + for (var key in v1) { + if (key === 'major' || key === 'minor' || key === 'patch') { + if (v1[key] !== v2[key]) { + return 'pre'+key; + } + } + } + return 'prerelease'; + } + for (var key in v1) { + if (key === 'major' || key === 'minor' || key === 'patch') { + if (v1[key] !== v2[key]) { + return key; + } + } + } + } +} + +exports.compareIdentifiers = compareIdentifiers; + +var numeric = /^[0-9]+$/; +function compareIdentifiers(a, b) { + var anum = numeric.test(a); + var bnum = numeric.test(b); + + if (anum && bnum) { + a = +a; + b = +b; + } + + return (anum && !bnum) ? -1 : + (bnum && !anum) ? 1 : + a < b ? -1 : + a > b ? 1 : + 0; +} + +exports.rcompareIdentifiers = rcompareIdentifiers; +function rcompareIdentifiers(a, b) { + return compareIdentifiers(b, a); +} + +exports.major = major; +function major(a, loose) { + return new SemVer(a, loose).major; +} + +exports.minor = minor; +function minor(a, loose) { + return new SemVer(a, loose).minor; +} + +exports.patch = patch; +function patch(a, loose) { + return new SemVer(a, loose).patch; +} + +exports.compare = compare; +function compare(a, b, loose) { + return new SemVer(a, loose).compare(new SemVer(b, loose)); +} + +exports.compareLoose = compareLoose; +function compareLoose(a, b) { + return compare(a, b, true); +} + +exports.rcompare = rcompare; +function rcompare(a, b, loose) { + return compare(b, a, loose); +} + +exports.sort = sort; +function sort(list, loose) { + return list.sort(function(a, b) { + return exports.compare(a, b, loose); + }); +} + +exports.rsort = rsort; +function rsort(list, loose) { + return list.sort(function(a, b) { + return exports.rcompare(a, b, loose); + }); +} + +exports.gt = gt; +function gt(a, b, loose) { + return compare(a, b, loose) > 0; +} + +exports.lt = lt; +function lt(a, b, loose) { + return compare(a, b, loose) < 0; +} + +exports.eq = eq; +function eq(a, b, loose) { + return compare(a, b, loose) === 0; +} + +exports.neq = neq; +function neq(a, b, loose) { + return compare(a, b, loose) !== 0; +} + +exports.gte = gte; +function gte(a, b, loose) { + return compare(a, b, loose) >= 0; +} + +exports.lte = lte; +function lte(a, b, loose) { + return compare(a, b, loose) <= 0; +} + +exports.cmp = cmp; +function cmp(a, op, b, loose) { + var ret; + switch (op) { + case '===': + if (typeof a === 'object') a = a.version; + if (typeof b === 'object') b = b.version; + ret = a === b; + break; + case '!==': + if (typeof a === 'object') a = a.version; + if (typeof b === 'object') b = b.version; + ret = a !== b; + break; + case '': case '=': case '==': ret = eq(a, b, loose); break; + case '!=': ret = neq(a, b, loose); break; + case '>': ret = gt(a, b, loose); break; + case '>=': ret = gte(a, b, loose); break; + case '<': ret = lt(a, b, loose); break; + case '<=': ret = lte(a, b, loose); break; + default: throw new TypeError('Invalid operator: ' + op); + } + return ret; +} + +exports.Comparator = Comparator; +function Comparator(comp, loose) { + if (comp instanceof Comparator) { + if (comp.loose === loose) + return comp; + else + comp = comp.value; + } + + if (!(this instanceof Comparator)) + return new Comparator(comp, loose); + + debug('comparator', comp, loose); + this.loose = loose; + this.parse(comp); + + if (this.semver === ANY) + this.value = ''; + else + this.value = this.operator + this.semver.version; + + debug('comp', this); +} + +var ANY = {}; +Comparator.prototype.parse = function(comp) { + var r = this.loose ? re[COMPARATORLOOSE] : re[COMPARATOR]; + var m = comp.match(r); + + if (!m) + throw new TypeError('Invalid comparator: ' + comp); + + this.operator = m[1]; + if (this.operator === '=') + this.operator = ''; + + // if it literally is just '>' or '' then allow anything. + if (!m[2]) + this.semver = ANY; + else + this.semver = new SemVer(m[2], this.loose); +}; + +Comparator.prototype.toString = function() { + return this.value; +}; + +Comparator.prototype.test = function(version) { + debug('Comparator.test', version, this.loose); + + if (this.semver === ANY) + return true; + + if (typeof version === 'string') + version = new SemVer(version, this.loose); + + return cmp(version, this.operator, this.semver, this.loose); +}; + +Comparator.prototype.intersects = function(comp, loose) { + if (!(comp instanceof Comparator)) { + throw new TypeError('a Comparator is required'); + } + + var rangeTmp; + + if (this.operator === '') { + rangeTmp = new Range(comp.value, loose); + return satisfies(this.value, rangeTmp, loose); + } else if (comp.operator === '') { + rangeTmp = new Range(this.value, loose); + return satisfies(comp.semver, rangeTmp, loose); + } + + var sameDirectionIncreasing = + (this.operator === '>=' || this.operator === '>') && + (comp.operator === '>=' || comp.operator === '>'); + var sameDirectionDecreasing = + (this.operator === '<=' || this.operator === '<') && + (comp.operator === '<=' || comp.operator === '<'); + var sameSemVer = this.semver.version === comp.semver.version; + var differentDirectionsInclusive = + (this.operator === '>=' || this.operator === '<=') && + (comp.operator === '>=' || comp.operator === '<='); + var oppositeDirectionsLessThan = + cmp(this.semver, '<', comp.semver, loose) && + ((this.operator === '>=' || this.operator === '>') && + (comp.operator === '<=' || comp.operator === '<')); + var oppositeDirectionsGreaterThan = + cmp(this.semver, '>', comp.semver, loose) && + ((this.operator === '<=' || this.operator === '<') && + (comp.operator === '>=' || comp.operator === '>')); + + return sameDirectionIncreasing || sameDirectionDecreasing || + (sameSemVer && differentDirectionsInclusive) || + oppositeDirectionsLessThan || oppositeDirectionsGreaterThan; +}; + + +exports.Range = Range; +function Range(range, loose) { + if (range instanceof Range) { + if (range.loose === loose) { + return range; + } else { + return new Range(range.raw, loose); + } + } + + if (range instanceof Comparator) { + return new Range(range.value, loose); + } + + if (!(this instanceof Range)) + return new Range(range, loose); + + this.loose = loose; + + // First, split based on boolean or || + this.raw = range; + this.set = range.split(/\s*\|\|\s*/).map(function(range) { + return this.parseRange(range.trim()); + }, this).filter(function(c) { + // throw out any that are not relevant for whatever reason + return c.length; + }); + + if (!this.set.length) { + throw new TypeError('Invalid SemVer Range: ' + range); + } + + this.format(); +} + +Range.prototype.format = function() { + this.range = this.set.map(function(comps) { + return comps.join(' ').trim(); + }).join('||').trim(); + return this.range; +}; + +Range.prototype.toString = function() { + return this.range; +}; + +Range.prototype.parseRange = function(range) { + var loose = this.loose; + range = range.trim(); + debug('range', range, loose); + // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` + var hr = loose ? re[HYPHENRANGELOOSE] : re[HYPHENRANGE]; + range = range.replace(hr, hyphenReplace); + debug('hyphen replace', range); + // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` + range = range.replace(re[COMPARATORTRIM], comparatorTrimReplace); + debug('comparator trim', range, re[COMPARATORTRIM]); + + // `~ 1.2.3` => `~1.2.3` + range = range.replace(re[TILDETRIM], tildeTrimReplace); + + // `^ 1.2.3` => `^1.2.3` + range = range.replace(re[CARETTRIM], caretTrimReplace); + + // normalize spaces + range = range.split(/\s+/).join(' '); + + // At this point, the range is completely trimmed and + // ready to be split into comparators. + + var compRe = loose ? re[COMPARATORLOOSE] : re[COMPARATOR]; + var set = range.split(' ').map(function(comp) { + return parseComparator(comp, loose); + }).join(' ').split(/\s+/); + if (this.loose) { + // in loose mode, throw out any that are not valid comparators + set = set.filter(function(comp) { + return !!comp.match(compRe); + }); + } + set = set.map(function(comp) { + return new Comparator(comp, loose); + }); + + return set; +}; + +Range.prototype.intersects = function(range, loose) { + if (!(range instanceof Range)) { + throw new TypeError('a Range is required'); + } + + return this.set.some(function(thisComparators) { + return thisComparators.every(function(thisComparator) { + return range.set.some(function(rangeComparators) { + return rangeComparators.every(function(rangeComparator) { + return thisComparator.intersects(rangeComparator, loose); + }); + }); + }); + }); +}; + +// Mostly just for testing and legacy API reasons +exports.toComparators = toComparators; +function toComparators(range, loose) { + return new Range(range, loose).set.map(function(comp) { + return comp.map(function(c) { + return c.value; + }).join(' ').trim().split(' '); + }); +} + +// comprised of xranges, tildes, stars, and gtlt's at this point. +// already replaced the hyphen ranges +// turn into a set of JUST comparators. +function parseComparator(comp, loose) { + debug('comp', comp); + comp = replaceCarets(comp, loose); + debug('caret', comp); + comp = replaceTildes(comp, loose); + debug('tildes', comp); + comp = replaceXRanges(comp, loose); + debug('xrange', comp); + comp = replaceStars(comp, loose); + debug('stars', comp); + return comp; +} + +function isX(id) { + return !id || id.toLowerCase() === 'x' || id === '*'; +} + +// ~, ~> --> * (any, kinda silly) +// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0 +// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0 +// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0 +// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0 +// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0 +function replaceTildes(comp, loose) { + return comp.trim().split(/\s+/).map(function(comp) { + return replaceTilde(comp, loose); + }).join(' '); +} + +function replaceTilde(comp, loose) { + var r = loose ? re[TILDELOOSE] : re[TILDE]; + return comp.replace(r, function(_, M, m, p, pr) { + debug('tilde', comp, _, M, m, p, pr); + var ret; + + if (isX(M)) + ret = ''; + else if (isX(m)) + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + else if (isX(p)) + // ~1.2 == >=1.2.0 <1.3.0 + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + else if (pr) { + debug('replaceTilde pr', pr); + if (pr.charAt(0) !== '-') + pr = '-' + pr; + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + // ~1.2.3 == >=1.2.3 <1.3.0 + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0'; + + debug('tilde return', ret); + return ret; + }); +} + +// ^ --> * (any, kinda silly) +// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0 +// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0 +// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0 +// ^1.2.3 --> >=1.2.3 <2.0.0 +// ^1.2.0 --> >=1.2.0 <2.0.0 +function replaceCarets(comp, loose) { + return comp.trim().split(/\s+/).map(function(comp) { + return replaceCaret(comp, loose); + }).join(' '); +} + +function replaceCaret(comp, loose) { + debug('caret', comp, loose); + var r = loose ? re[CARETLOOSE] : re[CARET]; + return comp.replace(r, function(_, M, m, p, pr) { + debug('caret', comp, _, M, m, p, pr); + var ret; + + if (isX(M)) + ret = ''; + else if (isX(m)) + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + else if (isX(p)) { + if (M === '0') + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + else + ret = '>=' + M + '.' + m + '.0 <' + (+M + 1) + '.0.0'; + } else if (pr) { + debug('replaceCaret pr', pr); + if (pr.charAt(0) !== '-') + pr = '-' + pr; + if (M === '0') { + if (m === '0') + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + m + '.' + (+p + 1); + else + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + (+M + 1) + '.0.0'; + } else { + debug('no pr'); + if (M === '0') { + if (m === '0') + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + m + '.' + (+p + 1); + else + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + ret = '>=' + M + '.' + m + '.' + p + + ' <' + (+M + 1) + '.0.0'; + } + + debug('caret return', ret); + return ret; + }); +} + +function replaceXRanges(comp, loose) { + debug('replaceXRanges', comp, loose); + return comp.split(/\s+/).map(function(comp) { + return replaceXRange(comp, loose); + }).join(' '); +} + +function replaceXRange(comp, loose) { + comp = comp.trim(); + var r = loose ? re[XRANGELOOSE] : re[XRANGE]; + return comp.replace(r, function(ret, gtlt, M, m, p, pr) { + debug('xRange', comp, ret, gtlt, M, m, p, pr); + var xM = isX(M); + var xm = xM || isX(m); + var xp = xm || isX(p); + var anyX = xp; + + if (gtlt === '=' && anyX) + gtlt = ''; + + if (xM) { + if (gtlt === '>' || gtlt === '<') { + // nothing is allowed + ret = '<0.0.0'; + } else { + // nothing is forbidden + ret = '*'; + } + } else if (gtlt && anyX) { + // replace X with 0 + if (xm) + m = 0; + if (xp) + p = 0; + + if (gtlt === '>') { + // >1 => >=2.0.0 + // >1.2 => >=1.3.0 + // >1.2.3 => >= 1.2.4 + gtlt = '>='; + if (xm) { + M = +M + 1; + m = 0; + p = 0; + } else if (xp) { + m = +m + 1; + p = 0; + } + } else if (gtlt === '<=') { + // <=0.7.x is actually <0.8.0, since any 0.7.x should + // pass. Similarly, <=7.x is actually <8.0.0, etc. + gtlt = '<'; + if (xm) + M = +M + 1; + else + m = +m + 1; + } + + ret = gtlt + M + '.' + m + '.' + p; + } else if (xm) { + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + } else if (xp) { + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + } + + debug('xRange return', ret); + + return ret; + }); +} + +// Because * is AND-ed with everything else in the comparator, +// and '' means "any version", just remove the *s entirely. +function replaceStars(comp, loose) { + debug('replaceStars', comp, loose); + // Looseness is ignored here. star is always as loose as it gets! + return comp.trim().replace(re[STAR], ''); +} + +// This function is passed to string.replace(re[HYPHENRANGE]) +// M, m, patch, prerelease, build +// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5 +// 1.2.3 - 3.4 => >=1.2.0 <3.5.0 Any 3.4.x will do +// 1.2 - 3.4 => >=1.2.0 <3.5.0 +function hyphenReplace($0, + from, fM, fm, fp, fpr, fb, + to, tM, tm, tp, tpr, tb) { + + if (isX(fM)) + from = ''; + else if (isX(fm)) + from = '>=' + fM + '.0.0'; + else if (isX(fp)) + from = '>=' + fM + '.' + fm + '.0'; + else + from = '>=' + from; + + if (isX(tM)) + to = ''; + else if (isX(tm)) + to = '<' + (+tM + 1) + '.0.0'; + else if (isX(tp)) + to = '<' + tM + '.' + (+tm + 1) + '.0'; + else if (tpr) + to = '<=' + tM + '.' + tm + '.' + tp + '-' + tpr; + else + to = '<=' + to; + + return (from + ' ' + to).trim(); +} + + +// if ANY of the sets match ALL of its comparators, then pass +Range.prototype.test = function(version) { + if (!version) + return false; + + if (typeof version === 'string') + version = new SemVer(version, this.loose); + + for (var i = 0; i < this.set.length; i++) { + if (testSet(this.set[i], version)) + return true; + } + return false; +}; + +function testSet(set, version) { + for (var i = 0; i < set.length; i++) { + if (!set[i].test(version)) + return false; + } + + if (version.prerelease.length) { + // Find the set of versions that are allowed to have prereleases + // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0 + // That should allow `1.2.3-pr.2` to pass. + // However, `1.2.4-alpha.notready` should NOT be allowed, + // even though it's within the range set by the comparators. + for (var i = 0; i < set.length; i++) { + debug(set[i].semver); + if (set[i].semver === ANY) + continue; + + if (set[i].semver.prerelease.length > 0) { + var allowed = set[i].semver; + if (allowed.major === version.major && + allowed.minor === version.minor && + allowed.patch === version.patch) + return true; + } + } + + // Version has a -pre, but it's not one of the ones we like. + return false; + } + + return true; +} + +exports.satisfies = satisfies; +function satisfies(version, range, loose) { + try { + range = new Range(range, loose); + } catch (er) { + return false; + } + return range.test(version); +} + +exports.maxSatisfying = maxSatisfying; +function maxSatisfying(versions, range, loose) { + var max = null; + var maxSV = null; + try { + var rangeObj = new Range(range, loose); + } catch (er) { + return null; + } + versions.forEach(function (v) { + if (rangeObj.test(v)) { // satisfies(v, range, loose) + if (!max || maxSV.compare(v) === -1) { // compare(max, v, true) + max = v; + maxSV = new SemVer(max, loose); + } + } + }) + return max; +} + +exports.minSatisfying = minSatisfying; +function minSatisfying(versions, range, loose) { + var min = null; + var minSV = null; + try { + var rangeObj = new Range(range, loose); + } catch (er) { + return null; + } + versions.forEach(function (v) { + if (rangeObj.test(v)) { // satisfies(v, range, loose) + if (!min || minSV.compare(v) === 1) { // compare(min, v, true) + min = v; + minSV = new SemVer(min, loose); + } + } + }) + return min; +} + +exports.validRange = validRange; +function validRange(range, loose) { + try { + // Return '*' instead of '' so that truthiness works. + // This will throw if it's invalid anyway + return new Range(range, loose).range || '*'; + } catch (er) { + return null; + } +} + +// Determine if version is less than all the versions possible in the range +exports.ltr = ltr; +function ltr(version, range, loose) { + return outside(version, range, '<', loose); +} + +// Determine if version is greater than all the versions possible in the range. +exports.gtr = gtr; +function gtr(version, range, loose) { + return outside(version, range, '>', loose); +} + +exports.outside = outside; +function outside(version, range, hilo, loose) { + version = new SemVer(version, loose); + range = new Range(range, loose); + + var gtfn, ltefn, ltfn, comp, ecomp; + switch (hilo) { + case '>': + gtfn = gt; + ltefn = lte; + ltfn = lt; + comp = '>'; + ecomp = '>='; + break; + case '<': + gtfn = lt; + ltefn = gte; + ltfn = gt; + comp = '<'; + ecomp = '<='; + break; + default: + throw new TypeError('Must provide a hilo val of "<" or ">"'); + } + + // If it satisifes the range it is not outside + if (satisfies(version, range, loose)) { + return false; + } + + // From now on, variable terms are as if we're in "gtr" mode. + // but note that everything is flipped for the "ltr" function. + + for (var i = 0; i < range.set.length; ++i) { + var comparators = range.set[i]; + + var high = null; + var low = null; + + comparators.forEach(function(comparator) { + if (comparator.semver === ANY) { + comparator = new Comparator('>=0.0.0') + } + high = high || comparator; + low = low || comparator; + if (gtfn(comparator.semver, high.semver, loose)) { + high = comparator; + } else if (ltfn(comparator.semver, low.semver, loose)) { + low = comparator; + } + }); + + // If the edge version comparator has a operator then our version + // isn't outside it + if (high.operator === comp || high.operator === ecomp) { + return false; + } + + // If the lowest version comparator has an operator and our version + // is less than it then it isn't higher than the range + if ((!low.operator || low.operator === comp) && + ltefn(version, low.semver)) { + return false; + } else if (low.operator === ecomp && ltfn(version, low.semver)) { + return false; + } + } + return true; +} + +exports.prerelease = prerelease; +function prerelease(version, loose) { + var parsed = parse(version, loose); + return (parsed && parsed.prerelease.length) ? parsed.prerelease : null; +} + +exports.intersects = intersects; +function intersects(r1, r2, loose) { + r1 = new Range(r1, loose) + r2 = new Range(r2, loose) + return r1.intersects(r2) +} + +exports.coerce = coerce; +function coerce(version) { + if (version instanceof SemVer) + return version; + + if (typeof version !== 'string') + return null; + + var match = version.match(re[COERCE]); + + if (match == null) + return null; + + return parse((match[1] || '0') + '.' + (match[2] || '0') + '.' + (match[3] || '0')); +} + + +/***/ }), +/* 23 */ +/***/ (function(module, exports) { + +module.exports = require("stream"); + +/***/ }), +/* 24 */ +/***/ (function(module, exports) { + +module.exports = require("url"); + +/***/ }), +/* 25 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Subscription; }); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__util_isArray__ = __webpack_require__(41); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__util_isObject__ = __webpack_require__(444); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__util_isFunction__ = __webpack_require__(154); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__util_tryCatch__ = __webpack_require__(56); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__util_errorObject__ = __webpack_require__(48); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__util_UnsubscriptionError__ = __webpack_require__(441); +/** PURE_IMPORTS_START _util_isArray,_util_isObject,_util_isFunction,_util_tryCatch,_util_errorObject,_util_UnsubscriptionError PURE_IMPORTS_END */ + + + + + + +var Subscription = /*@__PURE__*/ (function () { + function Subscription(unsubscribe) { + this.closed = false; + this._parent = null; + this._parents = null; + this._subscriptions = null; + if (unsubscribe) { + this._unsubscribe = unsubscribe; + } + } + Subscription.prototype.unsubscribe = function () { + var hasErrors = false; + var errors; + if (this.closed) { + return; + } + var _a = this, _parent = _a._parent, _parents = _a._parents, _unsubscribe = _a._unsubscribe, _subscriptions = _a._subscriptions; + this.closed = true; + this._parent = null; + this._parents = null; + this._subscriptions = null; + var index = -1; + var len = _parents ? _parents.length : 0; + while (_parent) { + _parent.remove(this); + _parent = ++index < len && _parents[index] || null; + } + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_2__util_isFunction__["a" /* isFunction */])(_unsubscribe)) { + var trial = __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_3__util_tryCatch__["a" /* tryCatch */])(_unsubscribe).call(this); + if (trial === __WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */]) { + hasErrors = true; + errors = errors || (__WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */].e instanceof __WEBPACK_IMPORTED_MODULE_5__util_UnsubscriptionError__["a" /* UnsubscriptionError */] ? + flattenUnsubscriptionErrors(__WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */].e.errors) : [__WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */].e]); + } + } + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__util_isArray__["a" /* isArray */])(_subscriptions)) { + index = -1; + len = _subscriptions.length; + while (++index < len) { + var sub = _subscriptions[index]; + if (__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_1__util_isObject__["a" /* isObject */])(sub)) { + var trial = __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_3__util_tryCatch__["a" /* tryCatch */])(sub.unsubscribe).call(sub); + if (trial === __WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */]) { + hasErrors = true; + errors = errors || []; + var err = __WEBPACK_IMPORTED_MODULE_4__util_errorObject__["a" /* errorObject */].e; + if (err instanceof __WEBPACK_IMPORTED_MODULE_5__util_UnsubscriptionError__["a" /* UnsubscriptionError */]) { + errors = errors.concat(flattenUnsubscriptionErrors(err.errors)); + } + else { + errors.push(err); + } + } + } + } + } + if (hasErrors) { + throw new __WEBPACK_IMPORTED_MODULE_5__util_UnsubscriptionError__["a" /* UnsubscriptionError */](errors); + } + }; + Subscription.prototype.add = function (teardown) { + if (!teardown || (teardown === Subscription.EMPTY)) { + return Subscription.EMPTY; + } + if (teardown === this) { + return this; + } + var subscription = teardown; + switch (typeof teardown) { + case 'function': + subscription = new Subscription(teardown); + case 'object': + if (subscription.closed || typeof subscription.unsubscribe !== 'function') { + return subscription; + } + else if (this.closed) { + subscription.unsubscribe(); + return subscription; + } + else if (typeof subscription._addParent !== 'function') { + var tmp = subscription; + subscription = new Subscription(); + subscription._subscriptions = [tmp]; + } + break; + default: + throw new Error('unrecognized teardown ' + teardown + ' added to Subscription.'); + } + var subscriptions = this._subscriptions || (this._subscriptions = []); + subscriptions.push(subscription); + subscription._addParent(this); + return subscription; + }; + Subscription.prototype.remove = function (subscription) { + var subscriptions = this._subscriptions; + if (subscriptions) { + var subscriptionIndex = subscriptions.indexOf(subscription); + if (subscriptionIndex !== -1) { + subscriptions.splice(subscriptionIndex, 1); + } + } + }; + Subscription.prototype._addParent = function (parent) { + var _a = this, _parent = _a._parent, _parents = _a._parents; + if (!_parent || _parent === parent) { + this._parent = parent; + } + else if (!_parents) { + this._parents = [parent]; + } + else if (_parents.indexOf(parent) === -1) { + _parents.push(parent); + } + }; + Subscription.EMPTY = (function (empty) { + empty.closed = true; + return empty; + }(new Subscription())); + return Subscription; +}()); + +function flattenUnsubscriptionErrors(errors) { + return errors.reduce(function (errs, err) { return errs.concat((err instanceof __WEBPACK_IMPORTED_MODULE_5__util_UnsubscriptionError__["a" /* UnsubscriptionError */]) ? err.errors : err); }, []); +} +//# sourceMappingURL=Subscription.js.map + + +/***/ }), +/* 26 */ +/***/ (function(module, exports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +module.exports = { + bufferSplit: bufferSplit, + addRSAMissing: addRSAMissing, + calculateDSAPublic: calculateDSAPublic, + calculateED25519Public: calculateED25519Public, + calculateX25519Public: calculateX25519Public, + mpNormalize: mpNormalize, + mpDenormalize: mpDenormalize, + ecNormalize: ecNormalize, + countZeros: countZeros, + assertCompatible: assertCompatible, + isCompatible: isCompatible, + opensslKeyDeriv: opensslKeyDeriv, + opensshCipherInfo: opensshCipherInfo, + publicFromPrivateECDSA: publicFromPrivateECDSA, + zeroPadToLength: zeroPadToLength, + writeBitString: writeBitString, + readBitString: readBitString +}; + +var assert = __webpack_require__(16); +var Buffer = __webpack_require__(15).Buffer; +var PrivateKey = __webpack_require__(33); +var Key = __webpack_require__(27); +var crypto = __webpack_require__(11); +var algs = __webpack_require__(32); +var asn1 = __webpack_require__(66); + +var ec, jsbn; +var nacl; + +var MAX_CLASS_DEPTH = 3; + +function isCompatible(obj, klass, needVer) { + if (obj === null || typeof (obj) !== 'object') + return (false); + if (needVer === undefined) + needVer = klass.prototype._sshpkApiVersion; + if (obj instanceof klass && + klass.prototype._sshpkApiVersion[0] == needVer[0]) + return (true); + var proto = Object.getPrototypeOf(obj); + var depth = 0; + while (proto.constructor.name !== klass.name) { + proto = Object.getPrototypeOf(proto); + if (!proto || ++depth > MAX_CLASS_DEPTH) + return (false); + } + if (proto.constructor.name !== klass.name) + return (false); + var ver = proto._sshpkApiVersion; + if (ver === undefined) + ver = klass._oldVersionDetect(obj); + if (ver[0] != needVer[0] || ver[1] < needVer[1]) + return (false); + return (true); +} + +function assertCompatible(obj, klass, needVer, name) { + if (name === undefined) + name = 'object'; + assert.ok(obj, name + ' must not be null'); + assert.object(obj, name + ' must be an object'); + if (needVer === undefined) + needVer = klass.prototype._sshpkApiVersion; + if (obj instanceof klass && + klass.prototype._sshpkApiVersion[0] == needVer[0]) + return; + var proto = Object.getPrototypeOf(obj); + var depth = 0; + while (proto.constructor.name !== klass.name) { + proto = Object.getPrototypeOf(proto); + assert.ok(proto && ++depth <= MAX_CLASS_DEPTH, + name + ' must be a ' + klass.name + ' instance'); + } + assert.strictEqual(proto.constructor.name, klass.name, + name + ' must be a ' + klass.name + ' instance'); + var ver = proto._sshpkApiVersion; + if (ver === undefined) + ver = klass._oldVersionDetect(obj); + assert.ok(ver[0] == needVer[0] && ver[1] >= needVer[1], + name + ' must be compatible with ' + klass.name + ' klass ' + + 'version ' + needVer[0] + '.' + needVer[1]); +} + +var CIPHER_LEN = { + 'des-ede3-cbc': { key: 7, iv: 8 }, + 'aes-128-cbc': { key: 16, iv: 16 } +}; +var PKCS5_SALT_LEN = 8; + +function opensslKeyDeriv(cipher, salt, passphrase, count) { + assert.buffer(salt, 'salt'); + assert.buffer(passphrase, 'passphrase'); + assert.number(count, 'iteration count'); + + var clen = CIPHER_LEN[cipher]; + assert.object(clen, 'supported cipher'); + + salt = salt.slice(0, PKCS5_SALT_LEN); + + var D, D_prev, bufs; + var material = Buffer.alloc(0); + while (material.length < clen.key + clen.iv) { + bufs = []; + if (D_prev) + bufs.push(D_prev); + bufs.push(passphrase); + bufs.push(salt); + D = Buffer.concat(bufs); + for (var j = 0; j < count; ++j) + D = crypto.createHash('md5').update(D).digest(); + material = Buffer.concat([material, D]); + D_prev = D; + } + + return ({ + key: material.slice(0, clen.key), + iv: material.slice(clen.key, clen.key + clen.iv) + }); +} + +/* Count leading zero bits on a buffer */ +function countZeros(buf) { + var o = 0, obit = 8; + while (o < buf.length) { + var mask = (1 << obit); + if ((buf[o] & mask) === mask) + break; + obit--; + if (obit < 0) { + o++; + obit = 8; + } + } + return (o*8 + (8 - obit) - 1); +} + +function bufferSplit(buf, chr) { + assert.buffer(buf); + assert.string(chr); + + var parts = []; + var lastPart = 0; + var matches = 0; + for (var i = 0; i < buf.length; ++i) { + if (buf[i] === chr.charCodeAt(matches)) + ++matches; + else if (buf[i] === chr.charCodeAt(0)) + matches = 1; + else + matches = 0; + + if (matches >= chr.length) { + var newPart = i + 1; + parts.push(buf.slice(lastPart, newPart - matches)); + lastPart = newPart; + matches = 0; + } + } + if (lastPart <= buf.length) + parts.push(buf.slice(lastPart, buf.length)); + + return (parts); +} + +function ecNormalize(buf, addZero) { + assert.buffer(buf); + if (buf[0] === 0x00 && buf[1] === 0x04) { + if (addZero) + return (buf); + return (buf.slice(1)); + } else if (buf[0] === 0x04) { + if (!addZero) + return (buf); + } else { + while (buf[0] === 0x00) + buf = buf.slice(1); + if (buf[0] === 0x02 || buf[0] === 0x03) + throw (new Error('Compressed elliptic curve points ' + + 'are not supported')); + if (buf[0] !== 0x04) + throw (new Error('Not a valid elliptic curve point')); + if (!addZero) + return (buf); + } + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x0; + buf.copy(b, 1); + return (b); +} + +function readBitString(der, tag) { + if (tag === undefined) + tag = asn1.Ber.BitString; + var buf = der.readString(tag, true); + assert.strictEqual(buf[0], 0x00, 'bit strings with unused bits are ' + + 'not supported (0x' + buf[0].toString(16) + ')'); + return (buf.slice(1)); +} + +function writeBitString(der, buf, tag) { + if (tag === undefined) + tag = asn1.Ber.BitString; + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + der.writeBuffer(b, tag); +} + +function mpNormalize(buf) { + assert.buffer(buf); + while (buf.length > 1 && buf[0] === 0x00 && (buf[1] & 0x80) === 0x00) + buf = buf.slice(1); + if ((buf[0] & 0x80) === 0x80) { + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + buf = b; + } + return (buf); +} + +function mpDenormalize(buf) { + assert.buffer(buf); + while (buf.length > 1 && buf[0] === 0x00) + buf = buf.slice(1); + return (buf); +} + +function zeroPadToLength(buf, len) { + assert.buffer(buf); + assert.number(len); + while (buf.length > len) { + assert.equal(buf[0], 0x00); + buf = buf.slice(1); + } + while (buf.length < len) { + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + buf = b; + } + return (buf); +} + +function bigintToMpBuf(bigint) { + var buf = Buffer.from(bigint.toByteArray()); + buf = mpNormalize(buf); + return (buf); +} + +function calculateDSAPublic(g, p, x) { + assert.buffer(g); + assert.buffer(p); + assert.buffer(x); + try { + var bigInt = __webpack_require__(81).BigInteger; + } catch (e) { + throw (new Error('To load a PKCS#8 format DSA private key, ' + + 'the node jsbn library is required.')); + } + g = new bigInt(g); + p = new bigInt(p); + x = new bigInt(x); + var y = g.modPow(x, p); + var ybuf = bigintToMpBuf(y); + return (ybuf); +} + +function calculateED25519Public(k) { + assert.buffer(k); + + if (nacl === undefined) + nacl = __webpack_require__(76); + + var kp = nacl.sign.keyPair.fromSeed(new Uint8Array(k)); + return (Buffer.from(kp.publicKey)); +} + +function calculateX25519Public(k) { + assert.buffer(k); + + if (nacl === undefined) + nacl = __webpack_require__(76); + + var kp = nacl.box.keyPair.fromSeed(new Uint8Array(k)); + return (Buffer.from(kp.publicKey)); +} + +function addRSAMissing(key) { + assert.object(key); + assertCompatible(key, PrivateKey, [1, 1]); + try { + var bigInt = __webpack_require__(81).BigInteger; + } catch (e) { + throw (new Error('To write a PEM private key from ' + + 'this source, the node jsbn lib is required.')); + } + + var d = new bigInt(key.part.d.data); + var buf; + + if (!key.part.dmodp) { + var p = new bigInt(key.part.p.data); + var dmodp = d.mod(p.subtract(1)); + + buf = bigintToMpBuf(dmodp); + key.part.dmodp = {name: 'dmodp', data: buf}; + key.parts.push(key.part.dmodp); + } + if (!key.part.dmodq) { + var q = new bigInt(key.part.q.data); + var dmodq = d.mod(q.subtract(1)); + + buf = bigintToMpBuf(dmodq); + key.part.dmodq = {name: 'dmodq', data: buf}; + key.parts.push(key.part.dmodq); + } +} + +function publicFromPrivateECDSA(curveName, priv) { + assert.string(curveName, 'curveName'); + assert.buffer(priv); + if (ec === undefined) + ec = __webpack_require__(139); + if (jsbn === undefined) + jsbn = __webpack_require__(81).BigInteger; + var params = algs.curves[curveName]; + var p = new jsbn(params.p); + var a = new jsbn(params.a); + var b = new jsbn(params.b); + var curve = new ec.ECCurveFp(p, a, b); + var G = curve.decodePointHex(params.G.toString('hex')); + + var d = new jsbn(mpNormalize(priv)); + var pub = G.multiply(d); + pub = Buffer.from(curve.encodePointHex(pub), 'hex'); + + var parts = []; + parts.push({name: 'curve', data: Buffer.from(curveName)}); + parts.push({name: 'Q', data: pub}); + + var key = new Key({type: 'ecdsa', curve: curve, parts: parts}); + return (key); +} + +function opensshCipherInfo(cipher) { + var inf = {}; + switch (cipher) { + case '3des-cbc': + inf.keySize = 24; + inf.blockSize = 8; + inf.opensslName = 'des-ede3-cbc'; + break; + case 'blowfish-cbc': + inf.keySize = 16; + inf.blockSize = 8; + inf.opensslName = 'bf-cbc'; + break; + case 'aes128-cbc': + case 'aes128-ctr': + case 'aes128-gcm@openssh.com': + inf.keySize = 16; + inf.blockSize = 16; + inf.opensslName = 'aes-128-' + cipher.slice(7, 10); + break; + case 'aes192-cbc': + case 'aes192-ctr': + case 'aes192-gcm@openssh.com': + inf.keySize = 24; + inf.blockSize = 16; + inf.opensslName = 'aes-192-' + cipher.slice(7, 10); + break; + case 'aes256-cbc': + case 'aes256-ctr': + case 'aes256-gcm@openssh.com': + inf.keySize = 32; + inf.blockSize = 16; + inf.opensslName = 'aes-256-' + cipher.slice(7, 10); + break; + default: + throw (new Error( + 'Unsupported openssl cipher "' + cipher + '"')); + } + return (inf); +} + + +/***/ }), +/* 27 */ +/***/ (function(module, exports, __webpack_require__) { + +// Copyright 2017 Joyent, Inc. + +module.exports = Key; + +var assert = __webpack_require__(16); +var algs = __webpack_require__(32); +var crypto = __webpack_require__(11); +var Fingerprint = __webpack_require__(156); +var Signature = __webpack_require__(75); +var DiffieHellman = __webpack_require__(325).DiffieHellman; +var errs = __webpack_require__(74); +var utils = __webpack_require__(26); +var PrivateKey = __webpack_require__(33); +var edCompat; + +try { + edCompat = __webpack_require__(454); +} catch (e) { + /* Just continue through, and bail out if we try to use it. */ +} + +var InvalidAlgorithmError = errs.InvalidAlgorithmError; +var KeyParseError = errs.KeyParseError; + +var formats = {}; +formats['auto'] = __webpack_require__(455); +formats['pem'] = __webpack_require__(86); +formats['pkcs1'] = __webpack_require__(327); +formats['pkcs8'] = __webpack_require__(157); +formats['rfc4253'] = __webpack_require__(103); +formats['ssh'] = __webpack_require__(456); +formats['ssh-private'] = __webpack_require__(192); +formats['openssh'] = formats['ssh-private']; +formats['dnssec'] = __webpack_require__(326); + +function Key(opts) { + assert.object(opts, 'options'); + assert.arrayOfObject(opts.parts, 'options.parts'); + assert.string(opts.type, 'options.type'); + assert.optionalString(opts.comment, 'options.comment'); + + var algInfo = algs.info[opts.type]; + if (typeof (algInfo) !== 'object') + throw (new InvalidAlgorithmError(opts.type)); + + var partLookup = {}; + for (var i = 0; i < opts.parts.length; ++i) { + var part = opts.parts[i]; + partLookup[part.name] = part; + } + + this.type = opts.type; + this.parts = opts.parts; + this.part = partLookup; + this.comment = undefined; + this.source = opts.source; + + /* for speeding up hashing/fingerprint operations */ + this._rfc4253Cache = opts._rfc4253Cache; + this._hashCache = {}; + + var sz; + this.curve = undefined; + if (this.type === 'ecdsa') { + var curve = this.part.curve.data.toString(); + this.curve = curve; + sz = algs.curves[curve].size; + } else if (this.type === 'ed25519' || this.type === 'curve25519') { + sz = 256; + this.curve = 'curve25519'; + } else { + var szPart = this.part[algInfo.sizePart]; + sz = szPart.data.length; + sz = sz * 8 - utils.countZeros(szPart.data); + } + this.size = sz; +} + +Key.formats = formats; + +Key.prototype.toBuffer = function (format, options) { + if (format === undefined) + format = 'ssh'; + assert.string(format, 'format'); + assert.object(formats[format], 'formats[format]'); + assert.optionalObject(options, 'options'); + + if (format === 'rfc4253') { + if (this._rfc4253Cache === undefined) + this._rfc4253Cache = formats['rfc4253'].write(this); + return (this._rfc4253Cache); + } + + return (formats[format].write(this, options)); +}; + +Key.prototype.toString = function (format, options) { + return (this.toBuffer(format, options).toString()); +}; + +Key.prototype.hash = function (algo) { + assert.string(algo, 'algorithm'); + algo = algo.toLowerCase(); + if (algs.hashAlgs[algo] === undefined) + throw (new InvalidAlgorithmError(algo)); + + if (this._hashCache[algo]) + return (this._hashCache[algo]); + var hash = crypto.createHash(algo). + update(this.toBuffer('rfc4253')).digest(); + this._hashCache[algo] = hash; + return (hash); +}; + +Key.prototype.fingerprint = function (algo) { + if (algo === undefined) + algo = 'sha256'; + assert.string(algo, 'algorithm'); + var opts = { + type: 'key', + hash: this.hash(algo), + algorithm: algo + }; + return (new Fingerprint(opts)); +}; + +Key.prototype.defaultHashAlgorithm = function () { + var hashAlgo = 'sha1'; + if (this.type === 'rsa') + hashAlgo = 'sha256'; + if (this.type === 'dsa' && this.size > 1024) + hashAlgo = 'sha256'; + if (this.type === 'ed25519') + hashAlgo = 'sha512'; + if (this.type === 'ecdsa') { + if (this.size <= 256) + hashAlgo = 'sha256'; + else if (this.size <= 384) + hashAlgo = 'sha384'; + else + hashAlgo = 'sha512'; + } + return (hashAlgo); +}; + +Key.prototype.createVerify = function (hashAlgo) { + if (hashAlgo === undefined) + hashAlgo = this.defaultHashAlgorithm(); + assert.string(hashAlgo, 'hash algorithm'); + + /* ED25519 is not supported by OpenSSL, use a javascript impl. */ + if (this.type === 'ed25519' && edCompat !== undefined) + return (new edCompat.Verifier(this, hashAlgo)); + if (this.type === 'curve25519') + throw (new Error('Curve25519 keys are not suitable for ' + + 'signing or verification')); + + var v, nm, err; + try { + nm = hashAlgo.toUpperCase(); + v = crypto.createVerify(nm); + } catch (e) { + err = e; + } + if (v === undefined || (err instanceof Error && + err.message.match(/Unknown message digest/))) { + nm = 'RSA-'; + nm += hashAlgo.toUpperCase(); + v = crypto.createVerify(nm); + } + assert.ok(v, 'failed to create verifier'); + var oldVerify = v.verify.bind(v); + var key = this.toBuffer('pkcs8'); + var curve = this.curve; + var self = this; + v.verify = function (signature, fmt) { + if (Signature.isSignature(signature, [2, 0])) { + if (signature.type !== self.type) + return (false); + if (signature.hashAlgorithm && + signature.hashAlgorithm !== hashAlgo) + return (false); + if (signature.curve && self.type === 'ecdsa' && + signature.curve !== curve) + return (false); + return (oldVerify(key, signature.toBuffer('asn1'))); + + } else if (typeof (signature) === 'string' || + Buffer.isBuffer(signature)) { + return (oldVerify(key, signature, fmt)); + + /* + * Avoid doing this on valid arguments, walking the prototype + * chain can be quite slow. + */ + } else if (Signature.isSignature(signature, [1, 0])) { + throw (new Error('signature was created by too old ' + + 'a version of sshpk and cannot be verified')); + + } else { + throw (new TypeError('signature must be a string, ' + + 'Buffer, or Signature object')); + } + }; + return (v); +}; + +Key.prototype.createDiffieHellman = function () { + if (this.type === 'rsa') + throw (new Error('RSA keys do not support Diffie-Hellman')); + + return (new DiffieHellman(this)); +}; +Key.prototype.createDH = Key.prototype.createDiffieHellman; + +Key.parse = function (data, format, options) { + if (typeof (data) !== 'string') + assert.buffer(data, 'data'); + if (format === undefined) + format = 'auto'; + assert.string(format, 'format'); + if (typeof (options) === 'string') + options = { filename: options }; + assert.optionalObject(options, 'options'); + if (options === undefined) + options = {}; + assert.optionalString(options.filename, 'options.filename'); + if (options.filename === undefined) + options.filename = '(unnamed)'; + + assert.object(formats[format], 'formats[format]'); + + try { + var k = formats[format].read(data, options); + if (k instanceof PrivateKey) + k = k.toPublic(); + if (!k.comment) + k.comment = options.filename; + return (k); + } catch (e) { + if (e.name === 'KeyEncryptedError') + throw (e); + throw (new KeyParseError(options.filename, format, e)); + } +}; + +Key.isKey = function (obj, ver) { + return (utils.isCompatible(obj, Key, ver)); +}; + +/* + * API versions for Key: + * [1,0] -- initial ver, may take Signature for createVerify or may not + * [1,1] -- added pkcs1, pkcs8 formats + * [1,2] -- added auto, ssh-private, openssh formats + * [1,3] -- added defaultHashAlgorithm + * [1,4] -- added ed support, createDH + * [1,5] -- first explicitly tagged version + * [1,6] -- changed ed25519 part names + */ +Key.prototype._sshpkApiVersion = [1, 6]; + +Key._oldVersionDetect = function (obj) { + assert.func(obj.toBuffer); + assert.func(obj.fingerprint); + if (obj.createDH) + return ([1, 4]); + if (obj.defaultHashAlgorithm) + return ([1, 3]); + if (obj.formats['auto']) + return ([1, 2]); + if (obj.formats['pkcs1']) + return ([1, 1]); + return ([1, 0]); +}; + + +/***/ }), +/* 28 */ +/***/ (function(module, exports) { + +module.exports = require("assert"); + +/***/ }), +/* 29 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = nullify; +function nullify(obj = {}) { + if (Array.isArray(obj)) { + for (var _iterator = obj, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { + var _ref; + + if (_isArray) { + if (_i >= _iterator.length) break; + _ref = _iterator[_i++]; + } else { + _i = _iterator.next(); + if (_i.done) break; + _ref = _i.value; + } + + const item = _ref; + + nullify(item); + } + } else if (obj !== null && typeof obj === 'object' || typeof obj === 'function') { + Object.setPrototypeOf(obj, null); + + // for..in can only be applied to 'object', not 'function' + if (typeof obj === 'object') { + for (const key in obj) { + nullify(obj[key]); + } + } + } + + return obj; +} + +/***/ }), +/* 30 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const escapeStringRegexp = __webpack_require__(388); +const ansiStyles = __webpack_require__(506); +const stdoutColor = __webpack_require__(598).stdout; + +const template = __webpack_require__(599); + +const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); + +// `supportsColor.level` → `ansiStyles.color[name]` mapping +const levelMapping = ['ansi', 'ansi', 'ansi256', 'ansi16m']; + +// `color-convert` models to exclude from the Chalk API due to conflicts and such +const skipModels = new Set(['gray']); + +const styles = Object.create(null); + +function applyOptions(obj, options) { + options = options || {}; + + // Detect level if not set manually + const scLevel = stdoutColor ? stdoutColor.level : 0; + obj.level = options.level === undefined ? scLevel : options.level; + obj.enabled = 'enabled' in options ? options.enabled : obj.level > 0; +} + +function Chalk(options) { + // We check for this.template here since calling `chalk.constructor()` + // by itself will have a `this` of a previously constructed chalk object + if (!this || !(this instanceof Chalk) || this.template) { + const chalk = {}; + applyOptions(chalk, options); + + chalk.template = function () { + const args = [].slice.call(arguments); + return chalkTag.apply(null, [chalk.template].concat(args)); + }; + + Object.setPrototypeOf(chalk, Chalk.prototype); + Object.setPrototypeOf(chalk.template, chalk); + + chalk.template.constructor = Chalk; + + return chalk.template; + } + + applyOptions(this, options); +} + +// Use bright blue on Windows as the normal blue color is illegible +if (isSimpleWindowsTerm) { + ansiStyles.blue.open = '\u001B[94m'; +} + +for (const key of Object.keys(ansiStyles)) { + ansiStyles[key].closeRe = new RegExp(escapeStringRegexp(ansiStyles[key].close), 'g'); + + styles[key] = { + get() { + const codes = ansiStyles[key]; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, key); + } + }; +} + +styles.visible = { + get() { + return build.call(this, this._styles || [], true, 'visible'); + } +}; + +ansiStyles.color.closeRe = new RegExp(escapeStringRegexp(ansiStyles.color.close), 'g'); +for (const model of Object.keys(ansiStyles.color.ansi)) { + if (skipModels.has(model)) { + continue; + } + + styles[model] = { + get() { + const level = this.level; + return function () { + const open = ansiStyles.color[levelMapping[level]][model].apply(null, arguments); + const codes = { + open, + close: ansiStyles.color.close, + closeRe: ansiStyles.color.closeRe + }; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, model); + }; + } + }; +} + +ansiStyles.bgColor.closeRe = new RegExp(escapeStringRegexp(ansiStyles.bgColor.close), 'g'); +for (const model of Object.keys(ansiStyles.bgColor.ansi)) { + if (skipModels.has(model)) { + continue; + } + + const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1); + styles[bgModel] = { + get() { + const level = this.level; + return function () { + const open = ansiStyles.bgColor[levelMapping[level]][model].apply(null, arguments); + const codes = { + open, + close: ansiStyles.bgColor.close, + closeRe: ansiStyles.bgColor.closeRe + }; + return build.call(this, this._styles ? this._styles.concat(codes) : [codes], this._empty, model); + }; + } + }; +} + +const proto = Object.defineProperties(() => {}, styles); + +function build(_styles, _empty, key) { + const builder = function () { + return applyStyle.apply(builder, arguments); + }; + + builder._styles = _styles; + builder._empty = _empty; + + const self = this; + + Object.defineProperty(builder, 'level', { + enumerable: true, + get() { + return self.level; + }, + set(level) { + self.level = level; + } + }); + + Object.defineProperty(builder, 'enabled', { + enumerable: true, + get() { + return self.enabled; + }, + set(enabled) { + self.enabled = enabled; + } + }); + + // See below for fix regarding invisible grey/dim combination on Windows + builder.hasGrey = this.hasGrey || key === 'gray' || key === 'grey'; + + // `__proto__` is used because we must return a function, but there is + // no way to create a function with a different prototype + builder.__proto__ = proto; // eslint-disable-line no-proto + + return builder; +} + +function applyStyle() { + // Support varags, but simply cast to string in case there's only one arg + const args = arguments; + const argsLen = args.length; + let str = String(arguments[0]); + + if (argsLen === 0) { + return ''; + } + + if (argsLen > 1) { + // Don't slice `arguments`, it prevents V8 optimizations + for (let a = 1; a < argsLen; a++) { + str += ' ' + args[a]; + } + } + + if (!this.enabled || this.level <= 0 || !str) { + return this._empty ? '' : str; + } + + // Turns out that on Windows dimmed gray text becomes invisible in cmd.exe, + // see https://github.com/chalk/chalk/issues/58 + // If we're on Windows and we're dealing with a gray color, temporarily make 'dim' a noop. + const originalDim = ansiStyles.dim.open; + if (isSimpleWindowsTerm && this.hasGrey) { + ansiStyles.dim.open = ''; + } + + for (const code of this._styles.slice().reverse()) { + // Replace any instances already present with a re-opening code + // otherwise only the part of the string until said closing code + // will be colored, and the rest will simply be 'plain'. + str = code.open + str.replace(code.closeRe, code.open) + code.close; + + // Close the styling before a linebreak and reopen + // after next line to fix a bleed issue on macOS + // https://github.com/chalk/chalk/pull/92 + str = str.replace(/\r?\n/g, `${code.close}$&${code.open}`); + } + + // Reset the original `dim` if we changed it to work around the Windows dimmed gray issue + ansiStyles.dim.open = originalDim; + + return str; +} + +function chalkTag(chalk, strings) { + if (!Array.isArray(strings)) { + // If chalk() was called by itself or with a string, + // return the string itself as a string. + return [].slice.call(arguments, 1).join(' '); + } + + const args = [].slice.call(arguments, 2); + const parts = [strings.raw[0]]; + + for (let i = 1; i < strings.length; i++) { + parts.push(String(args[i - 1]).replace(/[{}\\]/g, '\\$&')); + parts.push(String(strings.raw[i])); + } + + return template(chalk, parts.join('')); +} + +Object.defineProperties(Chalk.prototype, styles); + +module.exports = Chalk(); // eslint-disable-line new-cap +module.exports.supportsColor = stdoutColor; +module.exports.default = module.exports; // For TypeScript + + +/***/ }), +/* 31 */ +/***/ (function(module, exports) { + +var core = module.exports = { version: '2.5.7' }; +if (typeof __e == 'number') __e = core; // eslint-disable-line no-undef + + +/***/ }), +/* 32 */ +/***/ (function(module, exports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +var Buffer = __webpack_require__(15).Buffer; + +var algInfo = { + 'dsa': { + parts: ['p', 'q', 'g', 'y'], + sizePart: 'p' + }, + 'rsa': { + parts: ['e', 'n'], + sizePart: 'n' + }, + 'ecdsa': { + parts: ['curve', 'Q'], + sizePart: 'Q' + }, + 'ed25519': { + parts: ['A'], + sizePart: 'A' + } +}; +algInfo['curve25519'] = algInfo['ed25519']; + +var algPrivInfo = { + 'dsa': { + parts: ['p', 'q', 'g', 'y', 'x'] + }, + 'rsa': { + parts: ['n', 'e', 'd', 'iqmp', 'p', 'q'] + }, + 'ecdsa': { + parts: ['curve', 'Q', 'd'] + }, + 'ed25519': { + parts: ['A', 'k'] + } +}; +algPrivInfo['curve25519'] = algPrivInfo['ed25519']; + +var hashAlgs = { + 'md5': true, + 'sha1': true, + 'sha256': true, + 'sha384': true, + 'sha512': true +}; + +/* + * Taken from + * http://csrc.nist.gov/groups/ST/toolkit/documents/dss/NISTReCur.pdf + */ +var curves = { + 'nistp256': { + size: 256, + pkcs8oid: '1.2.840.10045.3.1.7', + p: Buffer.from(('00' + + 'ffffffff 00000001 00000000 00000000' + + '00000000 ffffffff ffffffff ffffffff'). + replace(/ /g, ''), 'hex'), + a: Buffer.from(('00' + + 'FFFFFFFF 00000001 00000000 00000000' + + '00000000 FFFFFFFF FFFFFFFF FFFFFFFC'). + replace(/ /g, ''), 'hex'), + b: Buffer.from(( + '5ac635d8 aa3a93e7 b3ebbd55 769886bc' + + '651d06b0 cc53b0f6 3bce3c3e 27d2604b'). + replace(/ /g, ''), 'hex'), + s: Buffer.from(('00' + + 'c49d3608 86e70493 6a6678e1 139d26b7' + + '819f7e90'). + replace(/ /g, ''), 'hex'), + n: Buffer.from(('00' + + 'ffffffff 00000000 ffffffff ffffffff' + + 'bce6faad a7179e84 f3b9cac2 fc632551'). + replace(/ /g, ''), 'hex'), + G: Buffer.from(('04' + + '6b17d1f2 e12c4247 f8bce6e5 63a440f2' + + '77037d81 2deb33a0 f4a13945 d898c296' + + '4fe342e2 fe1a7f9b 8ee7eb4a 7c0f9e16' + + '2bce3357 6b315ece cbb64068 37bf51f5'). + replace(/ /g, ''), 'hex') + }, + 'nistp384': { + size: 384, + pkcs8oid: '1.3.132.0.34', + p: Buffer.from(('00' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff fffffffe' + + 'ffffffff 00000000 00000000 ffffffff'). + replace(/ /g, ''), 'hex'), + a: Buffer.from(('00' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE' + + 'FFFFFFFF 00000000 00000000 FFFFFFFC'). + replace(/ /g, ''), 'hex'), + b: Buffer.from(( + 'b3312fa7 e23ee7e4 988e056b e3f82d19' + + '181d9c6e fe814112 0314088f 5013875a' + + 'c656398d 8a2ed19d 2a85c8ed d3ec2aef'). + replace(/ /g, ''), 'hex'), + s: Buffer.from(('00' + + 'a335926a a319a27a 1d00896a 6773a482' + + '7acdac73'). + replace(/ /g, ''), 'hex'), + n: Buffer.from(('00' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff c7634d81 f4372ddf' + + '581a0db2 48b0a77a ecec196a ccc52973'). + replace(/ /g, ''), 'hex'), + G: Buffer.from(('04' + + 'aa87ca22 be8b0537 8eb1c71e f320ad74' + + '6e1d3b62 8ba79b98 59f741e0 82542a38' + + '5502f25d bf55296c 3a545e38 72760ab7' + + '3617de4a 96262c6f 5d9e98bf 9292dc29' + + 'f8f41dbd 289a147c e9da3113 b5f0b8c0' + + '0a60b1ce 1d7e819d 7a431d7c 90ea0e5f'). + replace(/ /g, ''), 'hex') + }, + 'nistp521': { + size: 521, + pkcs8oid: '1.3.132.0.35', + p: Buffer.from(( + '01ffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffff').replace(/ /g, ''), 'hex'), + a: Buffer.from(('01FF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFC'). + replace(/ /g, ''), 'hex'), + b: Buffer.from(('51' + + '953eb961 8e1c9a1f 929a21a0 b68540ee' + + 'a2da725b 99b315f3 b8b48991 8ef109e1' + + '56193951 ec7e937b 1652c0bd 3bb1bf07' + + '3573df88 3d2c34f1 ef451fd4 6b503f00'). + replace(/ /g, ''), 'hex'), + s: Buffer.from(('00' + + 'd09e8800 291cb853 96cc6717 393284aa' + + 'a0da64ba').replace(/ /g, ''), 'hex'), + n: Buffer.from(('01ff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff fffffffa' + + '51868783 bf2f966b 7fcc0148 f709a5d0' + + '3bb5c9b8 899c47ae bb6fb71e 91386409'). + replace(/ /g, ''), 'hex'), + G: Buffer.from(('04' + + '00c6 858e06b7 0404e9cd 9e3ecb66 2395b442' + + '9c648139 053fb521 f828af60 6b4d3dba' + + 'a14b5e77 efe75928 fe1dc127 a2ffa8de' + + '3348b3c1 856a429b f97e7e31 c2e5bd66' + + '0118 39296a78 9a3bc004 5c8a5fb4 2c7d1bd9' + + '98f54449 579b4468 17afbd17 273e662c' + + '97ee7299 5ef42640 c550b901 3fad0761' + + '353c7086 a272c240 88be9476 9fd16650'). + replace(/ /g, ''), 'hex') + } +}; + +module.exports = { + info: algInfo, + privInfo: algPrivInfo, + hashAlgs: hashAlgs, + curves: curves +}; + + +/***/ }), +/* 33 */ +/***/ (function(module, exports, __webpack_require__) { + +// Copyright 2017 Joyent, Inc. + +module.exports = PrivateKey; + +var assert = __webpack_require__(16); +var Buffer = __webpack_require__(15).Buffer; +var algs = __webpack_require__(32); +var crypto = __webpack_require__(11); +var Fingerprint = __webpack_require__(156); +var Signature = __webpack_require__(75); +var errs = __webpack_require__(74); +var util = __webpack_require__(3); +var utils = __webpack_require__(26); +var dhe = __webpack_require__(325); +var generateECDSA = dhe.generateECDSA; +var generateED25519 = dhe.generateED25519; +var edCompat; +var nacl; + +try { + edCompat = __webpack_require__(454); +} catch (e) { + /* Just continue through, and bail out if we try to use it. */ +} + +var Key = __webpack_require__(27); + +var InvalidAlgorithmError = errs.InvalidAlgorithmError; +var KeyParseError = errs.KeyParseError; +var KeyEncryptedError = errs.KeyEncryptedError; + +var formats = {}; +formats['auto'] = __webpack_require__(455); +formats['pem'] = __webpack_require__(86); +formats['pkcs1'] = __webpack_require__(327); +formats['pkcs8'] = __webpack_require__(157); +formats['rfc4253'] = __webpack_require__(103); +formats['ssh-private'] = __webpack_require__(192); +formats['openssh'] = formats['ssh-private']; +formats['ssh'] = formats['ssh-private']; +formats['dnssec'] = __webpack_require__(326); + +function PrivateKey(opts) { + assert.object(opts, 'options'); + Key.call(this, opts); + + this._pubCache = undefined; +} +util.inherits(PrivateKey, Key); + +PrivateKey.formats = formats; + +PrivateKey.prototype.toBuffer = function (format, options) { + if (format === undefined) + format = 'pkcs1'; + assert.string(format, 'format'); + assert.object(formats[format], 'formats[format]'); + assert.optionalObject(options, 'options'); + + return (formats[format].write(this, options)); +}; + +PrivateKey.prototype.hash = function (algo) { + return (this.toPublic().hash(algo)); +}; + +PrivateKey.prototype.toPublic = function () { + if (this._pubCache) + return (this._pubCache); + + var algInfo = algs.info[this.type]; + var pubParts = []; + for (var i = 0; i < algInfo.parts.length; ++i) { + var p = algInfo.parts[i]; + pubParts.push(this.part[p]); + } + + this._pubCache = new Key({ + type: this.type, + source: this, + parts: pubParts + }); + if (this.comment) + this._pubCache.comment = this.comment; + return (this._pubCache); +}; + +PrivateKey.prototype.derive = function (newType) { + assert.string(newType, 'type'); + var priv, pub, pair; + + if (this.type === 'ed25519' && newType === 'curve25519') { + if (nacl === undefined) + nacl = __webpack_require__(76); + + priv = this.part.k.data; + if (priv[0] === 0x00) + priv = priv.slice(1); + + pair = nacl.box.keyPair.fromSecretKey(new Uint8Array(priv)); + pub = Buffer.from(pair.publicKey); + + return (new PrivateKey({ + type: 'curve25519', + parts: [ + { name: 'A', data: utils.mpNormalize(pub) }, + { name: 'k', data: utils.mpNormalize(priv) } + ] + })); + } else if (this.type === 'curve25519' && newType === 'ed25519') { + if (nacl === undefined) + nacl = __webpack_require__(76); + + priv = this.part.k.data; + if (priv[0] === 0x00) + priv = priv.slice(1); + + pair = nacl.sign.keyPair.fromSeed(new Uint8Array(priv)); + pub = Buffer.from(pair.publicKey); + + return (new PrivateKey({ + type: 'ed25519', + parts: [ + { name: 'A', data: utils.mpNormalize(pub) }, + { name: 'k', data: utils.mpNormalize(priv) } + ] + })); + } + throw (new Error('Key derivation not supported from ' + this.type + + ' to ' + newType)); +}; + +PrivateKey.prototype.createVerify = function (hashAlgo) { + return (this.toPublic().createVerify(hashAlgo)); +}; + +PrivateKey.prototype.createSign = function (hashAlgo) { + if (hashAlgo === undefined) + hashAlgo = this.defaultHashAlgorithm(); + assert.string(hashAlgo, 'hash algorithm'); + + /* ED25519 is not supported by OpenSSL, use a javascript impl. */ + if (this.type === 'ed25519' && edCompat !== undefined) + return (new edCompat.Signer(this, hashAlgo)); + if (this.type === 'curve25519') + throw (new Error('Curve25519 keys are not suitable for ' + + 'signing or verification')); + + var v, nm, err; + try { + nm = hashAlgo.toUpperCase(); + v = crypto.createSign(nm); + } catch (e) { + err = e; + } + if (v === undefined || (err instanceof Error && + err.message.match(/Unknown message digest/))) { + nm = 'RSA-'; + nm += hashAlgo.toUpperCase(); + v = crypto.createSign(nm); + } + assert.ok(v, 'failed to create verifier'); + var oldSign = v.sign.bind(v); + var key = this.toBuffer('pkcs1'); + var type = this.type; + var curve = this.curve; + v.sign = function () { + var sig = oldSign(key); + if (typeof (sig) === 'string') + sig = Buffer.from(sig, 'binary'); + sig = Signature.parse(sig, type, 'asn1'); + sig.hashAlgorithm = hashAlgo; + sig.curve = curve; + return (sig); + }; + return (v); +}; + +PrivateKey.parse = function (data, format, options) { + if (typeof (data) !== 'string') + assert.buffer(data, 'data'); + if (format === undefined) + format = 'auto'; + assert.string(format, 'format'); + if (typeof (options) === 'string') + options = { filename: options }; + assert.optionalObject(options, 'options'); + if (options === undefined) + options = {}; + assert.optionalString(options.filename, 'options.filename'); + if (options.filename === undefined) + options.filename = '(unnamed)'; + + assert.object(formats[format], 'formats[format]'); + + try { + var k = formats[format].read(data, options); + assert.ok(k instanceof PrivateKey, 'key is not a private key'); + if (!k.comment) + k.comment = options.filename; + return (k); + } catch (e) { + if (e.name === 'KeyEncryptedError') + throw (e); + throw (new KeyParseError(options.filename, format, e)); + } +}; + +PrivateKey.isPrivateKey = function (obj, ver) { + return (utils.isCompatible(obj, PrivateKey, ver)); +}; + +PrivateKey.generate = function (type, options) { + if (options === undefined) + options = {}; + assert.object(options, 'options'); + + switch (type) { + case 'ecdsa': + if (options.curve === undefined) + options.curve = 'nistp256'; + assert.string(options.curve, 'options.curve'); + return (generateECDSA(options.curve)); + case 'ed25519': + return (generateED25519()); + default: + throw (new Error('Key generation not supported with key ' + + 'type "' + type + '"')); + } +}; + +/* + * API versions for PrivateKey: + * [1,0] -- initial ver + * [1,1] -- added auto, pkcs[18], openssh/ssh-private formats + * [1,2] -- added defaultHashAlgorithm + * [1,3] -- added derive, ed, createDH + * [1,4] -- first tagged version + * [1,5] -- changed ed25519 part names and format + */ +PrivateKey.prototype._sshpkApiVersion = [1, 5]; + +PrivateKey._oldVersionDetect = function (obj) { + assert.func(obj.toPublic); + assert.func(obj.createSign); + if (obj.derive) + return ([1, 3]); + if (obj.defaultHashAlgorithm) + return ([1, 2]); + if (obj.formats['auto']) + return ([1, 1]); + return ([1, 0]); +}; + + +/***/ }), +/* 34 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.wrapLifecycle = exports.run = exports.install = exports.Install = undefined; + +var _extends2; + +function _load_extends() { + return _extends2 = _interopRequireDefault(__webpack_require__(21)); +} + +var _asyncToGenerator2; + +function _load_asyncToGenerator() { + return _asyncToGenerator2 = _interopRequireDefault(__webpack_require__(2)); +} + +let install = exports.install = (() => { + var _ref29 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (config, reporter, flags, lockfile) { + yield wrapLifecycle(config, flags, (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const install = new Install(flags, config, reporter, lockfile); + yield install.init(); + })); + }); + + return function install(_x7, _x8, _x9, _x10) { + return _ref29.apply(this, arguments); + }; +})(); + +let run = exports.run = (() => { + var _ref31 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (config, reporter, flags, args) { + let lockfile; + let error = 'installCommandRenamed'; + if (flags.lockfile === false) { + lockfile = new (_lockfile || _load_lockfile()).default(); + } else { + lockfile = yield (_lockfile || _load_lockfile()).default.fromDirectory(config.lockfileFolder, reporter); + } + + if (args.length) { + const exampleArgs = args.slice(); + + if (flags.saveDev) { + exampleArgs.push('--dev'); + } + if (flags.savePeer) { + exampleArgs.push('--peer'); + } + if (flags.saveOptional) { + exampleArgs.push('--optional'); + } + if (flags.saveExact) { + exampleArgs.push('--exact'); + } + if (flags.saveTilde) { + exampleArgs.push('--tilde'); + } + let command = 'add'; + if (flags.global) { + error = 'globalFlagRemoved'; + command = 'global add'; + } + throw new (_errors || _load_errors()).MessageError(reporter.lang(error, `yarn ${command} ${exampleArgs.join(' ')}`)); + } + + yield install(config, reporter, flags, lockfile); + }); + + return function run(_x11, _x12, _x13, _x14) { + return _ref31.apply(this, arguments); + }; +})(); + +let wrapLifecycle = exports.wrapLifecycle = (() => { + var _ref32 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (config, flags, factory) { + yield config.executeLifecycleScript('preinstall'); + + yield factory(); + + // npm behaviour, seems kinda funky but yay compatibility + yield config.executeLifecycleScript('install'); + yield config.executeLifecycleScript('postinstall'); + + if (!config.production) { + if (!config.disablePrepublish) { + yield config.executeLifecycleScript('prepublish'); + } + yield config.executeLifecycleScript('prepare'); + } + }); + + return function wrapLifecycle(_x15, _x16, _x17) { + return _ref32.apply(this, arguments); + }; +})(); + +exports.hasWrapper = hasWrapper; +exports.setFlags = setFlags; + +var _objectPath; + +function _load_objectPath() { + return _objectPath = _interopRequireDefault(__webpack_require__(304)); +} + +var _hooks; + +function _load_hooks() { + return _hooks = __webpack_require__(374); +} + +var _index; + +function _load_index() { + return _index = _interopRequireDefault(__webpack_require__(220)); +} + +var _errors; + +function _load_errors() { + return _errors = __webpack_require__(6); +} + +var _integrityChecker; + +function _load_integrityChecker() { + return _integrityChecker = _interopRequireDefault(__webpack_require__(208)); +} + +var _lockfile; + +function _load_lockfile() { + return _lockfile = _interopRequireDefault(__webpack_require__(19)); +} + +var _lockfile2; + +function _load_lockfile2() { + return _lockfile2 = __webpack_require__(19); +} + +var _packageFetcher; + +function _load_packageFetcher() { + return _packageFetcher = _interopRequireWildcard(__webpack_require__(210)); +} + +var _packageInstallScripts; + +function _load_packageInstallScripts() { + return _packageInstallScripts = _interopRequireDefault(__webpack_require__(557)); +} + +var _packageCompatibility; + +function _load_packageCompatibility() { + return _packageCompatibility = _interopRequireWildcard(__webpack_require__(209)); +} + +var _packageResolver; + +function _load_packageResolver() { + return _packageResolver = _interopRequireDefault(__webpack_require__(366)); +} + +var _packageLinker; + +function _load_packageLinker() { + return _packageLinker = _interopRequireDefault(__webpack_require__(211)); +} + +var _index2; + +function _load_index2() { + return _index2 = __webpack_require__(57); +} + +var _index3; + +function _load_index3() { + return _index3 = __webpack_require__(78); +} + +var _autoclean; + +function _load_autoclean() { + return _autoclean = __webpack_require__(354); +} + +var _constants; + +function _load_constants() { + return _constants = _interopRequireWildcard(__webpack_require__(8)); +} + +var _normalizePattern; + +function _load_normalizePattern() { + return _normalizePattern = __webpack_require__(37); +} + +var _fs; + +function _load_fs() { + return _fs = _interopRequireWildcard(__webpack_require__(4)); +} + +var _map; + +function _load_map() { + return _map = _interopRequireDefault(__webpack_require__(29)); +} + +var _yarnVersion; + +function _load_yarnVersion() { + return _yarnVersion = __webpack_require__(120); +} + +var _generatePnpMap; + +function _load_generatePnpMap() { + return _generatePnpMap = __webpack_require__(579); +} + +var _workspaceLayout; + +function _load_workspaceLayout() { + return _workspaceLayout = _interopRequireDefault(__webpack_require__(90)); +} + +var _resolutionMap; + +function _load_resolutionMap() { + return _resolutionMap = _interopRequireDefault(__webpack_require__(214)); +} + +var _guessName; + +function _load_guessName() { + return _guessName = _interopRequireDefault(__webpack_require__(169)); +} + +var _audit; + +function _load_audit() { + return _audit = _interopRequireDefault(__webpack_require__(353)); +} + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const deepEqual = __webpack_require__(631); + +const emoji = __webpack_require__(302); +const invariant = __webpack_require__(9); +const path = __webpack_require__(0); +const semver = __webpack_require__(22); +const uuid = __webpack_require__(119); +const ssri = __webpack_require__(65); + +const ONE_DAY = 1000 * 60 * 60 * 24; + +/** + * Try and detect the installation method for Yarn and provide a command to update it with. + */ + +function getUpdateCommand(installationMethod) { + if (installationMethod === 'tar') { + return `curl --compressed -o- -L ${(_constants || _load_constants()).YARN_INSTALLER_SH} | bash`; + } + + if (installationMethod === 'homebrew') { + return 'brew upgrade yarn'; + } + + if (installationMethod === 'deb') { + return 'sudo apt-get update && sudo apt-get install yarn'; + } + + if (installationMethod === 'rpm') { + return 'sudo yum install yarn'; + } + + if (installationMethod === 'npm') { + return 'npm install --global yarn'; + } + + if (installationMethod === 'chocolatey') { + return 'choco upgrade yarn'; + } + + if (installationMethod === 'apk') { + return 'apk update && apk add -u yarn'; + } + + if (installationMethod === 'portage') { + return 'sudo emerge --sync && sudo emerge -au sys-apps/yarn'; + } + + return null; +} + +function getUpdateInstaller(installationMethod) { + // Windows + if (installationMethod === 'msi') { + return (_constants || _load_constants()).YARN_INSTALLER_MSI; + } + + return null; +} + +function normalizeFlags(config, rawFlags) { + const flags = { + // install + har: !!rawFlags.har, + ignorePlatform: !!rawFlags.ignorePlatform, + ignoreEngines: !!rawFlags.ignoreEngines, + ignoreScripts: !!rawFlags.ignoreScripts, + ignoreOptional: !!rawFlags.ignoreOptional, + force: !!rawFlags.force, + flat: !!rawFlags.flat, + lockfile: rawFlags.lockfile !== false, + pureLockfile: !!rawFlags.pureLockfile, + updateChecksums: !!rawFlags.updateChecksums, + skipIntegrityCheck: !!rawFlags.skipIntegrityCheck, + frozenLockfile: !!rawFlags.frozenLockfile, + linkDuplicates: !!rawFlags.linkDuplicates, + checkFiles: !!rawFlags.checkFiles, + audit: !!rawFlags.audit, + + // add + peer: !!rawFlags.peer, + dev: !!rawFlags.dev, + optional: !!rawFlags.optional, + exact: !!rawFlags.exact, + tilde: !!rawFlags.tilde, + ignoreWorkspaceRootCheck: !!rawFlags.ignoreWorkspaceRootCheck, + + // outdated, update-interactive + includeWorkspaceDeps: !!rawFlags.includeWorkspaceDeps, + + // add, remove, update + workspaceRootIsCwd: rawFlags.workspaceRootIsCwd !== false + }; + + if (config.getOption('ignore-scripts')) { + flags.ignoreScripts = true; + } + + if (config.getOption('ignore-platform')) { + flags.ignorePlatform = true; + } + + if (config.getOption('ignore-engines')) { + flags.ignoreEngines = true; + } + + if (config.getOption('ignore-optional')) { + flags.ignoreOptional = true; + } + + if (config.getOption('force')) { + flags.force = true; + } + + return flags; +} + +class Install { + constructor(flags, config, reporter, lockfile) { + this.rootManifestRegistries = []; + this.rootPatternsToOrigin = (0, (_map || _load_map()).default)(); + this.lockfile = lockfile; + this.reporter = reporter; + this.config = config; + this.flags = normalizeFlags(config, flags); + this.resolutions = (0, (_map || _load_map()).default)(); // Legacy resolutions field used for flat install mode + this.resolutionMap = new (_resolutionMap || _load_resolutionMap()).default(config); // Selective resolutions for nested dependencies + this.resolver = new (_packageResolver || _load_packageResolver()).default(config, lockfile, this.resolutionMap); + this.integrityChecker = new (_integrityChecker || _load_integrityChecker()).default(config); + this.linker = new (_packageLinker || _load_packageLinker()).default(config, this.resolver); + this.scripts = new (_packageInstallScripts || _load_packageInstallScripts()).default(config, this.resolver, this.flags.force); + } + + /** + * Create a list of dependency requests from the current directories manifests. + */ + + fetchRequestFromCwd(excludePatterns = [], ignoreUnusedPatterns = false) { + var _this = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const patterns = []; + const deps = []; + let resolutionDeps = []; + const manifest = {}; + + const ignorePatterns = []; + const usedPatterns = []; + let workspaceLayout; + + // some commands should always run in the context of the entire workspace + const cwd = _this.flags.includeWorkspaceDeps || _this.flags.workspaceRootIsCwd ? _this.config.lockfileFolder : _this.config.cwd; + + // non-workspaces are always root, otherwise check for workspace root + const cwdIsRoot = !_this.config.workspaceRootFolder || _this.config.lockfileFolder === cwd; + + // exclude package names that are in install args + const excludeNames = []; + for (var _iterator = excludePatterns, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { + var _ref; + + if (_isArray) { + if (_i >= _iterator.length) break; + _ref = _iterator[_i++]; + } else { + _i = _iterator.next(); + if (_i.done) break; + _ref = _i.value; + } + + const pattern = _ref; + + if ((0, (_index3 || _load_index3()).getExoticResolver)(pattern)) { + excludeNames.push((0, (_guessName || _load_guessName()).default)(pattern)); + } else { + // extract the name + const parts = (0, (_normalizePattern || _load_normalizePattern()).normalizePattern)(pattern); + excludeNames.push(parts.name); + } + } + + const stripExcluded = function stripExcluded(manifest) { + for (var _iterator2 = excludeNames, _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { + var _ref2; + + if (_isArray2) { + if (_i2 >= _iterator2.length) break; + _ref2 = _iterator2[_i2++]; + } else { + _i2 = _iterator2.next(); + if (_i2.done) break; + _ref2 = _i2.value; + } + + const exclude = _ref2; + + if (manifest.dependencies && manifest.dependencies[exclude]) { + delete manifest.dependencies[exclude]; + } + if (manifest.devDependencies && manifest.devDependencies[exclude]) { + delete manifest.devDependencies[exclude]; + } + if (manifest.optionalDependencies && manifest.optionalDependencies[exclude]) { + delete manifest.optionalDependencies[exclude]; + } + } + }; + + for (var _iterator3 = Object.keys((_index2 || _load_index2()).registries), _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) { + var _ref3; + + if (_isArray3) { + if (_i3 >= _iterator3.length) break; + _ref3 = _iterator3[_i3++]; + } else { + _i3 = _iterator3.next(); + if (_i3.done) break; + _ref3 = _i3.value; + } + + const registry = _ref3; + + const filename = (_index2 || _load_index2()).registries[registry].filename; + + const loc = path.join(cwd, filename); + if (!(yield (_fs || _load_fs()).exists(loc))) { + continue; + } + + _this.rootManifestRegistries.push(registry); + + const projectManifestJson = yield _this.config.readJson(loc); + yield (0, (_index || _load_index()).default)(projectManifestJson, cwd, _this.config, cwdIsRoot); + + Object.assign(_this.resolutions, projectManifestJson.resolutions); + Object.assign(manifest, projectManifestJson); + + _this.resolutionMap.init(_this.resolutions); + for (var _iterator4 = Object.keys(_this.resolutionMap.resolutionsByPackage), _isArray4 = Array.isArray(_iterator4), _i4 = 0, _iterator4 = _isArray4 ? _iterator4 : _iterator4[Symbol.iterator]();;) { + var _ref4; + + if (_isArray4) { + if (_i4 >= _iterator4.length) break; + _ref4 = _iterator4[_i4++]; + } else { + _i4 = _iterator4.next(); + if (_i4.done) break; + _ref4 = _i4.value; + } + + const packageName = _ref4; + + const optional = (_objectPath || _load_objectPath()).default.has(manifest.optionalDependencies, packageName) && _this.flags.ignoreOptional; + for (var _iterator8 = _this.resolutionMap.resolutionsByPackage[packageName], _isArray8 = Array.isArray(_iterator8), _i8 = 0, _iterator8 = _isArray8 ? _iterator8 : _iterator8[Symbol.iterator]();;) { + var _ref9; + + if (_isArray8) { + if (_i8 >= _iterator8.length) break; + _ref9 = _iterator8[_i8++]; + } else { + _i8 = _iterator8.next(); + if (_i8.done) break; + _ref9 = _i8.value; + } + + const _ref8 = _ref9; + const pattern = _ref8.pattern; + + resolutionDeps = [...resolutionDeps, { registry, pattern, optional, hint: 'resolution' }]; + } + } + + const pushDeps = function pushDeps(depType, manifest, { hint, optional }, isUsed) { + if (ignoreUnusedPatterns && !isUsed) { + return; + } + // We only take unused dependencies into consideration to get deterministic hoisting. + // Since flat mode doesn't care about hoisting and everything is top level and specified then we can safely + // leave these out. + if (_this.flags.flat && !isUsed) { + return; + } + const depMap = manifest[depType]; + for (const name in depMap) { + if (excludeNames.indexOf(name) >= 0) { + continue; + } + + let pattern = name; + if (!_this.lockfile.getLocked(pattern)) { + // when we use --save we save the dependency to the lockfile with just the name rather than the + // version combo + pattern += '@' + depMap[name]; + } + + // normalization made sure packages are mentioned only once + if (isUsed) { + usedPatterns.push(pattern); + } else { + ignorePatterns.push(pattern); + } + + _this.rootPatternsToOrigin[pattern] = depType; + patterns.push(pattern); + deps.push({ pattern, registry, hint, optional, workspaceName: manifest.name, workspaceLoc: manifest._loc }); + } + }; + + if (cwdIsRoot) { + pushDeps('dependencies', projectManifestJson, { hint: null, optional: false }, true); + pushDeps('devDependencies', projectManifestJson, { hint: 'dev', optional: false }, !_this.config.production); + pushDeps('optionalDependencies', projectManifestJson, { hint: 'optional', optional: true }, true); + } + + if (_this.config.workspaceRootFolder) { + const workspaceLoc = cwdIsRoot ? loc : path.join(_this.config.lockfileFolder, filename); + const workspacesRoot = path.dirname(workspaceLoc); + + let workspaceManifestJson = projectManifestJson; + if (!cwdIsRoot) { + // the manifest we read before was a child workspace, so get the root + workspaceManifestJson = yield _this.config.readJson(workspaceLoc); + yield (0, (_index || _load_index()).default)(workspaceManifestJson, workspacesRoot, _this.config, true); + } + + const workspaces = yield _this.config.resolveWorkspaces(workspacesRoot, workspaceManifestJson); + workspaceLayout = new (_workspaceLayout || _load_workspaceLayout()).default(workspaces, _this.config); + + // add virtual manifest that depends on all workspaces, this way package hoisters and resolvers will work fine + const workspaceDependencies = (0, (_extends2 || _load_extends()).default)({}, workspaceManifestJson.dependencies); + for (var _iterator5 = Object.keys(workspaces), _isArray5 = Array.isArray(_iterator5), _i5 = 0, _iterator5 = _isArray5 ? _iterator5 : _iterator5[Symbol.iterator]();;) { + var _ref5; + + if (_isArray5) { + if (_i5 >= _iterator5.length) break; + _ref5 = _iterator5[_i5++]; + } else { + _i5 = _iterator5.next(); + if (_i5.done) break; + _ref5 = _i5.value; + } + + const workspaceName = _ref5; + + const workspaceManifest = workspaces[workspaceName].manifest; + workspaceDependencies[workspaceName] = workspaceManifest.version; + + // include dependencies from all workspaces + if (_this.flags.includeWorkspaceDeps) { + pushDeps('dependencies', workspaceManifest, { hint: null, optional: false }, true); + pushDeps('devDependencies', workspaceManifest, { hint: 'dev', optional: false }, !_this.config.production); + pushDeps('optionalDependencies', workspaceManifest, { hint: 'optional', optional: true }, true); + } + } + const virtualDependencyManifest = { + _uid: '', + name: `workspace-aggregator-${uuid.v4()}`, + version: '1.0.0', + _registry: 'npm', + _loc: workspacesRoot, + dependencies: workspaceDependencies, + devDependencies: (0, (_extends2 || _load_extends()).default)({}, workspaceManifestJson.devDependencies), + optionalDependencies: (0, (_extends2 || _load_extends()).default)({}, workspaceManifestJson.optionalDependencies), + private: workspaceManifestJson.private, + workspaces: workspaceManifestJson.workspaces + }; + workspaceLayout.virtualManifestName = virtualDependencyManifest.name; + const virtualDep = {}; + virtualDep[virtualDependencyManifest.name] = virtualDependencyManifest.version; + workspaces[virtualDependencyManifest.name] = { loc: workspacesRoot, manifest: virtualDependencyManifest }; + + // ensure dependencies that should be excluded are stripped from the correct manifest + stripExcluded(cwdIsRoot ? virtualDependencyManifest : workspaces[projectManifestJson.name].manifest); + + pushDeps('workspaces', { workspaces: virtualDep }, { hint: 'workspaces', optional: false }, true); + + const implicitWorkspaceDependencies = (0, (_extends2 || _load_extends()).default)({}, workspaceDependencies); + + for (var _iterator6 = (_constants || _load_constants()).OWNED_DEPENDENCY_TYPES, _isArray6 = Array.isArray(_iterator6), _i6 = 0, _iterator6 = _isArray6 ? _iterator6 : _iterator6[Symbol.iterator]();;) { + var _ref6; + + if (_isArray6) { + if (_i6 >= _iterator6.length) break; + _ref6 = _iterator6[_i6++]; + } else { + _i6 = _iterator6.next(); + if (_i6.done) break; + _ref6 = _i6.value; + } + + const type = _ref6; + + for (var _iterator7 = Object.keys(projectManifestJson[type] || {}), _isArray7 = Array.isArray(_iterator7), _i7 = 0, _iterator7 = _isArray7 ? _iterator7 : _iterator7[Symbol.iterator]();;) { + var _ref7; + + if (_isArray7) { + if (_i7 >= _iterator7.length) break; + _ref7 = _iterator7[_i7++]; + } else { + _i7 = _iterator7.next(); + if (_i7.done) break; + _ref7 = _i7.value; + } + + const dependencyName = _ref7; + + delete implicitWorkspaceDependencies[dependencyName]; + } + } + + pushDeps('dependencies', { dependencies: implicitWorkspaceDependencies }, { hint: 'workspaces', optional: false }, true); + } + + break; + } + + // inherit root flat flag + if (manifest.flat) { + _this.flags.flat = true; + } + + return { + requests: [...resolutionDeps, ...deps], + patterns, + manifest, + usedPatterns, + ignorePatterns, + workspaceLayout + }; + })(); + } + + /** + * TODO description + */ + + prepareRequests(requests) { + return requests; + } + + preparePatterns(patterns) { + return patterns; + } + preparePatternsForLinking(patterns, cwdManifest, cwdIsRoot) { + return patterns; + } + + prepareManifests() { + var _this2 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const manifests = yield _this2.config.getRootManifests(); + return manifests; + })(); + } + + bailout(patterns, workspaceLayout) { + var _this3 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + // We don't want to skip the audit - it could yield important errors + if (_this3.flags.audit) { + return false; + } + // PNP is so fast that the integrity check isn't pertinent + if (_this3.config.plugnplayEnabled) { + return false; + } + if (_this3.flags.skipIntegrityCheck || _this3.flags.force) { + return false; + } + const lockfileCache = _this3.lockfile.cache; + if (!lockfileCache) { + return false; + } + const lockfileClean = _this3.lockfile.parseResultType === 'success'; + const match = yield _this3.integrityChecker.check(patterns, lockfileCache, _this3.flags, workspaceLayout); + if (_this3.flags.frozenLockfile && (!lockfileClean || match.missingPatterns.length > 0)) { + throw new (_errors || _load_errors()).MessageError(_this3.reporter.lang('frozenLockfileError')); + } + + const haveLockfile = yield (_fs || _load_fs()).exists(path.join(_this3.config.lockfileFolder, (_constants || _load_constants()).LOCKFILE_FILENAME)); + + const lockfileIntegrityPresent = !_this3.lockfile.hasEntriesExistWithoutIntegrity(); + const integrityBailout = lockfileIntegrityPresent || !_this3.config.autoAddIntegrity; + + if (match.integrityMatches && haveLockfile && lockfileClean && integrityBailout) { + _this3.reporter.success(_this3.reporter.lang('upToDate')); + return true; + } + + if (match.integrityFileMissing && haveLockfile) { + // Integrity file missing, force script installations + _this3.scripts.setForce(true); + return false; + } + + if (match.hardRefreshRequired) { + // e.g. node version doesn't match, force script installations + _this3.scripts.setForce(true); + return false; + } + + if (!patterns.length && !match.integrityFileMissing) { + _this3.reporter.success(_this3.reporter.lang('nothingToInstall')); + yield _this3.createEmptyManifestFolders(); + yield _this3.saveLockfileAndIntegrity(patterns, workspaceLayout); + return true; + } + + return false; + })(); + } + + /** + * Produce empty folders for all used root manifests. + */ + + createEmptyManifestFolders() { + var _this4 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + if (_this4.config.modulesFolder) { + // already created + return; + } + + for (var _iterator9 = _this4.rootManifestRegistries, _isArray9 = Array.isArray(_iterator9), _i9 = 0, _iterator9 = _isArray9 ? _iterator9 : _iterator9[Symbol.iterator]();;) { + var _ref10; + + if (_isArray9) { + if (_i9 >= _iterator9.length) break; + _ref10 = _iterator9[_i9++]; + } else { + _i9 = _iterator9.next(); + if (_i9.done) break; + _ref10 = _i9.value; + } + + const registryName = _ref10; + const folder = _this4.config.registries[registryName].folder; + + yield (_fs || _load_fs()).mkdirp(path.join(_this4.config.lockfileFolder, folder)); + } + })(); + } + + /** + * TODO description + */ + + markIgnored(patterns) { + for (var _iterator10 = patterns, _isArray10 = Array.isArray(_iterator10), _i10 = 0, _iterator10 = _isArray10 ? _iterator10 : _iterator10[Symbol.iterator]();;) { + var _ref11; + + if (_isArray10) { + if (_i10 >= _iterator10.length) break; + _ref11 = _iterator10[_i10++]; + } else { + _i10 = _iterator10.next(); + if (_i10.done) break; + _ref11 = _i10.value; + } + + const pattern = _ref11; + + const manifest = this.resolver.getStrictResolvedPattern(pattern); + const ref = manifest._reference; + invariant(ref, 'expected package reference'); + + // just mark the package as ignored. if the package is used by a required package, the hoister + // will take care of that. + ref.ignore = true; + } + } + + /** + * helper method that gets only recent manifests + * used by global.ls command + */ + getFlattenedDeps() { + var _this5 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + var _ref12 = yield _this5.fetchRequestFromCwd(); + + const depRequests = _ref12.requests, + rawPatterns = _ref12.patterns; + + + yield _this5.resolver.init(depRequests, {}); + + const manifests = yield (_packageFetcher || _load_packageFetcher()).fetch(_this5.resolver.getManifests(), _this5.config); + _this5.resolver.updateManifests(manifests); + + return _this5.flatten(rawPatterns); + })(); + } + + /** + * TODO description + */ + + init() { + var _this6 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + _this6.checkUpdate(); + + // warn if we have a shrinkwrap + if (yield (_fs || _load_fs()).exists(path.join(_this6.config.lockfileFolder, (_constants || _load_constants()).NPM_SHRINKWRAP_FILENAME))) { + _this6.reporter.warn(_this6.reporter.lang('shrinkwrapWarning')); + } + + // warn if we have an npm lockfile + if (yield (_fs || _load_fs()).exists(path.join(_this6.config.lockfileFolder, (_constants || _load_constants()).NPM_LOCK_FILENAME))) { + _this6.reporter.warn(_this6.reporter.lang('npmLockfileWarning')); + } + + if (_this6.config.plugnplayEnabled) { + _this6.reporter.info(_this6.reporter.lang('plugnplaySuggestV2L1')); + _this6.reporter.info(_this6.reporter.lang('plugnplaySuggestV2L2')); + } + + let flattenedTopLevelPatterns = []; + const steps = []; + + var _ref13 = yield _this6.fetchRequestFromCwd(); + + const depRequests = _ref13.requests, + rawPatterns = _ref13.patterns, + ignorePatterns = _ref13.ignorePatterns, + workspaceLayout = _ref13.workspaceLayout, + manifest = _ref13.manifest; + + let topLevelPatterns = []; + + const artifacts = yield _this6.integrityChecker.getArtifacts(); + if (artifacts) { + _this6.linker.setArtifacts(artifacts); + _this6.scripts.setArtifacts(artifacts); + } + + if ((_packageCompatibility || _load_packageCompatibility()).shouldCheck(manifest, _this6.flags)) { + steps.push((() => { + var _ref14 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (curr, total) { + _this6.reporter.step(curr, total, _this6.reporter.lang('checkingManifest'), emoji.get('mag')); + yield _this6.checkCompatibility(); + }); + + return function (_x, _x2) { + return _ref14.apply(this, arguments); + }; + })()); + } + + const audit = new (_audit || _load_audit()).default(_this6.config, _this6.reporter, { groups: (_constants || _load_constants()).OWNED_DEPENDENCY_TYPES }); + let auditFoundProblems = false; + + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('resolveStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + _this6.reporter.step(curr, total, _this6.reporter.lang('resolvingPackages'), emoji.get('mag')); + yield _this6.resolver.init(_this6.prepareRequests(depRequests), { + isFlat: _this6.flags.flat, + isFrozen: _this6.flags.frozenLockfile, + workspaceLayout + }); + topLevelPatterns = _this6.preparePatterns(rawPatterns); + flattenedTopLevelPatterns = yield _this6.flatten(topLevelPatterns); + return { bailout: !_this6.flags.audit && (yield _this6.bailout(topLevelPatterns, workspaceLayout)) }; + })); + }); + + if (_this6.flags.audit) { + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('auditStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + _this6.reporter.step(curr, total, _this6.reporter.lang('auditRunning'), emoji.get('mag')); + if (_this6.flags.offline) { + _this6.reporter.warn(_this6.reporter.lang('auditOffline')); + return { bailout: false }; + } + const preparedManifests = yield _this6.prepareManifests(); + // $FlowFixMe - Flow considers `m` in the map operation to be "mixed", so does not recognize `m.object` + const mergedManifest = Object.assign({}, ...Object.values(preparedManifests).map(function (m) { + return m.object; + })); + const auditVulnerabilityCounts = yield audit.performAudit(mergedManifest, _this6.lockfile, _this6.resolver, _this6.linker, topLevelPatterns); + auditFoundProblems = auditVulnerabilityCounts.info || auditVulnerabilityCounts.low || auditVulnerabilityCounts.moderate || auditVulnerabilityCounts.high || auditVulnerabilityCounts.critical; + return { bailout: yield _this6.bailout(topLevelPatterns, workspaceLayout) }; + })); + }); + } + + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('fetchStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + _this6.markIgnored(ignorePatterns); + _this6.reporter.step(curr, total, _this6.reporter.lang('fetchingPackages'), emoji.get('truck')); + const manifests = yield (_packageFetcher || _load_packageFetcher()).fetch(_this6.resolver.getManifests(), _this6.config); + _this6.resolver.updateManifests(manifests); + yield (_packageCompatibility || _load_packageCompatibility()).check(_this6.resolver.getManifests(), _this6.config, _this6.flags.ignoreEngines); + })); + }); + + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('linkStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + // remove integrity hash to make this operation atomic + yield _this6.integrityChecker.removeIntegrityFile(); + _this6.reporter.step(curr, total, _this6.reporter.lang('linkingDependencies'), emoji.get('link')); + flattenedTopLevelPatterns = _this6.preparePatternsForLinking(flattenedTopLevelPatterns, manifest, _this6.config.lockfileFolder === _this6.config.cwd); + yield _this6.linker.init(flattenedTopLevelPatterns, workspaceLayout, { + linkDuplicates: _this6.flags.linkDuplicates, + ignoreOptional: _this6.flags.ignoreOptional + }); + })); + }); + + if (_this6.config.plugnplayEnabled) { + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('pnpStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const pnpPath = `${_this6.config.lockfileFolder}/${(_constants || _load_constants()).PNP_FILENAME}`; + + const code = yield (0, (_generatePnpMap || _load_generatePnpMap()).generatePnpMap)(_this6.config, flattenedTopLevelPatterns, { + resolver: _this6.resolver, + reporter: _this6.reporter, + targetPath: pnpPath, + workspaceLayout + }); + + try { + const file = yield (_fs || _load_fs()).readFile(pnpPath); + if (file === code) { + return; + } + } catch (error) {} + + yield (_fs || _load_fs()).writeFile(pnpPath, code); + yield (_fs || _load_fs()).chmod(pnpPath, 0o755); + })); + }); + } + + steps.push(function (curr, total) { + return (0, (_hooks || _load_hooks()).callThroughHook)('buildStep', (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + _this6.reporter.step(curr, total, _this6.flags.force ? _this6.reporter.lang('rebuildingPackages') : _this6.reporter.lang('buildingFreshPackages'), emoji.get('hammer')); + + if (_this6.config.ignoreScripts) { + _this6.reporter.warn(_this6.reporter.lang('ignoredScripts')); + } else { + yield _this6.scripts.init(flattenedTopLevelPatterns); + } + })); + }); + + if (_this6.flags.har) { + steps.push((() => { + var _ref21 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (curr, total) { + const formattedDate = new Date().toISOString().replace(/:/g, '-'); + const filename = `yarn-install_${formattedDate}.har`; + _this6.reporter.step(curr, total, _this6.reporter.lang('savingHar', filename), emoji.get('black_circle_for_record')); + yield _this6.config.requestManager.saveHar(filename); + }); + + return function (_x3, _x4) { + return _ref21.apply(this, arguments); + }; + })()); + } + + if (yield _this6.shouldClean()) { + steps.push((() => { + var _ref22 = (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* (curr, total) { + _this6.reporter.step(curr, total, _this6.reporter.lang('cleaningModules'), emoji.get('recycle')); + yield (0, (_autoclean || _load_autoclean()).clean)(_this6.config, _this6.reporter); + }); + + return function (_x5, _x6) { + return _ref22.apply(this, arguments); + }; + })()); + } + + let currentStep = 0; + for (var _iterator11 = steps, _isArray11 = Array.isArray(_iterator11), _i11 = 0, _iterator11 = _isArray11 ? _iterator11 : _iterator11[Symbol.iterator]();;) { + var _ref23; + + if (_isArray11) { + if (_i11 >= _iterator11.length) break; + _ref23 = _iterator11[_i11++]; + } else { + _i11 = _iterator11.next(); + if (_i11.done) break; + _ref23 = _i11.value; + } + + const step = _ref23; + + const stepResult = yield step(++currentStep, steps.length); + if (stepResult && stepResult.bailout) { + if (_this6.flags.audit) { + audit.summary(); + } + if (auditFoundProblems) { + _this6.reporter.warn(_this6.reporter.lang('auditRunAuditForDetails')); + } + _this6.maybeOutputUpdate(); + return flattenedTopLevelPatterns; + } + } + + // fin! + if (_this6.flags.audit) { + audit.summary(); + } + if (auditFoundProblems) { + _this6.reporter.warn(_this6.reporter.lang('auditRunAuditForDetails')); + } + yield _this6.saveLockfileAndIntegrity(topLevelPatterns, workspaceLayout); + yield _this6.persistChanges(); + _this6.maybeOutputUpdate(); + _this6.config.requestManager.clearCache(); + return flattenedTopLevelPatterns; + })(); + } + + checkCompatibility() { + var _this7 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + var _ref24 = yield _this7.fetchRequestFromCwd(); + + const manifest = _ref24.manifest; + + yield (_packageCompatibility || _load_packageCompatibility()).checkOne(manifest, _this7.config, _this7.flags.ignoreEngines); + })(); + } + + persistChanges() { + var _this8 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + // get all the different registry manifests in this folder + const manifests = yield _this8.config.getRootManifests(); + + if (yield _this8.applyChanges(manifests)) { + yield _this8.config.saveRootManifests(manifests); + } + })(); + } + + applyChanges(manifests) { + let hasChanged = false; + + if (this.config.plugnplayPersist) { + const object = manifests.npm.object; + + + if (typeof object.installConfig !== 'object') { + object.installConfig = {}; + } + + if (this.config.plugnplayEnabled && object.installConfig.pnp !== true) { + object.installConfig.pnp = true; + hasChanged = true; + } else if (!this.config.plugnplayEnabled && typeof object.installConfig.pnp !== 'undefined') { + delete object.installConfig.pnp; + hasChanged = true; + } + + if (Object.keys(object.installConfig).length === 0) { + delete object.installConfig; + } + } + + return Promise.resolve(hasChanged); + } + + /** + * Check if we should run the cleaning step. + */ + + shouldClean() { + return (_fs || _load_fs()).exists(path.join(this.config.lockfileFolder, (_constants || _load_constants()).CLEAN_FILENAME)); + } + + /** + * TODO + */ + + flatten(patterns) { + var _this9 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + if (!_this9.flags.flat) { + return patterns; + } + + const flattenedPatterns = []; + + for (var _iterator12 = _this9.resolver.getAllDependencyNamesByLevelOrder(patterns), _isArray12 = Array.isArray(_iterator12), _i12 = 0, _iterator12 = _isArray12 ? _iterator12 : _iterator12[Symbol.iterator]();;) { + var _ref25; + + if (_isArray12) { + if (_i12 >= _iterator12.length) break; + _ref25 = _iterator12[_i12++]; + } else { + _i12 = _iterator12.next(); + if (_i12.done) break; + _ref25 = _i12.value; + } + + const name = _ref25; + + const infos = _this9.resolver.getAllInfoForPackageName(name).filter(function (manifest) { + const ref = manifest._reference; + invariant(ref, 'expected package reference'); + return !ref.ignore; + }); + + if (infos.length === 0) { + continue; + } + + if (infos.length === 1) { + // single version of this package + // take out a single pattern as multiple patterns may have resolved to this package + flattenedPatterns.push(_this9.resolver.patternsByPackage[name][0]); + continue; + } + + const options = infos.map(function (info) { + const ref = info._reference; + invariant(ref, 'expected reference'); + return { + // TODO `and is required by {PARENT}`, + name: _this9.reporter.lang('manualVersionResolutionOption', ref.patterns.join(', '), info.version), + + value: info.version + }; + }); + const versions = infos.map(function (info) { + return info.version; + }); + let version; + + const resolutionVersion = _this9.resolutions[name]; + if (resolutionVersion && versions.indexOf(resolutionVersion) >= 0) { + // use json `resolution` version + version = resolutionVersion; + } else { + version = yield _this9.reporter.select(_this9.reporter.lang('manualVersionResolution', name), _this9.reporter.lang('answer'), options); + _this9.resolutions[name] = version; + } + + flattenedPatterns.push(_this9.resolver.collapseAllVersionsOfPackage(name, version)); + } + + // save resolutions to their appropriate root manifest + if (Object.keys(_this9.resolutions).length) { + const manifests = yield _this9.config.getRootManifests(); + + for (const name in _this9.resolutions) { + const version = _this9.resolutions[name]; + + const patterns = _this9.resolver.patternsByPackage[name]; + if (!patterns) { + continue; + } + + let manifest; + for (var _iterator13 = patterns, _isArray13 = Array.isArray(_iterator13), _i13 = 0, _iterator13 = _isArray13 ? _iterator13 : _iterator13[Symbol.iterator]();;) { + var _ref26; + + if (_isArray13) { + if (_i13 >= _iterator13.length) break; + _ref26 = _iterator13[_i13++]; + } else { + _i13 = _iterator13.next(); + if (_i13.done) break; + _ref26 = _i13.value; + } + + const pattern = _ref26; + + manifest = _this9.resolver.getResolvedPattern(pattern); + if (manifest) { + break; + } + } + invariant(manifest, 'expected manifest'); + + const ref = manifest._reference; + invariant(ref, 'expected reference'); + + const object = manifests[ref.registry].object; + object.resolutions = object.resolutions || {}; + object.resolutions[name] = version; + } + + yield _this9.config.saveRootManifests(manifests); + } + + return flattenedPatterns; + })(); + } + + /** + * Remove offline tarballs that are no longer required + */ + + pruneOfflineMirror(lockfile) { + var _this10 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const mirror = _this10.config.getOfflineMirrorPath(); + if (!mirror) { + return; + } + + const requiredTarballs = new Set(); + for (const dependency in lockfile) { + const resolved = lockfile[dependency].resolved; + if (resolved) { + const basename = path.basename(resolved.split('#')[0]); + if (dependency[0] === '@' && basename[0] !== '@') { + requiredTarballs.add(`${dependency.split('/')[0]}-${basename}`); + } + requiredTarballs.add(basename); + } + } + + const mirrorFiles = yield (_fs || _load_fs()).walk(mirror); + for (var _iterator14 = mirrorFiles, _isArray14 = Array.isArray(_iterator14), _i14 = 0, _iterator14 = _isArray14 ? _iterator14 : _iterator14[Symbol.iterator]();;) { + var _ref27; + + if (_isArray14) { + if (_i14 >= _iterator14.length) break; + _ref27 = _iterator14[_i14++]; + } else { + _i14 = _iterator14.next(); + if (_i14.done) break; + _ref27 = _i14.value; + } + + const file = _ref27; + + const isTarball = path.extname(file.basename) === '.tgz'; + // if using experimental-pack-script-packages-in-mirror flag, don't unlink prebuilt packages + const hasPrebuiltPackage = file.relative.startsWith('prebuilt/'); + if (isTarball && !hasPrebuiltPackage && !requiredTarballs.has(file.basename)) { + yield (_fs || _load_fs()).unlink(file.absolute); + } + } + })(); + } + + /** + * Save updated integrity and lockfiles. + */ + + saveLockfileAndIntegrity(patterns, workspaceLayout) { + var _this11 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const resolvedPatterns = {}; + Object.keys(_this11.resolver.patterns).forEach(function (pattern) { + if (!workspaceLayout || !workspaceLayout.getManifestByPattern(pattern)) { + resolvedPatterns[pattern] = _this11.resolver.patterns[pattern]; + } + }); + + // TODO this code is duplicated in a few places, need a common way to filter out workspace patterns from lockfile + patterns = patterns.filter(function (p) { + return !workspaceLayout || !workspaceLayout.getManifestByPattern(p); + }); + + const lockfileBasedOnResolver = _this11.lockfile.getLockfile(resolvedPatterns); + + if (_this11.config.pruneOfflineMirror) { + yield _this11.pruneOfflineMirror(lockfileBasedOnResolver); + } + + // write integrity hash + if (!_this11.config.plugnplayEnabled) { + yield _this11.integrityChecker.save(patterns, lockfileBasedOnResolver, _this11.flags, workspaceLayout, _this11.scripts.getArtifacts()); + } + + // --no-lockfile or --pure-lockfile or --frozen-lockfile + if (_this11.flags.lockfile === false || _this11.flags.pureLockfile || _this11.flags.frozenLockfile) { + return; + } + + const lockFileHasAllPatterns = patterns.every(function (p) { + return _this11.lockfile.getLocked(p); + }); + const lockfilePatternsMatch = Object.keys(_this11.lockfile.cache || {}).every(function (p) { + return lockfileBasedOnResolver[p]; + }); + const resolverPatternsAreSameAsInLockfile = Object.keys(lockfileBasedOnResolver).every(function (pattern) { + const manifest = _this11.lockfile.getLocked(pattern); + return manifest && manifest.resolved === lockfileBasedOnResolver[pattern].resolved && deepEqual(manifest.prebuiltVariants, lockfileBasedOnResolver[pattern].prebuiltVariants); + }); + const integrityPatternsAreSameAsInLockfile = Object.keys(lockfileBasedOnResolver).every(function (pattern) { + const existingIntegrityInfo = lockfileBasedOnResolver[pattern].integrity; + if (!existingIntegrityInfo) { + // if this entry does not have an integrity, no need to re-write the lockfile because of it + return true; + } + const manifest = _this11.lockfile.getLocked(pattern); + if (manifest && manifest.integrity) { + const manifestIntegrity = ssri.stringify(manifest.integrity); + return manifestIntegrity === existingIntegrityInfo; + } + return false; + }); + + // remove command is followed by install with force, lockfile will be rewritten in any case then + if (!_this11.flags.force && _this11.lockfile.parseResultType === 'success' && lockFileHasAllPatterns && lockfilePatternsMatch && resolverPatternsAreSameAsInLockfile && integrityPatternsAreSameAsInLockfile && patterns.length) { + return; + } + + // build lockfile location + const loc = path.join(_this11.config.lockfileFolder, (_constants || _load_constants()).LOCKFILE_FILENAME); + + // write lockfile + const lockSource = (0, (_lockfile2 || _load_lockfile2()).stringify)(lockfileBasedOnResolver, false, _this11.config.enableLockfileVersions); + yield (_fs || _load_fs()).writeFilePreservingEol(loc, lockSource); + + _this11._logSuccessSaveLockfile(); + })(); + } + + _logSuccessSaveLockfile() { + this.reporter.success(this.reporter.lang('savedLockfile')); + } + + /** + * Load the dependency graph of the current install. Only does package resolving and wont write to the cwd. + */ + hydrate(ignoreUnusedPatterns) { + var _this12 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + const request = yield _this12.fetchRequestFromCwd([], ignoreUnusedPatterns); + const depRequests = request.requests, + rawPatterns = request.patterns, + ignorePatterns = request.ignorePatterns, + workspaceLayout = request.workspaceLayout; + + + yield _this12.resolver.init(depRequests, { + isFlat: _this12.flags.flat, + isFrozen: _this12.flags.frozenLockfile, + workspaceLayout + }); + yield _this12.flatten(rawPatterns); + _this12.markIgnored(ignorePatterns); + + // fetch packages, should hit cache most of the time + const manifests = yield (_packageFetcher || _load_packageFetcher()).fetch(_this12.resolver.getManifests(), _this12.config); + _this12.resolver.updateManifests(manifests); + yield (_packageCompatibility || _load_packageCompatibility()).check(_this12.resolver.getManifests(), _this12.config, _this12.flags.ignoreEngines); + + // expand minimal manifests + for (var _iterator15 = _this12.resolver.getManifests(), _isArray15 = Array.isArray(_iterator15), _i15 = 0, _iterator15 = _isArray15 ? _iterator15 : _iterator15[Symbol.iterator]();;) { + var _ref28; + + if (_isArray15) { + if (_i15 >= _iterator15.length) break; + _ref28 = _iterator15[_i15++]; + } else { + _i15 = _iterator15.next(); + if (_i15.done) break; + _ref28 = _i15.value; + } + + const manifest = _ref28; + + const ref = manifest._reference; + invariant(ref, 'expected reference'); + const type = ref.remote.type; + // link specifier won't ever hit cache + + let loc = ''; + if (type === 'link') { + continue; + } else if (type === 'workspace') { + if (!ref.remote.reference) { + continue; + } + loc = ref.remote.reference; + } else { + loc = _this12.config.generateModuleCachePath(ref); + } + const newPkg = yield _this12.config.readManifest(loc); + yield _this12.resolver.updateManifest(ref, newPkg); + } + + return request; + })(); + } + + /** + * Check for updates every day and output a nag message if there's a newer version. + */ + + checkUpdate() { + if (this.config.nonInteractive) { + // don't show upgrade dialog on CI or non-TTY terminals + return; + } + + // don't check if disabled + if (this.config.getOption('disable-self-update-check')) { + return; + } + + // only check for updates once a day + const lastUpdateCheck = Number(this.config.getOption('lastUpdateCheck')) || 0; + if (lastUpdateCheck && Date.now() - lastUpdateCheck < ONE_DAY) { + return; + } + + // don't bug for updates on tagged releases + if ((_yarnVersion || _load_yarnVersion()).version.indexOf('-') >= 0) { + return; + } + + this._checkUpdate().catch(() => { + // swallow errors + }); + } + + _checkUpdate() { + var _this13 = this; + + return (0, (_asyncToGenerator2 || _load_asyncToGenerator()).default)(function* () { + let latestVersion = yield _this13.config.requestManager.request({ + url: (_constants || _load_constants()).SELF_UPDATE_VERSION_URL + }); + invariant(typeof latestVersion === 'string', 'expected string'); + latestVersion = latestVersion.trim(); + if (!semver.valid(latestVersion)) { + return; + } + + // ensure we only check for updates periodically + _this13.config.registries.yarn.saveHomeConfig({ + lastUpdateCheck: Date.now() + }); + + if (semver.gt(latestVersion, (_yarnVersion || _load_yarnVersion()).version)) { + const installationMethod = yield (0, (_yarnVersion || _load_yarnVersion()).getInstallationMethod)(); + _this13.maybeOutputUpdate = function () { + _this13.reporter.warn(_this13.reporter.lang('yarnOutdated', latestVersion, (_yarnVersion || _load_yarnVersion()).version)); + + const command = getUpdateCommand(installationMethod); + if (command) { + _this13.reporter.info(_this13.reporter.lang('yarnOutdatedCommand')); + _this13.reporter.command(command); + } else { + const installer = getUpdateInstaller(installationMethod); + if (installer) { + _this13.reporter.info(_this13.reporter.lang('yarnOutdatedInstaller', installer)); + } + } + }; + } + })(); + } + + /** + * Method to override with a possible upgrade message. + */ + + maybeOutputUpdate() {} +} + +exports.Install = Install; +function hasWrapper(commander, args) { + return true; +} + +function setFlags(commander) { + commander.description('Yarn install is used to install all dependencies for a project.'); + commander.usage('install [flags]'); + commander.option('-A, --audit', 'Run vulnerability audit on installed packages'); + commander.option('-g, --global', 'DEPRECATED'); + commander.option('-S, --save', 'DEPRECATED - save package to your `dependencies`'); + commander.option('-D, --save-dev', 'DEPRECATED - save package to your `devDependencies`'); + commander.option('-P, --save-peer', 'DEPRECATED - save package to your `peerDependencies`'); + commander.option('-O, --save-optional', 'DEPRECATED - save package to your `optionalDependencies`'); + commander.option('-E, --save-exact', 'DEPRECATED'); + commander.option('-T, --save-tilde', 'DEPRECATED'); +} + +/***/ }), +/* 35 */ +/***/ (function(module, exports, __webpack_require__) { + +var isObject = __webpack_require__(52); +module.exports = function (it) { + if (!isObject(it)) throw TypeError(it + ' is not an object!'); + return it; +}; + + +/***/ }), +/* 36 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return SubjectSubscriber; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Subject; }); +/* unused harmony export AnonymousSubject */ +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_tslib__ = __webpack_require__(1); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__Observable__ = __webpack_require__(12); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__Subscriber__ = __webpack_require__(7); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_3__Subscription__ = __webpack_require__(25); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__ = __webpack_require__(189); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_5__SubjectSubscription__ = __webpack_require__(422); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_6__internal_symbol_rxSubscriber__ = __webpack_require__(321); +/** PURE_IMPORTS_START tslib,_Observable,_Subscriber,_Subscription,_util_ObjectUnsubscribedError,_SubjectSubscription,_internal_symbol_rxSubscriber PURE_IMPORTS_END */ + + + + + + + +var SubjectSubscriber = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](SubjectSubscriber, _super); + function SubjectSubscriber(destination) { + var _this = _super.call(this, destination) || this; + _this.destination = destination; + return _this; + } + return SubjectSubscriber; +}(__WEBPACK_IMPORTED_MODULE_2__Subscriber__["a" /* Subscriber */])); + +var Subject = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](Subject, _super); + function Subject() { + var _this = _super.call(this) || this; + _this.observers = []; + _this.closed = false; + _this.isStopped = false; + _this.hasError = false; + _this.thrownError = null; + return _this; + } + Subject.prototype[__WEBPACK_IMPORTED_MODULE_6__internal_symbol_rxSubscriber__["a" /* rxSubscriber */]] = function () { + return new SubjectSubscriber(this); + }; + Subject.prototype.lift = function (operator) { + var subject = new AnonymousSubject(this, this); + subject.operator = operator; + return subject; + }; + Subject.prototype.next = function (value) { + if (this.closed) { + throw new __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__["a" /* ObjectUnsubscribedError */](); + } + if (!this.isStopped) { + var observers = this.observers; + var len = observers.length; + var copy = observers.slice(); + for (var i = 0; i < len; i++) { + copy[i].next(value); + } + } + }; + Subject.prototype.error = function (err) { + if (this.closed) { + throw new __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__["a" /* ObjectUnsubscribedError */](); + } + this.hasError = true; + this.thrownError = err; + this.isStopped = true; + var observers = this.observers; + var len = observers.length; + var copy = observers.slice(); + for (var i = 0; i < len; i++) { + copy[i].error(err); + } + this.observers.length = 0; + }; + Subject.prototype.complete = function () { + if (this.closed) { + throw new __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__["a" /* ObjectUnsubscribedError */](); + } + this.isStopped = true; + var observers = this.observers; + var len = observers.length; + var copy = observers.slice(); + for (var i = 0; i < len; i++) { + copy[i].complete(); + } + this.observers.length = 0; + }; + Subject.prototype.unsubscribe = function () { + this.isStopped = true; + this.closed = true; + this.observers = null; + }; + Subject.prototype._trySubscribe = function (subscriber) { + if (this.closed) { + throw new __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__["a" /* ObjectUnsubscribedError */](); + } + else { + return _super.prototype._trySubscribe.call(this, subscriber); + } + }; + Subject.prototype._subscribe = function (subscriber) { + if (this.closed) { + throw new __WEBPACK_IMPORTED_MODULE_4__util_ObjectUnsubscribedError__["a" /* ObjectUnsubscribedError */](); + } + else if (this.hasError) { + subscriber.error(this.thrownError); + return __WEBPACK_IMPORTED_MODULE_3__Subscription__["a" /* Subscription */].EMPTY; + } + else if (this.isStopped) { + subscriber.complete(); + return __WEBPACK_IMPORTED_MODULE_3__Subscription__["a" /* Subscription */].EMPTY; + } + else { + this.observers.push(subscriber); + return new __WEBPACK_IMPORTED_MODULE_5__SubjectSubscription__["a" /* SubjectSubscription */](this, subscriber); + } + }; + Subject.prototype.asObservable = function () { + var observable = new __WEBPACK_IMPORTED_MODULE_1__Observable__["a" /* Observable */](); + observable.source = this; + return observable; + }; + Subject.create = function (destination, source) { + return new AnonymousSubject(destination, source); + }; + return Subject; +}(__WEBPACK_IMPORTED_MODULE_1__Observable__["a" /* Observable */])); + +var AnonymousSubject = /*@__PURE__*/ (function (_super) { + __WEBPACK_IMPORTED_MODULE_0_tslib__["a" /* __extends */](AnonymousSubject, _super); + function AnonymousSubject(destination, source) { + var _this = _super.call(this) || this; + _this.destination = destination; + _this.source = source; + return _this; + } + AnonymousSubject.prototype.next = function (value) { + var destination = this.destination; + if (destination && destination.next) { + destination.next(value); + } + }; + AnonymousSubject.prototype.error = function (err) { + var destination = this.destination; + if (destination && destination.error) { + this.destination.error(err); + } + }; + AnonymousSubject.prototype.complete = function () { + var destination = this.destination; + if (destination && destination.complete) { + this.destination.complete(); + } + }; + AnonymousSubject.prototype._subscribe = function (subscriber) { + var source = this.source; + if (source) { + return this.source.subscribe(subscriber); + } + else { + return __WEBPACK_IMPORTED_MODULE_3__Subscription__["a" /* Subscription */].EMPTY; + } + }; + return AnonymousSubject; +}(Subject)); + +//# sourceMappingURL=Subject.js.map + + +/***/ }), +/* 37 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.normalizePattern = normalizePattern; + +/** + * Explode and normalize a pattern into its name and range. + */ + +function normalizePattern(pattern) { + let hasVersion = false; + let range = 'latest'; + let name = pattern; + + // if we're a scope then remove the @ and add it back later + let isScoped = false; + if (name[0] === '@') { + isScoped = true; + name = name.slice(1); + } + + // take first part as the name + const parts = name.split('@'); + if (parts.length > 1) { + name = parts.shift(); + range = parts.join('@'); + + if (range) { + hasVersion = true; + } else { + range = '*'; + } + } + + // add back @ scope suffix + if (isScoped) { + name = `@${name}`; + } + + return { name, range, hasVersion }; +} + +/***/ }), +/* 38 */ +/***/ (function(module, exports, __webpack_require__) { + +/* WEBPACK VAR INJECTION */(function(module) {var __WEBPACK_AMD_DEFINE_RESULT__;/** + * @license + * Lodash + * Copyright JS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ +;(function() { + + /** Used as a safe reference for `undefined` in pre-ES5 environments. */ + var undefined; + + /** Used as the semantic version number. */ + var VERSION = '4.17.10'; + + /** Used as the size to enable large array optimizations. */ + var LARGE_ARRAY_SIZE = 200; + + /** Error message constants. */ + var CORE_ERROR_TEXT = 'Unsupported core-js use. Try https://npms.io/search?q=ponyfill.', + FUNC_ERROR_TEXT = 'Expected a function'; + + /** Used to stand-in for `undefined` hash values. */ + var HASH_UNDEFINED = '__lodash_hash_undefined__'; + + /** Used as the maximum memoize cache size. */ + var MAX_MEMOIZE_SIZE = 500; + + /** Used as the internal argument placeholder. */ + var PLACEHOLDER = '__lodash_placeholder__'; + + /** Used to compose bitmasks for cloning. */ + var CLONE_DEEP_FLAG = 1, + CLONE_FLAT_FLAG = 2, + CLONE_SYMBOLS_FLAG = 4; + + /** Used to compose bitmasks for value comparisons. */ + var COMPARE_PARTIAL_FLAG = 1, + COMPARE_UNORDERED_FLAG = 2; + + /** Used to compose bitmasks for function metadata. */ + var WRAP_BIND_FLAG = 1, + WRAP_BIND_KEY_FLAG = 2, + WRAP_CURRY_BOUND_FLAG = 4, + WRAP_CURRY_FLAG = 8, + WRAP_CURRY_RIGHT_FLAG = 16, + WRAP_PARTIAL_FLAG = 32, + WRAP_PARTIAL_RIGHT_FLAG = 64, + WRAP_ARY_FLAG = 128, + WRAP_REARG_FLAG = 256, + WRAP_FLIP_FLAG = 512; + + /** Used as default options for `_.truncate`. */ + var DEFAULT_TRUNC_LENGTH = 30, + DEFAULT_TRUNC_OMISSION = '...'; + + /** Used to detect hot functions by number of calls within a span of milliseconds. */ + var HOT_COUNT = 800, + HOT_SPAN = 16; + + /** Used to indicate the type of lazy iteratees. */ + var LAZY_FILTER_FLAG = 1, + LAZY_MAP_FLAG = 2, + LAZY_WHILE_FLAG = 3; + + /** Used as references for various `Number` constants. */ + var INFINITY = 1 / 0, + MAX_SAFE_INTEGER = 9007199254740991, + MAX_INTEGER = 1.7976931348623157e+308, + NAN = 0 / 0; + + /** Used as references for the maximum length and index of an array. */ + var MAX_ARRAY_LENGTH = 4294967295, + MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1, + HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1; + + /** Used to associate wrap methods with their bit flags. */ + var wrapFlags = [ + ['ary', WRAP_ARY_FLAG], + ['bind', WRAP_BIND_FLAG], + ['bindKey', WRAP_BIND_KEY_FLAG], + ['curry', WRAP_CURRY_FLAG], + ['curryRight', WRAP_CURRY_RIGHT_FLAG], + ['flip', WRAP_FLIP_FLAG], + ['partial', WRAP_PARTIAL_FLAG], + ['partialRight', WRAP_PARTIAL_RIGHT_FLAG], + ['rearg', WRAP_REARG_FLAG] + ]; + + /** `Object#toString` result references. */ + var argsTag = '[object Arguments]', + arrayTag = '[object Array]', + asyncTag = '[object AsyncFunction]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + domExcTag = '[object DOMException]', + errorTag = '[object Error]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + mapTag = '[object Map]', + numberTag = '[object Number]', + nullTag = '[object Null]', + objectTag = '[object Object]', + promiseTag = '[object Promise]', + proxyTag = '[object Proxy]', + regexpTag = '[object RegExp]', + setTag = '[object Set]', + stringTag = '[object String]', + symbolTag = '[object Symbol]', + undefinedTag = '[object Undefined]', + weakMapTag = '[object WeakMap]', + weakSetTag = '[object WeakSet]'; + + var arrayBufferTag = '[object ArrayBuffer]', + dataViewTag = '[object DataView]', + float32Tag = '[object Float32Array]', + float64Tag = '[object Float64Array]', + int8Tag = '[object Int8Array]', + int16Tag = '[object Int16Array]', + int32Tag = '[object Int32Array]', + uint8Tag = '[object Uint8Array]', + uint8ClampedTag = '[object Uint8ClampedArray]', + uint16Tag = '[object Uint16Array]', + uint32Tag = '[object Uint32Array]'; + + /** Used to match empty string literals in compiled template source. */ + var reEmptyStringLeading = /\b__p \+= '';/g, + reEmptyStringMiddle = /\b(__p \+=) '' \+/g, + reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; + + /** Used to match HTML entities and HTML characters. */ + var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g, + reUnescapedHtml = /[&<>"']/g, + reHasEscapedHtml = RegExp(reEscapedHtml.source), + reHasUnescapedHtml = RegExp(reUnescapedHtml.source); + + /** Used to match template delimiters. */ + var reEscape = /<%-([\s\S]+?)%>/g, + reEvaluate = /<%([\s\S]+?)%>/g, + reInterpolate = /<%=([\s\S]+?)%>/g; + + /** Used to match property names within property paths. */ + var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, + reIsPlainProp = /^\w*$/, + rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; + + /** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ + var reRegExpChar = /[\\^$.*+?()[\]{}|]/g, + reHasRegExpChar = RegExp(reRegExpChar.source); + + /** Used to match leading and trailing whitespace. */ + var reTrim = /^\s+|\s+$/g, + reTrimStart = /^\s+/, + reTrimEnd = /\s+$/; + + /** Used to match wrap detail comments. */ + var reWrapComment = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/, + reWrapDetails = /\{\n\/\* \[wrapped with (.+)\] \*/, + reSplitDetails = /,? & /; + + /** Used to match words composed of alphanumeric characters. */ + var reAsciiWord = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g; + + /** Used to match backslashes in property paths. */ + var reEscapeChar = /\\(\\)?/g; + + /** + * Used to match + * [ES template delimiters](http://ecma-international.org/ecma-262/7.0/#sec-template-literal-lexical-components). + */ + var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; + + /** Used to match `RegExp` flags from their coerced string values. */ + var reFlags = /\w*$/; + + /** Used to detect bad signed hexadecimal string values. */ + var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + + /** Used to detect binary string values. */ + var reIsBinary = /^0b[01]+$/i; + + /** Used to detect host constructors (Safari). */ + var reIsHostCtor = /^\[object .+?Constructor\]$/; + + /** Used to detect octal string values. */ + var reIsOctal = /^0o[0-7]+$/i; + + /** Used to detect unsigned integer values. */ + var reIsUint = /^(?:0|[1-9]\d*)$/; + + /** Used to match Latin Unicode letters (excluding mathematical operators). */ + var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; + + /** Used to ensure capturing order of template delimiters. */ + var reNoMatch = /($^)/; + + /** Used to match unescaped characters in compiled string literals. */ + var reUnescapedString = /['\n\r\u2028\u2029\\]/g; + + /** Used to compose unicode character classes. */ + var rsAstralRange = '\\ud800-\\udfff', + rsComboMarksRange = '\\u0300-\\u036f', + reComboHalfMarksRange = '\\ufe20-\\ufe2f', + rsComboSymbolsRange = '\\u20d0-\\u20ff', + rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange, + rsDingbatRange = '\\u2700-\\u27bf', + rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff', + rsMathOpRange = '\\xac\\xb1\\xd7\\xf7', + rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf', + rsPunctuationRange = '\\u2000-\\u206f', + rsSpaceRange = ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000', + rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde', + rsVarRange = '\\ufe0e\\ufe0f', + rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange; + + /** Used to compose unicode capture groups. */ + var rsApos = "['\u2019]", + rsAstral = '[' + rsAstralRange + ']', + rsBreak = '[' + rsBreakRange + ']', + rsCombo = '[' + rsComboRange + ']', + rsDigits = '\\d+', + rsDingbat = '[' + rsDingbatRange + ']', + rsLower = '[' + rsLowerRange + ']', + rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']', + rsFitz = '\\ud83c[\\udffb-\\udfff]', + rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')', + rsNonAstral = '[^' + rsAstralRange + ']', + rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}', + rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]', + rsUpper = '[' + rsUpperRange + ']', + rsZWJ = '\\u200d'; + + /** Used to compose unicode regexes. */ + var rsMiscLower = '(?:' + rsLower + '|' + rsMisc + ')', + rsMiscUpper = '(?:' + rsUpper + '|' + rsMisc + ')', + rsOptContrLower = '(?:' + rsApos + '(?:d|ll|m|re|s|t|ve))?', + rsOptContrUpper = '(?:' + rsApos + '(?:D|LL|M|RE|S|T|VE))?', + reOptMod = rsModifier + '?', + rsOptVar = '[' + rsVarRange + ']?', + rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*', + rsOrdLower = '\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])', + rsOrdUpper = '\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])', + rsSeq = rsOptVar + reOptMod + rsOptJoin, + rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq, + rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')'; + + /** Used to match apostrophes. */ + var reApos = RegExp(rsApos, 'g'); + + /** + * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and + * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). + */ + var reComboMark = RegExp(rsCombo, 'g'); + + /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */ + var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g'); + + /** Used to match complex or compound words. */ + var reUnicodeWord = RegExp([ + rsUpper + '?' + rsLower + '+' + rsOptContrLower + '(?=' + [rsBreak, rsUpper, '$'].join('|') + ')', + rsMiscUpper + '+' + rsOptContrUpper + '(?=' + [rsBreak, rsUpper + rsMiscLower, '$'].join('|') + ')', + rsUpper + '?' + rsMiscLower + '+' + rsOptContrLower, + rsUpper + '+' + rsOptContrUpper, + rsOrdUpper, + rsOrdLower, + rsDigits, + rsEmoji + ].join('|'), 'g'); + + /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */ + var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']'); + + /** Used to detect strings that need a more robust regexp to match words. */ + var reHasUnicodeWord = /[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/; + + /** Used to assign default `context` object properties. */ + var contextProps = [ + 'Array', 'Buffer', 'DataView', 'Date', 'Error', 'Float32Array', 'Float64Array', + 'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object', + 'Promise', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array', + 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', + '_', 'clearTimeout', 'isFinite', 'parseInt', 'setTimeout' + ]; + + /** Used to make template sourceURLs easier to identify. */ + var templateCounter = -1; + + /** Used to identify `toStringTag` values of typed arrays. */ + var typedArrayTags = {}; + typedArrayTags[float32Tag] = typedArrayTags[float64Tag] = + typedArrayTags[int8Tag] = typedArrayTags[int16Tag] = + typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] = + typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] = + typedArrayTags[uint32Tag] = true; + typedArrayTags[argsTag] = typedArrayTags[arrayTag] = + typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] = + typedArrayTags[dataViewTag] = typedArrayTags[dateTag] = + typedArrayTags[errorTag] = typedArrayTags[funcTag] = + typedArrayTags[mapTag] = typedArrayTags[numberTag] = + typedArrayTags[objectTag] = typedArrayTags[regexpTag] = + typedArrayTags[setTag] = typedArrayTags[stringTag] = + typedArrayTags[weakMapTag] = false; + + /** Used to identify `toStringTag` values supported by `_.clone`. */ + var cloneableTags = {}; + cloneableTags[argsTag] = cloneableTags[arrayTag] = + cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] = + cloneableTags[boolTag] = cloneableTags[dateTag] = + cloneableTags[float32Tag] = cloneableTags[float64Tag] = + cloneableTags[int8Tag] = cloneableTags[int16Tag] = + cloneableTags[int32Tag] = cloneableTags[mapTag] = + cloneableTags[numberTag] = cloneableTags[objectTag] = + cloneableTags[regexpTag] = cloneableTags[setTag] = + cloneableTags[stringTag] = cloneableTags[symbolTag] = + cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = + cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true; + cloneableTags[errorTag] = cloneableTags[funcTag] = + cloneableTags[weakMapTag] = false; + + /** Used to map Latin Unicode letters to basic Latin letters. */ + var deburredLetters = { + // Latin-1 Supplement block. + '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', + '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', + '\xc7': 'C', '\xe7': 'c', + '\xd0': 'D', '\xf0': 'd', + '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', + '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', + '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', + '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', + '\xd1': 'N', '\xf1': 'n', + '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', + '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', + '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', + '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', + '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', + '\xc6': 'Ae', '\xe6': 'ae', + '\xde': 'Th', '\xfe': 'th', + '\xdf': 'ss', + // Latin Extended-A block. + '\u0100': 'A', '\u0102': 'A', '\u0104': 'A', + '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', + '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', + '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', + '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', + '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', + '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', + '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', + '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', + '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', + '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', + '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', + '\u0134': 'J', '\u0135': 'j', + '\u0136': 'K', '\u0137': 'k', '\u0138': 'k', + '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', + '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', + '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', + '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', + '\u014c': 'O', '\u014e': 'O', '\u0150': 'O', + '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', + '\u0154': 'R', '\u0156': 'R', '\u0158': 'R', + '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', + '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', + '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', + '\u0162': 'T', '\u0164': 'T', '\u0166': 'T', + '\u0163': 't', '\u0165': 't', '\u0167': 't', + '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', + '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', + '\u0174': 'W', '\u0175': 'w', + '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', + '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', + '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', + '\u0132': 'IJ', '\u0133': 'ij', + '\u0152': 'Oe', '\u0153': 'oe', + '\u0149': "'n", '\u017f': 's' + }; + + /** Used to map characters to HTML entities. */ + var htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + + /** Used to map HTML entities to characters. */ + var htmlUnescapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'" + }; + + /** Used to escape characters for inclusion in compiled string literals. */ + var stringEscapes = { + '\\': '\\', + "'": "'", + '\n': 'n', + '\r': 'r', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + /** Built-in method references without a dependency on `root`. */ + var freeParseFloat = parseFloat, + freeParseInt = parseInt; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; + + /** Detect free variable `self`. */ + var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root = freeGlobal || freeSelf || Function('return this')(); + + /** Detect free variable `exports`. */ + var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports; + + /** Detect free variable `module`. */ + var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; + + /** Detect the popular CommonJS extension `module.exports`. */ + var moduleExports = freeModule && freeModule.exports === freeExports; + + /** Detect free variable `process` from Node.js. */ + var freeProcess = moduleExports && freeGlobal.process; + + /** Used to access faster Node.js helpers. */ + var nodeUtil = (function() { + try { + // Use `util.types` for Node.js 10+. + var types = freeModule && freeModule.require && freeModule.require('util').types; + + if (types) { + return types; + } + + // Legacy `process.binding('util')` for Node.js < 10. + return freeProcess && freeProcess.binding && freeProcess.binding('util'); + } catch (e) {} + }()); + + /* Node.js helper references. */ + var nodeIsArrayBuffer = nodeUtil && nodeUtil.isArrayBuffer, + nodeIsDate = nodeUtil && nodeUtil.isDate, + nodeIsMap = nodeUtil && nodeUtil.isMap, + nodeIsRegExp = nodeUtil && nodeUtil.isRegExp, + nodeIsSet = nodeUtil && nodeUtil.isSet, + nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray; + + /*--------------------------------------------------------------------------*/ + + /** + * A faster alternative to `Function#apply`, this function invokes `func` + * with the `this` binding of `thisArg` and the arguments of `args`. + * + * @private + * @param {Function} func The function to invoke. + * @param {*} thisArg The `this` binding of `func`. + * @param {Array} args The arguments to invoke `func` with. + * @returns {*} Returns the result of `func`. + */ + function apply(func, thisArg, args) { + switch (args.length) { + case 0: return func.call(thisArg); + case 1: return func.call(thisArg, args[0]); + case 2: return func.call(thisArg, args[0], args[1]); + case 3: return func.call(thisArg, args[0], args[1], args[2]); + } + return func.apply(thisArg, args); + } + + /** + * A specialized version of `baseAggregator` for arrays. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} setter The function to set `accumulator` values. + * @param {Function} iteratee The iteratee to transform keys. + * @param {Object} accumulator The initial aggregated object. + * @returns {Function} Returns `accumulator`. + */ + function arrayAggregator(array, setter, iteratee, accumulator) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + var value = array[index]; + setter(accumulator, value, iteratee(value), array); + } + return accumulator; + } + + /** + * A specialized version of `_.forEach` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEach(array, iteratee) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; + } + + /** + * A specialized version of `_.forEachRight` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEachRight(array, iteratee) { + var length = array == null ? 0 : array.length; + + while (length--) { + if (iteratee(array[length], length, array) === false) { + break; + } + } + return array; + } + + /** + * A specialized version of `_.every` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if all elements pass the predicate check, + * else `false`. + */ + function arrayEvery(array, predicate) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + if (!predicate(array[index], index, array)) { + return false; + } + } + return true; + } + + /** + * A specialized version of `_.filter` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + */ + function arrayFilter(array, predicate) { + var index = -1, + length = array == null ? 0 : array.length, + resIndex = 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (predicate(value, index, array)) { + result[resIndex++] = value; + } + } + return result; + } + + /** + * A specialized version of `_.includes` for arrays without support for + * specifying an index to search from. + * + * @private + * @param {Array} [array] The array to inspect. + * @param {*} target The value to search for. + * @returns {boolean} Returns `true` if `target` is found, else `false`. + */ + function arrayIncludes(array, value) { + var length = array == null ? 0 : array.length; + return !!length && baseIndexOf(array, value, 0) > -1; + } + + /** + * This function is like `arrayIncludes` except that it accepts a comparator. + * + * @private + * @param {Array} [array] The array to inspect. + * @param {*} target The value to search for. + * @param {Function} comparator The comparator invoked per element. + * @returns {boolean} Returns `true` if `target` is found, else `false`. + */ + function arrayIncludesWith(array, value, comparator) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + if (comparator(value, array[index])) { + return true; + } + } + return false; + } + + /** + * A specialized version of `_.map` for arrays without support for iteratee + * shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ + function arrayMap(array, iteratee) { + var index = -1, + length = array == null ? 0 : array.length, + result = Array(length); + + while (++index < length) { + result[index] = iteratee(array[index], index, array); + } + return result; + } + + /** + * Appends the elements of `values` to `array`. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to append. + * @returns {Array} Returns `array`. + */ + function arrayPush(array, values) { + var index = -1, + length = values.length, + offset = array.length; + + while (++index < length) { + array[offset + index] = values[index]; + } + return array; + } + + /** + * A specialized version of `_.reduce` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initAccum] Specify using the first element of `array` as + * the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduce(array, iteratee, accumulator, initAccum) { + var index = -1, + length = array == null ? 0 : array.length; + + if (initAccum && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; + } + + /** + * A specialized version of `_.reduceRight` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initAccum] Specify using the last element of `array` as + * the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduceRight(array, iteratee, accumulator, initAccum) { + var length = array == null ? 0 : array.length; + if (initAccum && length) { + accumulator = array[--length]; + } + while (length--) { + accumulator = iteratee(accumulator, array[length], length, array); + } + return accumulator; + } + + /** + * A specialized version of `_.some` for arrays without support for iteratee + * shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if any element passes the predicate check, + * else `false`. + */ + function arraySome(array, predicate) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + if (predicate(array[index], index, array)) { + return true; + } + } + return false; + } + + /** + * Gets the size of an ASCII `string`. + * + * @private + * @param {string} string The string inspect. + * @returns {number} Returns the string size. + */ + var asciiSize = baseProperty('length'); + + /** + * Converts an ASCII `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function asciiToArray(string) { + return string.split(''); + } + + /** + * Splits an ASCII `string` into an array of its words. + * + * @private + * @param {string} The string to inspect. + * @returns {Array} Returns the words of `string`. + */ + function asciiWords(string) { + return string.match(reAsciiWord) || []; + } + + /** + * The base implementation of methods like `_.findKey` and `_.findLastKey`, + * without support for iteratee shorthands, which iterates over `collection` + * using `eachFunc`. + * + * @private + * @param {Array|Object} collection The collection to inspect. + * @param {Function} predicate The function invoked per iteration. + * @param {Function} eachFunc The function to iterate over `collection`. + * @returns {*} Returns the found element or its key, else `undefined`. + */ + function baseFindKey(collection, predicate, eachFunc) { + var result; + eachFunc(collection, function(value, key, collection) { + if (predicate(value, key, collection)) { + result = key; + return false; + } + }); + return result; + } + + /** + * The base implementation of `_.findIndex` and `_.findLastIndex` without + * support for iteratee shorthands. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} predicate The function invoked per iteration. + * @param {number} fromIndex The index to search from. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function baseFindIndex(array, predicate, fromIndex, fromRight) { + var length = array.length, + index = fromIndex + (fromRight ? 1 : -1); + + while ((fromRight ? index-- : ++index < length)) { + if (predicate(array[index], index, array)) { + return index; + } + } + return -1; + } + + /** + * The base implementation of `_.indexOf` without `fromIndex` bounds checks. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function baseIndexOf(array, value, fromIndex) { + return value === value + ? strictIndexOf(array, value, fromIndex) + : baseFindIndex(array, baseIsNaN, fromIndex); + } + + /** + * This function is like `baseIndexOf` except that it accepts a comparator. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @param {Function} comparator The comparator invoked per element. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function baseIndexOfWith(array, value, fromIndex, comparator) { + var index = fromIndex - 1, + length = array.length; + + while (++index < length) { + if (comparator(array[index], value)) { + return index; + } + } + return -1; + } + + /** + * The base implementation of `_.isNaN` without support for number objects. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. + */ + function baseIsNaN(value) { + return value !== value; + } + + /** + * The base implementation of `_.mean` and `_.meanBy` without support for + * iteratee shorthands. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {number} Returns the mean. + */ + function baseMean(array, iteratee) { + var length = array == null ? 0 : array.length; + return length ? (baseSum(array, iteratee) / length) : NAN; + } + + /** + * The base implementation of `_.property` without support for deep paths. + * + * @private + * @param {string} key The key of the property to get. + * @returns {Function} Returns the new accessor function. + */ + function baseProperty(key) { + return function(object) { + return object == null ? undefined : object[key]; + }; + } + + /** + * The base implementation of `_.propertyOf` without support for deep paths. + * + * @private + * @param {Object} object The object to query. + * @returns {Function} Returns the new accessor function. + */ + function basePropertyOf(object) { + return function(key) { + return object == null ? undefined : object[key]; + }; + } + + /** + * The base implementation of `_.reduce` and `_.reduceRight`, without support + * for iteratee shorthands, which iterates over `collection` using `eachFunc`. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} accumulator The initial value. + * @param {boolean} initAccum Specify using the first or last element of + * `collection` as the initial value. + * @param {Function} eachFunc The function to iterate over `collection`. + * @returns {*} Returns the accumulated value. + */ + function baseReduce(collection, iteratee, accumulator, initAccum, eachFunc) { + eachFunc(collection, function(value, index, collection) { + accumulator = initAccum + ? (initAccum = false, value) + : iteratee(accumulator, value, index, collection); + }); + return accumulator; + } + + /** + * The base implementation of `_.sortBy` which uses `comparer` to define the + * sort order of `array` and replaces criteria objects with their corresponding + * values. + * + * @private + * @param {Array} array The array to sort. + * @param {Function} comparer The function to define sort order. + * @returns {Array} Returns `array`. + */ + function baseSortBy(array, comparer) { + var length = array.length; + + array.sort(comparer); + while (length--) { + array[length] = array[length].value; + } + return array; + } + + /** + * The base implementation of `_.sum` and `_.sumBy` without support for + * iteratee shorthands. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {number} Returns the sum. + */ + function baseSum(array, iteratee) { + var result, + index = -1, + length = array.length; + + while (++index < length) { + var current = iteratee(array[index]); + if (current !== undefined) { + result = result === undefined ? current : (result + current); + } + } + return result; + } + + /** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ + function baseTimes(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; + } + + /** + * The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array + * of key-value pairs for `object` corresponding to the property names of `props`. + * + * @private + * @param {Object} object The object to query. + * @param {Array} props The property names to get values for. + * @returns {Object} Returns the key-value pairs. + */ + function baseToPairs(object, props) { + return arrayMap(props, function(key) { + return [key, object[key]]; + }); + } + + /** + * The base implementation of `_.unary` without support for storing metadata. + * + * @private + * @param {Function} func The function to cap arguments for. + * @returns {Function} Returns the new capped function. + */ + function baseUnary(func) { + return function(value) { + return func(value); + }; + } + + /** + * The base implementation of `_.values` and `_.valuesIn` which creates an + * array of `object` property values corresponding to the property names + * of `props`. + * + * @private + * @param {Object} object The object to query. + * @param {Array} props The property names to get values for. + * @returns {Object} Returns the array of property values. + */ + function baseValues(object, props) { + return arrayMap(props, function(key) { + return object[key]; + }); + } + + /** + * Checks if a `cache` value for `key` exists. + * + * @private + * @param {Object} cache The cache to query. + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function cacheHas(cache, key) { + return cache.has(key); + } + + /** + * Used by `_.trim` and `_.trimStart` to get the index of the first string symbol + * that is not found in the character symbols. + * + * @private + * @param {Array} strSymbols The string symbols to inspect. + * @param {Array} chrSymbols The character symbols to find. + * @returns {number} Returns the index of the first unmatched string symbol. + */ + function charsStartIndex(strSymbols, chrSymbols) { + var index = -1, + length = strSymbols.length; + + while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} + return index; + } + + /** + * Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol + * that is not found in the character symbols. + * + * @private + * @param {Array} strSymbols The string symbols to inspect. + * @param {Array} chrSymbols The character symbols to find. + * @returns {number} Returns the index of the last unmatched string symbol. + */ + function charsEndIndex(strSymbols, chrSymbols) { + var index = strSymbols.length; + + while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} + return index; + } + + /** + * Gets the number of `placeholder` occurrences in `array`. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} placeholder The placeholder to search for. + * @returns {number} Returns the placeholder count. + */ + function countHolders(array, placeholder) { + var length = array.length, + result = 0; + + while (length--) { + if (array[length] === placeholder) { + ++result; + } + } + return result; + } + + /** + * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A + * letters to basic Latin letters. + * + * @private + * @param {string} letter The matched letter to deburr. + * @returns {string} Returns the deburred letter. + */ + var deburrLetter = basePropertyOf(deburredLetters); + + /** + * Used by `_.escape` to convert characters to HTML entities. + * + * @private + * @param {string} chr The matched character to escape. + * @returns {string} Returns the escaped character. + */ + var escapeHtmlChar = basePropertyOf(htmlEscapes); + + /** + * Used by `_.template` to escape characters for inclusion in compiled string literals. + * + * @private + * @param {string} chr The matched character to escape. + * @returns {string} Returns the escaped character. + */ + function escapeStringChar(chr) { + return '\\' + stringEscapes[chr]; + } + + /** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ + function getValue(object, key) { + return object == null ? undefined : object[key]; + } + + /** + * Checks if `string` contains Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a symbol is found, else `false`. + */ + function hasUnicode(string) { + return reHasUnicode.test(string); + } + + /** + * Checks if `string` contains a word composed of Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a word is found, else `false`. + */ + function hasUnicodeWord(string) { + return reHasUnicodeWord.test(string); + } + + /** + * Converts `iterator` to an array. + * + * @private + * @param {Object} iterator The iterator to convert. + * @returns {Array} Returns the converted array. + */ + function iteratorToArray(iterator) { + var data, + result = []; + + while (!(data = iterator.next()).done) { + result.push(data.value); + } + return result; + } + + /** + * Converts `map` to its key-value pairs. + * + * @private + * @param {Object} map The map to convert. + * @returns {Array} Returns the key-value pairs. + */ + function mapToArray(map) { + var index = -1, + result = Array(map.size); + + map.forEach(function(value, key) { + result[++index] = [key, value]; + }); + return result; + } + + /** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ + function overArg(func, transform) { + return function(arg) { + return func(transform(arg)); + }; + } + + /** + * Replaces all `placeholder` elements in `array` with an internal placeholder + * and returns an array of their indexes. + * + * @private + * @param {Array} array The array to modify. + * @param {*} placeholder The placeholder to replace. + * @returns {Array} Returns the new array of placeholder indexes. + */ + function replaceHolders(array, placeholder) { + var index = -1, + length = array.length, + resIndex = 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (value === placeholder || value === PLACEHOLDER) { + array[index] = PLACEHOLDER; + result[resIndex++] = index; + } + } + return result; + } + + /** + * Gets the value at `key`, unless `key` is "__proto__". + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ + function safeGet(object, key) { + return key == '__proto__' + ? undefined + : object[key]; + } + + /** + * Converts `set` to an array of its values. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the values. + */ + function setToArray(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = value; + }); + return result; + } + + /** + * Converts `set` to its value-value pairs. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the value-value pairs. + */ + function setToPairs(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = [value, value]; + }); + return result; + } + + /** + * A specialized version of `_.indexOf` which performs strict equality + * comparisons of values, i.e. `===`. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function strictIndexOf(array, value, fromIndex) { + var index = fromIndex - 1, + length = array.length; + + while (++index < length) { + if (array[index] === value) { + return index; + } + } + return -1; + } + + /** + * A specialized version of `_.lastIndexOf` which performs strict equality + * comparisons of values, i.e. `===`. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function strictLastIndexOf(array, value, fromIndex) { + var index = fromIndex + 1; + while (index--) { + if (array[index] === value) { + return index; + } + } + return index; + } + + /** + * Gets the number of symbols in `string`. + * + * @private + * @param {string} string The string to inspect. + * @returns {number} Returns the string size. + */ + function stringSize(string) { + return hasUnicode(string) + ? unicodeSize(string) + : asciiSize(string); + } + + /** + * Converts `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function stringToArray(string) { + return hasUnicode(string) + ? unicodeToArray(string) + : asciiToArray(string); + } + + /** + * Used by `_.unescape` to convert HTML entities to characters. + * + * @private + * @param {string} chr The matched character to unescape. + * @returns {string} Returns the unescaped character. + */ + var unescapeHtmlChar = basePropertyOf(htmlUnescapes); + + /** + * Gets the size of a Unicode `string`. + * + * @private + * @param {string} string The string inspect. + * @returns {number} Returns the string size. + */ + function unicodeSize(string) { + var result = reUnicode.lastIndex = 0; + while (reUnicode.test(string)) { + ++result; + } + return result; + } + + /** + * Converts a Unicode `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function unicodeToArray(string) { + return string.match(reUnicode) || []; + } + + /** + * Splits a Unicode `string` into an array of its words. + * + * @private + * @param {string} The string to inspect. + * @returns {Array} Returns the words of `string`. + */ + function unicodeWords(string) { + return string.match(reUnicodeWord) || []; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Create a new pristine `lodash` function using the `context` object. + * + * @static + * @memberOf _ + * @since 1.1.0 + * @category Util + * @param {Object} [context=root] The context object. + * @returns {Function} Returns a new `lodash` function. + * @example + * + * _.mixin({ 'foo': _.constant('foo') }); + * + * var lodash = _.runInContext(); + * lodash.mixin({ 'bar': lodash.constant('bar') }); + * + * _.isFunction(_.foo); + * // => true + * _.isFunction(_.bar); + * // => false + * + * lodash.isFunction(lodash.foo); + * // => false + * lodash.isFunction(lodash.bar); + * // => true + * + * // Create a suped-up `defer` in Node.js. + * var defer = _.runInContext({ 'setTimeout': setImmediate }).defer; + */ + var runInContext = (function runInContext(context) { + context = context == null ? root : _.defaults(root.Object(), context, _.pick(root, contextProps)); + + /** Built-in constructor references. */ + var Array = context.Array, + Date = context.Date, + Error = context.Error, + Function = context.Function, + Math = context.Math, + Object = context.Object, + RegExp = context.RegExp, + String = context.String, + TypeError = context.TypeError; + + /** Used for built-in method references. */ + var arrayProto = Array.prototype, + funcProto = Function.prototype, + objectProto = Object.prototype; + + /** Used to detect overreaching core-js shims. */ + var coreJsData = context['__core-js_shared__']; + + /** Used to resolve the decompiled source of functions. */ + var funcToString = funcProto.toString; + + /** Used to check objects for own properties. */ + var hasOwnProperty = objectProto.hasOwnProperty; + + /** Used to generate unique IDs. */ + var idCounter = 0; + + /** Used to detect methods masquerading as native. */ + var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; + }()); + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var nativeObjectToString = objectProto.toString; + + /** Used to infer the `Object` constructor. */ + var objectCtorString = funcToString.call(Object); + + /** Used to restore the original `_` reference in `_.noConflict`. */ + var oldDash = root._; + + /** Used to detect if a method is native. */ + var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' + ); + + /** Built-in value references. */ + var Buffer = moduleExports ? context.Buffer : undefined, + Symbol = context.Symbol, + Uint8Array = context.Uint8Array, + allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined, + getPrototype = overArg(Object.getPrototypeOf, Object), + objectCreate = Object.create, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + splice = arrayProto.splice, + spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined, + symIterator = Symbol ? Symbol.iterator : undefined, + symToStringTag = Symbol ? Symbol.toStringTag : undefined; + + var defineProperty = (function() { + try { + var func = getNative(Object, 'defineProperty'); + func({}, '', {}); + return func; + } catch (e) {} + }()); + + /** Mocked built-ins. */ + var ctxClearTimeout = context.clearTimeout !== root.clearTimeout && context.clearTimeout, + ctxNow = Date && Date.now !== root.Date.now && Date.now, + ctxSetTimeout = context.setTimeout !== root.setTimeout && context.setTimeout; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeCeil = Math.ceil, + nativeFloor = Math.floor, + nativeGetSymbols = Object.getOwnPropertySymbols, + nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined, + nativeIsFinite = context.isFinite, + nativeJoin = arrayProto.join, + nativeKeys = overArg(Object.keys, Object), + nativeMax = Math.max, + nativeMin = Math.min, + nativeNow = Date.now, + nativeParseInt = context.parseInt, + nativeRandom = Math.random, + nativeReverse = arrayProto.reverse; + + /* Built-in method references that are verified to be native. */ + var DataView = getNative(context, 'DataView'), + Map = getNative(context, 'Map'), + Promise = getNative(context, 'Promise'), + Set = getNative(context, 'Set'), + WeakMap = getNative(context, 'WeakMap'), + nativeCreate = getNative(Object, 'create'); + + /** Used to store function metadata. */ + var metaMap = WeakMap && new WeakMap; + + /** Used to lookup unminified function names. */ + var realNames = {}; + + /** Used to detect maps, sets, and weakmaps. */ + var dataViewCtorString = toSource(DataView), + mapCtorString = toSource(Map), + promiseCtorString = toSource(Promise), + setCtorString = toSource(Set), + weakMapCtorString = toSource(WeakMap); + + /** Used to convert symbols to primitives and strings. */ + var symbolProto = Symbol ? Symbol.prototype : undefined, + symbolValueOf = symbolProto ? symbolProto.valueOf : undefined, + symbolToString = symbolProto ? symbolProto.toString : undefined; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a `lodash` object which wraps `value` to enable implicit method + * chain sequences. Methods that operate on and return arrays, collections, + * and functions can be chained together. Methods that retrieve a single value + * or may return a primitive value will automatically end the chain sequence + * and return the unwrapped value. Otherwise, the value must be unwrapped + * with `_#value`. + * + * Explicit chain sequences, which must be unwrapped with `_#value`, may be + * enabled using `_.chain`. + * + * The execution of chained methods is lazy, that is, it's deferred until + * `_#value` is implicitly or explicitly called. + * + * Lazy evaluation allows several methods to support shortcut fusion. + * Shortcut fusion is an optimization to merge iteratee calls; this avoids + * the creation of intermediate arrays and can greatly reduce the number of + * iteratee executions. Sections of a chain sequence qualify for shortcut + * fusion if the section is applied to an array and iteratees accept only + * one argument. The heuristic for whether a section qualifies for shortcut + * fusion is subject to change. + * + * Chaining is supported in custom builds as long as the `_#value` method is + * directly or indirectly included in the build. + * + * In addition to lodash methods, wrappers have `Array` and `String` methods. + * + * The wrapper `Array` methods are: + * `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift` + * + * The wrapper `String` methods are: + * `replace` and `split` + * + * The wrapper methods that support shortcut fusion are: + * `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`, + * `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`, + * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray` + * + * The chainable wrapper methods are: + * `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, `at`, + * `before`, `bind`, `bindAll`, `bindKey`, `castArray`, `chain`, `chunk`, + * `commit`, `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`, + * `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`, + * `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`, + * `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`, + * `flatMap`, `flatMapDeep`, `flatMapDepth`, `flatten`, `flattenDeep`, + * `flattenDepth`, `flip`, `flow`, `flowRight`, `fromPairs`, `functions`, + * `functionsIn`, `groupBy`, `initial`, `intersection`, `intersectionBy`, + * `intersectionWith`, `invert`, `invertBy`, `invokeMap`, `iteratee`, `keyBy`, + * `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`, + * `memoize`, `merge`, `mergeWith`, `method`, `methodOf`, `mixin`, `negate`, + * `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, `over`, `overArgs`, + * `overEvery`, `overSome`, `partial`, `partialRight`, `partition`, `pick`, + * `pickBy`, `plant`, `property`, `propertyOf`, `pull`, `pullAll`, `pullAllBy`, + * `pullAllWith`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, `reject`, + * `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, `shuffle`, + * `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, `takeRight`, + * `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `toArray`, + * `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, `unary`, + * `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, `unset`, + * `unshift`, `unzip`, `unzipWith`, `update`, `updateWith`, `values`, + * `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`, + * `zipObject`, `zipObjectDeep`, and `zipWith` + * + * The wrapper methods that are **not** chainable by default are: + * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`, + * `cloneDeep`, `cloneDeepWith`, `cloneWith`, `conformsTo`, `deburr`, + * `defaultTo`, `divide`, `each`, `eachRight`, `endsWith`, `eq`, `escape`, + * `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, `findLast`, + * `findLastIndex`, `findLastKey`, `first`, `floor`, `forEach`, `forEachRight`, + * `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `get`, `gt`, `gte`, `has`, + * `hasIn`, `head`, `identity`, `includes`, `indexOf`, `inRange`, `invoke`, + * `isArguments`, `isArray`, `isArrayBuffer`, `isArrayLike`, `isArrayLikeObject`, + * `isBoolean`, `isBuffer`, `isDate`, `isElement`, `isEmpty`, `isEqual`, + * `isEqualWith`, `isError`, `isFinite`, `isFunction`, `isInteger`, `isLength`, + * `isMap`, `isMatch`, `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`, + * `isNumber`, `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`, + * `isSafeInteger`, `isSet`, `isString`, `isUndefined`, `isTypedArray`, + * `isWeakMap`, `isWeakSet`, `join`, `kebabCase`, `last`, `lastIndexOf`, + * `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`, `maxBy`, `mean`, `meanBy`, + * `min`, `minBy`, `multiply`, `noConflict`, `noop`, `now`, `nth`, `pad`, + * `padEnd`, `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`, + * `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`, + * `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`, + * `sortedLastIndexBy`, `startCase`, `startsWith`, `stubArray`, `stubFalse`, + * `stubObject`, `stubString`, `stubTrue`, `subtract`, `sum`, `sumBy`, + * `template`, `times`, `toFinite`, `toInteger`, `toJSON`, `toLength`, + * `toLower`, `toNumber`, `toSafeInteger`, `toString`, `toUpper`, `trim`, + * `trimEnd`, `trimStart`, `truncate`, `unescape`, `uniqueId`, `upperCase`, + * `upperFirst`, `value`, and `words` + * + * @name _ + * @constructor + * @category Seq + * @param {*} value The value to wrap in a `lodash` instance. + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * function square(n) { + * return n * n; + * } + * + * var wrapped = _([1, 2, 3]); + * + * // Returns an unwrapped value. + * wrapped.reduce(_.add); + * // => 6 + * + * // Returns a wrapped value. + * var squares = wrapped.map(square); + * + * _.isArray(squares); + * // => false + * + * _.isArray(squares.value()); + * // => true + */ + function lodash(value) { + if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) { + if (value instanceof LodashWrapper) { + return value; + } + if (hasOwnProperty.call(value, '__wrapped__')) { + return wrapperClone(value); + } + } + return new LodashWrapper(value); + } + + /** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} proto The object to inherit from. + * @returns {Object} Returns the new object. + */ + var baseCreate = (function() { + function object() {} + return function(proto) { + if (!isObject(proto)) { + return {}; + } + if (objectCreate) { + return objectCreate(proto); + } + object.prototype = proto; + var result = new object; + object.prototype = undefined; + return result; + }; + }()); + + /** + * The function whose prototype chain sequence wrappers inherit from. + * + * @private + */ + function baseLodash() { + // No operation performed. + } + + /** + * The base constructor for creating `lodash` wrapper objects. + * + * @private + * @param {*} value The value to wrap. + * @param {boolean} [chainAll] Enable explicit method chain sequences. + */ + function LodashWrapper(value, chainAll) { + this.__wrapped__ = value; + this.__actions__ = []; + this.__chain__ = !!chainAll; + this.__index__ = 0; + this.__values__ = undefined; + } + + /** + * By default, the template delimiters used by lodash are like those in + * embedded Ruby (ERB) as well as ES2015 template strings. Change the + * following template settings to use alternative delimiters. + * + * @static + * @memberOf _ + * @type {Object} + */ + lodash.templateSettings = { + + /** + * Used to detect `data` property values to be HTML-escaped. + * + * @memberOf _.templateSettings + * @type {RegExp} + */ + 'escape': reEscape, + + /** + * Used to detect code to be evaluated. + * + * @memberOf _.templateSettings + * @type {RegExp} + */ + 'evaluate': reEvaluate, + + /** + * Used to detect `data` property values to inject. + * + * @memberOf _.templateSettings + * @type {RegExp} + */ + 'interpolate': reInterpolate, + + /** + * Used to reference the data object in the template text. + * + * @memberOf _.templateSettings + * @type {string} + */ + 'variable': '', + + /** + * Used to import variables into the compiled template. + * + * @memberOf _.templateSettings + * @type {Object} + */ + 'imports': { + + /** + * A reference to the `lodash` function. + * + * @memberOf _.templateSettings.imports + * @type {Function} + */ + '_': lodash + } + }; + + // Ensure wrappers are instances of `baseLodash`. + lodash.prototype = baseLodash.prototype; + lodash.prototype.constructor = lodash; + + LodashWrapper.prototype = baseCreate(baseLodash.prototype); + LodashWrapper.prototype.constructor = LodashWrapper; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a lazy wrapper object which wraps `value` to enable lazy evaluation. + * + * @private + * @constructor + * @param {*} value The value to wrap. + */ + function LazyWrapper(value) { + this.__wrapped__ = value; + this.__actions__ = []; + this.__dir__ = 1; + this.__filtered__ = false; + this.__iteratees__ = []; + this.__takeCount__ = MAX_ARRAY_LENGTH; + this.__views__ = []; + } + + /** + * Creates a clone of the lazy wrapper object. + * + * @private + * @name clone + * @memberOf LazyWrapper + * @returns {Object} Returns the cloned `LazyWrapper` object. + */ + function lazyClone() { + var result = new LazyWrapper(this.__wrapped__); + result.__actions__ = copyArray(this.__actions__); + result.__dir__ = this.__dir__; + result.__filtered__ = this.__filtered__; + result.__iteratees__ = copyArray(this.__iteratees__); + result.__takeCount__ = this.__takeCount__; + result.__views__ = copyArray(this.__views__); + return result; + } + + /** + * Reverses the direction of lazy iteration. + * + * @private + * @name reverse + * @memberOf LazyWrapper + * @returns {Object} Returns the new reversed `LazyWrapper` object. + */ + function lazyReverse() { + if (this.__filtered__) { + var result = new LazyWrapper(this); + result.__dir__ = -1; + result.__filtered__ = true; + } else { + result = this.clone(); + result.__dir__ *= -1; + } + return result; + } + + /** + * Extracts the unwrapped value from its lazy wrapper. + * + * @private + * @name value + * @memberOf LazyWrapper + * @returns {*} Returns the unwrapped value. + */ + function lazyValue() { + var array = this.__wrapped__.value(), + dir = this.__dir__, + isArr = isArray(array), + isRight = dir < 0, + arrLength = isArr ? array.length : 0, + view = getView(0, arrLength, this.__views__), + start = view.start, + end = view.end, + length = end - start, + index = isRight ? end : (start - 1), + iteratees = this.__iteratees__, + iterLength = iteratees.length, + resIndex = 0, + takeCount = nativeMin(length, this.__takeCount__); + + if (!isArr || (!isRight && arrLength == length && takeCount == length)) { + return baseWrapperValue(array, this.__actions__); + } + var result = []; + + outer: + while (length-- && resIndex < takeCount) { + index += dir; + + var iterIndex = -1, + value = array[index]; + + while (++iterIndex < iterLength) { + var data = iteratees[iterIndex], + iteratee = data.iteratee, + type = data.type, + computed = iteratee(value); + + if (type == LAZY_MAP_FLAG) { + value = computed; + } else if (!computed) { + if (type == LAZY_FILTER_FLAG) { + continue outer; + } else { + break outer; + } + } + } + result[resIndex++] = value; + } + return result; + } + + // Ensure `LazyWrapper` is an instance of `baseLodash`. + LazyWrapper.prototype = baseCreate(baseLodash.prototype); + LazyWrapper.prototype.constructor = LazyWrapper; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function Hash(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ + function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; + this.size = 0; + } + + /** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function hashDelete(key) { + var result = this.has(key) && delete this.__data__[key]; + this.size -= result ? 1 : 0; + return result; + } + + /** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty.call(data, key) ? data[key] : undefined; + } + + /** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function hashHas(key) { + var data = this.__data__; + return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key); + } + + /** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ + function hashSet(key, value) { + var data = this.__data__; + this.size += this.has(key) ? 0 : 1; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; + } + + // Add methods to `Hash`. + Hash.prototype.clear = hashClear; + Hash.prototype['delete'] = hashDelete; + Hash.prototype.get = hashGet; + Hash.prototype.has = hashHas; + Hash.prototype.set = hashSet; + + /*------------------------------------------------------------------------*/ + + /** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function ListCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ + function listCacheClear() { + this.__data__ = []; + this.size = 0; + } + + /** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + --this.size; + return true; + } + + /** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; + } + + /** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; + } + + /** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ + function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + ++this.size; + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; + } + + // Add methods to `ListCache`. + ListCache.prototype.clear = listCacheClear; + ListCache.prototype['delete'] = listCacheDelete; + ListCache.prototype.get = listCacheGet; + ListCache.prototype.has = listCacheHas; + ListCache.prototype.set = listCacheSet; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function MapCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ + function mapCacheClear() { + this.size = 0; + this.__data__ = { + 'hash': new Hash, + 'map': new (Map || ListCache), + 'string': new Hash + }; + } + + /** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function mapCacheDelete(key) { + var result = getMapData(this, key)['delete'](key); + this.size -= result ? 1 : 0; + return result; + } + + /** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function mapCacheGet(key) { + return getMapData(this, key).get(key); + } + + /** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function mapCacheHas(key) { + return getMapData(this, key).has(key); + } + + /** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ + function mapCacheSet(key, value) { + var data = getMapData(this, key), + size = data.size; + + data.set(key, value); + this.size += data.size == size ? 0 : 1; + return this; + } + + // Add methods to `MapCache`. + MapCache.prototype.clear = mapCacheClear; + MapCache.prototype['delete'] = mapCacheDelete; + MapCache.prototype.get = mapCacheGet; + MapCache.prototype.has = mapCacheHas; + MapCache.prototype.set = mapCacheSet; + + /*------------------------------------------------------------------------*/ + + /** + * + * Creates an array cache object to store unique values. + * + * @private + * @constructor + * @param {Array} [values] The values to cache. + */ + function SetCache(values) { + var index = -1, + length = values == null ? 0 : values.length; + + this.__data__ = new MapCache; + while (++index < length) { + this.add(values[index]); + } + } + + /** + * Adds `value` to the array cache. + * + * @private + * @name add + * @memberOf SetCache + * @alias push + * @param {*} value The value to cache. + * @returns {Object} Returns the cache instance. + */ + function setCacheAdd(value) { + this.__data__.set(value, HASH_UNDEFINED); + return this; + } + + /** + * Checks if `value` is in the array cache. + * + * @private + * @name has + * @memberOf SetCache + * @param {*} value The value to search for. + * @returns {number} Returns `true` if `value` is found, else `false`. + */ + function setCacheHas(value) { + return this.__data__.has(value); + } + + // Add methods to `SetCache`. + SetCache.prototype.add = SetCache.prototype.push = setCacheAdd; + SetCache.prototype.has = setCacheHas; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a stack cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function Stack(entries) { + var data = this.__data__ = new ListCache(entries); + this.size = data.size; + } + + /** + * Removes all key-value entries from the stack. + * + * @private + * @name clear + * @memberOf Stack + */ + function stackClear() { + this.__data__ = new ListCache; + this.size = 0; + } + + /** + * Removes `key` and its value from the stack. + * + * @private + * @name delete + * @memberOf Stack + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function stackDelete(key) { + var data = this.__data__, + result = data['delete'](key); + + this.size = data.size; + return result; + } + + /** + * Gets the stack value for `key`. + * + * @private + * @name get + * @memberOf Stack + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function stackGet(key) { + return this.__data__.get(key); + } + + /** + * Checks if a stack value for `key` exists. + * + * @private + * @name has + * @memberOf Stack + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function stackHas(key) { + return this.__data__.has(key); + } + + /** + * Sets the stack `key` to `value`. + * + * @private + * @name set + * @memberOf Stack + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the stack cache instance. + */ + function stackSet(key, value) { + var data = this.__data__; + if (data instanceof ListCache) { + var pairs = data.__data__; + if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) { + pairs.push([key, value]); + this.size = ++data.size; + return this; + } + data = this.__data__ = new MapCache(pairs); + } + data.set(key, value); + this.size = data.size; + return this; + } + + // Add methods to `Stack`. + Stack.prototype.clear = stackClear; + Stack.prototype['delete'] = stackDelete; + Stack.prototype.get = stackGet; + Stack.prototype.has = stackHas; + Stack.prototype.set = stackSet; + + /*------------------------------------------------------------------------*/ + + /** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ + function arrayLikeKeys(value, inherited) { + var isArr = isArray(value), + isArg = !isArr && isArguments(value), + isBuff = !isArr && !isArg && isBuffer(value), + isType = !isArr && !isArg && !isBuff && isTypedArray(value), + skipIndexes = isArr || isArg || isBuff || isType, + result = skipIndexes ? baseTimes(value.length, String) : [], + length = result.length; + + for (var key in value) { + if ((inherited || hasOwnProperty.call(value, key)) && + !(skipIndexes && ( + // Safari 9 has enumerable `arguments.length` in strict mode. + key == 'length' || + // Node.js 0.10 has enumerable non-index properties on buffers. + (isBuff && (key == 'offset' || key == 'parent')) || + // PhantomJS 2 has enumerable non-index properties on typed arrays. + (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) || + // Skip index properties. + isIndex(key, length) + ))) { + result.push(key); + } + } + return result; + } + + /** + * A specialized version of `_.sample` for arrays. + * + * @private + * @param {Array} array The array to sample. + * @returns {*} Returns the random element. + */ + function arraySample(array) { + var length = array.length; + return length ? array[baseRandom(0, length - 1)] : undefined; + } + + /** + * A specialized version of `_.sampleSize` for arrays. + * + * @private + * @param {Array} array The array to sample. + * @param {number} n The number of elements to sample. + * @returns {Array} Returns the random elements. + */ + function arraySampleSize(array, n) { + return shuffleSelf(copyArray(array), baseClamp(n, 0, array.length)); + } + + /** + * A specialized version of `_.shuffle` for arrays. + * + * @private + * @param {Array} array The array to shuffle. + * @returns {Array} Returns the new shuffled array. + */ + function arrayShuffle(array) { + return shuffleSelf(copyArray(array)); + } + + /** + * This function is like `assignValue` except that it doesn't assign + * `undefined` values. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ + function assignMergeValue(object, key, value) { + if ((value !== undefined && !eq(object[key], value)) || + (value === undefined && !(key in object))) { + baseAssignValue(object, key, value); + } + } + + /** + * Assigns `value` to `key` of `object` if the existing value is not equivalent + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ + function assignValue(object, key, value) { + var objValue = object[key]; + if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || + (value === undefined && !(key in object))) { + baseAssignValue(object, key, value); + } + } + + /** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; + } + + /** + * Aggregates elements of `collection` on `accumulator` with keys transformed + * by `iteratee` and values set by `setter`. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} setter The function to set `accumulator` values. + * @param {Function} iteratee The iteratee to transform keys. + * @param {Object} accumulator The initial aggregated object. + * @returns {Function} Returns `accumulator`. + */ + function baseAggregator(collection, setter, iteratee, accumulator) { + baseEach(collection, function(value, key, collection) { + setter(accumulator, value, iteratee(value), collection); + }); + return accumulator; + } + + /** + * The base implementation of `_.assign` without support for multiple sources + * or `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @returns {Object} Returns `object`. + */ + function baseAssign(object, source) { + return object && copyObject(source, keys(source), object); + } + + /** + * The base implementation of `_.assignIn` without support for multiple sources + * or `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @returns {Object} Returns `object`. + */ + function baseAssignIn(object, source) { + return object && copyObject(source, keysIn(source), object); + } + + /** + * The base implementation of `assignValue` and `assignMergeValue` without + * value checks. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ + function baseAssignValue(object, key, value) { + if (key == '__proto__' && defineProperty) { + defineProperty(object, key, { + 'configurable': true, + 'enumerable': true, + 'value': value, + 'writable': true + }); + } else { + object[key] = value; + } + } + + /** + * The base implementation of `_.at` without support for individual paths. + * + * @private + * @param {Object} object The object to iterate over. + * @param {string[]} paths The property paths to pick. + * @returns {Array} Returns the picked elements. + */ + function baseAt(object, paths) { + var index = -1, + length = paths.length, + result = Array(length), + skip = object == null; + + while (++index < length) { + result[index] = skip ? undefined : get(object, paths[index]); + } + return result; + } + + /** + * The base implementation of `_.clamp` which doesn't coerce arguments. + * + * @private + * @param {number} number The number to clamp. + * @param {number} [lower] The lower bound. + * @param {number} upper The upper bound. + * @returns {number} Returns the clamped number. + */ + function baseClamp(number, lower, upper) { + if (number === number) { + if (upper !== undefined) { + number = number <= upper ? number : upper; + } + if (lower !== undefined) { + number = number >= lower ? number : lower; + } + } + return number; + } + + /** + * The base implementation of `_.clone` and `_.cloneDeep` which tracks + * traversed objects. + * + * @private + * @param {*} value The value to clone. + * @param {boolean} bitmask The bitmask flags. + * 1 - Deep clone + * 2 - Flatten inherited properties + * 4 - Clone symbols + * @param {Function} [customizer] The function to customize cloning. + * @param {string} [key] The key of `value`. + * @param {Object} [object] The parent object of `value`. + * @param {Object} [stack] Tracks traversed objects and their clone counterparts. + * @returns {*} Returns the cloned value. + */ + function baseClone(value, bitmask, customizer, key, object, stack) { + var result, + isDeep = bitmask & CLONE_DEEP_FLAG, + isFlat = bitmask & CLONE_FLAT_FLAG, + isFull = bitmask & CLONE_SYMBOLS_FLAG; + + if (customizer) { + result = object ? customizer(value, key, object, stack) : customizer(value); + } + if (result !== undefined) { + return result; + } + if (!isObject(value)) { + return value; + } + var isArr = isArray(value); + if (isArr) { + result = initCloneArray(value); + if (!isDeep) { + return copyArray(value, result); + } + } else { + var tag = getTag(value), + isFunc = tag == funcTag || tag == genTag; + + if (isBuffer(value)) { + return cloneBuffer(value, isDeep); + } + if (tag == objectTag || tag == argsTag || (isFunc && !object)) { + result = (isFlat || isFunc) ? {} : initCloneObject(value); + if (!isDeep) { + return isFlat + ? copySymbolsIn(value, baseAssignIn(result, value)) + : copySymbols(value, baseAssign(result, value)); + } + } else { + if (!cloneableTags[tag]) { + return object ? value : {}; + } + result = initCloneByTag(value, tag, isDeep); + } + } + // Check for circular references and return its corresponding clone. + stack || (stack = new Stack); + var stacked = stack.get(value); + if (stacked) { + return stacked; + } + stack.set(value, result); + + if (isSet(value)) { + value.forEach(function(subValue) { + result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack)); + }); + + return result; + } + + if (isMap(value)) { + value.forEach(function(subValue, key) { + result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack)); + }); + + return result; + } + + var keysFunc = isFull + ? (isFlat ? getAllKeysIn : getAllKeys) + : (isFlat ? keysIn : keys); + + var props = isArr ? undefined : keysFunc(value); + arrayEach(props || value, function(subValue, key) { + if (props) { + key = subValue; + subValue = value[key]; + } + // Recursively populate clone (susceptible to call stack limits). + assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)); + }); + return result; + } + + /** + * The base implementation of `_.conforms` which doesn't clone `source`. + * + * @private + * @param {Object} source The object of property predicates to conform to. + * @returns {Function} Returns the new spec function. + */ + function baseConforms(source) { + var props = keys(source); + return function(object) { + return baseConformsTo(object, source, props); + }; + } + + /** + * The base implementation of `_.conformsTo` which accepts `props` to check. + * + * @private + * @param {Object} object The object to inspect. + * @param {Object} source The object of property predicates to conform to. + * @returns {boolean} Returns `true` if `object` conforms, else `false`. + */ + function baseConformsTo(object, source, props) { + var length = props.length; + if (object == null) { + return !length; + } + object = Object(object); + while (length--) { + var key = props[length], + predicate = source[key], + value = object[key]; + + if ((value === undefined && !(key in object)) || !predicate(value)) { + return false; + } + } + return true; + } + + /** + * The base implementation of `_.delay` and `_.defer` which accepts `args` + * to provide to `func`. + * + * @private + * @param {Function} func The function to delay. + * @param {number} wait The number of milliseconds to delay invocation. + * @param {Array} args The arguments to provide to `func`. + * @returns {number|Object} Returns the timer id or timeout object. + */ + function baseDelay(func, wait, args) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + return setTimeout(function() { func.apply(undefined, args); }, wait); + } + + /** + * The base implementation of methods like `_.difference` without support + * for excluding multiple arrays or iteratee shorthands. + * + * @private + * @param {Array} array The array to inspect. + * @param {Array} values The values to exclude. + * @param {Function} [iteratee] The iteratee invoked per element. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of filtered values. + */ + function baseDifference(array, values, iteratee, comparator) { + var index = -1, + includes = arrayIncludes, + isCommon = true, + length = array.length, + result = [], + valuesLength = values.length; + + if (!length) { + return result; + } + if (iteratee) { + values = arrayMap(values, baseUnary(iteratee)); + } + if (comparator) { + includes = arrayIncludesWith; + isCommon = false; + } + else if (values.length >= LARGE_ARRAY_SIZE) { + includes = cacheHas; + isCommon = false; + values = new SetCache(values); + } + outer: + while (++index < length) { + var value = array[index], + computed = iteratee == null ? value : iteratee(value); + + value = (comparator || value !== 0) ? value : 0; + if (isCommon && computed === computed) { + var valuesIndex = valuesLength; + while (valuesIndex--) { + if (values[valuesIndex] === computed) { + continue outer; + } + } + result.push(value); + } + else if (!includes(values, computed, comparator)) { + result.push(value); + } + } + return result; + } + + /** + * The base implementation of `_.forEach` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + */ + var baseEach = createBaseEach(baseForOwn); + + /** + * The base implementation of `_.forEachRight` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + */ + var baseEachRight = createBaseEach(baseForOwnRight, true); + + /** + * The base implementation of `_.every` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if all elements pass the predicate check, + * else `false` + */ + function baseEvery(collection, predicate) { + var result = true; + baseEach(collection, function(value, index, collection) { + result = !!predicate(value, index, collection); + return result; + }); + return result; + } + + /** + * The base implementation of methods like `_.max` and `_.min` which accepts a + * `comparator` to determine the extremum value. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The iteratee invoked per iteration. + * @param {Function} comparator The comparator used to compare values. + * @returns {*} Returns the extremum value. + */ + function baseExtremum(array, iteratee, comparator) { + var index = -1, + length = array.length; + + while (++index < length) { + var value = array[index], + current = iteratee(value); + + if (current != null && (computed === undefined + ? (current === current && !isSymbol(current)) + : comparator(current, computed) + )) { + var computed = current, + result = value; + } + } + return result; + } + + /** + * The base implementation of `_.fill` without an iteratee call guard. + * + * @private + * @param {Array} array The array to fill. + * @param {*} value The value to fill `array` with. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns `array`. + */ + function baseFill(array, value, start, end) { + var length = array.length; + + start = toInteger(start); + if (start < 0) { + start = -start > length ? 0 : (length + start); + } + end = (end === undefined || end > length) ? length : toInteger(end); + if (end < 0) { + end += length; + } + end = start > end ? 0 : toLength(end); + while (start < end) { + array[start++] = value; + } + return array; + } + + /** + * The base implementation of `_.filter` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + */ + function baseFilter(collection, predicate) { + var result = []; + baseEach(collection, function(value, index, collection) { + if (predicate(value, index, collection)) { + result.push(value); + } + }); + return result; + } + + /** + * The base implementation of `_.flatten` with support for restricting flattening. + * + * @private + * @param {Array} array The array to flatten. + * @param {number} depth The maximum recursion depth. + * @param {boolean} [predicate=isFlattenable] The function invoked per iteration. + * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks. + * @param {Array} [result=[]] The initial result value. + * @returns {Array} Returns the new flattened array. + */ + function baseFlatten(array, depth, predicate, isStrict, result) { + var index = -1, + length = array.length; + + predicate || (predicate = isFlattenable); + result || (result = []); + + while (++index < length) { + var value = array[index]; + if (depth > 0 && predicate(value)) { + if (depth > 1) { + // Recursively flatten arrays (susceptible to call stack limits). + baseFlatten(value, depth - 1, predicate, isStrict, result); + } else { + arrayPush(result, value); + } + } else if (!isStrict) { + result[result.length] = value; + } + } + return result; + } + + /** + * The base implementation of `baseForOwn` which iterates over `object` + * properties returned by `keysFunc` and invokes `iteratee` for each property. + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} keysFunc The function to get the keys of `object`. + * @returns {Object} Returns `object`. + */ + var baseFor = createBaseFor(); + + /** + * This function is like `baseFor` except that it iterates over properties + * in the opposite order. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} keysFunc The function to get the keys of `object`. + * @returns {Object} Returns `object`. + */ + var baseForRight = createBaseFor(true); + + /** + * The base implementation of `_.forOwn` without support for iteratee shorthands. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ + function baseForOwn(object, iteratee) { + return object && baseFor(object, iteratee, keys); + } + + /** + * The base implementation of `_.forOwnRight` without support for iteratee shorthands. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ + function baseForOwnRight(object, iteratee) { + return object && baseForRight(object, iteratee, keys); + } + + /** + * The base implementation of `_.functions` which creates an array of + * `object` function property names filtered from `props`. + * + * @private + * @param {Object} object The object to inspect. + * @param {Array} props The property names to filter. + * @returns {Array} Returns the function names. + */ + function baseFunctions(object, props) { + return arrayFilter(props, function(key) { + return isFunction(object[key]); + }); + } + + /** + * The base implementation of `_.get` without support for default values. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to get. + * @returns {*} Returns the resolved value. + */ + function baseGet(object, path) { + path = castPath(path, object); + + var index = 0, + length = path.length; + + while (object != null && index < length) { + object = object[toKey(path[index++])]; + } + return (index && index == length) ? object : undefined; + } + + /** + * The base implementation of `getAllKeys` and `getAllKeysIn` which uses + * `keysFunc` and `symbolsFunc` to get the enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Function} keysFunc The function to get the keys of `object`. + * @param {Function} symbolsFunc The function to get the symbols of `object`. + * @returns {Array} Returns the array of property names and symbols. + */ + function baseGetAllKeys(object, keysFunc, symbolsFunc) { + var result = keysFunc(object); + return isArray(object) ? result : arrayPush(result, symbolsFunc(object)); + } + + /** + * The base implementation of `getTag` without fallbacks for buggy environments. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + function baseGetTag(value) { + if (value == null) { + return value === undefined ? undefinedTag : nullTag; + } + return (symToStringTag && symToStringTag in Object(value)) + ? getRawTag(value) + : objectToString(value); + } + + /** + * The base implementation of `_.gt` which doesn't coerce arguments. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is greater than `other`, + * else `false`. + */ + function baseGt(value, other) { + return value > other; + } + + /** + * The base implementation of `_.has` without support for deep paths. + * + * @private + * @param {Object} [object] The object to query. + * @param {Array|string} key The key to check. + * @returns {boolean} Returns `true` if `key` exists, else `false`. + */ + function baseHas(object, key) { + return object != null && hasOwnProperty.call(object, key); + } + + /** + * The base implementation of `_.hasIn` without support for deep paths. + * + * @private + * @param {Object} [object] The object to query. + * @param {Array|string} key The key to check. + * @returns {boolean} Returns `true` if `key` exists, else `false`. + */ + function baseHasIn(object, key) { + return object != null && key in Object(object); + } + + /** + * The base implementation of `_.inRange` which doesn't coerce arguments. + * + * @private + * @param {number} number The number to check. + * @param {number} start The start of the range. + * @param {number} end The end of the range. + * @returns {boolean} Returns `true` if `number` is in the range, else `false`. + */ + function baseInRange(number, start, end) { + return number >= nativeMin(start, end) && number < nativeMax(start, end); + } + + /** + * The base implementation of methods like `_.intersection`, without support + * for iteratee shorthands, that accepts an array of arrays to inspect. + * + * @private + * @param {Array} arrays The arrays to inspect. + * @param {Function} [iteratee] The iteratee invoked per element. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of shared values. + */ + function baseIntersection(arrays, iteratee, comparator) { + var includes = comparator ? arrayIncludesWith : arrayIncludes, + length = arrays[0].length, + othLength = arrays.length, + othIndex = othLength, + caches = Array(othLength), + maxLength = Infinity, + result = []; + + while (othIndex--) { + var array = arrays[othIndex]; + if (othIndex && iteratee) { + array = arrayMap(array, baseUnary(iteratee)); + } + maxLength = nativeMin(array.length, maxLength); + caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120)) + ? new SetCache(othIndex && array) + : undefined; + } + array = arrays[0]; + + var index = -1, + seen = caches[0]; + + outer: + while (++index < length && result.length < maxLength) { + var value = array[index], + computed = iteratee ? iteratee(value) : value; + + value = (comparator || value !== 0) ? value : 0; + if (!(seen + ? cacheHas(seen, computed) + : includes(result, computed, comparator) + )) { + othIndex = othLength; + while (--othIndex) { + var cache = caches[othIndex]; + if (!(cache + ? cacheHas(cache, computed) + : includes(arrays[othIndex], computed, comparator)) + ) { + continue outer; + } + } + if (seen) { + seen.push(computed); + } + result.push(value); + } + } + return result; + } + + /** + * The base implementation of `_.invert` and `_.invertBy` which inverts + * `object` with values transformed by `iteratee` and set by `setter`. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} setter The function to set `accumulator` values. + * @param {Function} iteratee The iteratee to transform values. + * @param {Object} accumulator The initial inverted object. + * @returns {Function} Returns `accumulator`. + */ + function baseInverter(object, setter, iteratee, accumulator) { + baseForOwn(object, function(value, key, object) { + setter(accumulator, iteratee(value), key, object); + }); + return accumulator; + } + + /** + * The base implementation of `_.invoke` without support for individual + * method arguments. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path of the method to invoke. + * @param {Array} args The arguments to invoke the method with. + * @returns {*} Returns the result of the invoked method. + */ + function baseInvoke(object, path, args) { + path = castPath(path, object); + object = parent(object, path); + var func = object == null ? object : object[toKey(last(path))]; + return func == null ? undefined : apply(func, object, args); + } + + /** + * The base implementation of `_.isArguments`. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + */ + function baseIsArguments(value) { + return isObjectLike(value) && baseGetTag(value) == argsTag; + } + + /** + * The base implementation of `_.isArrayBuffer` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`. + */ + function baseIsArrayBuffer(value) { + return isObjectLike(value) && baseGetTag(value) == arrayBufferTag; + } + + /** + * The base implementation of `_.isDate` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a date object, else `false`. + */ + function baseIsDate(value) { + return isObjectLike(value) && baseGetTag(value) == dateTag; + } + + /** + * The base implementation of `_.isEqual` which supports partial comparisons + * and tracks traversed objects. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @param {boolean} bitmask The bitmask flags. + * 1 - Unordered comparison + * 2 - Partial comparison + * @param {Function} [customizer] The function to customize comparisons. + * @param {Object} [stack] Tracks traversed `value` and `other` objects. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + */ + function baseIsEqual(value, other, bitmask, customizer, stack) { + if (value === other) { + return true; + } + if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) { + return value !== value && other !== other; + } + return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack); + } + + /** + * A specialized version of `baseIsEqual` for arrays and objects which performs + * deep comparisons and tracks traversed objects enabling objects with circular + * references to be compared. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} [stack] Tracks traversed `object` and `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ + function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) { + var objIsArr = isArray(object), + othIsArr = isArray(other), + objTag = objIsArr ? arrayTag : getTag(object), + othTag = othIsArr ? arrayTag : getTag(other); + + objTag = objTag == argsTag ? objectTag : objTag; + othTag = othTag == argsTag ? objectTag : othTag; + + var objIsObj = objTag == objectTag, + othIsObj = othTag == objectTag, + isSameTag = objTag == othTag; + + if (isSameTag && isBuffer(object)) { + if (!isBuffer(other)) { + return false; + } + objIsArr = true; + objIsObj = false; + } + if (isSameTag && !objIsObj) { + stack || (stack = new Stack); + return (objIsArr || isTypedArray(object)) + ? equalArrays(object, other, bitmask, customizer, equalFunc, stack) + : equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack); + } + if (!(bitmask & COMPARE_PARTIAL_FLAG)) { + var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'), + othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__'); + + if (objIsWrapped || othIsWrapped) { + var objUnwrapped = objIsWrapped ? object.value() : object, + othUnwrapped = othIsWrapped ? other.value() : other; + + stack || (stack = new Stack); + return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack); + } + } + if (!isSameTag) { + return false; + } + stack || (stack = new Stack); + return equalObjects(object, other, bitmask, customizer, equalFunc, stack); + } + + /** + * The base implementation of `_.isMap` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a map, else `false`. + */ + function baseIsMap(value) { + return isObjectLike(value) && getTag(value) == mapTag; + } + + /** + * The base implementation of `_.isMatch` without support for iteratee shorthands. + * + * @private + * @param {Object} object The object to inspect. + * @param {Object} source The object of property values to match. + * @param {Array} matchData The property names, values, and compare flags to match. + * @param {Function} [customizer] The function to customize comparisons. + * @returns {boolean} Returns `true` if `object` is a match, else `false`. + */ + function baseIsMatch(object, source, matchData, customizer) { + var index = matchData.length, + length = index, + noCustomizer = !customizer; + + if (object == null) { + return !length; + } + object = Object(object); + while (index--) { + var data = matchData[index]; + if ((noCustomizer && data[2]) + ? data[1] !== object[data[0]] + : !(data[0] in object) + ) { + return false; + } + } + while (++index < length) { + data = matchData[index]; + var key = data[0], + objValue = object[key], + srcValue = data[1]; + + if (noCustomizer && data[2]) { + if (objValue === undefined && !(key in object)) { + return false; + } + } else { + var stack = new Stack; + if (customizer) { + var result = customizer(objValue, srcValue, key, object, source, stack); + } + if (!(result === undefined + ? baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG, customizer, stack) + : result + )) { + return false; + } + } + } + return true; + } + + /** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ + function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = isFunction(value) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); + } + + /** + * The base implementation of `_.isRegExp` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a regexp, else `false`. + */ + function baseIsRegExp(value) { + return isObjectLike(value) && baseGetTag(value) == regexpTag; + } + + /** + * The base implementation of `_.isSet` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a set, else `false`. + */ + function baseIsSet(value) { + return isObjectLike(value) && getTag(value) == setTag; + } + + /** + * The base implementation of `_.isTypedArray` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a typed array, else `false`. + */ + function baseIsTypedArray(value) { + return isObjectLike(value) && + isLength(value.length) && !!typedArrayTags[baseGetTag(value)]; + } + + /** + * The base implementation of `_.iteratee`. + * + * @private + * @param {*} [value=_.identity] The value to convert to an iteratee. + * @returns {Function} Returns the iteratee. + */ + function baseIteratee(value) { + // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9. + // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details. + if (typeof value == 'function') { + return value; + } + if (value == null) { + return identity; + } + if (typeof value == 'object') { + return isArray(value) + ? baseMatchesProperty(value[0], value[1]) + : baseMatches(value); + } + return property(value); + } + + /** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function baseKeys(object) { + if (!isPrototype(object)) { + return nativeKeys(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; + } + + /** + * The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function baseKeysIn(object) { + if (!isObject(object)) { + return nativeKeysIn(object); + } + var isProto = isPrototype(object), + result = []; + + for (var key in object) { + if (!(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) { + result.push(key); + } + } + return result; + } + + /** + * The base implementation of `_.lt` which doesn't coerce arguments. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is less than `other`, + * else `false`. + */ + function baseLt(value, other) { + return value < other; + } + + /** + * The base implementation of `_.map` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ + function baseMap(collection, iteratee) { + var index = -1, + result = isArrayLike(collection) ? Array(collection.length) : []; + + baseEach(collection, function(value, key, collection) { + result[++index] = iteratee(value, key, collection); + }); + return result; + } + + /** + * The base implementation of `_.matches` which doesn't clone `source`. + * + * @private + * @param {Object} source The object of property values to match. + * @returns {Function} Returns the new spec function. + */ + function baseMatches(source) { + var matchData = getMatchData(source); + if (matchData.length == 1 && matchData[0][2]) { + return matchesStrictComparable(matchData[0][0], matchData[0][1]); + } + return function(object) { + return object === source || baseIsMatch(object, source, matchData); + }; + } + + /** + * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`. + * + * @private + * @param {string} path The path of the property to get. + * @param {*} srcValue The value to match. + * @returns {Function} Returns the new spec function. + */ + function baseMatchesProperty(path, srcValue) { + if (isKey(path) && isStrictComparable(srcValue)) { + return matchesStrictComparable(toKey(path), srcValue); + } + return function(object) { + var objValue = get(object, path); + return (objValue === undefined && objValue === srcValue) + ? hasIn(object, path) + : baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG); + }; + } + + /** + * The base implementation of `_.merge` without support for multiple sources. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {number} srcIndex The index of `source`. + * @param {Function} [customizer] The function to customize merged values. + * @param {Object} [stack] Tracks traversed source values and their merged + * counterparts. + */ + function baseMerge(object, source, srcIndex, customizer, stack) { + if (object === source) { + return; + } + baseFor(source, function(srcValue, key) { + if (isObject(srcValue)) { + stack || (stack = new Stack); + baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack); + } + else { + var newValue = customizer + ? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack) + : undefined; + + if (newValue === undefined) { + newValue = srcValue; + } + assignMergeValue(object, key, newValue); + } + }, keysIn); + } + + /** + * A specialized version of `baseMerge` for arrays and objects which performs + * deep merges and tracks traversed objects enabling objects with circular + * references to be merged. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {string} key The key of the value to merge. + * @param {number} srcIndex The index of `source`. + * @param {Function} mergeFunc The function to merge values. + * @param {Function} [customizer] The function to customize assigned values. + * @param {Object} [stack] Tracks traversed source values and their merged + * counterparts. + */ + function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) { + var objValue = safeGet(object, key), + srcValue = safeGet(source, key), + stacked = stack.get(srcValue); + + if (stacked) { + assignMergeValue(object, key, stacked); + return; + } + var newValue = customizer + ? customizer(objValue, srcValue, (key + ''), object, source, stack) + : undefined; + + var isCommon = newValue === undefined; + + if (isCommon) { + var isArr = isArray(srcValue), + isBuff = !isArr && isBuffer(srcValue), + isTyped = !isArr && !isBuff && isTypedArray(srcValue); + + newValue = srcValue; + if (isArr || isBuff || isTyped) { + if (isArray(objValue)) { + newValue = objValue; + } + else if (isArrayLikeObject(objValue)) { + newValue = copyArray(objValue); + } + else if (isBuff) { + isCommon = false; + newValue = cloneBuffer(srcValue, true); + } + else if (isTyped) { + isCommon = false; + newValue = cloneTypedArray(srcValue, true); + } + else { + newValue = []; + } + } + else if (isPlainObject(srcValue) || isArguments(srcValue)) { + newValue = objValue; + if (isArguments(objValue)) { + newValue = toPlainObject(objValue); + } + else if (!isObject(objValue) || (srcIndex && isFunction(objValue))) { + newValue = initCloneObject(srcValue); + } + } + else { + isCommon = false; + } + } + if (isCommon) { + // Recursively merge objects and arrays (susceptible to call stack limits). + stack.set(srcValue, newValue); + mergeFunc(newValue, srcValue, srcIndex, customizer, stack); + stack['delete'](srcValue); + } + assignMergeValue(object, key, newValue); + } + + /** + * The base implementation of `_.nth` which doesn't coerce arguments. + * + * @private + * @param {Array} array The array to query. + * @param {number} n The index of the element to return. + * @returns {*} Returns the nth element of `array`. + */ + function baseNth(array, n) { + var length = array.length; + if (!length) { + return; + } + n += n < 0 ? length : 0; + return isIndex(n, length) ? array[n] : undefined; + } + + /** + * The base implementation of `_.orderBy` without param guards. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by. + * @param {string[]} orders The sort orders of `iteratees`. + * @returns {Array} Returns the new sorted array. + */ + function baseOrderBy(collection, iteratees, orders) { + var index = -1; + iteratees = arrayMap(iteratees.length ? iteratees : [identity], baseUnary(getIteratee())); + + var result = baseMap(collection, function(value, key, collection) { + var criteria = arrayMap(iteratees, function(iteratee) { + return iteratee(value); + }); + return { 'criteria': criteria, 'index': ++index, 'value': value }; + }); + + return baseSortBy(result, function(object, other) { + return compareMultiple(object, other, orders); + }); + } + + /** + * The base implementation of `_.pick` without support for individual + * property identifiers. + * + * @private + * @param {Object} object The source object. + * @param {string[]} paths The property paths to pick. + * @returns {Object} Returns the new object. + */ + function basePick(object, paths) { + return basePickBy(object, paths, function(value, path) { + return hasIn(object, path); + }); + } + + /** + * The base implementation of `_.pickBy` without support for iteratee shorthands. + * + * @private + * @param {Object} object The source object. + * @param {string[]} paths The property paths to pick. + * @param {Function} predicate The function invoked per property. + * @returns {Object} Returns the new object. + */ + function basePickBy(object, paths, predicate) { + var index = -1, + length = paths.length, + result = {}; + + while (++index < length) { + var path = paths[index], + value = baseGet(object, path); + + if (predicate(value, path)) { + baseSet(result, castPath(path, object), value); + } + } + return result; + } + + /** + * A specialized version of `baseProperty` which supports deep paths. + * + * @private + * @param {Array|string} path The path of the property to get. + * @returns {Function} Returns the new accessor function. + */ + function basePropertyDeep(path) { + return function(object) { + return baseGet(object, path); + }; + } + + /** + * The base implementation of `_.pullAllBy` without support for iteratee + * shorthands. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to remove. + * @param {Function} [iteratee] The iteratee invoked per element. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns `array`. + */ + function basePullAll(array, values, iteratee, comparator) { + var indexOf = comparator ? baseIndexOfWith : baseIndexOf, + index = -1, + length = values.length, + seen = array; + + if (array === values) { + values = copyArray(values); + } + if (iteratee) { + seen = arrayMap(array, baseUnary(iteratee)); + } + while (++index < length) { + var fromIndex = 0, + value = values[index], + computed = iteratee ? iteratee(value) : value; + + while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) { + if (seen !== array) { + splice.call(seen, fromIndex, 1); + } + splice.call(array, fromIndex, 1); + } + } + return array; + } + + /** + * The base implementation of `_.pullAt` without support for individual + * indexes or capturing the removed elements. + * + * @private + * @param {Array} array The array to modify. + * @param {number[]} indexes The indexes of elements to remove. + * @returns {Array} Returns `array`. + */ + function basePullAt(array, indexes) { + var length = array ? indexes.length : 0, + lastIndex = length - 1; + + while (length--) { + var index = indexes[length]; + if (length == lastIndex || index !== previous) { + var previous = index; + if (isIndex(index)) { + splice.call(array, index, 1); + } else { + baseUnset(array, index); + } + } + } + return array; + } + + /** + * The base implementation of `_.random` without support for returning + * floating-point numbers. + * + * @private + * @param {number} lower The lower bound. + * @param {number} upper The upper bound. + * @returns {number} Returns the random number. + */ + function baseRandom(lower, upper) { + return lower + nativeFloor(nativeRandom() * (upper - lower + 1)); + } + + /** + * The base implementation of `_.range` and `_.rangeRight` which doesn't + * coerce arguments. + * + * @private + * @param {number} start The start of the range. + * @param {number} end The end of the range. + * @param {number} step The value to increment or decrement by. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Array} Returns the range of numbers. + */ + function baseRange(start, end, step, fromRight) { + var index = -1, + length = nativeMax(nativeCeil((end - start) / (step || 1)), 0), + result = Array(length); + + while (length--) { + result[fromRight ? length : ++index] = start; + start += step; + } + return result; + } + + /** + * The base implementation of `_.repeat` which doesn't coerce arguments. + * + * @private + * @param {string} string The string to repeat. + * @param {number} n The number of times to repeat the string. + * @returns {string} Returns the repeated string. + */ + function baseRepeat(string, n) { + var result = ''; + if (!string || n < 1 || n > MAX_SAFE_INTEGER) { + return result; + } + // Leverage the exponentiation by squaring algorithm for a faster repeat. + // See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details. + do { + if (n % 2) { + result += string; + } + n = nativeFloor(n / 2); + if (n) { + string += string; + } + } while (n); + + return result; + } + + /** + * The base implementation of `_.rest` which doesn't validate or coerce arguments. + * + * @private + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @returns {Function} Returns the new function. + */ + function baseRest(func, start) { + return setToString(overRest(func, start, identity), func + ''); + } + + /** + * The base implementation of `_.sample`. + * + * @private + * @param {Array|Object} collection The collection to sample. + * @returns {*} Returns the random element. + */ + function baseSample(collection) { + return arraySample(values(collection)); + } + + /** + * The base implementation of `_.sampleSize` without param guards. + * + * @private + * @param {Array|Object} collection The collection to sample. + * @param {number} n The number of elements to sample. + * @returns {Array} Returns the random elements. + */ + function baseSampleSize(collection, n) { + var array = values(collection); + return shuffleSelf(array, baseClamp(n, 0, array.length)); + } + + /** + * The base implementation of `_.set`. + * + * @private + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @param {Function} [customizer] The function to customize path creation. + * @returns {Object} Returns `object`. + */ + function baseSet(object, path, value, customizer) { + if (!isObject(object)) { + return object; + } + path = castPath(path, object); + + var index = -1, + length = path.length, + lastIndex = length - 1, + nested = object; + + while (nested != null && ++index < length) { + var key = toKey(path[index]), + newValue = value; + + if (index != lastIndex) { + var objValue = nested[key]; + newValue = customizer ? customizer(objValue, key, nested) : undefined; + if (newValue === undefined) { + newValue = isObject(objValue) + ? objValue + : (isIndex(path[index + 1]) ? [] : {}); + } + } + assignValue(nested, key, newValue); + nested = nested[key]; + } + return object; + } + + /** + * The base implementation of `setData` without support for hot loop shorting. + * + * @private + * @param {Function} func The function to associate metadata with. + * @param {*} data The metadata. + * @returns {Function} Returns `func`. + */ + var baseSetData = !metaMap ? identity : function(func, data) { + metaMap.set(func, data); + return func; + }; + + /** + * The base implementation of `setToString` without support for hot loop shorting. + * + * @private + * @param {Function} func The function to modify. + * @param {Function} string The `toString` result. + * @returns {Function} Returns `func`. + */ + var baseSetToString = !defineProperty ? identity : function(func, string) { + return defineProperty(func, 'toString', { + 'configurable': true, + 'enumerable': false, + 'value': constant(string), + 'writable': true + }); + }; + + /** + * The base implementation of `_.shuffle`. + * + * @private + * @param {Array|Object} collection The collection to shuffle. + * @returns {Array} Returns the new shuffled array. + */ + function baseShuffle(collection) { + return shuffleSelf(values(collection)); + } + + /** + * The base implementation of `_.slice` without an iteratee call guard. + * + * @private + * @param {Array} array The array to slice. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the slice of `array`. + */ + function baseSlice(array, start, end) { + var index = -1, + length = array.length; + + if (start < 0) { + start = -start > length ? 0 : (length + start); + } + end = end > length ? length : end; + if (end < 0) { + end += length; + } + length = start > end ? 0 : ((end - start) >>> 0); + start >>>= 0; + + var result = Array(length); + while (++index < length) { + result[index] = array[index + start]; + } + return result; + } + + /** + * The base implementation of `_.some` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if any element passes the predicate check, + * else `false`. + */ + function baseSome(collection, predicate) { + var result; + + baseEach(collection, function(value, index, collection) { + result = predicate(value, index, collection); + return !result; + }); + return !!result; + } + + /** + * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which + * performs a binary search of `array` to determine the index at which `value` + * should be inserted into `array` in order to maintain its sort order. + * + * @private + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {boolean} [retHighest] Specify returning the highest qualified index. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + */ + function baseSortedIndex(array, value, retHighest) { + var low = 0, + high = array == null ? low : array.length; + + if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) { + while (low < high) { + var mid = (low + high) >>> 1, + computed = array[mid]; + + if (computed !== null && !isSymbol(computed) && + (retHighest ? (computed <= value) : (computed < value))) { + low = mid + 1; + } else { + high = mid; + } + } + return high; + } + return baseSortedIndexBy(array, value, identity, retHighest); + } + + /** + * The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy` + * which invokes `iteratee` for `value` and each element of `array` to compute + * their sort ranking. The iteratee is invoked with one argument; (value). + * + * @private + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {Function} iteratee The iteratee invoked per element. + * @param {boolean} [retHighest] Specify returning the highest qualified index. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + */ + function baseSortedIndexBy(array, value, iteratee, retHighest) { + value = iteratee(value); + + var low = 0, + high = array == null ? 0 : array.length, + valIsNaN = value !== value, + valIsNull = value === null, + valIsSymbol = isSymbol(value), + valIsUndefined = value === undefined; + + while (low < high) { + var mid = nativeFloor((low + high) / 2), + computed = iteratee(array[mid]), + othIsDefined = computed !== undefined, + othIsNull = computed === null, + othIsReflexive = computed === computed, + othIsSymbol = isSymbol(computed); + + if (valIsNaN) { + var setLow = retHighest || othIsReflexive; + } else if (valIsUndefined) { + setLow = othIsReflexive && (retHighest || othIsDefined); + } else if (valIsNull) { + setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull); + } else if (valIsSymbol) { + setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol); + } else if (othIsNull || othIsSymbol) { + setLow = false; + } else { + setLow = retHighest ? (computed <= value) : (computed < value); + } + if (setLow) { + low = mid + 1; + } else { + high = mid; + } + } + return nativeMin(high, MAX_ARRAY_INDEX); + } + + /** + * The base implementation of `_.sortedUniq` and `_.sortedUniqBy` without + * support for iteratee shorthands. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The iteratee invoked per element. + * @returns {Array} Returns the new duplicate free array. + */ + function baseSortedUniq(array, iteratee) { + var index = -1, + length = array.length, + resIndex = 0, + result = []; + + while (++index < length) { + var value = array[index], + computed = iteratee ? iteratee(value) : value; + + if (!index || !eq(computed, seen)) { + var seen = computed; + result[resIndex++] = value === 0 ? 0 : value; + } + } + return result; + } + + /** + * The base implementation of `_.toNumber` which doesn't ensure correct + * conversions of binary, hexadecimal, or octal string values. + * + * @private + * @param {*} value The value to process. + * @returns {number} Returns the number. + */ + function baseToNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + return +value; + } + + /** + * The base implementation of `_.toString` which doesn't convert nullish + * values to empty strings. + * + * @private + * @param {*} value The value to process. + * @returns {string} Returns the string. + */ + function baseToString(value) { + // Exit early for strings to avoid a performance hit in some environments. + if (typeof value == 'string') { + return value; + } + if (isArray(value)) { + // Recursively convert values (susceptible to call stack limits). + return arrayMap(value, baseToString) + ''; + } + if (isSymbol(value)) { + return symbolToString ? symbolToString.call(value) : ''; + } + var result = (value + ''); + return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; + } + + /** + * The base implementation of `_.uniqBy` without support for iteratee shorthands. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The iteratee invoked per element. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new duplicate free array. + */ + function baseUniq(array, iteratee, comparator) { + var index = -1, + includes = arrayIncludes, + length = array.length, + isCommon = true, + result = [], + seen = result; + + if (comparator) { + isCommon = false; + includes = arrayIncludesWith; + } + else if (length >= LARGE_ARRAY_SIZE) { + var set = iteratee ? null : createSet(array); + if (set) { + return setToArray(set); + } + isCommon = false; + includes = cacheHas; + seen = new SetCache; + } + else { + seen = iteratee ? [] : result; + } + outer: + while (++index < length) { + var value = array[index], + computed = iteratee ? iteratee(value) : value; + + value = (comparator || value !== 0) ? value : 0; + if (isCommon && computed === computed) { + var seenIndex = seen.length; + while (seenIndex--) { + if (seen[seenIndex] === computed) { + continue outer; + } + } + if (iteratee) { + seen.push(computed); + } + result.push(value); + } + else if (!includes(seen, computed, comparator)) { + if (seen !== result) { + seen.push(computed); + } + result.push(value); + } + } + return result; + } + + /** + * The base implementation of `_.unset`. + * + * @private + * @param {Object} object The object to modify. + * @param {Array|string} path The property path to unset. + * @returns {boolean} Returns `true` if the property is deleted, else `false`. + */ + function baseUnset(object, path) { + path = castPath(path, object); + object = parent(object, path); + return object == null || delete object[toKey(last(path))]; + } + + /** + * The base implementation of `_.update`. + * + * @private + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to update. + * @param {Function} updater The function to produce the updated value. + * @param {Function} [customizer] The function to customize path creation. + * @returns {Object} Returns `object`. + */ + function baseUpdate(object, path, updater, customizer) { + return baseSet(object, path, updater(baseGet(object, path)), customizer); + } + + /** + * The base implementation of methods like `_.dropWhile` and `_.takeWhile` + * without support for iteratee shorthands. + * + * @private + * @param {Array} array The array to query. + * @param {Function} predicate The function invoked per iteration. + * @param {boolean} [isDrop] Specify dropping elements instead of taking them. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Array} Returns the slice of `array`. + */ + function baseWhile(array, predicate, isDrop, fromRight) { + var length = array.length, + index = fromRight ? length : -1; + + while ((fromRight ? index-- : ++index < length) && + predicate(array[index], index, array)) {} + + return isDrop + ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length)) + : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index)); + } + + /** + * The base implementation of `wrapperValue` which returns the result of + * performing a sequence of actions on the unwrapped `value`, where each + * successive action is supplied the return value of the previous. + * + * @private + * @param {*} value The unwrapped value. + * @param {Array} actions Actions to perform to resolve the unwrapped value. + * @returns {*} Returns the resolved value. + */ + function baseWrapperValue(value, actions) { + var result = value; + if (result instanceof LazyWrapper) { + result = result.value(); + } + return arrayReduce(actions, function(result, action) { + return action.func.apply(action.thisArg, arrayPush([result], action.args)); + }, result); + } + + /** + * The base implementation of methods like `_.xor`, without support for + * iteratee shorthands, that accepts an array of arrays to inspect. + * + * @private + * @param {Array} arrays The arrays to inspect. + * @param {Function} [iteratee] The iteratee invoked per element. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of values. + */ + function baseXor(arrays, iteratee, comparator) { + var length = arrays.length; + if (length < 2) { + return length ? baseUniq(arrays[0]) : []; + } + var index = -1, + result = Array(length); + + while (++index < length) { + var array = arrays[index], + othIndex = -1; + + while (++othIndex < length) { + if (othIndex != index) { + result[index] = baseDifference(result[index] || array, arrays[othIndex], iteratee, comparator); + } + } + } + return baseUniq(baseFlatten(result, 1), iteratee, comparator); + } + + /** + * This base implementation of `_.zipObject` which assigns values using `assignFunc`. + * + * @private + * @param {Array} props The property identifiers. + * @param {Array} values The property values. + * @param {Function} assignFunc The function to assign values. + * @returns {Object} Returns the new object. + */ + function baseZipObject(props, values, assignFunc) { + var index = -1, + length = props.length, + valsLength = values.length, + result = {}; + + while (++index < length) { + var value = index < valsLength ? values[index] : undefined; + assignFunc(result, props[index], value); + } + return result; + } + + /** + * Casts `value` to an empty array if it's not an array like object. + * + * @private + * @param {*} value The value to inspect. + * @returns {Array|Object} Returns the cast array-like object. + */ + function castArrayLikeObject(value) { + return isArrayLikeObject(value) ? value : []; + } + + /** + * Casts `value` to `identity` if it's not a function. + * + * @private + * @param {*} value The value to inspect. + * @returns {Function} Returns cast function. + */ + function castFunction(value) { + return typeof value == 'function' ? value : identity; + } + + /** + * Casts `value` to a path array if it's not one. + * + * @private + * @param {*} value The value to inspect. + * @param {Object} [object] The object to query keys on. + * @returns {Array} Returns the cast property path array. + */ + function castPath(value, object) { + if (isArray(value)) { + return value; + } + return isKey(value, object) ? [value] : stringToPath(toString(value)); + } + + /** + * A `baseRest` alias which can be replaced with `identity` by module + * replacement plugins. + * + * @private + * @type {Function} + * @param {Function} func The function to apply a rest parameter to. + * @returns {Function} Returns the new function. + */ + var castRest = baseRest; + + /** + * Casts `array` to a slice if it's needed. + * + * @private + * @param {Array} array The array to inspect. + * @param {number} start The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the cast slice. + */ + function castSlice(array, start, end) { + var length = array.length; + end = end === undefined ? length : end; + return (!start && end >= length) ? array : baseSlice(array, start, end); + } + + /** + * A simple wrapper around the global [`clearTimeout`](https://mdn.io/clearTimeout). + * + * @private + * @param {number|Object} id The timer id or timeout object of the timer to clear. + */ + var clearTimeout = ctxClearTimeout || function(id) { + return root.clearTimeout(id); + }; + + /** + * Creates a clone of `buffer`. + * + * @private + * @param {Buffer} buffer The buffer to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Buffer} Returns the cloned buffer. + */ + function cloneBuffer(buffer, isDeep) { + if (isDeep) { + return buffer.slice(); + } + var length = buffer.length, + result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length); + + buffer.copy(result); + return result; + } + + /** + * Creates a clone of `arrayBuffer`. + * + * @private + * @param {ArrayBuffer} arrayBuffer The array buffer to clone. + * @returns {ArrayBuffer} Returns the cloned array buffer. + */ + function cloneArrayBuffer(arrayBuffer) { + var result = new arrayBuffer.constructor(arrayBuffer.byteLength); + new Uint8Array(result).set(new Uint8Array(arrayBuffer)); + return result; + } + + /** + * Creates a clone of `dataView`. + * + * @private + * @param {Object} dataView The data view to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned data view. + */ + function cloneDataView(dataView, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer; + return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength); + } + + /** + * Creates a clone of `regexp`. + * + * @private + * @param {Object} regexp The regexp to clone. + * @returns {Object} Returns the cloned regexp. + */ + function cloneRegExp(regexp) { + var result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); + result.lastIndex = regexp.lastIndex; + return result; + } + + /** + * Creates a clone of the `symbol` object. + * + * @private + * @param {Object} symbol The symbol object to clone. + * @returns {Object} Returns the cloned symbol object. + */ + function cloneSymbol(symbol) { + return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {}; + } + + /** + * Creates a clone of `typedArray`. + * + * @private + * @param {Object} typedArray The typed array to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned typed array. + */ + function cloneTypedArray(typedArray, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; + return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); + } + + /** + * Compares values to sort them in ascending order. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {number} Returns the sort order indicator for `value`. + */ + function compareAscending(value, other) { + if (value !== other) { + var valIsDefined = value !== undefined, + valIsNull = value === null, + valIsReflexive = value === value, + valIsSymbol = isSymbol(value); + + var othIsDefined = other !== undefined, + othIsNull = other === null, + othIsReflexive = other === other, + othIsSymbol = isSymbol(other); + + if ((!othIsNull && !othIsSymbol && !valIsSymbol && value > other) || + (valIsSymbol && othIsDefined && othIsReflexive && !othIsNull && !othIsSymbol) || + (valIsNull && othIsDefined && othIsReflexive) || + (!valIsDefined && othIsReflexive) || + !valIsReflexive) { + return 1; + } + if ((!valIsNull && !valIsSymbol && !othIsSymbol && value < other) || + (othIsSymbol && valIsDefined && valIsReflexive && !valIsNull && !valIsSymbol) || + (othIsNull && valIsDefined && valIsReflexive) || + (!othIsDefined && valIsReflexive) || + !othIsReflexive) { + return -1; + } + } + return 0; + } + + /** + * Used by `_.orderBy` to compare multiple properties of a value to another + * and stable sort them. + * + * If `orders` is unspecified, all values are sorted in ascending order. Otherwise, + * specify an order of "desc" for descending or "asc" for ascending sort order + * of corresponding values. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {boolean[]|string[]} orders The order to sort by for each property. + * @returns {number} Returns the sort order indicator for `object`. + */ + function compareMultiple(object, other, orders) { + var index = -1, + objCriteria = object.criteria, + othCriteria = other.criteria, + length = objCriteria.length, + ordersLength = orders.length; + + while (++index < length) { + var result = compareAscending(objCriteria[index], othCriteria[index]); + if (result) { + if (index >= ordersLength) { + return result; + } + var order = orders[index]; + return result * (order == 'desc' ? -1 : 1); + } + } + // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications + // that causes it, under certain circumstances, to provide the same value for + // `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247 + // for more details. + // + // This also ensures a stable sort in V8 and other engines. + // See https://bugs.chromium.org/p/v8/issues/detail?id=90 for more details. + return object.index - other.index; + } + + /** + * Creates an array that is the composition of partially applied arguments, + * placeholders, and provided arguments into a single array of arguments. + * + * @private + * @param {Array} args The provided arguments. + * @param {Array} partials The arguments to prepend to those provided. + * @param {Array} holders The `partials` placeholder indexes. + * @params {boolean} [isCurried] Specify composing for a curried function. + * @returns {Array} Returns the new array of composed arguments. + */ + function composeArgs(args, partials, holders, isCurried) { + var argsIndex = -1, + argsLength = args.length, + holdersLength = holders.length, + leftIndex = -1, + leftLength = partials.length, + rangeLength = nativeMax(argsLength - holdersLength, 0), + result = Array(leftLength + rangeLength), + isUncurried = !isCurried; + + while (++leftIndex < leftLength) { + result[leftIndex] = partials[leftIndex]; + } + while (++argsIndex < holdersLength) { + if (isUncurried || argsIndex < argsLength) { + result[holders[argsIndex]] = args[argsIndex]; + } + } + while (rangeLength--) { + result[leftIndex++] = args[argsIndex++]; + } + return result; + } + + /** + * This function is like `composeArgs` except that the arguments composition + * is tailored for `_.partialRight`. + * + * @private + * @param {Array} args The provided arguments. + * @param {Array} partials The arguments to append to those provided. + * @param {Array} holders The `partials` placeholder indexes. + * @params {boolean} [isCurried] Specify composing for a curried function. + * @returns {Array} Returns the new array of composed arguments. + */ + function composeArgsRight(args, partials, holders, isCurried) { + var argsIndex = -1, + argsLength = args.length, + holdersIndex = -1, + holdersLength = holders.length, + rightIndex = -1, + rightLength = partials.length, + rangeLength = nativeMax(argsLength - holdersLength, 0), + result = Array(rangeLength + rightLength), + isUncurried = !isCurried; + + while (++argsIndex < rangeLength) { + result[argsIndex] = args[argsIndex]; + } + var offset = argsIndex; + while (++rightIndex < rightLength) { + result[offset + rightIndex] = partials[rightIndex]; + } + while (++holdersIndex < holdersLength) { + if (isUncurried || argsIndex < argsLength) { + result[offset + holders[holdersIndex]] = args[argsIndex++]; + } + } + return result; + } + + /** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ + function copyArray(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; + } + + /** + * Copies properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy properties from. + * @param {Array} props The property identifiers to copy. + * @param {Object} [object={}] The object to copy properties to. + * @param {Function} [customizer] The function to customize copied values. + * @returns {Object} Returns `object`. + */ + function copyObject(source, props, object, customizer) { + var isNew = !object; + object || (object = {}); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + + var newValue = customizer + ? customizer(object[key], source[key], key, object, source) + : undefined; + + if (newValue === undefined) { + newValue = source[key]; + } + if (isNew) { + baseAssignValue(object, key, newValue); + } else { + assignValue(object, key, newValue); + } + } + return object; + } + + /** + * Copies own symbols of `source` to `object`. + * + * @private + * @param {Object} source The object to copy symbols from. + * @param {Object} [object={}] The object to copy symbols to. + * @returns {Object} Returns `object`. + */ + function copySymbols(source, object) { + return copyObject(source, getSymbols(source), object); + } + + /** + * Copies own and inherited symbols of `source` to `object`. + * + * @private + * @param {Object} source The object to copy symbols from. + * @param {Object} [object={}] The object to copy symbols to. + * @returns {Object} Returns `object`. + */ + function copySymbolsIn(source, object) { + return copyObject(source, getSymbolsIn(source), object); + } + + /** + * Creates a function like `_.groupBy`. + * + * @private + * @param {Function} setter The function to set accumulator values. + * @param {Function} [initializer] The accumulator object initializer. + * @returns {Function} Returns the new aggregator function. + */ + function createAggregator(setter, initializer) { + return function(collection, iteratee) { + var func = isArray(collection) ? arrayAggregator : baseAggregator, + accumulator = initializer ? initializer() : {}; + + return func(collection, setter, getIteratee(iteratee, 2), accumulator); + }; + } + + /** + * Creates a function like `_.assign`. + * + * @private + * @param {Function} assigner The function to assign values. + * @returns {Function} Returns the new assigner function. + */ + function createAssigner(assigner) { + return baseRest(function(object, sources) { + var index = -1, + length = sources.length, + customizer = length > 1 ? sources[length - 1] : undefined, + guard = length > 2 ? sources[2] : undefined; + + customizer = (assigner.length > 3 && typeof customizer == 'function') + ? (length--, customizer) + : undefined; + + if (guard && isIterateeCall(sources[0], sources[1], guard)) { + customizer = length < 3 ? undefined : customizer; + length = 1; + } + object = Object(object); + while (++index < length) { + var source = sources[index]; + if (source) { + assigner(object, source, index, customizer); + } + } + return object; + }); + } + + /** + * Creates a `baseEach` or `baseEachRight` function. + * + * @private + * @param {Function} eachFunc The function to iterate over a collection. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseEach(eachFunc, fromRight) { + return function(collection, iteratee) { + if (collection == null) { + return collection; + } + if (!isArrayLike(collection)) { + return eachFunc(collection, iteratee); + } + var length = collection.length, + index = fromRight ? length : -1, + iterable = Object(collection); + + while ((fromRight ? index-- : ++index < length)) { + if (iteratee(iterable[index], index, iterable) === false) { + break; + } + } + return collection; + }; + } + + /** + * Creates a base function for methods like `_.forIn` and `_.forOwn`. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseFor(fromRight) { + return function(object, iteratee, keysFunc) { + var index = -1, + iterable = Object(object), + props = keysFunc(object), + length = props.length; + + while (length--) { + var key = props[fromRight ? length : ++index]; + if (iteratee(iterable[key], key, iterable) === false) { + break; + } + } + return object; + }; + } + + /** + * Creates a function that wraps `func` to invoke it with the optional `this` + * binding of `thisArg`. + * + * @private + * @param {Function} func The function to wrap. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @param {*} [thisArg] The `this` binding of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createBind(func, bitmask, thisArg) { + var isBind = bitmask & WRAP_BIND_FLAG, + Ctor = createCtor(func); + + function wrapper() { + var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; + return fn.apply(isBind ? thisArg : this, arguments); + } + return wrapper; + } + + /** + * Creates a function like `_.lowerFirst`. + * + * @private + * @param {string} methodName The name of the `String` case method to use. + * @returns {Function} Returns the new case function. + */ + function createCaseFirst(methodName) { + return function(string) { + string = toString(string); + + var strSymbols = hasUnicode(string) + ? stringToArray(string) + : undefined; + + var chr = strSymbols + ? strSymbols[0] + : string.charAt(0); + + var trailing = strSymbols + ? castSlice(strSymbols, 1).join('') + : string.slice(1); + + return chr[methodName]() + trailing; + }; + } + + /** + * Creates a function like `_.camelCase`. + * + * @private + * @param {Function} callback The function to combine each word. + * @returns {Function} Returns the new compounder function. + */ + function createCompounder(callback) { + return function(string) { + return arrayReduce(words(deburr(string).replace(reApos, '')), callback, ''); + }; + } + + /** + * Creates a function that produces an instance of `Ctor` regardless of + * whether it was invoked as part of a `new` expression or by `call` or `apply`. + * + * @private + * @param {Function} Ctor The constructor to wrap. + * @returns {Function} Returns the new wrapped function. + */ + function createCtor(Ctor) { + return function() { + // Use a `switch` statement to work with class constructors. See + // http://ecma-international.org/ecma-262/7.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist + // for more details. + var args = arguments; + switch (args.length) { + case 0: return new Ctor; + case 1: return new Ctor(args[0]); + case 2: return new Ctor(args[0], args[1]); + case 3: return new Ctor(args[0], args[1], args[2]); + case 4: return new Ctor(args[0], args[1], args[2], args[3]); + case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]); + case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]); + case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); + } + var thisBinding = baseCreate(Ctor.prototype), + result = Ctor.apply(thisBinding, args); + + // Mimic the constructor's `return` behavior. + // See https://es5.github.io/#x13.2.2 for more details. + return isObject(result) ? result : thisBinding; + }; + } + + /** + * Creates a function that wraps `func` to enable currying. + * + * @private + * @param {Function} func The function to wrap. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @param {number} arity The arity of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createCurry(func, bitmask, arity) { + var Ctor = createCtor(func); + + function wrapper() { + var length = arguments.length, + args = Array(length), + index = length, + placeholder = getHolder(wrapper); + + while (index--) { + args[index] = arguments[index]; + } + var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder) + ? [] + : replaceHolders(args, placeholder); + + length -= holders.length; + if (length < arity) { + return createRecurry( + func, bitmask, createHybrid, wrapper.placeholder, undefined, + args, holders, undefined, undefined, arity - length); + } + var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; + return apply(fn, this, args); + } + return wrapper; + } + + /** + * Creates a `_.find` or `_.findLast` function. + * + * @private + * @param {Function} findIndexFunc The function to find the collection index. + * @returns {Function} Returns the new find function. + */ + function createFind(findIndexFunc) { + return function(collection, predicate, fromIndex) { + var iterable = Object(collection); + if (!isArrayLike(collection)) { + var iteratee = getIteratee(predicate, 3); + collection = keys(collection); + predicate = function(key) { return iteratee(iterable[key], key, iterable); }; + } + var index = findIndexFunc(collection, predicate, fromIndex); + return index > -1 ? iterable[iteratee ? collection[index] : index] : undefined; + }; + } + + /** + * Creates a `_.flow` or `_.flowRight` function. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new flow function. + */ + function createFlow(fromRight) { + return flatRest(function(funcs) { + var length = funcs.length, + index = length, + prereq = LodashWrapper.prototype.thru; + + if (fromRight) { + funcs.reverse(); + } + while (index--) { + var func = funcs[index]; + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (prereq && !wrapper && getFuncName(func) == 'wrapper') { + var wrapper = new LodashWrapper([], true); + } + } + index = wrapper ? index : length; + while (++index < length) { + func = funcs[index]; + + var funcName = getFuncName(func), + data = funcName == 'wrapper' ? getData(func) : undefined; + + if (data && isLaziable(data[0]) && + data[1] == (WRAP_ARY_FLAG | WRAP_CURRY_FLAG | WRAP_PARTIAL_FLAG | WRAP_REARG_FLAG) && + !data[4].length && data[9] == 1 + ) { + wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]); + } else { + wrapper = (func.length == 1 && isLaziable(func)) + ? wrapper[funcName]() + : wrapper.thru(func); + } + } + return function() { + var args = arguments, + value = args[0]; + + if (wrapper && args.length == 1 && isArray(value)) { + return wrapper.plant(value).value(); + } + var index = 0, + result = length ? funcs[index].apply(this, args) : value; + + while (++index < length) { + result = funcs[index].call(this, result); + } + return result; + }; + }); + } + + /** + * Creates a function that wraps `func` to invoke it with optional `this` + * binding of `thisArg`, partial application, and currying. + * + * @private + * @param {Function|string} func The function or method name to wrap. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {Array} [partials] The arguments to prepend to those provided to + * the new function. + * @param {Array} [holders] The `partials` placeholder indexes. + * @param {Array} [partialsRight] The arguments to append to those provided + * to the new function. + * @param {Array} [holdersRight] The `partialsRight` placeholder indexes. + * @param {Array} [argPos] The argument positions of the new function. + * @param {number} [ary] The arity cap of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) { + var isAry = bitmask & WRAP_ARY_FLAG, + isBind = bitmask & WRAP_BIND_FLAG, + isBindKey = bitmask & WRAP_BIND_KEY_FLAG, + isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG), + isFlip = bitmask & WRAP_FLIP_FLAG, + Ctor = isBindKey ? undefined : createCtor(func); + + function wrapper() { + var length = arguments.length, + args = Array(length), + index = length; + + while (index--) { + args[index] = arguments[index]; + } + if (isCurried) { + var placeholder = getHolder(wrapper), + holdersCount = countHolders(args, placeholder); + } + if (partials) { + args = composeArgs(args, partials, holders, isCurried); + } + if (partialsRight) { + args = composeArgsRight(args, partialsRight, holdersRight, isCurried); + } + length -= holdersCount; + if (isCurried && length < arity) { + var newHolders = replaceHolders(args, placeholder); + return createRecurry( + func, bitmask, createHybrid, wrapper.placeholder, thisArg, + args, newHolders, argPos, ary, arity - length + ); + } + var thisBinding = isBind ? thisArg : this, + fn = isBindKey ? thisBinding[func] : func; + + length = args.length; + if (argPos) { + args = reorder(args, argPos); + } else if (isFlip && length > 1) { + args.reverse(); + } + if (isAry && ary < length) { + args.length = ary; + } + if (this && this !== root && this instanceof wrapper) { + fn = Ctor || createCtor(fn); + } + return fn.apply(thisBinding, args); + } + return wrapper; + } + + /** + * Creates a function like `_.invertBy`. + * + * @private + * @param {Function} setter The function to set accumulator values. + * @param {Function} toIteratee The function to resolve iteratees. + * @returns {Function} Returns the new inverter function. + */ + function createInverter(setter, toIteratee) { + return function(object, iteratee) { + return baseInverter(object, setter, toIteratee(iteratee), {}); + }; + } + + /** + * Creates a function that performs a mathematical operation on two values. + * + * @private + * @param {Function} operator The function to perform the operation. + * @param {number} [defaultValue] The value used for `undefined` arguments. + * @returns {Function} Returns the new mathematical operation function. + */ + function createMathOperation(operator, defaultValue) { + return function(value, other) { + var result; + if (value === undefined && other === undefined) { + return defaultValue; + } + if (value !== undefined) { + result = value; + } + if (other !== undefined) { + if (result === undefined) { + return other; + } + if (typeof value == 'string' || typeof other == 'string') { + value = baseToString(value); + other = baseToString(other); + } else { + value = baseToNumber(value); + other = baseToNumber(other); + } + result = operator(value, other); + } + return result; + }; + } + + /** + * Creates a function like `_.over`. + * + * @private + * @param {Function} arrayFunc The function to iterate over iteratees. + * @returns {Function} Returns the new over function. + */ + function createOver(arrayFunc) { + return flatRest(function(iteratees) { + iteratees = arrayMap(iteratees, baseUnary(getIteratee())); + return baseRest(function(args) { + var thisArg = this; + return arrayFunc(iteratees, function(iteratee) { + return apply(iteratee, thisArg, args); + }); + }); + }); + } + + /** + * Creates the padding for `string` based on `length`. The `chars` string + * is truncated if the number of characters exceeds `length`. + * + * @private + * @param {number} length The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padding for `string`. + */ + function createPadding(length, chars) { + chars = chars === undefined ? ' ' : baseToString(chars); + + var charsLength = chars.length; + if (charsLength < 2) { + return charsLength ? baseRepeat(chars, length) : chars; + } + var result = baseRepeat(chars, nativeCeil(length / stringSize(chars))); + return hasUnicode(chars) + ? castSlice(stringToArray(result), 0, length).join('') + : result.slice(0, length); + } + + /** + * Creates a function that wraps `func` to invoke it with the `this` binding + * of `thisArg` and `partials` prepended to the arguments it receives. + * + * @private + * @param {Function} func The function to wrap. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @param {*} thisArg The `this` binding of `func`. + * @param {Array} partials The arguments to prepend to those provided to + * the new function. + * @returns {Function} Returns the new wrapped function. + */ + function createPartial(func, bitmask, thisArg, partials) { + var isBind = bitmask & WRAP_BIND_FLAG, + Ctor = createCtor(func); + + function wrapper() { + var argsIndex = -1, + argsLength = arguments.length, + leftIndex = -1, + leftLength = partials.length, + args = Array(leftLength + argsLength), + fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; + + while (++leftIndex < leftLength) { + args[leftIndex] = partials[leftIndex]; + } + while (argsLength--) { + args[leftIndex++] = arguments[++argsIndex]; + } + return apply(fn, isBind ? thisArg : this, args); + } + return wrapper; + } + + /** + * Creates a `_.range` or `_.rangeRight` function. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new range function. + */ + function createRange(fromRight) { + return function(start, end, step) { + if (step && typeof step != 'number' && isIterateeCall(start, end, step)) { + end = step = undefined; + } + // Ensure the sign of `-0` is preserved. + start = toFinite(start); + if (end === undefined) { + end = start; + start = 0; + } else { + end = toFinite(end); + } + step = step === undefined ? (start < end ? 1 : -1) : toFinite(step); + return baseRange(start, end, step, fromRight); + }; + } + + /** + * Creates a function that performs a relational operation on two values. + * + * @private + * @param {Function} operator The function to perform the operation. + * @returns {Function} Returns the new relational operation function. + */ + function createRelationalOperation(operator) { + return function(value, other) { + if (!(typeof value == 'string' && typeof other == 'string')) { + value = toNumber(value); + other = toNumber(other); + } + return operator(value, other); + }; + } + + /** + * Creates a function that wraps `func` to continue currying. + * + * @private + * @param {Function} func The function to wrap. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @param {Function} wrapFunc The function to create the `func` wrapper. + * @param {*} placeholder The placeholder value. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {Array} [partials] The arguments to prepend to those provided to + * the new function. + * @param {Array} [holders] The `partials` placeholder indexes. + * @param {Array} [argPos] The argument positions of the new function. + * @param {number} [ary] The arity cap of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) { + var isCurry = bitmask & WRAP_CURRY_FLAG, + newHolders = isCurry ? holders : undefined, + newHoldersRight = isCurry ? undefined : holders, + newPartials = isCurry ? partials : undefined, + newPartialsRight = isCurry ? undefined : partials; + + bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG); + bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG); + + if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) { + bitmask &= ~(WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG); + } + var newData = [ + func, bitmask, thisArg, newPartials, newHolders, newPartialsRight, + newHoldersRight, argPos, ary, arity + ]; + + var result = wrapFunc.apply(undefined, newData); + if (isLaziable(func)) { + setData(result, newData); + } + result.placeholder = placeholder; + return setWrapToString(result, func, bitmask); + } + + /** + * Creates a function like `_.round`. + * + * @private + * @param {string} methodName The name of the `Math` method to use when rounding. + * @returns {Function} Returns the new round function. + */ + function createRound(methodName) { + var func = Math[methodName]; + return function(number, precision) { + number = toNumber(number); + precision = precision == null ? 0 : nativeMin(toInteger(precision), 292); + if (precision) { + // Shift with exponential notation to avoid floating-point issues. + // See [MDN](https://mdn.io/round#Examples) for more details. + var pair = (toString(number) + 'e').split('e'), + value = func(pair[0] + 'e' + (+pair[1] + precision)); + + pair = (toString(value) + 'e').split('e'); + return +(pair[0] + 'e' + (+pair[1] - precision)); + } + return func(number); + }; + } + + /** + * Creates a set object of `values`. + * + * @private + * @param {Array} values The values to add to the set. + * @returns {Object} Returns the new set. + */ + var createSet = !(Set && (1 / setToArray(new Set([,-0]))[1]) == INFINITY) ? noop : function(values) { + return new Set(values); + }; + + /** + * Creates a `_.toPairs` or `_.toPairsIn` function. + * + * @private + * @param {Function} keysFunc The function to get the keys of a given object. + * @returns {Function} Returns the new pairs function. + */ + function createToPairs(keysFunc) { + return function(object) { + var tag = getTag(object); + if (tag == mapTag) { + return mapToArray(object); + } + if (tag == setTag) { + return setToPairs(object); + } + return baseToPairs(object, keysFunc(object)); + }; + } + + /** + * Creates a function that either curries or invokes `func` with optional + * `this` binding and partially applied arguments. + * + * @private + * @param {Function|string} func The function or method name to wrap. + * @param {number} bitmask The bitmask flags. + * 1 - `_.bind` + * 2 - `_.bindKey` + * 4 - `_.curry` or `_.curryRight` of a bound function + * 8 - `_.curry` + * 16 - `_.curryRight` + * 32 - `_.partial` + * 64 - `_.partialRight` + * 128 - `_.rearg` + * 256 - `_.ary` + * 512 - `_.flip` + * @param {*} [thisArg] The `this` binding of `func`. + * @param {Array} [partials] The arguments to be partially applied. + * @param {Array} [holders] The `partials` placeholder indexes. + * @param {Array} [argPos] The argument positions of the new function. + * @param {number} [ary] The arity cap of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { + var isBindKey = bitmask & WRAP_BIND_KEY_FLAG; + if (!isBindKey && typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + var length = partials ? partials.length : 0; + if (!length) { + bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG); + partials = holders = undefined; + } + ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0); + arity = arity === undefined ? arity : toInteger(arity); + length -= holders ? holders.length : 0; + + if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) { + var partialsRight = partials, + holdersRight = holders; + + partials = holders = undefined; + } + var data = isBindKey ? undefined : getData(func); + + var newData = [ + func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, + argPos, ary, arity + ]; + + if (data) { + mergeData(newData, data); + } + func = newData[0]; + bitmask = newData[1]; + thisArg = newData[2]; + partials = newData[3]; + holders = newData[4]; + arity = newData[9] = newData[9] === undefined + ? (isBindKey ? 0 : func.length) + : nativeMax(newData[9] - length, 0); + + if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) { + bitmask &= ~(WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG); + } + if (!bitmask || bitmask == WRAP_BIND_FLAG) { + var result = createBind(func, bitmask, thisArg); + } else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) { + result = createCurry(func, bitmask, arity); + } else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) { + result = createPartial(func, bitmask, thisArg, partials); + } else { + result = createHybrid.apply(undefined, newData); + } + var setter = data ? baseSetData : setData; + return setWrapToString(setter(result, newData), func, bitmask); + } + + /** + * Used by `_.defaults` to customize its `_.assignIn` use to assign properties + * of source objects to the destination object for all destination properties + * that resolve to `undefined`. + * + * @private + * @param {*} objValue The destination value. + * @param {*} srcValue The source value. + * @param {string} key The key of the property to assign. + * @param {Object} object The parent object of `objValue`. + * @returns {*} Returns the value to assign. + */ + function customDefaultsAssignIn(objValue, srcValue, key, object) { + if (objValue === undefined || + (eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) { + return srcValue; + } + return objValue; + } + + /** + * Used by `_.defaultsDeep` to customize its `_.merge` use to merge source + * objects into destination objects that are passed thru. + * + * @private + * @param {*} objValue The destination value. + * @param {*} srcValue The source value. + * @param {string} key The key of the property to merge. + * @param {Object} object The parent object of `objValue`. + * @param {Object} source The parent object of `srcValue`. + * @param {Object} [stack] Tracks traversed source values and their merged + * counterparts. + * @returns {*} Returns the value to assign. + */ + function customDefaultsMerge(objValue, srcValue, key, object, source, stack) { + if (isObject(objValue) && isObject(srcValue)) { + // Recursively merge objects and arrays (susceptible to call stack limits). + stack.set(srcValue, objValue); + baseMerge(objValue, srcValue, undefined, customDefaultsMerge, stack); + stack['delete'](srcValue); + } + return objValue; + } + + /** + * Used by `_.omit` to customize its `_.cloneDeep` use to only clone plain + * objects. + * + * @private + * @param {*} value The value to inspect. + * @param {string} key The key of the property to inspect. + * @returns {*} Returns the uncloned value or `undefined` to defer cloning to `_.cloneDeep`. + */ + function customOmitClone(value) { + return isPlainObject(value) ? undefined : value; + } + + /** + * A specialized version of `baseIsEqualDeep` for arrays with support for + * partial deep comparisons. + * + * @private + * @param {Array} array The array to compare. + * @param {Array} other The other array to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} stack Tracks traversed `array` and `other` objects. + * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`. + */ + function equalArrays(array, other, bitmask, customizer, equalFunc, stack) { + var isPartial = bitmask & COMPARE_PARTIAL_FLAG, + arrLength = array.length, + othLength = other.length; + + if (arrLength != othLength && !(isPartial && othLength > arrLength)) { + return false; + } + // Assume cyclic values are equal. + var stacked = stack.get(array); + if (stacked && stack.get(other)) { + return stacked == other; + } + var index = -1, + result = true, + seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined; + + stack.set(array, other); + stack.set(other, array); + + // Ignore non-index properties. + while (++index < arrLength) { + var arrValue = array[index], + othValue = other[index]; + + if (customizer) { + var compared = isPartial + ? customizer(othValue, arrValue, index, other, array, stack) + : customizer(arrValue, othValue, index, array, other, stack); + } + if (compared !== undefined) { + if (compared) { + continue; + } + result = false; + break; + } + // Recursively compare arrays (susceptible to call stack limits). + if (seen) { + if (!arraySome(other, function(othValue, othIndex) { + if (!cacheHas(seen, othIndex) && + (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) { + return seen.push(othIndex); + } + })) { + result = false; + break; + } + } else if (!( + arrValue === othValue || + equalFunc(arrValue, othValue, bitmask, customizer, stack) + )) { + result = false; + break; + } + } + stack['delete'](array); + stack['delete'](other); + return result; + } + + /** + * A specialized version of `baseIsEqualDeep` for comparing objects of + * the same `toStringTag`. + * + * **Note:** This function only supports comparing values with tags of + * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {string} tag The `toStringTag` of the objects to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} stack Tracks traversed `object` and `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ + function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) { + switch (tag) { + case dataViewTag: + if ((object.byteLength != other.byteLength) || + (object.byteOffset != other.byteOffset)) { + return false; + } + object = object.buffer; + other = other.buffer; + + case arrayBufferTag: + if ((object.byteLength != other.byteLength) || + !equalFunc(new Uint8Array(object), new Uint8Array(other))) { + return false; + } + return true; + + case boolTag: + case dateTag: + case numberTag: + // Coerce booleans to `1` or `0` and dates to milliseconds. + // Invalid dates are coerced to `NaN`. + return eq(+object, +other); + + case errorTag: + return object.name == other.name && object.message == other.message; + + case regexpTag: + case stringTag: + // Coerce regexes to strings and treat strings, primitives and objects, + // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring + // for more details. + return object == (other + ''); + + case mapTag: + var convert = mapToArray; + + case setTag: + var isPartial = bitmask & COMPARE_PARTIAL_FLAG; + convert || (convert = setToArray); + + if (object.size != other.size && !isPartial) { + return false; + } + // Assume cyclic values are equal. + var stacked = stack.get(object); + if (stacked) { + return stacked == other; + } + bitmask |= COMPARE_UNORDERED_FLAG; + + // Recursively compare objects (susceptible to call stack limits). + stack.set(object, other); + var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack); + stack['delete'](object); + return result; + + case symbolTag: + if (symbolValueOf) { + return symbolValueOf.call(object) == symbolValueOf.call(other); + } + } + return false; + } + + /** + * A specialized version of `baseIsEqualDeep` for objects with support for + * partial deep comparisons. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} stack Tracks traversed `object` and `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ + function equalObjects(object, other, bitmask, customizer, equalFunc, stack) { + var isPartial = bitmask & COMPARE_PARTIAL_FLAG, + objProps = getAllKeys(object), + objLength = objProps.length, + othProps = getAllKeys(other), + othLength = othProps.length; + + if (objLength != othLength && !isPartial) { + return false; + } + var index = objLength; + while (index--) { + var key = objProps[index]; + if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) { + return false; + } + } + // Assume cyclic values are equal. + var stacked = stack.get(object); + if (stacked && stack.get(other)) { + return stacked == other; + } + var result = true; + stack.set(object, other); + stack.set(other, object); + + var skipCtor = isPartial; + while (++index < objLength) { + key = objProps[index]; + var objValue = object[key], + othValue = other[key]; + + if (customizer) { + var compared = isPartial + ? customizer(othValue, objValue, key, other, object, stack) + : customizer(objValue, othValue, key, object, other, stack); + } + // Recursively compare objects (susceptible to call stack limits). + if (!(compared === undefined + ? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack)) + : compared + )) { + result = false; + break; + } + skipCtor || (skipCtor = key == 'constructor'); + } + if (result && !skipCtor) { + var objCtor = object.constructor, + othCtor = other.constructor; + + // Non `Object` object instances with different constructors are not equal. + if (objCtor != othCtor && + ('constructor' in object && 'constructor' in other) && + !(typeof objCtor == 'function' && objCtor instanceof objCtor && + typeof othCtor == 'function' && othCtor instanceof othCtor)) { + result = false; + } + } + stack['delete'](object); + stack['delete'](other); + return result; + } + + /** + * A specialized version of `baseRest` which flattens the rest array. + * + * @private + * @param {Function} func The function to apply a rest parameter to. + * @returns {Function} Returns the new function. + */ + function flatRest(func) { + return setToString(overRest(func, undefined, flatten), func + ''); + } + + /** + * Creates an array of own enumerable property names and symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ + function getAllKeys(object) { + return baseGetAllKeys(object, keys, getSymbols); + } + + /** + * Creates an array of own and inherited enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ + function getAllKeysIn(object) { + return baseGetAllKeys(object, keysIn, getSymbolsIn); + } + + /** + * Gets metadata for `func`. + * + * @private + * @param {Function} func The function to query. + * @returns {*} Returns the metadata for `func`. + */ + var getData = !metaMap ? noop : function(func) { + return metaMap.get(func); + }; + + /** + * Gets the name of `func`. + * + * @private + * @param {Function} func The function to query. + * @returns {string} Returns the function name. + */ + function getFuncName(func) { + var result = (func.name + ''), + array = realNames[result], + length = hasOwnProperty.call(realNames, result) ? array.length : 0; + + while (length--) { + var data = array[length], + otherFunc = data.func; + if (otherFunc == null || otherFunc == func) { + return data.name; + } + } + return result; + } + + /** + * Gets the argument placeholder value for `func`. + * + * @private + * @param {Function} func The function to inspect. + * @returns {*} Returns the placeholder value. + */ + function getHolder(func) { + var object = hasOwnProperty.call(lodash, 'placeholder') ? lodash : func; + return object.placeholder; + } + + /** + * Gets the appropriate "iteratee" function. If `_.iteratee` is customized, + * this function returns the custom method, otherwise it returns `baseIteratee`. + * If arguments are provided, the chosen function is invoked with them and + * its result is returned. + * + * @private + * @param {*} [value] The value to convert to an iteratee. + * @param {number} [arity] The arity of the created iteratee. + * @returns {Function} Returns the chosen function or its result. + */ + function getIteratee() { + var result = lodash.iteratee || iteratee; + result = result === iteratee ? baseIteratee : result; + return arguments.length ? result(arguments[0], arguments[1]) : result; + } + + /** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ + function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; + } + + /** + * Gets the property names, values, and compare flags of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the match data of `object`. + */ + function getMatchData(object) { + var result = keys(object), + length = result.length; + + while (length--) { + var key = result[length], + value = object[key]; + + result[length] = [key, value, isStrictComparable(value)]; + } + return result; + } + + /** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ + function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; + } + + /** + * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the raw `toStringTag`. + */ + function getRawTag(value) { + var isOwn = hasOwnProperty.call(value, symToStringTag), + tag = value[symToStringTag]; + + try { + value[symToStringTag] = undefined; + var unmasked = true; + } catch (e) {} + + var result = nativeObjectToString.call(value); + if (unmasked) { + if (isOwn) { + value[symToStringTag] = tag; + } else { + delete value[symToStringTag]; + } + } + return result; + } + + /** + * Creates an array of the own enumerable symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of symbols. + */ + var getSymbols = !nativeGetSymbols ? stubArray : function(object) { + if (object == null) { + return []; + } + object = Object(object); + return arrayFilter(nativeGetSymbols(object), function(symbol) { + return propertyIsEnumerable.call(object, symbol); + }); + }; + + /** + * Creates an array of the own and inherited enumerable symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of symbols. + */ + var getSymbolsIn = !nativeGetSymbols ? stubArray : function(object) { + var result = []; + while (object) { + arrayPush(result, getSymbols(object)); + object = getPrototype(object); + } + return result; + }; + + /** + * Gets the `toStringTag` of `value`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + var getTag = baseGetTag; + + // Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6. + if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || + (Map && getTag(new Map) != mapTag) || + (Promise && getTag(Promise.resolve()) != promiseTag) || + (Set && getTag(new Set) != setTag) || + (WeakMap && getTag(new WeakMap) != weakMapTag)) { + getTag = function(value) { + var result = baseGetTag(value), + Ctor = result == objectTag ? value.constructor : undefined, + ctorString = Ctor ? toSource(Ctor) : ''; + + if (ctorString) { + switch (ctorString) { + case dataViewCtorString: return dataViewTag; + case mapCtorString: return mapTag; + case promiseCtorString: return promiseTag; + case setCtorString: return setTag; + case weakMapCtorString: return weakMapTag; + } + } + return result; + }; + } + + /** + * Gets the view, applying any `transforms` to the `start` and `end` positions. + * + * @private + * @param {number} start The start of the view. + * @param {number} end The end of the view. + * @param {Array} transforms The transformations to apply to the view. + * @returns {Object} Returns an object containing the `start` and `end` + * positions of the view. + */ + function getView(start, end, transforms) { + var index = -1, + length = transforms.length; + + while (++index < length) { + var data = transforms[index], + size = data.size; + + switch (data.type) { + case 'drop': start += size; break; + case 'dropRight': end -= size; break; + case 'take': end = nativeMin(end, start + size); break; + case 'takeRight': start = nativeMax(start, end - size); break; + } + } + return { 'start': start, 'end': end }; + } + + /** + * Extracts wrapper details from the `source` body comment. + * + * @private + * @param {string} source The source to inspect. + * @returns {Array} Returns the wrapper details. + */ + function getWrapDetails(source) { + var match = source.match(reWrapDetails); + return match ? match[1].split(reSplitDetails) : []; + } + + /** + * Checks if `path` exists on `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @param {Function} hasFunc The function to check properties. + * @returns {boolean} Returns `true` if `path` exists, else `false`. + */ + function hasPath(object, path, hasFunc) { + path = castPath(path, object); + + var index = -1, + length = path.length, + result = false; + + while (++index < length) { + var key = toKey(path[index]); + if (!(result = object != null && hasFunc(object, key))) { + break; + } + object = object[key]; + } + if (result || ++index != length) { + return result; + } + length = object == null ? 0 : object.length; + return !!length && isLength(length) && isIndex(key, length) && + (isArray(object) || isArguments(object)); + } + + /** + * Initializes an array clone. + * + * @private + * @param {Array} array The array to clone. + * @returns {Array} Returns the initialized clone. + */ + function initCloneArray(array) { + var length = array.length, + result = new array.constructor(length); + + // Add properties assigned by `RegExp#exec`. + if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { + result.index = array.index; + result.input = array.input; + } + return result; + } + + /** + * Initializes an object clone. + * + * @private + * @param {Object} object The object to clone. + * @returns {Object} Returns the initialized clone. + */ + function initCloneObject(object) { + return (typeof object.constructor == 'function' && !isPrototype(object)) + ? baseCreate(getPrototype(object)) + : {}; + } + + /** + * Initializes an object clone based on its `toStringTag`. + * + * **Note:** This function only supports cloning values with tags of + * `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`. + * + * @private + * @param {Object} object The object to clone. + * @param {string} tag The `toStringTag` of the object to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the initialized clone. + */ + function initCloneByTag(object, tag, isDeep) { + var Ctor = object.constructor; + switch (tag) { + case arrayBufferTag: + return cloneArrayBuffer(object); + + case boolTag: + case dateTag: + return new Ctor(+object); + + case dataViewTag: + return cloneDataView(object, isDeep); + + case float32Tag: case float64Tag: + case int8Tag: case int16Tag: case int32Tag: + case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: + return cloneTypedArray(object, isDeep); + + case mapTag: + return new Ctor; + + case numberTag: + case stringTag: + return new Ctor(object); + + case regexpTag: + return cloneRegExp(object); + + case setTag: + return new Ctor; + + case symbolTag: + return cloneSymbol(object); + } + } + + /** + * Inserts wrapper `details` in a comment at the top of the `source` body. + * + * @private + * @param {string} source The source to modify. + * @returns {Array} details The details to insert. + * @returns {string} Returns the modified source. + */ + function insertWrapDetails(source, details) { + var length = details.length; + if (!length) { + return source; + } + var lastIndex = length - 1; + details[lastIndex] = (length > 1 ? '& ' : '') + details[lastIndex]; + details = details.join(length > 2 ? ', ' : ' '); + return source.replace(reWrapComment, '{\n/* [wrapped with ' + details + '] */\n'); + } + + /** + * Checks if `value` is a flattenable `arguments` object or array. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is flattenable, else `false`. + */ + function isFlattenable(value) { + return isArray(value) || isArguments(value) || + !!(spreadableSymbol && value && value[spreadableSymbol]); + } + + /** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ + function isIndex(value, length) { + var type = typeof value; + length = length == null ? MAX_SAFE_INTEGER : length; + + return !!length && + (type == 'number' || + (type != 'symbol' && reIsUint.test(value))) && + (value > -1 && value % 1 == 0 && value < length); + } + + /** + * Checks if the given arguments are from an iteratee call. + * + * @private + * @param {*} value The potential iteratee value argument. + * @param {*} index The potential iteratee index or key argument. + * @param {*} object The potential iteratee object argument. + * @returns {boolean} Returns `true` if the arguments are from an iteratee call, + * else `false`. + */ + function isIterateeCall(value, index, object) { + if (!isObject(object)) { + return false; + } + var type = typeof index; + if (type == 'number' + ? (isArrayLike(object) && isIndex(index, object.length)) + : (type == 'string' && index in object) + ) { + return eq(object[index], value); + } + return false; + } + + /** + * Checks if `value` is a property name and not a property path. + * + * @private + * @param {*} value The value to check. + * @param {Object} [object] The object to query keys on. + * @returns {boolean} Returns `true` if `value` is a property name, else `false`. + */ + function isKey(value, object) { + if (isArray(value)) { + return false; + } + var type = typeof value; + if (type == 'number' || type == 'symbol' || type == 'boolean' || + value == null || isSymbol(value)) { + return true; + } + return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || + (object != null && value in Object(object)); + } + + /** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ + function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); + } + + /** + * Checks if `func` has a lazy counterpart. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` has a lazy counterpart, + * else `false`. + */ + function isLaziable(func) { + var funcName = getFuncName(func), + other = lodash[funcName]; + + if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) { + return false; + } + if (func === other) { + return true; + } + var data = getData(other); + return !!data && func === data[0]; + } + + /** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ + function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); + } + + /** + * Checks if `func` is capable of being masked. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `func` is maskable, else `false`. + */ + var isMaskable = coreJsData ? isFunction : stubFalse; + + /** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ + function isPrototype(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto; + + return value === proto; + } + + /** + * Checks if `value` is suitable for strict equality comparisons, i.e. `===`. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` if suitable for strict + * equality comparisons, else `false`. + */ + function isStrictComparable(value) { + return value === value && !isObject(value); + } + + /** + * A specialized version of `matchesProperty` for source values suitable + * for strict equality comparisons, i.e. `===`. + * + * @private + * @param {string} key The key of the property to get. + * @param {*} srcValue The value to match. + * @returns {Function} Returns the new spec function. + */ + function matchesStrictComparable(key, srcValue) { + return function(object) { + if (object == null) { + return false; + } + return object[key] === srcValue && + (srcValue !== undefined || (key in Object(object))); + }; + } + + /** + * A specialized version of `_.memoize` which clears the memoized function's + * cache when it exceeds `MAX_MEMOIZE_SIZE`. + * + * @private + * @param {Function} func The function to have its output memoized. + * @returns {Function} Returns the new memoized function. + */ + function memoizeCapped(func) { + var result = memoize(func, function(key) { + if (cache.size === MAX_MEMOIZE_SIZE) { + cache.clear(); + } + return key; + }); + + var cache = result.cache; + return result; + } + + /** + * Merges the function metadata of `source` into `data`. + * + * Merging metadata reduces the number of wrappers used to invoke a function. + * This is possible because methods like `_.bind`, `_.curry`, and `_.partial` + * may be applied regardless of execution order. Methods like `_.ary` and + * `_.rearg` modify function arguments, making the order in which they are + * executed important, preventing the merging of metadata. However, we make + * an exception for a safe combined case where curried functions have `_.ary` + * and or `_.rearg` applied. + * + * @private + * @param {Array} data The destination metadata. + * @param {Array} source The source metadata. + * @returns {Array} Returns `data`. + */ + function mergeData(data, source) { + var bitmask = data[1], + srcBitmask = source[1], + newBitmask = bitmask | srcBitmask, + isCommon = newBitmask < (WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG | WRAP_ARY_FLAG); + + var isCombo = + ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_CURRY_FLAG)) || + ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_REARG_FLAG) && (data[7].length <= source[8])) || + ((srcBitmask == (WRAP_ARY_FLAG | WRAP_REARG_FLAG)) && (source[7].length <= source[8]) && (bitmask == WRAP_CURRY_FLAG)); + + // Exit early if metadata can't be merged. + if (!(isCommon || isCombo)) { + return data; + } + // Use source `thisArg` if available. + if (srcBitmask & WRAP_BIND_FLAG) { + data[2] = source[2]; + // Set when currying a bound function. + newBitmask |= bitmask & WRAP_BIND_FLAG ? 0 : WRAP_CURRY_BOUND_FLAG; + } + // Compose partial arguments. + var value = source[3]; + if (value) { + var partials = data[3]; + data[3] = partials ? composeArgs(partials, value, source[4]) : value; + data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : source[4]; + } + // Compose partial right arguments. + value = source[5]; + if (value) { + partials = data[5]; + data[5] = partials ? composeArgsRight(partials, value, source[6]) : value; + data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : source[6]; + } + // Use source `argPos` if available. + value = source[7]; + if (value) { + data[7] = value; + } + // Use source `ary` if it's smaller. + if (srcBitmask & WRAP_ARY_FLAG) { + data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]); + } + // Use source `arity` if one is not provided. + if (data[9] == null) { + data[9] = source[9]; + } + // Use source `func` and merge bitmasks. + data[0] = source[0]; + data[1] = newBitmask; + + return data; + } + + /** + * This function is like + * [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * except that it includes inherited enumerable properties. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function nativeKeysIn(object) { + var result = []; + if (object != null) { + for (var key in Object(object)) { + result.push(key); + } + } + return result; + } + + /** + * Converts `value` to a string using `Object.prototype.toString`. + * + * @private + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + */ + function objectToString(value) { + return nativeObjectToString.call(value); + } + + /** + * A specialized version of `baseRest` which transforms the rest array. + * + * @private + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @param {Function} transform The rest array transform. + * @returns {Function} Returns the new function. + */ + function overRest(func, start, transform) { + start = nativeMax(start === undefined ? (func.length - 1) : start, 0); + return function() { + var args = arguments, + index = -1, + length = nativeMax(args.length - start, 0), + array = Array(length); + + while (++index < length) { + array[index] = args[start + index]; + } + index = -1; + var otherArgs = Array(start + 1); + while (++index < start) { + otherArgs[index] = args[index]; + } + otherArgs[start] = transform(array); + return apply(func, this, otherArgs); + }; + } + + /** + * Gets the parent value at `path` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Array} path The path to get the parent value of. + * @returns {*} Returns the parent value. + */ + function parent(object, path) { + return path.length < 2 ? object : baseGet(object, baseSlice(path, 0, -1)); + } + + /** + * Reorder `array` according to the specified indexes where the element at + * the first index is assigned as the first element, the element at + * the second index is assigned as the second element, and so on. + * + * @private + * @param {Array} array The array to reorder. + * @param {Array} indexes The arranged array indexes. + * @returns {Array} Returns `array`. + */ + function reorder(array, indexes) { + var arrLength = array.length, + length = nativeMin(indexes.length, arrLength), + oldArray = copyArray(array); + + while (length--) { + var index = indexes[length]; + array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined; + } + return array; + } + + /** + * Sets metadata for `func`. + * + * **Note:** If this function becomes hot, i.e. is invoked a lot in a short + * period of time, it will trip its breaker and transition to an identity + * function to avoid garbage collection pauses in V8. See + * [V8 issue 2070](https://bugs.chromium.org/p/v8/issues/detail?id=2070) + * for more details. + * + * @private + * @param {Function} func The function to associate metadata with. + * @param {*} data The metadata. + * @returns {Function} Returns `func`. + */ + var setData = shortOut(baseSetData); + + /** + * A simple wrapper around the global [`setTimeout`](https://mdn.io/setTimeout). + * + * @private + * @param {Function} func The function to delay. + * @param {number} wait The number of milliseconds to delay invocation. + * @returns {number|Object} Returns the timer id or timeout object. + */ + var setTimeout = ctxSetTimeout || function(func, wait) { + return root.setTimeout(func, wait); + }; + + /** + * Sets the `toString` method of `func` to return `string`. + * + * @private + * @param {Function} func The function to modify. + * @param {Function} string The `toString` result. + * @returns {Function} Returns `func`. + */ + var setToString = shortOut(baseSetToString); + + /** + * Sets the `toString` method of `wrapper` to mimic the source of `reference` + * with wrapper details in a comment at the top of the source body. + * + * @private + * @param {Function} wrapper The function to modify. + * @param {Function} reference The reference function. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @returns {Function} Returns `wrapper`. + */ + function setWrapToString(wrapper, reference, bitmask) { + var source = (reference + ''); + return setToString(wrapper, insertWrapDetails(source, updateWrapDetails(getWrapDetails(source), bitmask))); + } + + /** + * Creates a function that'll short out and invoke `identity` instead + * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN` + * milliseconds. + * + * @private + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new shortable function. + */ + function shortOut(func) { + var count = 0, + lastCalled = 0; + + return function() { + var stamp = nativeNow(), + remaining = HOT_SPAN - (stamp - lastCalled); + + lastCalled = stamp; + if (remaining > 0) { + if (++count >= HOT_COUNT) { + return arguments[0]; + } + } else { + count = 0; + } + return func.apply(undefined, arguments); + }; + } + + /** + * A specialized version of `_.shuffle` which mutates and sets the size of `array`. + * + * @private + * @param {Array} array The array to shuffle. + * @param {number} [size=array.length] The size of `array`. + * @returns {Array} Returns `array`. + */ + function shuffleSelf(array, size) { + var index = -1, + length = array.length, + lastIndex = length - 1; + + size = size === undefined ? length : size; + while (++index < size) { + var rand = baseRandom(index, lastIndex), + value = array[rand]; + + array[rand] = array[index]; + array[index] = value; + } + array.length = size; + return array; + } + + /** + * Converts `string` to a property path array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the property path array. + */ + var stringToPath = memoizeCapped(function(string) { + var result = []; + if (string.charCodeAt(0) === 46 /* . */) { + result.push(''); + } + string.replace(rePropName, function(match, number, quote, subString) { + result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match)); + }); + return result; + }); + + /** + * Converts `value` to a string key if it's not a string or symbol. + * + * @private + * @param {*} value The value to inspect. + * @returns {string|symbol} Returns the key. + */ + function toKey(value) { + if (typeof value == 'string' || isSymbol(value)) { + return value; + } + var result = (value + ''); + return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; + } + + /** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to convert. + * @returns {string} Returns the source code. + */ + function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; + } + + /** + * Updates wrapper `details` based on `bitmask` flags. + * + * @private + * @returns {Array} details The details to modify. + * @param {number} bitmask The bitmask flags. See `createWrap` for more details. + * @returns {Array} Returns `details`. + */ + function updateWrapDetails(details, bitmask) { + arrayEach(wrapFlags, function(pair) { + var value = '_.' + pair[0]; + if ((bitmask & pair[1]) && !arrayIncludes(details, value)) { + details.push(value); + } + }); + return details.sort(); + } + + /** + * Creates a clone of `wrapper`. + * + * @private + * @param {Object} wrapper The wrapper to clone. + * @returns {Object} Returns the cloned wrapper. + */ + function wrapperClone(wrapper) { + if (wrapper instanceof LazyWrapper) { + return wrapper.clone(); + } + var result = new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__); + result.__actions__ = copyArray(wrapper.__actions__); + result.__index__ = wrapper.__index__; + result.__values__ = wrapper.__values__; + return result; + } + + /*------------------------------------------------------------------------*/ + + /** + * Creates an array of elements split into groups the length of `size`. + * If `array` can't be split evenly, the final chunk will be the remaining + * elements. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to process. + * @param {number} [size=1] The length of each chunk + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the new array of chunks. + * @example + * + * _.chunk(['a', 'b', 'c', 'd'], 2); + * // => [['a', 'b'], ['c', 'd']] + * + * _.chunk(['a', 'b', 'c', 'd'], 3); + * // => [['a', 'b', 'c'], ['d']] + */ + function chunk(array, size, guard) { + if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) { + size = 1; + } else { + size = nativeMax(toInteger(size), 0); + } + var length = array == null ? 0 : array.length; + if (!length || size < 1) { + return []; + } + var index = 0, + resIndex = 0, + result = Array(nativeCeil(length / size)); + + while (index < length) { + result[resIndex++] = baseSlice(array, index, (index += size)); + } + return result; + } + + /** + * Creates an array with all falsey values removed. The values `false`, `null`, + * `0`, `""`, `undefined`, and `NaN` are falsey. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to compact. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * _.compact([0, 1, false, 2, '', 3]); + * // => [1, 2, 3] + */ + function compact(array) { + var index = -1, + length = array == null ? 0 : array.length, + resIndex = 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (value) { + result[resIndex++] = value; + } + } + return result; + } + + /** + * Creates a new array concatenating `array` with any additional arrays + * and/or values. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to concatenate. + * @param {...*} [values] The values to concatenate. + * @returns {Array} Returns the new concatenated array. + * @example + * + * var array = [1]; + * var other = _.concat(array, 2, [3], [[4]]); + * + * console.log(other); + * // => [1, 2, 3, [4]] + * + * console.log(array); + * // => [1] + */ + function concat() { + var length = arguments.length; + if (!length) { + return []; + } + var args = Array(length - 1), + array = arguments[0], + index = length; + + while (index--) { + args[index - 1] = arguments[index]; + } + return arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1)); + } + + /** + * Creates an array of `array` values not included in the other given arrays + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. The order and references of result values are + * determined by the first array. + * + * **Note:** Unlike `_.pullAll`, this method returns a new array. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {...Array} [values] The values to exclude. + * @returns {Array} Returns the new array of filtered values. + * @see _.without, _.xor + * @example + * + * _.difference([2, 1], [2, 3]); + * // => [1] + */ + var difference = baseRest(function(array, values) { + return isArrayLikeObject(array) + ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true)) + : []; + }); + + /** + * This method is like `_.difference` except that it accepts `iteratee` which + * is invoked for each element of `array` and `values` to generate the criterion + * by which they're compared. The order and references of result values are + * determined by the first array. The iteratee is invoked with one argument: + * (value). + * + * **Note:** Unlike `_.pullAllBy`, this method returns a new array. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {...Array} [values] The values to exclude. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); + * // => [1.2] + * + * // The `_.property` iteratee shorthand. + * _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x'); + * // => [{ 'x': 2 }] + */ + var differenceBy = baseRest(function(array, values) { + var iteratee = last(values); + if (isArrayLikeObject(iteratee)) { + iteratee = undefined; + } + return isArrayLikeObject(array) + ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), getIteratee(iteratee, 2)) + : []; + }); + + /** + * This method is like `_.difference` except that it accepts `comparator` + * which is invoked to compare elements of `array` to `values`. The order and + * references of result values are determined by the first array. The comparator + * is invoked with two arguments: (arrVal, othVal). + * + * **Note:** Unlike `_.pullAllWith`, this method returns a new array. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {...Array} [values] The values to exclude. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; + * + * _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual); + * // => [{ 'x': 2, 'y': 1 }] + */ + var differenceWith = baseRest(function(array, values) { + var comparator = last(values); + if (isArrayLikeObject(comparator)) { + comparator = undefined; + } + return isArrayLikeObject(array) + ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), undefined, comparator) + : []; + }); + + /** + * Creates a slice of `array` with `n` elements dropped from the beginning. + * + * @static + * @memberOf _ + * @since 0.5.0 + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to drop. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.drop([1, 2, 3]); + * // => [2, 3] + * + * _.drop([1, 2, 3], 2); + * // => [3] + * + * _.drop([1, 2, 3], 5); + * // => [] + * + * _.drop([1, 2, 3], 0); + * // => [1, 2, 3] + */ + function drop(array, n, guard) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + n = (guard || n === undefined) ? 1 : toInteger(n); + return baseSlice(array, n < 0 ? 0 : n, length); + } + + /** + * Creates a slice of `array` with `n` elements dropped from the end. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to drop. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.dropRight([1, 2, 3]); + * // => [1, 2] + * + * _.dropRight([1, 2, 3], 2); + * // => [1] + * + * _.dropRight([1, 2, 3], 5); + * // => [] + * + * _.dropRight([1, 2, 3], 0); + * // => [1, 2, 3] + */ + function dropRight(array, n, guard) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + n = (guard || n === undefined) ? 1 : toInteger(n); + n = length - n; + return baseSlice(array, 0, n < 0 ? 0 : n); + } + + /** + * Creates a slice of `array` excluding elements dropped from the end. + * Elements are dropped until `predicate` returns falsey. The predicate is + * invoked with three arguments: (value, index, array). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the slice of `array`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': false } + * ]; + * + * _.dropRightWhile(users, function(o) { return !o.active; }); + * // => objects for ['barney'] + * + * // The `_.matches` iteratee shorthand. + * _.dropRightWhile(users, { 'user': 'pebbles', 'active': false }); + * // => objects for ['barney', 'fred'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.dropRightWhile(users, ['active', false]); + * // => objects for ['barney'] + * + * // The `_.property` iteratee shorthand. + * _.dropRightWhile(users, 'active'); + * // => objects for ['barney', 'fred', 'pebbles'] + */ + function dropRightWhile(array, predicate) { + return (array && array.length) + ? baseWhile(array, getIteratee(predicate, 3), true, true) + : []; + } + + /** + * Creates a slice of `array` excluding elements dropped from the beginning. + * Elements are dropped until `predicate` returns falsey. The predicate is + * invoked with three arguments: (value, index, array). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the slice of `array`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': false }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': true } + * ]; + * + * _.dropWhile(users, function(o) { return !o.active; }); + * // => objects for ['pebbles'] + * + * // The `_.matches` iteratee shorthand. + * _.dropWhile(users, { 'user': 'barney', 'active': false }); + * // => objects for ['fred', 'pebbles'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.dropWhile(users, ['active', false]); + * // => objects for ['pebbles'] + * + * // The `_.property` iteratee shorthand. + * _.dropWhile(users, 'active'); + * // => objects for ['barney', 'fred', 'pebbles'] + */ + function dropWhile(array, predicate) { + return (array && array.length) + ? baseWhile(array, getIteratee(predicate, 3), true) + : []; + } + + /** + * Fills elements of `array` with `value` from `start` up to, but not + * including, `end`. + * + * **Note:** This method mutates `array`. + * + * @static + * @memberOf _ + * @since 3.2.0 + * @category Array + * @param {Array} array The array to fill. + * @param {*} value The value to fill `array` with. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns `array`. + * @example + * + * var array = [1, 2, 3]; + * + * _.fill(array, 'a'); + * console.log(array); + * // => ['a', 'a', 'a'] + * + * _.fill(Array(3), 2); + * // => [2, 2, 2] + * + * _.fill([4, 6, 8, 10], '*', 1, 3); + * // => [4, '*', '*', 10] + */ + function fill(array, value, start, end) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + if (start && typeof start != 'number' && isIterateeCall(array, value, start)) { + start = 0; + end = length; + } + return baseFill(array, value, start, end); + } + + /** + * This method is like `_.find` except that it returns the index of the first + * element `predicate` returns truthy for instead of the element itself. + * + * @static + * @memberOf _ + * @since 1.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param {number} [fromIndex=0] The index to search from. + * @returns {number} Returns the index of the found element, else `-1`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': false }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': true } + * ]; + * + * _.findIndex(users, function(o) { return o.user == 'barney'; }); + * // => 0 + * + * // The `_.matches` iteratee shorthand. + * _.findIndex(users, { 'user': 'fred', 'active': false }); + * // => 1 + * + * // The `_.matchesProperty` iteratee shorthand. + * _.findIndex(users, ['active', false]); + * // => 0 + * + * // The `_.property` iteratee shorthand. + * _.findIndex(users, 'active'); + * // => 2 + */ + function findIndex(array, predicate, fromIndex) { + var length = array == null ? 0 : array.length; + if (!length) { + return -1; + } + var index = fromIndex == null ? 0 : toInteger(fromIndex); + if (index < 0) { + index = nativeMax(length + index, 0); + } + return baseFindIndex(array, getIteratee(predicate, 3), index); + } + + /** + * This method is like `_.findIndex` except that it iterates over elements + * of `collection` from right to left. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param {number} [fromIndex=array.length-1] The index to search from. + * @returns {number} Returns the index of the found element, else `-1`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': false } + * ]; + * + * _.findLastIndex(users, function(o) { return o.user == 'pebbles'; }); + * // => 2 + * + * // The `_.matches` iteratee shorthand. + * _.findLastIndex(users, { 'user': 'barney', 'active': true }); + * // => 0 + * + * // The `_.matchesProperty` iteratee shorthand. + * _.findLastIndex(users, ['active', false]); + * // => 2 + * + * // The `_.property` iteratee shorthand. + * _.findLastIndex(users, 'active'); + * // => 0 + */ + function findLastIndex(array, predicate, fromIndex) { + var length = array == null ? 0 : array.length; + if (!length) { + return -1; + } + var index = length - 1; + if (fromIndex !== undefined) { + index = toInteger(fromIndex); + index = fromIndex < 0 + ? nativeMax(length + index, 0) + : nativeMin(index, length - 1); + } + return baseFindIndex(array, getIteratee(predicate, 3), index, true); + } + + /** + * Flattens `array` a single level deep. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to flatten. + * @returns {Array} Returns the new flattened array. + * @example + * + * _.flatten([1, [2, [3, [4]], 5]]); + * // => [1, 2, [3, [4]], 5] + */ + function flatten(array) { + var length = array == null ? 0 : array.length; + return length ? baseFlatten(array, 1) : []; + } + + /** + * Recursively flattens `array`. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to flatten. + * @returns {Array} Returns the new flattened array. + * @example + * + * _.flattenDeep([1, [2, [3, [4]], 5]]); + * // => [1, 2, 3, 4, 5] + */ + function flattenDeep(array) { + var length = array == null ? 0 : array.length; + return length ? baseFlatten(array, INFINITY) : []; + } + + /** + * Recursively flatten `array` up to `depth` times. + * + * @static + * @memberOf _ + * @since 4.4.0 + * @category Array + * @param {Array} array The array to flatten. + * @param {number} [depth=1] The maximum recursion depth. + * @returns {Array} Returns the new flattened array. + * @example + * + * var array = [1, [2, [3, [4]], 5]]; + * + * _.flattenDepth(array, 1); + * // => [1, 2, [3, [4]], 5] + * + * _.flattenDepth(array, 2); + * // => [1, 2, 3, [4], 5] + */ + function flattenDepth(array, depth) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + depth = depth === undefined ? 1 : toInteger(depth); + return baseFlatten(array, depth); + } + + /** + * The inverse of `_.toPairs`; this method returns an object composed + * from key-value `pairs`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} pairs The key-value pairs. + * @returns {Object} Returns the new object. + * @example + * + * _.fromPairs([['a', 1], ['b', 2]]); + * // => { 'a': 1, 'b': 2 } + */ + function fromPairs(pairs) { + var index = -1, + length = pairs == null ? 0 : pairs.length, + result = {}; + + while (++index < length) { + var pair = pairs[index]; + result[pair[0]] = pair[1]; + } + return result; + } + + /** + * Gets the first element of `array`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @alias first + * @category Array + * @param {Array} array The array to query. + * @returns {*} Returns the first element of `array`. + * @example + * + * _.head([1, 2, 3]); + * // => 1 + * + * _.head([]); + * // => undefined + */ + function head(array) { + return (array && array.length) ? array[0] : undefined; + } + + /** + * Gets the index at which the first occurrence of `value` is found in `array` + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. If `fromIndex` is negative, it's used as the + * offset from the end of `array`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} [fromIndex=0] The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.indexOf([1, 2, 1, 2], 2); + * // => 1 + * + * // Search from the `fromIndex`. + * _.indexOf([1, 2, 1, 2], 2, 2); + * // => 3 + */ + function indexOf(array, value, fromIndex) { + var length = array == null ? 0 : array.length; + if (!length) { + return -1; + } + var index = fromIndex == null ? 0 : toInteger(fromIndex); + if (index < 0) { + index = nativeMax(length + index, 0); + } + return baseIndexOf(array, value, index); + } + + /** + * Gets all but the last element of `array`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to query. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.initial([1, 2, 3]); + * // => [1, 2] + */ + function initial(array) { + var length = array == null ? 0 : array.length; + return length ? baseSlice(array, 0, -1) : []; + } + + /** + * Creates an array of unique values that are included in all given arrays + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. The order and references of result values are + * determined by the first array. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @returns {Array} Returns the new array of intersecting values. + * @example + * + * _.intersection([2, 1], [2, 3]); + * // => [2] + */ + var intersection = baseRest(function(arrays) { + var mapped = arrayMap(arrays, castArrayLikeObject); + return (mapped.length && mapped[0] === arrays[0]) + ? baseIntersection(mapped) + : []; + }); + + /** + * This method is like `_.intersection` except that it accepts `iteratee` + * which is invoked for each element of each `arrays` to generate the criterion + * by which they're compared. The order and references of result values are + * determined by the first array. The iteratee is invoked with one argument: + * (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns the new array of intersecting values. + * @example + * + * _.intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); + * // => [2.1] + * + * // The `_.property` iteratee shorthand. + * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 1 }] + */ + var intersectionBy = baseRest(function(arrays) { + var iteratee = last(arrays), + mapped = arrayMap(arrays, castArrayLikeObject); + + if (iteratee === last(mapped)) { + iteratee = undefined; + } else { + mapped.pop(); + } + return (mapped.length && mapped[0] === arrays[0]) + ? baseIntersection(mapped, getIteratee(iteratee, 2)) + : []; + }); + + /** + * This method is like `_.intersection` except that it accepts `comparator` + * which is invoked to compare elements of `arrays`. The order and references + * of result values are determined by the first array. The comparator is + * invoked with two arguments: (arrVal, othVal). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of intersecting values. + * @example + * + * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; + * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; + * + * _.intersectionWith(objects, others, _.isEqual); + * // => [{ 'x': 1, 'y': 2 }] + */ + var intersectionWith = baseRest(function(arrays) { + var comparator = last(arrays), + mapped = arrayMap(arrays, castArrayLikeObject); + + comparator = typeof comparator == 'function' ? comparator : undefined; + if (comparator) { + mapped.pop(); + } + return (mapped.length && mapped[0] === arrays[0]) + ? baseIntersection(mapped, undefined, comparator) + : []; + }); + + /** + * Converts all elements in `array` into a string separated by `separator`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to convert. + * @param {string} [separator=','] The element separator. + * @returns {string} Returns the joined string. + * @example + * + * _.join(['a', 'b', 'c'], '~'); + * // => 'a~b~c' + */ + function join(array, separator) { + return array == null ? '' : nativeJoin.call(array, separator); + } + + /** + * Gets the last element of `array`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to query. + * @returns {*} Returns the last element of `array`. + * @example + * + * _.last([1, 2, 3]); + * // => 3 + */ + function last(array) { + var length = array == null ? 0 : array.length; + return length ? array[length - 1] : undefined; + } + + /** + * This method is like `_.indexOf` except that it iterates over elements of + * `array` from right to left. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} [fromIndex=array.length-1] The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.lastIndexOf([1, 2, 1, 2], 2); + * // => 3 + * + * // Search from the `fromIndex`. + * _.lastIndexOf([1, 2, 1, 2], 2, 2); + * // => 1 + */ + function lastIndexOf(array, value, fromIndex) { + var length = array == null ? 0 : array.length; + if (!length) { + return -1; + } + var index = length; + if (fromIndex !== undefined) { + index = toInteger(fromIndex); + index = index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1); + } + return value === value + ? strictLastIndexOf(array, value, index) + : baseFindIndex(array, baseIsNaN, index, true); + } + + /** + * Gets the element at index `n` of `array`. If `n` is negative, the nth + * element from the end is returned. + * + * @static + * @memberOf _ + * @since 4.11.0 + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=0] The index of the element to return. + * @returns {*} Returns the nth element of `array`. + * @example + * + * var array = ['a', 'b', 'c', 'd']; + * + * _.nth(array, 1); + * // => 'b' + * + * _.nth(array, -2); + * // => 'c'; + */ + function nth(array, n) { + return (array && array.length) ? baseNth(array, toInteger(n)) : undefined; + } + + /** + * Removes all given values from `array` using + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove` + * to remove elements from an array by predicate. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Array + * @param {Array} array The array to modify. + * @param {...*} [values] The values to remove. + * @returns {Array} Returns `array`. + * @example + * + * var array = ['a', 'b', 'c', 'a', 'b', 'c']; + * + * _.pull(array, 'a', 'c'); + * console.log(array); + * // => ['b', 'b'] + */ + var pull = baseRest(pullAll); + + /** + * This method is like `_.pull` except that it accepts an array of values to remove. + * + * **Note:** Unlike `_.difference`, this method mutates `array`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to modify. + * @param {Array} values The values to remove. + * @returns {Array} Returns `array`. + * @example + * + * var array = ['a', 'b', 'c', 'a', 'b', 'c']; + * + * _.pullAll(array, ['a', 'c']); + * console.log(array); + * // => ['b', 'b'] + */ + function pullAll(array, values) { + return (array && array.length && values && values.length) + ? basePullAll(array, values) + : array; + } + + /** + * This method is like `_.pullAll` except that it accepts `iteratee` which is + * invoked for each element of `array` and `values` to generate the criterion + * by which they're compared. The iteratee is invoked with one argument: (value). + * + * **Note:** Unlike `_.differenceBy`, this method mutates `array`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to modify. + * @param {Array} values The values to remove. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns `array`. + * @example + * + * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }]; + * + * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x'); + * console.log(array); + * // => [{ 'x': 2 }] + */ + function pullAllBy(array, values, iteratee) { + return (array && array.length && values && values.length) + ? basePullAll(array, values, getIteratee(iteratee, 2)) + : array; + } + + /** + * This method is like `_.pullAll` except that it accepts `comparator` which + * is invoked to compare elements of `array` to `values`. The comparator is + * invoked with two arguments: (arrVal, othVal). + * + * **Note:** Unlike `_.differenceWith`, this method mutates `array`. + * + * @static + * @memberOf _ + * @since 4.6.0 + * @category Array + * @param {Array} array The array to modify. + * @param {Array} values The values to remove. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns `array`. + * @example + * + * var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }]; + * + * _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual); + * console.log(array); + * // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }] + */ + function pullAllWith(array, values, comparator) { + return (array && array.length && values && values.length) + ? basePullAll(array, values, undefined, comparator) + : array; + } + + /** + * Removes elements from `array` corresponding to `indexes` and returns an + * array of removed elements. + * + * **Note:** Unlike `_.at`, this method mutates `array`. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to modify. + * @param {...(number|number[])} [indexes] The indexes of elements to remove. + * @returns {Array} Returns the new array of removed elements. + * @example + * + * var array = ['a', 'b', 'c', 'd']; + * var pulled = _.pullAt(array, [1, 3]); + * + * console.log(array); + * // => ['a', 'c'] + * + * console.log(pulled); + * // => ['b', 'd'] + */ + var pullAt = flatRest(function(array, indexes) { + var length = array == null ? 0 : array.length, + result = baseAt(array, indexes); + + basePullAt(array, arrayMap(indexes, function(index) { + return isIndex(index, length) ? +index : index; + }).sort(compareAscending)); + + return result; + }); + + /** + * Removes all elements from `array` that `predicate` returns truthy for + * and returns an array of the removed elements. The predicate is invoked + * with three arguments: (value, index, array). + * + * **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull` + * to pull elements from an array by value. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Array + * @param {Array} array The array to modify. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new array of removed elements. + * @example + * + * var array = [1, 2, 3, 4]; + * var evens = _.remove(array, function(n) { + * return n % 2 == 0; + * }); + * + * console.log(array); + * // => [1, 3] + * + * console.log(evens); + * // => [2, 4] + */ + function remove(array, predicate) { + var result = []; + if (!(array && array.length)) { + return result; + } + var index = -1, + indexes = [], + length = array.length; + + predicate = getIteratee(predicate, 3); + while (++index < length) { + var value = array[index]; + if (predicate(value, index, array)) { + result.push(value); + indexes.push(index); + } + } + basePullAt(array, indexes); + return result; + } + + /** + * Reverses `array` so that the first element becomes the last, the second + * element becomes the second to last, and so on. + * + * **Note:** This method mutates `array` and is based on + * [`Array#reverse`](https://mdn.io/Array/reverse). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to modify. + * @returns {Array} Returns `array`. + * @example + * + * var array = [1, 2, 3]; + * + * _.reverse(array); + * // => [3, 2, 1] + * + * console.log(array); + * // => [3, 2, 1] + */ + function reverse(array) { + return array == null ? array : nativeReverse.call(array); + } + + /** + * Creates a slice of `array` from `start` up to, but not including, `end`. + * + * **Note:** This method is used instead of + * [`Array#slice`](https://mdn.io/Array/slice) to ensure dense arrays are + * returned. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to slice. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the slice of `array`. + */ + function slice(array, start, end) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + if (end && typeof end != 'number' && isIterateeCall(array, start, end)) { + start = 0; + end = length; + } + else { + start = start == null ? 0 : toInteger(start); + end = end === undefined ? length : toInteger(end); + } + return baseSlice(array, start, end); + } + + /** + * Uses a binary search to determine the lowest index at which `value` + * should be inserted into `array` in order to maintain its sort order. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * _.sortedIndex([30, 50], 40); + * // => 1 + */ + function sortedIndex(array, value) { + return baseSortedIndex(array, value); + } + + /** + * This method is like `_.sortedIndex` except that it accepts `iteratee` + * which is invoked for `value` and each element of `array` to compute their + * sort ranking. The iteratee is invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * var objects = [{ 'x': 4 }, { 'x': 5 }]; + * + * _.sortedIndexBy(objects, { 'x': 4 }, function(o) { return o.x; }); + * // => 0 + * + * // The `_.property` iteratee shorthand. + * _.sortedIndexBy(objects, { 'x': 4 }, 'x'); + * // => 0 + */ + function sortedIndexBy(array, value, iteratee) { + return baseSortedIndexBy(array, value, getIteratee(iteratee, 2)); + } + + /** + * This method is like `_.indexOf` except that it performs a binary + * search on a sorted `array`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.sortedIndexOf([4, 5, 5, 5, 6], 5); + * // => 1 + */ + function sortedIndexOf(array, value) { + var length = array == null ? 0 : array.length; + if (length) { + var index = baseSortedIndex(array, value); + if (index < length && eq(array[index], value)) { + return index; + } + } + return -1; + } + + /** + * This method is like `_.sortedIndex` except that it returns the highest + * index at which `value` should be inserted into `array` in order to + * maintain its sort order. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * _.sortedLastIndex([4, 5, 5, 5, 6], 5); + * // => 4 + */ + function sortedLastIndex(array, value) { + return baseSortedIndex(array, value, true); + } + + /** + * This method is like `_.sortedLastIndex` except that it accepts `iteratee` + * which is invoked for `value` and each element of `array` to compute their + * sort ranking. The iteratee is invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * var objects = [{ 'x': 4 }, { 'x': 5 }]; + * + * _.sortedLastIndexBy(objects, { 'x': 4 }, function(o) { return o.x; }); + * // => 1 + * + * // The `_.property` iteratee shorthand. + * _.sortedLastIndexBy(objects, { 'x': 4 }, 'x'); + * // => 1 + */ + function sortedLastIndexBy(array, value, iteratee) { + return baseSortedIndexBy(array, value, getIteratee(iteratee, 2), true); + } + + /** + * This method is like `_.lastIndexOf` except that it performs a binary + * search on a sorted `array`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.sortedLastIndexOf([4, 5, 5, 5, 6], 5); + * // => 3 + */ + function sortedLastIndexOf(array, value) { + var length = array == null ? 0 : array.length; + if (length) { + var index = baseSortedIndex(array, value, true) - 1; + if (eq(array[index], value)) { + return index; + } + } + return -1; + } + + /** + * This method is like `_.uniq` except that it's designed and optimized + * for sorted arrays. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * _.sortedUniq([1, 1, 2]); + * // => [1, 2] + */ + function sortedUniq(array) { + return (array && array.length) + ? baseSortedUniq(array) + : []; + } + + /** + * This method is like `_.uniqBy` except that it's designed and optimized + * for sorted arrays. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The iteratee invoked per element. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor); + * // => [1.1, 2.3] + */ + function sortedUniqBy(array, iteratee) { + return (array && array.length) + ? baseSortedUniq(array, getIteratee(iteratee, 2)) + : []; + } + + /** + * Gets all but the first element of `array`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to query. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.tail([1, 2, 3]); + * // => [2, 3] + */ + function tail(array) { + var length = array == null ? 0 : array.length; + return length ? baseSlice(array, 1, length) : []; + } + + /** + * Creates a slice of `array` with `n` elements taken from the beginning. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to take. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.take([1, 2, 3]); + * // => [1] + * + * _.take([1, 2, 3], 2); + * // => [1, 2] + * + * _.take([1, 2, 3], 5); + * // => [1, 2, 3] + * + * _.take([1, 2, 3], 0); + * // => [] + */ + function take(array, n, guard) { + if (!(array && array.length)) { + return []; + } + n = (guard || n === undefined) ? 1 : toInteger(n); + return baseSlice(array, 0, n < 0 ? 0 : n); + } + + /** + * Creates a slice of `array` with `n` elements taken from the end. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to take. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.takeRight([1, 2, 3]); + * // => [3] + * + * _.takeRight([1, 2, 3], 2); + * // => [2, 3] + * + * _.takeRight([1, 2, 3], 5); + * // => [1, 2, 3] + * + * _.takeRight([1, 2, 3], 0); + * // => [] + */ + function takeRight(array, n, guard) { + var length = array == null ? 0 : array.length; + if (!length) { + return []; + } + n = (guard || n === undefined) ? 1 : toInteger(n); + n = length - n; + return baseSlice(array, n < 0 ? 0 : n, length); + } + + /** + * Creates a slice of `array` with elements taken from the end. Elements are + * taken until `predicate` returns falsey. The predicate is invoked with + * three arguments: (value, index, array). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the slice of `array`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': false } + * ]; + * + * _.takeRightWhile(users, function(o) { return !o.active; }); + * // => objects for ['fred', 'pebbles'] + * + * // The `_.matches` iteratee shorthand. + * _.takeRightWhile(users, { 'user': 'pebbles', 'active': false }); + * // => objects for ['pebbles'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.takeRightWhile(users, ['active', false]); + * // => objects for ['fred', 'pebbles'] + * + * // The `_.property` iteratee shorthand. + * _.takeRightWhile(users, 'active'); + * // => [] + */ + function takeRightWhile(array, predicate) { + return (array && array.length) + ? baseWhile(array, getIteratee(predicate, 3), false, true) + : []; + } + + /** + * Creates a slice of `array` with elements taken from the beginning. Elements + * are taken until `predicate` returns falsey. The predicate is invoked with + * three arguments: (value, index, array). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Array + * @param {Array} array The array to query. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the slice of `array`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': false }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': true } + * ]; + * + * _.takeWhile(users, function(o) { return !o.active; }); + * // => objects for ['barney', 'fred'] + * + * // The `_.matches` iteratee shorthand. + * _.takeWhile(users, { 'user': 'barney', 'active': false }); + * // => objects for ['barney'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.takeWhile(users, ['active', false]); + * // => objects for ['barney', 'fred'] + * + * // The `_.property` iteratee shorthand. + * _.takeWhile(users, 'active'); + * // => [] + */ + function takeWhile(array, predicate) { + return (array && array.length) + ? baseWhile(array, getIteratee(predicate, 3)) + : []; + } + + /** + * Creates an array of unique values, in order, from all given arrays using + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @returns {Array} Returns the new array of combined values. + * @example + * + * _.union([2], [1, 2]); + * // => [2, 1] + */ + var union = baseRest(function(arrays) { + return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true)); + }); + + /** + * This method is like `_.union` except that it accepts `iteratee` which is + * invoked for each element of each `arrays` to generate the criterion by + * which uniqueness is computed. Result values are chosen from the first + * array in which the value occurs. The iteratee is invoked with one argument: + * (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns the new array of combined values. + * @example + * + * _.unionBy([2.1], [1.2, 2.3], Math.floor); + * // => [2.1, 1.2] + * + * // The `_.property` iteratee shorthand. + * _.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 1 }, { 'x': 2 }] + */ + var unionBy = baseRest(function(arrays) { + var iteratee = last(arrays); + if (isArrayLikeObject(iteratee)) { + iteratee = undefined; + } + return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), getIteratee(iteratee, 2)); + }); + + /** + * This method is like `_.union` except that it accepts `comparator` which + * is invoked to compare elements of `arrays`. Result values are chosen from + * the first array in which the value occurs. The comparator is invoked + * with two arguments: (arrVal, othVal). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of combined values. + * @example + * + * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; + * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; + * + * _.unionWith(objects, others, _.isEqual); + * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }] + */ + var unionWith = baseRest(function(arrays) { + var comparator = last(arrays); + comparator = typeof comparator == 'function' ? comparator : undefined; + return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), undefined, comparator); + }); + + /** + * Creates a duplicate-free version of an array, using + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons, in which only the first occurrence of each element + * is kept. The order of result values is determined by the order they occur + * in the array. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * _.uniq([2, 1, 2]); + * // => [2, 1] + */ + function uniq(array) { + return (array && array.length) ? baseUniq(array) : []; + } + + /** + * This method is like `_.uniq` except that it accepts `iteratee` which is + * invoked for each element in `array` to generate the criterion by which + * uniqueness is computed. The order of result values is determined by the + * order they occur in the array. The iteratee is invoked with one argument: + * (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * _.uniqBy([2.1, 1.2, 2.3], Math.floor); + * // => [2.1, 1.2] + * + * // The `_.property` iteratee shorthand. + * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 1 }, { 'x': 2 }] + */ + function uniqBy(array, iteratee) { + return (array && array.length) ? baseUniq(array, getIteratee(iteratee, 2)) : []; + } + + /** + * This method is like `_.uniq` except that it accepts `comparator` which + * is invoked to compare elements of `array`. The order of result values is + * determined by the order they occur in the array.The comparator is invoked + * with two arguments: (arrVal, othVal). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }]; + * + * _.uniqWith(objects, _.isEqual); + * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }] + */ + function uniqWith(array, comparator) { + comparator = typeof comparator == 'function' ? comparator : undefined; + return (array && array.length) ? baseUniq(array, undefined, comparator) : []; + } + + /** + * This method is like `_.zip` except that it accepts an array of grouped + * elements and creates an array regrouping the elements to their pre-zip + * configuration. + * + * @static + * @memberOf _ + * @since 1.2.0 + * @category Array + * @param {Array} array The array of grouped elements to process. + * @returns {Array} Returns the new array of regrouped elements. + * @example + * + * var zipped = _.zip(['a', 'b'], [1, 2], [true, false]); + * // => [['a', 1, true], ['b', 2, false]] + * + * _.unzip(zipped); + * // => [['a', 'b'], [1, 2], [true, false]] + */ + function unzip(array) { + if (!(array && array.length)) { + return []; + } + var length = 0; + array = arrayFilter(array, function(group) { + if (isArrayLikeObject(group)) { + length = nativeMax(group.length, length); + return true; + } + }); + return baseTimes(length, function(index) { + return arrayMap(array, baseProperty(index)); + }); + } + + /** + * This method is like `_.unzip` except that it accepts `iteratee` to specify + * how regrouped values should be combined. The iteratee is invoked with the + * elements of each group: (...group). + * + * @static + * @memberOf _ + * @since 3.8.0 + * @category Array + * @param {Array} array The array of grouped elements to process. + * @param {Function} [iteratee=_.identity] The function to combine + * regrouped values. + * @returns {Array} Returns the new array of regrouped elements. + * @example + * + * var zipped = _.zip([1, 2], [10, 20], [100, 200]); + * // => [[1, 10, 100], [2, 20, 200]] + * + * _.unzipWith(zipped, _.add); + * // => [3, 30, 300] + */ + function unzipWith(array, iteratee) { + if (!(array && array.length)) { + return []; + } + var result = unzip(array); + if (iteratee == null) { + return result; + } + return arrayMap(result, function(group) { + return apply(iteratee, undefined, group); + }); + } + + /** + * Creates an array excluding all given values using + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * **Note:** Unlike `_.pull`, this method returns a new array. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {Array} array The array to inspect. + * @param {...*} [values] The values to exclude. + * @returns {Array} Returns the new array of filtered values. + * @see _.difference, _.xor + * @example + * + * _.without([2, 1, 2, 3], 1, 2); + * // => [3] + */ + var without = baseRest(function(array, values) { + return isArrayLikeObject(array) + ? baseDifference(array, values) + : []; + }); + + /** + * Creates an array of unique values that is the + * [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference) + * of the given arrays. The order of result values is determined by the order + * they occur in the arrays. + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @returns {Array} Returns the new array of filtered values. + * @see _.difference, _.without + * @example + * + * _.xor([2, 1], [2, 3]); + * // => [1, 3] + */ + var xor = baseRest(function(arrays) { + return baseXor(arrayFilter(arrays, isArrayLikeObject)); + }); + + /** + * This method is like `_.xor` except that it accepts `iteratee` which is + * invoked for each element of each `arrays` to generate the criterion by + * which by which they're compared. The order of result values is determined + * by the order they occur in the arrays. The iteratee is invoked with one + * argument: (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * _.xorBy([2.1, 1.2], [2.3, 3.4], Math.floor); + * // => [1.2, 3.4] + * + * // The `_.property` iteratee shorthand. + * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 2 }] + */ + var xorBy = baseRest(function(arrays) { + var iteratee = last(arrays); + if (isArrayLikeObject(iteratee)) { + iteratee = undefined; + } + return baseXor(arrayFilter(arrays, isArrayLikeObject), getIteratee(iteratee, 2)); + }); + + /** + * This method is like `_.xor` except that it accepts `comparator` which is + * invoked to compare elements of `arrays`. The order of result values is + * determined by the order they occur in the arrays. The comparator is invoked + * with two arguments: (arrVal, othVal). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @param {Function} [comparator] The comparator invoked per element. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; + * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; + * + * _.xorWith(objects, others, _.isEqual); + * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }] + */ + var xorWith = baseRest(function(arrays) { + var comparator = last(arrays); + comparator = typeof comparator == 'function' ? comparator : undefined; + return baseXor(arrayFilter(arrays, isArrayLikeObject), undefined, comparator); + }); + + /** + * Creates an array of grouped elements, the first of which contains the + * first elements of the given arrays, the second of which contains the + * second elements of the given arrays, and so on. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Array + * @param {...Array} [arrays] The arrays to process. + * @returns {Array} Returns the new array of grouped elements. + * @example + * + * _.zip(['a', 'b'], [1, 2], [true, false]); + * // => [['a', 1, true], ['b', 2, false]] + */ + var zip = baseRest(unzip); + + /** + * This method is like `_.fromPairs` except that it accepts two arrays, + * one of property identifiers and one of corresponding values. + * + * @static + * @memberOf _ + * @since 0.4.0 + * @category Array + * @param {Array} [props=[]] The property identifiers. + * @param {Array} [values=[]] The property values. + * @returns {Object} Returns the new object. + * @example + * + * _.zipObject(['a', 'b'], [1, 2]); + * // => { 'a': 1, 'b': 2 } + */ + function zipObject(props, values) { + return baseZipObject(props || [], values || [], assignValue); + } + + /** + * This method is like `_.zipObject` except that it supports property paths. + * + * @static + * @memberOf _ + * @since 4.1.0 + * @category Array + * @param {Array} [props=[]] The property identifiers. + * @param {Array} [values=[]] The property values. + * @returns {Object} Returns the new object. + * @example + * + * _.zipObjectDeep(['a.b[0].c', 'a.b[1].d'], [1, 2]); + * // => { 'a': { 'b': [{ 'c': 1 }, { 'd': 2 }] } } + */ + function zipObjectDeep(props, values) { + return baseZipObject(props || [], values || [], baseSet); + } + + /** + * This method is like `_.zip` except that it accepts `iteratee` to specify + * how grouped values should be combined. The iteratee is invoked with the + * elements of each group: (...group). + * + * @static + * @memberOf _ + * @since 3.8.0 + * @category Array + * @param {...Array} [arrays] The arrays to process. + * @param {Function} [iteratee=_.identity] The function to combine + * grouped values. + * @returns {Array} Returns the new array of grouped elements. + * @example + * + * _.zipWith([1, 2], [10, 20], [100, 200], function(a, b, c) { + * return a + b + c; + * }); + * // => [111, 222] + */ + var zipWith = baseRest(function(arrays) { + var length = arrays.length, + iteratee = length > 1 ? arrays[length - 1] : undefined; + + iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined; + return unzipWith(arrays, iteratee); + }); + + /*------------------------------------------------------------------------*/ + + /** + * Creates a `lodash` wrapper instance that wraps `value` with explicit method + * chain sequences enabled. The result of such sequences must be unwrapped + * with `_#value`. + * + * @static + * @memberOf _ + * @since 1.3.0 + * @category Seq + * @param {*} value The value to wrap. + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 }, + * { 'user': 'pebbles', 'age': 1 } + * ]; + * + * var youngest = _ + * .chain(users) + * .sortBy('age') + * .map(function(o) { + * return o.user + ' is ' + o.age; + * }) + * .head() + * .value(); + * // => 'pebbles is 1' + */ + function chain(value) { + var result = lodash(value); + result.__chain__ = true; + return result; + } + + /** + * This method invokes `interceptor` and returns `value`. The interceptor + * is invoked with one argument; (value). The purpose of this method is to + * "tap into" a method chain sequence in order to modify intermediate results. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Seq + * @param {*} value The value to provide to `interceptor`. + * @param {Function} interceptor The function to invoke. + * @returns {*} Returns `value`. + * @example + * + * _([1, 2, 3]) + * .tap(function(array) { + * // Mutate input array. + * array.pop(); + * }) + * .reverse() + * .value(); + * // => [2, 1] + */ + function tap(value, interceptor) { + interceptor(value); + return value; + } + + /** + * This method is like `_.tap` except that it returns the result of `interceptor`. + * The purpose of this method is to "pass thru" values replacing intermediate + * results in a method chain sequence. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Seq + * @param {*} value The value to provide to `interceptor`. + * @param {Function} interceptor The function to invoke. + * @returns {*} Returns the result of `interceptor`. + * @example + * + * _(' abc ') + * .chain() + * .trim() + * .thru(function(value) { + * return [value]; + * }) + * .value(); + * // => ['abc'] + */ + function thru(value, interceptor) { + return interceptor(value); + } + + /** + * This method is the wrapper version of `_.at`. + * + * @name at + * @memberOf _ + * @since 1.0.0 + * @category Seq + * @param {...(string|string[])} [paths] The property paths to pick. + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] }; + * + * _(object).at(['a[0].b.c', 'a[1]']).value(); + * // => [3, 4] + */ + var wrapperAt = flatRest(function(paths) { + var length = paths.length, + start = length ? paths[0] : 0, + value = this.__wrapped__, + interceptor = function(object) { return baseAt(object, paths); }; + + if (length > 1 || this.__actions__.length || + !(value instanceof LazyWrapper) || !isIndex(start)) { + return this.thru(interceptor); + } + value = value.slice(start, +start + (length ? 1 : 0)); + value.__actions__.push({ + 'func': thru, + 'args': [interceptor], + 'thisArg': undefined + }); + return new LodashWrapper(value, this.__chain__).thru(function(array) { + if (length && !array.length) { + array.push(undefined); + } + return array; + }); + }); + + /** + * Creates a `lodash` wrapper instance with explicit method chain sequences enabled. + * + * @name chain + * @memberOf _ + * @since 0.1.0 + * @category Seq + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 } + * ]; + * + * // A sequence without explicit chaining. + * _(users).head(); + * // => { 'user': 'barney', 'age': 36 } + * + * // A sequence with explicit chaining. + * _(users) + * .chain() + * .head() + * .pick('user') + * .value(); + * // => { 'user': 'barney' } + */ + function wrapperChain() { + return chain(this); + } + + /** + * Executes the chain sequence and returns the wrapped result. + * + * @name commit + * @memberOf _ + * @since 3.2.0 + * @category Seq + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var array = [1, 2]; + * var wrapped = _(array).push(3); + * + * console.log(array); + * // => [1, 2] + * + * wrapped = wrapped.commit(); + * console.log(array); + * // => [1, 2, 3] + * + * wrapped.last(); + * // => 3 + * + * console.log(array); + * // => [1, 2, 3] + */ + function wrapperCommit() { + return new LodashWrapper(this.value(), this.__chain__); + } + + /** + * Gets the next value on a wrapped object following the + * [iterator protocol](https://mdn.io/iteration_protocols#iterator). + * + * @name next + * @memberOf _ + * @since 4.0.0 + * @category Seq + * @returns {Object} Returns the next iterator value. + * @example + * + * var wrapped = _([1, 2]); + * + * wrapped.next(); + * // => { 'done': false, 'value': 1 } + * + * wrapped.next(); + * // => { 'done': false, 'value': 2 } + * + * wrapped.next(); + * // => { 'done': true, 'value': undefined } + */ + function wrapperNext() { + if (this.__values__ === undefined) { + this.__values__ = toArray(this.value()); + } + var done = this.__index__ >= this.__values__.length, + value = done ? undefined : this.__values__[this.__index__++]; + + return { 'done': done, 'value': value }; + } + + /** + * Enables the wrapper to be iterable. + * + * @name Symbol.iterator + * @memberOf _ + * @since 4.0.0 + * @category Seq + * @returns {Object} Returns the wrapper object. + * @example + * + * var wrapped = _([1, 2]); + * + * wrapped[Symbol.iterator]() === wrapped; + * // => true + * + * Array.from(wrapped); + * // => [1, 2] + */ + function wrapperToIterator() { + return this; + } + + /** + * Creates a clone of the chain sequence planting `value` as the wrapped value. + * + * @name plant + * @memberOf _ + * @since 3.2.0 + * @category Seq + * @param {*} value The value to plant. + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * function square(n) { + * return n * n; + * } + * + * var wrapped = _([1, 2]).map(square); + * var other = wrapped.plant([3, 4]); + * + * other.value(); + * // => [9, 16] + * + * wrapped.value(); + * // => [1, 4] + */ + function wrapperPlant(value) { + var result, + parent = this; + + while (parent instanceof baseLodash) { + var clone = wrapperClone(parent); + clone.__index__ = 0; + clone.__values__ = undefined; + if (result) { + previous.__wrapped__ = clone; + } else { + result = clone; + } + var previous = clone; + parent = parent.__wrapped__; + } + previous.__wrapped__ = value; + return result; + } + + /** + * This method is the wrapper version of `_.reverse`. + * + * **Note:** This method mutates the wrapped array. + * + * @name reverse + * @memberOf _ + * @since 0.1.0 + * @category Seq + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var array = [1, 2, 3]; + * + * _(array).reverse().value() + * // => [3, 2, 1] + * + * console.log(array); + * // => [3, 2, 1] + */ + function wrapperReverse() { + var value = this.__wrapped__; + if (value instanceof LazyWrapper) { + var wrapped = value; + if (this.__actions__.length) { + wrapped = new LazyWrapper(this); + } + wrapped = wrapped.reverse(); + wrapped.__actions__.push({ + 'func': thru, + 'args': [reverse], + 'thisArg': undefined + }); + return new LodashWrapper(wrapped, this.__chain__); + } + return this.thru(reverse); + } + + /** + * Executes the chain sequence to resolve the unwrapped value. + * + * @name value + * @memberOf _ + * @since 0.1.0 + * @alias toJSON, valueOf + * @category Seq + * @returns {*} Returns the resolved unwrapped value. + * @example + * + * _([1, 2, 3]).value(); + * // => [1, 2, 3] + */ + function wrapperValue() { + return baseWrapperValue(this.__wrapped__, this.__actions__); + } + + /*------------------------------------------------------------------------*/ + + /** + * Creates an object composed of keys generated from the results of running + * each element of `collection` thru `iteratee`. The corresponding value of + * each key is the number of times the key was returned by `iteratee`. The + * iteratee is invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 0.5.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The iteratee to transform keys. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * _.countBy([6.1, 4.2, 6.3], Math.floor); + * // => { '4': 1, '6': 2 } + * + * // The `_.property` iteratee shorthand. + * _.countBy(['one', 'two', 'three'], 'length'); + * // => { '3': 2, '5': 1 } + */ + var countBy = createAggregator(function(result, value, key) { + if (hasOwnProperty.call(result, key)) { + ++result[key]; + } else { + baseAssignValue(result, key, 1); + } + }); + + /** + * Checks if `predicate` returns truthy for **all** elements of `collection`. + * Iteration is stopped once `predicate` returns falsey. The predicate is + * invoked with three arguments: (value, index|key, collection). + * + * **Note:** This method returns `true` for + * [empty collections](https://en.wikipedia.org/wiki/Empty_set) because + * [everything is true](https://en.wikipedia.org/wiki/Vacuous_truth) of + * elements of empty collections. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {boolean} Returns `true` if all elements pass the predicate check, + * else `false`. + * @example + * + * _.every([true, 1, null, 'yes'], Boolean); + * // => false + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': false }, + * { 'user': 'fred', 'age': 40, 'active': false } + * ]; + * + * // The `_.matches` iteratee shorthand. + * _.every(users, { 'user': 'barney', 'active': false }); + * // => false + * + * // The `_.matchesProperty` iteratee shorthand. + * _.every(users, ['active', false]); + * // => true + * + * // The `_.property` iteratee shorthand. + * _.every(users, 'active'); + * // => false + */ + function every(collection, predicate, guard) { + var func = isArray(collection) ? arrayEvery : baseEvery; + if (guard && isIterateeCall(collection, predicate, guard)) { + predicate = undefined; + } + return func(collection, getIteratee(predicate, 3)); + } + + /** + * Iterates over elements of `collection`, returning an array of all elements + * `predicate` returns truthy for. The predicate is invoked with three + * arguments: (value, index|key, collection). + * + * **Note:** Unlike `_.remove`, this method returns a new array. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + * @see _.reject + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': true }, + * { 'user': 'fred', 'age': 40, 'active': false } + * ]; + * + * _.filter(users, function(o) { return !o.active; }); + * // => objects for ['fred'] + * + * // The `_.matches` iteratee shorthand. + * _.filter(users, { 'age': 36, 'active': true }); + * // => objects for ['barney'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.filter(users, ['active', false]); + * // => objects for ['fred'] + * + * // The `_.property` iteratee shorthand. + * _.filter(users, 'active'); + * // => objects for ['barney'] + */ + function filter(collection, predicate) { + var func = isArray(collection) ? arrayFilter : baseFilter; + return func(collection, getIteratee(predicate, 3)); + } + + /** + * Iterates over elements of `collection`, returning the first element + * `predicate` returns truthy for. The predicate is invoked with three + * arguments: (value, index|key, collection). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param {number} [fromIndex=0] The index to search from. + * @returns {*} Returns the matched element, else `undefined`. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': true }, + * { 'user': 'fred', 'age': 40, 'active': false }, + * { 'user': 'pebbles', 'age': 1, 'active': true } + * ]; + * + * _.find(users, function(o) { return o.age < 40; }); + * // => object for 'barney' + * + * // The `_.matches` iteratee shorthand. + * _.find(users, { 'age': 1, 'active': true }); + * // => object for 'pebbles' + * + * // The `_.matchesProperty` iteratee shorthand. + * _.find(users, ['active', false]); + * // => object for 'fred' + * + * // The `_.property` iteratee shorthand. + * _.find(users, 'active'); + * // => object for 'barney' + */ + var find = createFind(findIndex); + + /** + * This method is like `_.find` except that it iterates over elements of + * `collection` from right to left. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Collection + * @param {Array|Object} collection The collection to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param {number} [fromIndex=collection.length-1] The index to search from. + * @returns {*} Returns the matched element, else `undefined`. + * @example + * + * _.findLast([1, 2, 3, 4], function(n) { + * return n % 2 == 1; + * }); + * // => 3 + */ + var findLast = createFind(findLastIndex); + + /** + * Creates a flattened array of values by running each element in `collection` + * thru `iteratee` and flattening the mapped results. The iteratee is invoked + * with three arguments: (value, index|key, collection). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new flattened array. + * @example + * + * function duplicate(n) { + * return [n, n]; + * } + * + * _.flatMap([1, 2], duplicate); + * // => [1, 1, 2, 2] + */ + function flatMap(collection, iteratee) { + return baseFlatten(map(collection, iteratee), 1); + } + + /** + * This method is like `_.flatMap` except that it recursively flattens the + * mapped results. + * + * @static + * @memberOf _ + * @since 4.7.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new flattened array. + * @example + * + * function duplicate(n) { + * return [[[n, n]]]; + * } + * + * _.flatMapDeep([1, 2], duplicate); + * // => [1, 1, 2, 2] + */ + function flatMapDeep(collection, iteratee) { + return baseFlatten(map(collection, iteratee), INFINITY); + } + + /** + * This method is like `_.flatMap` except that it recursively flattens the + * mapped results up to `depth` times. + * + * @static + * @memberOf _ + * @since 4.7.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {number} [depth=1] The maximum recursion depth. + * @returns {Array} Returns the new flattened array. + * @example + * + * function duplicate(n) { + * return [[[n, n]]]; + * } + * + * _.flatMapDepth([1, 2], duplicate, 2); + * // => [[1, 1], [2, 2]] + */ + function flatMapDepth(collection, iteratee, depth) { + depth = depth === undefined ? 1 : toInteger(depth); + return baseFlatten(map(collection, iteratee), depth); + } + + /** + * Iterates over elements of `collection` and invokes `iteratee` for each element. + * The iteratee is invoked with three arguments: (value, index|key, collection). + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * **Note:** As with other "Collections" methods, objects with a "length" + * property are iterated like arrays. To avoid this behavior use `_.forIn` + * or `_.forOwn` for object iteration. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @alias each + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + * @see _.forEachRight + * @example + * + * _.forEach([1, 2], function(value) { + * console.log(value); + * }); + * // => Logs `1` then `2`. + * + * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) { + * console.log(key); + * }); + * // => Logs 'a' then 'b' (iteration order is not guaranteed). + */ + function forEach(collection, iteratee) { + var func = isArray(collection) ? arrayEach : baseEach; + return func(collection, getIteratee(iteratee, 3)); + } + + /** + * This method is like `_.forEach` except that it iterates over elements of + * `collection` from right to left. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @alias eachRight + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + * @see _.forEach + * @example + * + * _.forEachRight([1, 2], function(value) { + * console.log(value); + * }); + * // => Logs `2` then `1`. + */ + function forEachRight(collection, iteratee) { + var func = isArray(collection) ? arrayEachRight : baseEachRight; + return func(collection, getIteratee(iteratee, 3)); + } + + /** + * Creates an object composed of keys generated from the results of running + * each element of `collection` thru `iteratee`. The order of grouped values + * is determined by the order they occur in `collection`. The corresponding + * value of each key is an array of elements responsible for generating the + * key. The iteratee is invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The iteratee to transform keys. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * _.groupBy([6.1, 4.2, 6.3], Math.floor); + * // => { '4': [4.2], '6': [6.1, 6.3] } + * + * // The `_.property` iteratee shorthand. + * _.groupBy(['one', 'two', 'three'], 'length'); + * // => { '3': ['one', 'two'], '5': ['three'] } + */ + var groupBy = createAggregator(function(result, value, key) { + if (hasOwnProperty.call(result, key)) { + result[key].push(value); + } else { + baseAssignValue(result, key, [value]); + } + }); + + /** + * Checks if `value` is in `collection`. If `collection` is a string, it's + * checked for a substring of `value`, otherwise + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * is used for equality comparisons. If `fromIndex` is negative, it's used as + * the offset from the end of `collection`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object|string} collection The collection to inspect. + * @param {*} value The value to search for. + * @param {number} [fromIndex=0] The index to search from. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`. + * @returns {boolean} Returns `true` if `value` is found, else `false`. + * @example + * + * _.includes([1, 2, 3], 1); + * // => true + * + * _.includes([1, 2, 3], 1, 2); + * // => false + * + * _.includes({ 'a': 1, 'b': 2 }, 1); + * // => true + * + * _.includes('abcd', 'bc'); + * // => true + */ + function includes(collection, value, fromIndex, guard) { + collection = isArrayLike(collection) ? collection : values(collection); + fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0; + + var length = collection.length; + if (fromIndex < 0) { + fromIndex = nativeMax(length + fromIndex, 0); + } + return isString(collection) + ? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1) + : (!!length && baseIndexOf(collection, value, fromIndex) > -1); + } + + /** + * Invokes the method at `path` of each element in `collection`, returning + * an array of the results of each invoked method. Any additional arguments + * are provided to each invoked method. If `path` is a function, it's invoked + * for, and `this` bound to, each element in `collection`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Array|Function|string} path The path of the method to invoke or + * the function invoked per iteration. + * @param {...*} [args] The arguments to invoke each method with. + * @returns {Array} Returns the array of results. + * @example + * + * _.invokeMap([[5, 1, 7], [3, 2, 1]], 'sort'); + * // => [[1, 5, 7], [1, 2, 3]] + * + * _.invokeMap([123, 456], String.prototype.split, ''); + * // => [['1', '2', '3'], ['4', '5', '6']] + */ + var invokeMap = baseRest(function(collection, path, args) { + var index = -1, + isFunc = typeof path == 'function', + result = isArrayLike(collection) ? Array(collection.length) : []; + + baseEach(collection, function(value) { + result[++index] = isFunc ? apply(path, value, args) : baseInvoke(value, path, args); + }); + return result; + }); + + /** + * Creates an object composed of keys generated from the results of running + * each element of `collection` thru `iteratee`. The corresponding value of + * each key is the last element responsible for generating the key. The + * iteratee is invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The iteratee to transform keys. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * var array = [ + * { 'dir': 'left', 'code': 97 }, + * { 'dir': 'right', 'code': 100 } + * ]; + * + * _.keyBy(array, function(o) { + * return String.fromCharCode(o.code); + * }); + * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } + * + * _.keyBy(array, 'dir'); + * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } } + */ + var keyBy = createAggregator(function(result, value, key) { + baseAssignValue(result, key, value); + }); + + /** + * Creates an array of values by running each element in `collection` thru + * `iteratee`. The iteratee is invoked with three arguments: + * (value, index|key, collection). + * + * Many lodash methods are guarded to work as iteratees for methods like + * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`. + * + * The guarded methods are: + * `ary`, `chunk`, `curry`, `curryRight`, `drop`, `dropRight`, `every`, + * `fill`, `invert`, `parseInt`, `random`, `range`, `rangeRight`, `repeat`, + * `sampleSize`, `slice`, `some`, `sortBy`, `split`, `take`, `takeRight`, + * `template`, `trim`, `trimEnd`, `trimStart`, and `words` + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + * @example + * + * function square(n) { + * return n * n; + * } + * + * _.map([4, 8], square); + * // => [16, 64] + * + * _.map({ 'a': 4, 'b': 8 }, square); + * // => [16, 64] (iteration order is not guaranteed) + * + * var users = [ + * { 'user': 'barney' }, + * { 'user': 'fred' } + * ]; + * + * // The `_.property` iteratee shorthand. + * _.map(users, 'user'); + * // => ['barney', 'fred'] + */ + function map(collection, iteratee) { + var func = isArray(collection) ? arrayMap : baseMap; + return func(collection, getIteratee(iteratee, 3)); + } + + /** + * This method is like `_.sortBy` except that it allows specifying the sort + * orders of the iteratees to sort by. If `orders` is unspecified, all values + * are sorted in ascending order. Otherwise, specify an order of "desc" for + * descending or "asc" for ascending sort order of corresponding values. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Array[]|Function[]|Object[]|string[]} [iteratees=[_.identity]] + * The iteratees to sort by. + * @param {string[]} [orders] The sort orders of `iteratees`. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`. + * @returns {Array} Returns the new sorted array. + * @example + * + * var users = [ + * { 'user': 'fred', 'age': 48 }, + * { 'user': 'barney', 'age': 34 }, + * { 'user': 'fred', 'age': 40 }, + * { 'user': 'barney', 'age': 36 } + * ]; + * + * // Sort by `user` in ascending order and by `age` in descending order. + * _.orderBy(users, ['user', 'age'], ['asc', 'desc']); + * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]] + */ + function orderBy(collection, iteratees, orders, guard) { + if (collection == null) { + return []; + } + if (!isArray(iteratees)) { + iteratees = iteratees == null ? [] : [iteratees]; + } + orders = guard ? undefined : orders; + if (!isArray(orders)) { + orders = orders == null ? [] : [orders]; + } + return baseOrderBy(collection, iteratees, orders); + } + + /** + * Creates an array of elements split into two groups, the first of which + * contains elements `predicate` returns truthy for, the second of which + * contains elements `predicate` returns falsey for. The predicate is + * invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the array of grouped elements. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': false }, + * { 'user': 'fred', 'age': 40, 'active': true }, + * { 'user': 'pebbles', 'age': 1, 'active': false } + * ]; + * + * _.partition(users, function(o) { return o.active; }); + * // => objects for [['fred'], ['barney', 'pebbles']] + * + * // The `_.matches` iteratee shorthand. + * _.partition(users, { 'age': 1, 'active': false }); + * // => objects for [['pebbles'], ['barney', 'fred']] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.partition(users, ['active', false]); + * // => objects for [['barney', 'pebbles'], ['fred']] + * + * // The `_.property` iteratee shorthand. + * _.partition(users, 'active'); + * // => objects for [['fred'], ['barney', 'pebbles']] + */ + var partition = createAggregator(function(result, value, key) { + result[key ? 0 : 1].push(value); + }, function() { return [[], []]; }); + + /** + * Reduces `collection` to a value which is the accumulated result of running + * each element in `collection` thru `iteratee`, where each successive + * invocation is supplied the return value of the previous. If `accumulator` + * is not given, the first element of `collection` is used as the initial + * value. The iteratee is invoked with four arguments: + * (accumulator, value, index|key, collection). + * + * Many lodash methods are guarded to work as iteratees for methods like + * `_.reduce`, `_.reduceRight`, and `_.transform`. + * + * The guarded methods are: + * `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `orderBy`, + * and `sortBy` + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @returns {*} Returns the accumulated value. + * @see _.reduceRight + * @example + * + * _.reduce([1, 2], function(sum, n) { + * return sum + n; + * }, 0); + * // => 3 + * + * _.reduce({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) { + * (result[value] || (result[value] = [])).push(key); + * return result; + * }, {}); + * // => { '1': ['a', 'c'], '2': ['b'] } (iteration order is not guaranteed) + */ + function reduce(collection, iteratee, accumulator) { + var func = isArray(collection) ? arrayReduce : baseReduce, + initAccum = arguments.length < 3; + + return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach); + } + + /** + * This method is like `_.reduce` except that it iterates over elements of + * `collection` from right to left. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @returns {*} Returns the accumulated value. + * @see _.reduce + * @example + * + * var array = [[0, 1], [2, 3], [4, 5]]; + * + * _.reduceRight(array, function(flattened, other) { + * return flattened.concat(other); + * }, []); + * // => [4, 5, 2, 3, 0, 1] + */ + function reduceRight(collection, iteratee, accumulator) { + var func = isArray(collection) ? arrayReduceRight : baseReduce, + initAccum = arguments.length < 3; + + return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEachRight); + } + + /** + * The opposite of `_.filter`; this method returns the elements of `collection` + * that `predicate` does **not** return truthy for. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + * @see _.filter + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': false }, + * { 'user': 'fred', 'age': 40, 'active': true } + * ]; + * + * _.reject(users, function(o) { return !o.active; }); + * // => objects for ['fred'] + * + * // The `_.matches` iteratee shorthand. + * _.reject(users, { 'age': 40, 'active': true }); + * // => objects for ['barney'] + * + * // The `_.matchesProperty` iteratee shorthand. + * _.reject(users, ['active', false]); + * // => objects for ['fred'] + * + * // The `_.property` iteratee shorthand. + * _.reject(users, 'active'); + * // => objects for ['barney'] + */ + function reject(collection, predicate) { + var func = isArray(collection) ? arrayFilter : baseFilter; + return func(collection, negate(getIteratee(predicate, 3))); + } + + /** + * Gets a random element from `collection`. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Collection + * @param {Array|Object} collection The collection to sample. + * @returns {*} Returns the random element. + * @example + * + * _.sample([1, 2, 3, 4]); + * // => 2 + */ + function sample(collection) { + var func = isArray(collection) ? arraySample : baseSample; + return func(collection); + } + + /** + * Gets `n` random elements at unique keys from `collection` up to the + * size of `collection`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Collection + * @param {Array|Object} collection The collection to sample. + * @param {number} [n=1] The number of elements to sample. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the random elements. + * @example + * + * _.sampleSize([1, 2, 3], 2); + * // => [3, 1] + * + * _.sampleSize([1, 2, 3], 4); + * // => [2, 3, 1] + */ + function sampleSize(collection, n, guard) { + if ((guard ? isIterateeCall(collection, n, guard) : n === undefined)) { + n = 1; + } else { + n = toInteger(n); + } + var func = isArray(collection) ? arraySampleSize : baseSampleSize; + return func(collection, n); + } + + /** + * Creates an array of shuffled values, using a version of the + * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to shuffle. + * @returns {Array} Returns the new shuffled array. + * @example + * + * _.shuffle([1, 2, 3, 4]); + * // => [4, 1, 3, 2] + */ + function shuffle(collection) { + var func = isArray(collection) ? arrayShuffle : baseShuffle; + return func(collection); + } + + /** + * Gets the size of `collection` by returning its length for array-like + * values or the number of own enumerable string keyed properties for objects. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object|string} collection The collection to inspect. + * @returns {number} Returns the collection size. + * @example + * + * _.size([1, 2, 3]); + * // => 3 + * + * _.size({ 'a': 1, 'b': 2 }); + * // => 2 + * + * _.size('pebbles'); + * // => 7 + */ + function size(collection) { + if (collection == null) { + return 0; + } + if (isArrayLike(collection)) { + return isString(collection) ? stringSize(collection) : collection.length; + } + var tag = getTag(collection); + if (tag == mapTag || tag == setTag) { + return collection.size; + } + return baseKeys(collection).length; + } + + /** + * Checks if `predicate` returns truthy for **any** element of `collection`. + * Iteration is stopped once `predicate` returns truthy. The predicate is + * invoked with three arguments: (value, index|key, collection). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {boolean} Returns `true` if any element passes the predicate check, + * else `false`. + * @example + * + * _.some([null, 0, 'yes', false], Boolean); + * // => true + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false } + * ]; + * + * // The `_.matches` iteratee shorthand. + * _.some(users, { 'user': 'barney', 'active': false }); + * // => false + * + * // The `_.matchesProperty` iteratee shorthand. + * _.some(users, ['active', false]); + * // => true + * + * // The `_.property` iteratee shorthand. + * _.some(users, 'active'); + * // => true + */ + function some(collection, predicate, guard) { + var func = isArray(collection) ? arraySome : baseSome; + if (guard && isIterateeCall(collection, predicate, guard)) { + predicate = undefined; + } + return func(collection, getIteratee(predicate, 3)); + } + + /** + * Creates an array of elements, sorted in ascending order by the results of + * running each element in a collection thru each iteratee. This method + * performs a stable sort, that is, it preserves the original sort order of + * equal elements. The iteratees are invoked with one argument: (value). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {...(Function|Function[])} [iteratees=[_.identity]] + * The iteratees to sort by. + * @returns {Array} Returns the new sorted array. + * @example + * + * var users = [ + * { 'user': 'fred', 'age': 48 }, + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 }, + * { 'user': 'barney', 'age': 34 } + * ]; + * + * _.sortBy(users, [function(o) { return o.user; }]); + * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]] + * + * _.sortBy(users, ['user', 'age']); + * // => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]] + */ + var sortBy = baseRest(function(collection, iteratees) { + if (collection == null) { + return []; + } + var length = iteratees.length; + if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) { + iteratees = []; + } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) { + iteratees = [iteratees[0]]; + } + return baseOrderBy(collection, baseFlatten(iteratees, 1), []); + }); + + /*------------------------------------------------------------------------*/ + + /** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ + var now = ctxNow || function() { + return root.Date.now(); + }; + + /*------------------------------------------------------------------------*/ + + /** + * The opposite of `_.before`; this method creates a function that invokes + * `func` once it's called `n` or more times. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {number} n The number of calls before `func` is invoked. + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * var saves = ['profile', 'settings']; + * + * var done = _.after(saves.length, function() { + * console.log('done saving!'); + * }); + * + * _.forEach(saves, function(type) { + * asyncSave({ 'type': type, 'complete': done }); + * }); + * // => Logs 'done saving!' after the two async saves have completed. + */ + function after(n, func) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + n = toInteger(n); + return function() { + if (--n < 1) { + return func.apply(this, arguments); + } + }; + } + + /** + * Creates a function that invokes `func`, with up to `n` arguments, + * ignoring any additional arguments. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {Function} func The function to cap arguments for. + * @param {number} [n=func.length] The arity cap. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Function} Returns the new capped function. + * @example + * + * _.map(['6', '8', '10'], _.ary(parseInt, 1)); + * // => [6, 8, 10] + */ + function ary(func, n, guard) { + n = guard ? undefined : n; + n = (func && n == null) ? func.length : n; + return createWrap(func, WRAP_ARY_FLAG, undefined, undefined, undefined, undefined, n); + } + + /** + * Creates a function that invokes `func`, with the `this` binding and arguments + * of the created function, while it's called less than `n` times. Subsequent + * calls to the created function return the result of the last `func` invocation. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {number} n The number of calls at which `func` is no longer invoked. + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * jQuery(element).on('click', _.before(5, addContactToList)); + * // => Allows adding up to 4 contacts to the list. + */ + function before(n, func) { + var result; + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + n = toInteger(n); + return function() { + if (--n > 0) { + result = func.apply(this, arguments); + } + if (n <= 1) { + func = undefined; + } + return result; + }; + } + + /** + * Creates a function that invokes `func` with the `this` binding of `thisArg` + * and `partials` prepended to the arguments it receives. + * + * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds, + * may be used as a placeholder for partially applied arguments. + * + * **Note:** Unlike native `Function#bind`, this method doesn't set the "length" + * property of bound functions. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to bind. + * @param {*} thisArg The `this` binding of `func`. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * function greet(greeting, punctuation) { + * return greeting + ' ' + this.user + punctuation; + * } + * + * var object = { 'user': 'fred' }; + * + * var bound = _.bind(greet, object, 'hi'); + * bound('!'); + * // => 'hi fred!' + * + * // Bound with placeholders. + * var bound = _.bind(greet, object, _, '!'); + * bound('hi'); + * // => 'hi fred!' + */ + var bind = baseRest(function(func, thisArg, partials) { + var bitmask = WRAP_BIND_FLAG; + if (partials.length) { + var holders = replaceHolders(partials, getHolder(bind)); + bitmask |= WRAP_PARTIAL_FLAG; + } + return createWrap(func, bitmask, thisArg, partials, holders); + }); + + /** + * Creates a function that invokes the method at `object[key]` with `partials` + * prepended to the arguments it receives. + * + * This method differs from `_.bind` by allowing bound functions to reference + * methods that may be redefined or don't yet exist. See + * [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern) + * for more details. + * + * The `_.bindKey.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for partially applied arguments. + * + * @static + * @memberOf _ + * @since 0.10.0 + * @category Function + * @param {Object} object The object to invoke the method on. + * @param {string} key The key of the method. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var object = { + * 'user': 'fred', + * 'greet': function(greeting, punctuation) { + * return greeting + ' ' + this.user + punctuation; + * } + * }; + * + * var bound = _.bindKey(object, 'greet', 'hi'); + * bound('!'); + * // => 'hi fred!' + * + * object.greet = function(greeting, punctuation) { + * return greeting + 'ya ' + this.user + punctuation; + * }; + * + * bound('!'); + * // => 'hiya fred!' + * + * // Bound with placeholders. + * var bound = _.bindKey(object, 'greet', _, '!'); + * bound('hi'); + * // => 'hiya fred!' + */ + var bindKey = baseRest(function(object, key, partials) { + var bitmask = WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG; + if (partials.length) { + var holders = replaceHolders(partials, getHolder(bindKey)); + bitmask |= WRAP_PARTIAL_FLAG; + } + return createWrap(key, bitmask, object, partials, holders); + }); + + /** + * Creates a function that accepts arguments of `func` and either invokes + * `func` returning its result, if at least `arity` number of arguments have + * been provided, or returns a function that accepts the remaining `func` + * arguments, and so on. The arity of `func` may be specified if `func.length` + * is not sufficient. + * + * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds, + * may be used as a placeholder for provided arguments. + * + * **Note:** This method doesn't set the "length" property of curried functions. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Function + * @param {Function} func The function to curry. + * @param {number} [arity=func.length] The arity of `func`. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Function} Returns the new curried function. + * @example + * + * var abc = function(a, b, c) { + * return [a, b, c]; + * }; + * + * var curried = _.curry(abc); + * + * curried(1)(2)(3); + * // => [1, 2, 3] + * + * curried(1, 2)(3); + * // => [1, 2, 3] + * + * curried(1, 2, 3); + * // => [1, 2, 3] + * + * // Curried with placeholders. + * curried(1)(_, 3)(2); + * // => [1, 2, 3] + */ + function curry(func, arity, guard) { + arity = guard ? undefined : arity; + var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity); + result.placeholder = curry.placeholder; + return result; + } + + /** + * This method is like `_.curry` except that arguments are applied to `func` + * in the manner of `_.partialRight` instead of `_.partial`. + * + * The `_.curryRight.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for provided arguments. + * + * **Note:** This method doesn't set the "length" property of curried functions. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {Function} func The function to curry. + * @param {number} [arity=func.length] The arity of `func`. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Function} Returns the new curried function. + * @example + * + * var abc = function(a, b, c) { + * return [a, b, c]; + * }; + * + * var curried = _.curryRight(abc); + * + * curried(3)(2)(1); + * // => [1, 2, 3] + * + * curried(2, 3)(1); + * // => [1, 2, 3] + * + * curried(1, 2, 3); + * // => [1, 2, 3] + * + * // Curried with placeholders. + * curried(3)(1, _)(2); + * // => [1, 2, 3] + */ + function curryRight(func, arity, guard) { + arity = guard ? undefined : arity; + var result = createWrap(func, WRAP_CURRY_RIGHT_FLAG, undefined, undefined, undefined, undefined, undefined, arity); + result.placeholder = curryRight.placeholder; + return result; + } + + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ + function debounce(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + timeWaiting = wait - timeSinceLastCall; + + return maxing + ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) + : timeWaiting; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + + function debounced() { + var time = now(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; + } + + /** + * Defers invoking the `func` until the current call stack has cleared. Any + * additional arguments are provided to `func` when it's invoked. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to defer. + * @param {...*} [args] The arguments to invoke `func` with. + * @returns {number} Returns the timer id. + * @example + * + * _.defer(function(text) { + * console.log(text); + * }, 'deferred'); + * // => Logs 'deferred' after one millisecond. + */ + var defer = baseRest(function(func, args) { + return baseDelay(func, 1, args); + }); + + /** + * Invokes `func` after `wait` milliseconds. Any additional arguments are + * provided to `func` when it's invoked. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to delay. + * @param {number} wait The number of milliseconds to delay invocation. + * @param {...*} [args] The arguments to invoke `func` with. + * @returns {number} Returns the timer id. + * @example + * + * _.delay(function(text) { + * console.log(text); + * }, 1000, 'later'); + * // => Logs 'later' after one second. + */ + var delay = baseRest(function(func, wait, args) { + return baseDelay(func, toNumber(wait) || 0, args); + }); + + /** + * Creates a function that invokes `func` with arguments reversed. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Function + * @param {Function} func The function to flip arguments for. + * @returns {Function} Returns the new flipped function. + * @example + * + * var flipped = _.flip(function() { + * return _.toArray(arguments); + * }); + * + * flipped('a', 'b', 'c', 'd'); + * // => ['d', 'c', 'b', 'a'] + */ + function flip(func) { + return createWrap(func, WRAP_FLIP_FLAG); + } + + /** + * Creates a function that memoizes the result of `func`. If `resolver` is + * provided, it determines the cache key for storing the result based on the + * arguments provided to the memoized function. By default, the first argument + * provided to the memoized function is used as the map cache key. The `func` + * is invoked with the `this` binding of the memoized function. + * + * **Note:** The cache is exposed as the `cache` property on the memoized + * function. Its creation may be customized by replacing the `_.memoize.Cache` + * constructor with one whose instances implement the + * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) + * method interface of `clear`, `delete`, `get`, `has`, and `set`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to have its output memoized. + * @param {Function} [resolver] The function to resolve the cache key. + * @returns {Function} Returns the new memoized function. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * var other = { 'c': 3, 'd': 4 }; + * + * var values = _.memoize(_.values); + * values(object); + * // => [1, 2] + * + * values(other); + * // => [3, 4] + * + * object.a = 2; + * values(object); + * // => [1, 2] + * + * // Modify the result cache. + * values.cache.set(object, ['a', 'b']); + * values(object); + * // => ['a', 'b'] + * + * // Replace `_.memoize.Cache`. + * _.memoize.Cache = WeakMap; + */ + function memoize(func, resolver) { + if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { + throw new TypeError(FUNC_ERROR_TEXT); + } + var memoized = function() { + var args = arguments, + key = resolver ? resolver.apply(this, args) : args[0], + cache = memoized.cache; + + if (cache.has(key)) { + return cache.get(key); + } + var result = func.apply(this, args); + memoized.cache = cache.set(key, result) || cache; + return result; + }; + memoized.cache = new (memoize.Cache || MapCache); + return memoized; + } + + // Expose `MapCache`. + memoize.Cache = MapCache; + + /** + * Creates a function that negates the result of the predicate `func`. The + * `func` predicate is invoked with the `this` binding and arguments of the + * created function. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {Function} predicate The predicate to negate. + * @returns {Function} Returns the new negated function. + * @example + * + * function isEven(n) { + * return n % 2 == 0; + * } + * + * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven)); + * // => [1, 3, 5] + */ + function negate(predicate) { + if (typeof predicate != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + return function() { + var args = arguments; + switch (args.length) { + case 0: return !predicate.call(this); + case 1: return !predicate.call(this, args[0]); + case 2: return !predicate.call(this, args[0], args[1]); + case 3: return !predicate.call(this, args[0], args[1], args[2]); + } + return !predicate.apply(this, args); + }; + } + + /** + * Creates a function that is restricted to invoking `func` once. Repeat calls + * to the function return the value of the first invocation. The `func` is + * invoked with the `this` binding and arguments of the created function. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * var initialize = _.once(createApplication); + * initialize(); + * initialize(); + * // => `createApplication` is invoked once + */ + function once(func) { + return before(2, func); + } + + /** + * Creates a function that invokes `func` with its arguments transformed. + * + * @static + * @since 4.0.0 + * @memberOf _ + * @category Function + * @param {Function} func The function to wrap. + * @param {...(Function|Function[])} [transforms=[_.identity]] + * The argument transforms. + * @returns {Function} Returns the new function. + * @example + * + * function doubled(n) { + * return n * 2; + * } + * + * function square(n) { + * return n * n; + * } + * + * var func = _.overArgs(function(x, y) { + * return [x, y]; + * }, [square, doubled]); + * + * func(9, 3); + * // => [81, 6] + * + * func(10, 5); + * // => [100, 10] + */ + var overArgs = castRest(function(func, transforms) { + transforms = (transforms.length == 1 && isArray(transforms[0])) + ? arrayMap(transforms[0], baseUnary(getIteratee())) + : arrayMap(baseFlatten(transforms, 1), baseUnary(getIteratee())); + + var funcsLength = transforms.length; + return baseRest(function(args) { + var index = -1, + length = nativeMin(args.length, funcsLength); + + while (++index < length) { + args[index] = transforms[index].call(this, args[index]); + } + return apply(func, this, args); + }); + }); + + /** + * Creates a function that invokes `func` with `partials` prepended to the + * arguments it receives. This method is like `_.bind` except it does **not** + * alter the `this` binding. + * + * The `_.partial.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for partially applied arguments. + * + * **Note:** This method doesn't set the "length" property of partially + * applied functions. + * + * @static + * @memberOf _ + * @since 0.2.0 + * @category Function + * @param {Function} func The function to partially apply arguments to. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new partially applied function. + * @example + * + * function greet(greeting, name) { + * return greeting + ' ' + name; + * } + * + * var sayHelloTo = _.partial(greet, 'hello'); + * sayHelloTo('fred'); + * // => 'hello fred' + * + * // Partially applied with placeholders. + * var greetFred = _.partial(greet, _, 'fred'); + * greetFred('hi'); + * // => 'hi fred' + */ + var partial = baseRest(function(func, partials) { + var holders = replaceHolders(partials, getHolder(partial)); + return createWrap(func, WRAP_PARTIAL_FLAG, undefined, partials, holders); + }); + + /** + * This method is like `_.partial` except that partially applied arguments + * are appended to the arguments it receives. + * + * The `_.partialRight.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for partially applied arguments. + * + * **Note:** This method doesn't set the "length" property of partially + * applied functions. + * + * @static + * @memberOf _ + * @since 1.0.0 + * @category Function + * @param {Function} func The function to partially apply arguments to. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new partially applied function. + * @example + * + * function greet(greeting, name) { + * return greeting + ' ' + name; + * } + * + * var greetFred = _.partialRight(greet, 'fred'); + * greetFred('hi'); + * // => 'hi fred' + * + * // Partially applied with placeholders. + * var sayHelloTo = _.partialRight(greet, 'hello', _); + * sayHelloTo('fred'); + * // => 'hello fred' + */ + var partialRight = baseRest(function(func, partials) { + var holders = replaceHolders(partials, getHolder(partialRight)); + return createWrap(func, WRAP_PARTIAL_RIGHT_FLAG, undefined, partials, holders); + }); + + /** + * Creates a function that invokes `func` with arguments arranged according + * to the specified `indexes` where the argument value at the first index is + * provided as the first argument, the argument value at the second index is + * provided as the second argument, and so on. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {Function} func The function to rearrange arguments for. + * @param {...(number|number[])} indexes The arranged argument indexes. + * @returns {Function} Returns the new function. + * @example + * + * var rearged = _.rearg(function(a, b, c) { + * return [a, b, c]; + * }, [2, 0, 1]); + * + * rearged('b', 'c', 'a') + * // => ['a', 'b', 'c'] + */ + var rearg = flatRest(function(func, indexes) { + return createWrap(func, WRAP_REARG_FLAG, undefined, undefined, undefined, indexes); + }); + + /** + * Creates a function that invokes `func` with the `this` binding of the + * created function and arguments from `start` and beyond provided as + * an array. + * + * **Note:** This method is based on the + * [rest parameter](https://mdn.io/rest_parameters). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Function + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @returns {Function} Returns the new function. + * @example + * + * var say = _.rest(function(what, names) { + * return what + ' ' + _.initial(names).join(', ') + + * (_.size(names) > 1 ? ', & ' : '') + _.last(names); + * }); + * + * say('hello', 'fred', 'barney', 'pebbles'); + * // => 'hello fred, barney, & pebbles' + */ + function rest(func, start) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + start = start === undefined ? start : toInteger(start); + return baseRest(func, start); + } + + /** + * Creates a function that invokes `func` with the `this` binding of the + * create function and an array of arguments much like + * [`Function#apply`](http://www.ecma-international.org/ecma-262/7.0/#sec-function.prototype.apply). + * + * **Note:** This method is based on the + * [spread operator](https://mdn.io/spread_operator). + * + * @static + * @memberOf _ + * @since 3.2.0 + * @category Function + * @param {Function} func The function to spread arguments over. + * @param {number} [start=0] The start position of the spread. + * @returns {Function} Returns the new function. + * @example + * + * var say = _.spread(function(who, what) { + * return who + ' says ' + what; + * }); + * + * say(['fred', 'hello']); + * // => 'fred says hello' + * + * var numbers = Promise.all([ + * Promise.resolve(40), + * Promise.resolve(36) + * ]); + * + * numbers.then(_.spread(function(x, y) { + * return x + y; + * })); + * // => a Promise of 76 + */ + function spread(func, start) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + start = start == null ? 0 : nativeMax(toInteger(start), 0); + return baseRest(function(args) { + var array = args[start], + otherArgs = castSlice(args, 0, start); + + if (array) { + arrayPush(otherArgs, array); + } + return apply(func, this, otherArgs); + }); + } + + /** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed `func` invocations and a `flush` method to + * immediately invoke them. Provide `options` to indicate whether `func` + * should be invoked on the leading and/or trailing edge of the `wait` + * timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); + * jQuery(element).on('click', throttled); + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel); + */ + function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (isObject(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce(func, wait, { + 'leading': leading, + 'maxWait': wait, + 'trailing': trailing + }); + } + + /** + * Creates a function that accepts up to one argument, ignoring any + * additional arguments. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Function + * @param {Function} func The function to cap arguments for. + * @returns {Function} Returns the new capped function. + * @example + * + * _.map(['6', '8', '10'], _.unary(parseInt)); + * // => [6, 8, 10] + */ + function unary(func) { + return ary(func, 1); + } + + /** + * Creates a function that provides `value` to `wrapper` as its first + * argument. Any additional arguments provided to the function are appended + * to those provided to the `wrapper`. The wrapper is invoked with the `this` + * binding of the created function. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {*} value The value to wrap. + * @param {Function} [wrapper=identity] The wrapper function. + * @returns {Function} Returns the new function. + * @example + * + * var p = _.wrap(_.escape, function(func, text) { + * return '

' + func(text) + '

'; + * }); + * + * p('fred, barney, & pebbles'); + * // => '

fred, barney, & pebbles

' + */ + function wrap(value, wrapper) { + return partial(castFunction(wrapper), value); + } + + /*------------------------------------------------------------------------*/ + + /** + * Casts `value` as an array if it's not one. + * + * @static + * @memberOf _ + * @since 4.4.0 + * @category Lang + * @param {*} value The value to inspect. + * @returns {Array} Returns the cast array. + * @example + * + * _.castArray(1); + * // => [1] + * + * _.castArray({ 'a': 1 }); + * // => [{ 'a': 1 }] + * + * _.castArray('abc'); + * // => ['abc'] + * + * _.castArray(null); + * // => [null] + * + * _.castArray(undefined); + * // => [undefined] + * + * _.castArray(); + * // => [] + * + * var array = [1, 2, 3]; + * console.log(_.castArray(array) === array); + * // => true + */ + function castArray() { + if (!arguments.length) { + return []; + } + var value = arguments[0]; + return isArray(value) ? value : [value]; + } + + /** + * Creates a shallow clone of `value`. + * + * **Note:** This method is loosely based on the + * [structured clone algorithm](https://mdn.io/Structured_clone_algorithm) + * and supports cloning arrays, array buffers, booleans, date objects, maps, + * numbers, `Object` objects, regexes, sets, strings, symbols, and typed + * arrays. The own enumerable properties of `arguments` objects are cloned + * as plain objects. An empty object is returned for uncloneable values such + * as error objects, functions, DOM nodes, and WeakMaps. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to clone. + * @returns {*} Returns the cloned value. + * @see _.cloneDeep + * @example + * + * var objects = [{ 'a': 1 }, { 'b': 2 }]; + * + * var shallow = _.clone(objects); + * console.log(shallow[0] === objects[0]); + * // => true + */ + function clone(value) { + return baseClone(value, CLONE_SYMBOLS_FLAG); + } + + /** + * This method is like `_.clone` except that it accepts `customizer` which + * is invoked to produce the cloned value. If `customizer` returns `undefined`, + * cloning is handled by the method instead. The `customizer` is invoked with + * up to four arguments; (value [, index|key, object, stack]). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to clone. + * @param {Function} [customizer] The function to customize cloning. + * @returns {*} Returns the cloned value. + * @see _.cloneDeepWith + * @example + * + * function customizer(value) { + * if (_.isElement(value)) { + * return value.cloneNode(false); + * } + * } + * + * var el = _.cloneWith(document.body, customizer); + * + * console.log(el === document.body); + * // => false + * console.log(el.nodeName); + * // => 'BODY' + * console.log(el.childNodes.length); + * // => 0 + */ + function cloneWith(value, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return baseClone(value, CLONE_SYMBOLS_FLAG, customizer); + } + + /** + * This method is like `_.clone` except that it recursively clones `value`. + * + * @static + * @memberOf _ + * @since 1.0.0 + * @category Lang + * @param {*} value The value to recursively clone. + * @returns {*} Returns the deep cloned value. + * @see _.clone + * @example + * + * var objects = [{ 'a': 1 }, { 'b': 2 }]; + * + * var deep = _.cloneDeep(objects); + * console.log(deep[0] === objects[0]); + * // => false + */ + function cloneDeep(value) { + return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG); + } + + /** + * This method is like `_.cloneWith` except that it recursively clones `value`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to recursively clone. + * @param {Function} [customizer] The function to customize cloning. + * @returns {*} Returns the deep cloned value. + * @see _.cloneWith + * @example + * + * function customizer(value) { + * if (_.isElement(value)) { + * return value.cloneNode(true); + * } + * } + * + * var el = _.cloneDeepWith(document.body, customizer); + * + * console.log(el === document.body); + * // => false + * console.log(el.nodeName); + * // => 'BODY' + * console.log(el.childNodes.length); + * // => 20 + */ + function cloneDeepWith(value, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG, customizer); + } + + /** + * Checks if `object` conforms to `source` by invoking the predicate + * properties of `source` with the corresponding property values of `object`. + * + * **Note:** This method is equivalent to `_.conforms` when `source` is + * partially applied. + * + * @static + * @memberOf _ + * @since 4.14.0 + * @category Lang + * @param {Object} object The object to inspect. + * @param {Object} source The object of property predicates to conform to. + * @returns {boolean} Returns `true` if `object` conforms, else `false`. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * + * _.conformsTo(object, { 'b': function(n) { return n > 1; } }); + * // => true + * + * _.conformsTo(object, { 'b': function(n) { return n > 2; } }); + * // => false + */ + function conformsTo(object, source) { + return source == null || baseConformsTo(object, source, keys(source)); + } + + /** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ + function eq(value, other) { + return value === other || (value !== value && other !== other); + } + + /** + * Checks if `value` is greater than `other`. + * + * @static + * @memberOf _ + * @since 3.9.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is greater than `other`, + * else `false`. + * @see _.lt + * @example + * + * _.gt(3, 1); + * // => true + * + * _.gt(3, 3); + * // => false + * + * _.gt(1, 3); + * // => false + */ + var gt = createRelationalOperation(baseGt); + + /** + * Checks if `value` is greater than or equal to `other`. + * + * @static + * @memberOf _ + * @since 3.9.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is greater than or equal to + * `other`, else `false`. + * @see _.lte + * @example + * + * _.gte(3, 1); + * // => true + * + * _.gte(3, 3); + * // => true + * + * _.gte(1, 3); + * // => false + */ + var gte = createRelationalOperation(function(value, other) { + return value >= other; + }); + + /** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) { + return isObjectLike(value) && hasOwnProperty.call(value, 'callee') && + !propertyIsEnumerable.call(value, 'callee'); + }; + + /** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ + var isArray = Array.isArray; + + /** + * Checks if `value` is classified as an `ArrayBuffer` object. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`. + * @example + * + * _.isArrayBuffer(new ArrayBuffer(2)); + * // => true + * + * _.isArrayBuffer(new Array(2)); + * // => false + */ + var isArrayBuffer = nodeIsArrayBuffer ? baseUnary(nodeIsArrayBuffer) : baseIsArrayBuffer; + + /** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ + function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); + } + + /** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ + function isArrayLikeObject(value) { + return isObjectLike(value) && isArrayLike(value); + } + + /** + * Checks if `value` is classified as a boolean primitive or object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a boolean, else `false`. + * @example + * + * _.isBoolean(false); + * // => true + * + * _.isBoolean(null); + * // => false + */ + function isBoolean(value) { + return value === true || value === false || + (isObjectLike(value) && baseGetTag(value) == boolTag); + } + + /** + * Checks if `value` is a buffer. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a buffer, else `false`. + * @example + * + * _.isBuffer(new Buffer(2)); + * // => true + * + * _.isBuffer(new Uint8Array(2)); + * // => false + */ + var isBuffer = nativeIsBuffer || stubFalse; + + /** + * Checks if `value` is classified as a `Date` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a date object, else `false`. + * @example + * + * _.isDate(new Date); + * // => true + * + * _.isDate('Mon April 23 2012'); + * // => false + */ + var isDate = nodeIsDate ? baseUnary(nodeIsDate) : baseIsDate; + + /** + * Checks if `value` is likely a DOM element. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`. + * @example + * + * _.isElement(document.body); + * // => true + * + * _.isElement(''); + * // => false + */ + function isElement(value) { + return isObjectLike(value) && value.nodeType === 1 && !isPlainObject(value); + } + + /** + * Checks if `value` is an empty object, collection, map, or set. + * + * Objects are considered empty if they have no own enumerable string keyed + * properties. + * + * Array-like values such as `arguments` objects, arrays, buffers, strings, or + * jQuery-like collections are considered empty if they have a `length` of `0`. + * Similarly, maps and sets are considered empty if they have a `size` of `0`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is empty, else `false`. + * @example + * + * _.isEmpty(null); + * // => true + * + * _.isEmpty(true); + * // => true + * + * _.isEmpty(1); + * // => true + * + * _.isEmpty([1, 2, 3]); + * // => false + * + * _.isEmpty({ 'a': 1 }); + * // => false + */ + function isEmpty(value) { + if (value == null) { + return true; + } + if (isArrayLike(value) && + (isArray(value) || typeof value == 'string' || typeof value.splice == 'function' || + isBuffer(value) || isTypedArray(value) || isArguments(value))) { + return !value.length; + } + var tag = getTag(value); + if (tag == mapTag || tag == setTag) { + return !value.size; + } + if (isPrototype(value)) { + return !baseKeys(value).length; + } + for (var key in value) { + if (hasOwnProperty.call(value, key)) { + return false; + } + } + return true; + } + + /** + * Performs a deep comparison between two values to determine if they are + * equivalent. + * + * **Note:** This method supports comparing arrays, array buffers, booleans, + * date objects, error objects, maps, numbers, `Object` objects, regexes, + * sets, strings, symbols, and typed arrays. `Object` objects are compared + * by their own, not inherited, enumerable properties. Functions and DOM + * nodes are compared by strict equality, i.e. `===`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.isEqual(object, other); + * // => true + * + * object === other; + * // => false + */ + function isEqual(value, other) { + return baseIsEqual(value, other); + } + + /** + * This method is like `_.isEqual` except that it accepts `customizer` which + * is invoked to compare values. If `customizer` returns `undefined`, comparisons + * are handled by the method instead. The `customizer` is invoked with up to + * six arguments: (objValue, othValue [, index|key, object, other, stack]). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @param {Function} [customizer] The function to customize comparisons. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * function isGreeting(value) { + * return /^h(?:i|ello)$/.test(value); + * } + * + * function customizer(objValue, othValue) { + * if (isGreeting(objValue) && isGreeting(othValue)) { + * return true; + * } + * } + * + * var array = ['hello', 'goodbye']; + * var other = ['hi', 'goodbye']; + * + * _.isEqualWith(array, other, customizer); + * // => true + */ + function isEqualWith(value, other, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + var result = customizer ? customizer(value, other) : undefined; + return result === undefined ? baseIsEqual(value, other, undefined, customizer) : !!result; + } + + /** + * Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`, + * `SyntaxError`, `TypeError`, or `URIError` object. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an error object, else `false`. + * @example + * + * _.isError(new Error); + * // => true + * + * _.isError(Error); + * // => false + */ + function isError(value) { + if (!isObjectLike(value)) { + return false; + } + var tag = baseGetTag(value); + return tag == errorTag || tag == domExcTag || + (typeof value.message == 'string' && typeof value.name == 'string' && !isPlainObject(value)); + } + + /** + * Checks if `value` is a finite primitive number. + * + * **Note:** This method is based on + * [`Number.isFinite`](https://mdn.io/Number/isFinite). + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a finite number, else `false`. + * @example + * + * _.isFinite(3); + * // => true + * + * _.isFinite(Number.MIN_VALUE); + * // => true + * + * _.isFinite(Infinity); + * // => false + * + * _.isFinite('3'); + * // => false + */ + function isFinite(value) { + return typeof value == 'number' && nativeIsFinite(value); + } + + /** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ + function isFunction(value) { + if (!isObject(value)) { + return false; + } + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 9 which returns 'object' for typed arrays and other constructors. + var tag = baseGetTag(value); + return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag; + } + + /** + * Checks if `value` is an integer. + * + * **Note:** This method is based on + * [`Number.isInteger`](https://mdn.io/Number/isInteger). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an integer, else `false`. + * @example + * + * _.isInteger(3); + * // => true + * + * _.isInteger(Number.MIN_VALUE); + * // => false + * + * _.isInteger(Infinity); + * // => false + * + * _.isInteger('3'); + * // => false + */ + function isInteger(value) { + return typeof value == 'number' && value == toInteger(value); + } + + /** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ + function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike(value) { + return value != null && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `Map` object. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a map, else `false`. + * @example + * + * _.isMap(new Map); + * // => true + * + * _.isMap(new WeakMap); + * // => false + */ + var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap; + + /** + * Performs a partial deep comparison between `object` and `source` to + * determine if `object` contains equivalent property values. + * + * **Note:** This method is equivalent to `_.matches` when `source` is + * partially applied. + * + * Partial comparisons will match empty array and empty object `source` + * values against any array or object value, respectively. See `_.isEqual` + * for a list of supported value comparisons. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {Object} object The object to inspect. + * @param {Object} source The object of property values to match. + * @returns {boolean} Returns `true` if `object` is a match, else `false`. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * + * _.isMatch(object, { 'b': 2 }); + * // => true + * + * _.isMatch(object, { 'b': 1 }); + * // => false + */ + function isMatch(object, source) { + return object === source || baseIsMatch(object, source, getMatchData(source)); + } + + /** + * This method is like `_.isMatch` except that it accepts `customizer` which + * is invoked to compare values. If `customizer` returns `undefined`, comparisons + * are handled by the method instead. The `customizer` is invoked with five + * arguments: (objValue, srcValue, index|key, object, source). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {Object} object The object to inspect. + * @param {Object} source The object of property values to match. + * @param {Function} [customizer] The function to customize comparisons. + * @returns {boolean} Returns `true` if `object` is a match, else `false`. + * @example + * + * function isGreeting(value) { + * return /^h(?:i|ello)$/.test(value); + * } + * + * function customizer(objValue, srcValue) { + * if (isGreeting(objValue) && isGreeting(srcValue)) { + * return true; + * } + * } + * + * var object = { 'greeting': 'hello' }; + * var source = { 'greeting': 'hi' }; + * + * _.isMatchWith(object, source, customizer); + * // => true + */ + function isMatchWith(object, source, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return baseIsMatch(object, source, getMatchData(source), customizer); + } + + /** + * Checks if `value` is `NaN`. + * + * **Note:** This method is based on + * [`Number.isNaN`](https://mdn.io/Number/isNaN) and is not the same as + * global [`isNaN`](https://mdn.io/isNaN) which returns `true` for + * `undefined` and other non-number values. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. + * @example + * + * _.isNaN(NaN); + * // => true + * + * _.isNaN(new Number(NaN)); + * // => true + * + * isNaN(undefined); + * // => true + * + * _.isNaN(undefined); + * // => false + */ + function isNaN(value) { + // An `NaN` primitive is the only value that is not equal to itself. + // Perform the `toStringTag` check first to avoid errors with some + // ActiveX objects in IE. + return isNumber(value) && value != +value; + } + + /** + * Checks if `value` is a pristine native function. + * + * **Note:** This method can't reliably detect native functions in the presence + * of the core-js package because core-js circumvents this kind of detection. + * Despite multiple requests, the core-js maintainer has made it clear: any + * attempt to fix the detection will be obstructed. As a result, we're left + * with little choice but to throw an error. Unfortunately, this also affects + * packages, like [babel-polyfill](https://www.npmjs.com/package/babel-polyfill), + * which rely on core-js. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + * @example + * + * _.isNative(Array.prototype.push); + * // => true + * + * _.isNative(_); + * // => false + */ + function isNative(value) { + if (isMaskable(value)) { + throw new Error(CORE_ERROR_TEXT); + } + return baseIsNative(value); + } + + /** + * Checks if `value` is `null`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `null`, else `false`. + * @example + * + * _.isNull(null); + * // => true + * + * _.isNull(void 0); + * // => false + */ + function isNull(value) { + return value === null; + } + + /** + * Checks if `value` is `null` or `undefined`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is nullish, else `false`. + * @example + * + * _.isNil(null); + * // => true + * + * _.isNil(void 0); + * // => true + * + * _.isNil(NaN); + * // => false + */ + function isNil(value) { + return value == null; + } + + /** + * Checks if `value` is classified as a `Number` primitive or object. + * + * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are + * classified as numbers, use the `_.isFinite` method. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a number, else `false`. + * @example + * + * _.isNumber(3); + * // => true + * + * _.isNumber(Number.MIN_VALUE); + * // => true + * + * _.isNumber(Infinity); + * // => true + * + * _.isNumber('3'); + * // => false + */ + function isNumber(value) { + return typeof value == 'number' || + (isObjectLike(value) && baseGetTag(value) == numberTag); + } + + /** + * Checks if `value` is a plain object, that is, an object created by the + * `Object` constructor or one with a `[[Prototype]]` of `null`. + * + * @static + * @memberOf _ + * @since 0.8.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. + * @example + * + * function Foo() { + * this.a = 1; + * } + * + * _.isPlainObject(new Foo); + * // => false + * + * _.isPlainObject([1, 2, 3]); + * // => false + * + * _.isPlainObject({ 'x': 0, 'y': 0 }); + * // => true + * + * _.isPlainObject(Object.create(null)); + * // => true + */ + function isPlainObject(value) { + if (!isObjectLike(value) || baseGetTag(value) != objectTag) { + return false; + } + var proto = getPrototype(value); + if (proto === null) { + return true; + } + var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor; + return typeof Ctor == 'function' && Ctor instanceof Ctor && + funcToString.call(Ctor) == objectCtorString; + } + + /** + * Checks if `value` is classified as a `RegExp` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a regexp, else `false`. + * @example + * + * _.isRegExp(/abc/); + * // => true + * + * _.isRegExp('/abc/'); + * // => false + */ + var isRegExp = nodeIsRegExp ? baseUnary(nodeIsRegExp) : baseIsRegExp; + + /** + * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754 + * double precision number which isn't the result of a rounded unsafe integer. + * + * **Note:** This method is based on + * [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`. + * @example + * + * _.isSafeInteger(3); + * // => true + * + * _.isSafeInteger(Number.MIN_VALUE); + * // => false + * + * _.isSafeInteger(Infinity); + * // => false + * + * _.isSafeInteger('3'); + * // => false + */ + function isSafeInteger(value) { + return isInteger(value) && value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER; + } + + /** + * Checks if `value` is classified as a `Set` object. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a set, else `false`. + * @example + * + * _.isSet(new Set); + * // => true + * + * _.isSet(new WeakSet); + * // => false + */ + var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet; + + /** + * Checks if `value` is classified as a `String` primitive or object. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a string, else `false`. + * @example + * + * _.isString('abc'); + * // => true + * + * _.isString(1); + * // => false + */ + function isString(value) { + return typeof value == 'string' || + (!isArray(value) && isObjectLike(value) && baseGetTag(value) == stringTag); + } + + /** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ + function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && baseGetTag(value) == symbolTag); + } + + /** + * Checks if `value` is classified as a typed array. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a typed array, else `false`. + * @example + * + * _.isTypedArray(new Uint8Array); + * // => true + * + * _.isTypedArray([]); + * // => false + */ + var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray; + + /** + * Checks if `value` is `undefined`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`. + * @example + * + * _.isUndefined(void 0); + * // => true + * + * _.isUndefined(null); + * // => false + */ + function isUndefined(value) { + return value === undefined; + } + + /** + * Checks if `value` is classified as a `WeakMap` object. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a weak map, else `false`. + * @example + * + * _.isWeakMap(new WeakMap); + * // => true + * + * _.isWeakMap(new Map); + * // => false + */ + function isWeakMap(value) { + return isObjectLike(value) && getTag(value) == weakMapTag; + } + + /** + * Checks if `value` is classified as a `WeakSet` object. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a weak set, else `false`. + * @example + * + * _.isWeakSet(new WeakSet); + * // => true + * + * _.isWeakSet(new Set); + * // => false + */ + function isWeakSet(value) { + return isObjectLike(value) && baseGetTag(value) == weakSetTag; + } + + /** + * Checks if `value` is less than `other`. + * + * @static + * @memberOf _ + * @since 3.9.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is less than `other`, + * else `false`. + * @see _.gt + * @example + * + * _.lt(1, 3); + * // => true + * + * _.lt(3, 3); + * // => false + * + * _.lt(3, 1); + * // => false + */ + var lt = createRelationalOperation(baseLt); + + /** + * Checks if `value` is less than or equal to `other`. + * + * @static + * @memberOf _ + * @since 3.9.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is less than or equal to + * `other`, else `false`. + * @see _.gte + * @example + * + * _.lte(1, 3); + * // => true + * + * _.lte(3, 3); + * // => true + * + * _.lte(3, 1); + * // => false + */ + var lte = createRelationalOperation(function(value, other) { + return value <= other; + }); + + /** + * Converts `value` to an array. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to convert. + * @returns {Array} Returns the converted array. + * @example + * + * _.toArray({ 'a': 1, 'b': 2 }); + * // => [1, 2] + * + * _.toArray('abc'); + * // => ['a', 'b', 'c'] + * + * _.toArray(1); + * // => [] + * + * _.toArray(null); + * // => [] + */ + function toArray(value) { + if (!value) { + return []; + } + if (isArrayLike(value)) { + return isString(value) ? stringToArray(value) : copyArray(value); + } + if (symIterator && value[symIterator]) { + return iteratorToArray(value[symIterator]()); + } + var tag = getTag(value), + func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values); + + return func(value); + } + + /** + * Converts `value` to a finite number. + * + * @static + * @memberOf _ + * @since 4.12.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {number} Returns the converted number. + * @example + * + * _.toFinite(3.2); + * // => 3.2 + * + * _.toFinite(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toFinite(Infinity); + * // => 1.7976931348623157e+308 + * + * _.toFinite('3.2'); + * // => 3.2 + */ + function toFinite(value) { + if (!value) { + return value === 0 ? value : 0; + } + value = toNumber(value); + if (value === INFINITY || value === -INFINITY) { + var sign = (value < 0 ? -1 : 1); + return sign * MAX_INTEGER; + } + return value === value ? value : 0; + } + + /** + * Converts `value` to an integer. + * + * **Note:** This method is loosely based on + * [`ToInteger`](http://www.ecma-international.org/ecma-262/7.0/#sec-tointeger). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {number} Returns the converted integer. + * @example + * + * _.toInteger(3.2); + * // => 3 + * + * _.toInteger(Number.MIN_VALUE); + * // => 0 + * + * _.toInteger(Infinity); + * // => 1.7976931348623157e+308 + * + * _.toInteger('3.2'); + * // => 3 + */ + function toInteger(value) { + var result = toFinite(value), + remainder = result % 1; + + return result === result ? (remainder ? result - remainder : result) : 0; + } + + /** + * Converts `value` to an integer suitable for use as the length of an + * array-like object. + * + * **Note:** This method is based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {number} Returns the converted integer. + * @example + * + * _.toLength(3.2); + * // => 3 + * + * _.toLength(Number.MIN_VALUE); + * // => 0 + * + * _.toLength(Infinity); + * // => 4294967295 + * + * _.toLength('3.2'); + * // => 3 + */ + function toLength(value) { + return value ? baseClamp(toInteger(value), 0, MAX_ARRAY_LENGTH) : 0; + } + + /** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ + function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim, ''); + var isBinary = reIsBinary.test(value); + return (isBinary || reIsOctal.test(value)) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex.test(value) ? NAN : +value); + } + + /** + * Converts `value` to a plain object flattening inherited enumerable string + * keyed properties of `value` to own properties of the plain object. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {Object} Returns the converted plain object. + * @example + * + * function Foo() { + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.assign({ 'a': 1 }, new Foo); + * // => { 'a': 1, 'b': 2 } + * + * _.assign({ 'a': 1 }, _.toPlainObject(new Foo)); + * // => { 'a': 1, 'b': 2, 'c': 3 } + */ + function toPlainObject(value) { + return copyObject(value, keysIn(value)); + } + + /** + * Converts `value` to a safe integer. A safe integer can be compared and + * represented correctly. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {number} Returns the converted integer. + * @example + * + * _.toSafeInteger(3.2); + * // => 3 + * + * _.toSafeInteger(Number.MIN_VALUE); + * // => 0 + * + * _.toSafeInteger(Infinity); + * // => 9007199254740991 + * + * _.toSafeInteger('3.2'); + * // => 3 + */ + function toSafeInteger(value) { + return value + ? baseClamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER) + : (value === 0 ? value : 0); + } + + /** + * Converts `value` to a string. An empty string is returned for `null` + * and `undefined` values. The sign of `-0` is preserved. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + * @example + * + * _.toString(null); + * // => '' + * + * _.toString(-0); + * // => '-0' + * + * _.toString([1, 2, 3]); + * // => '1,2,3' + */ + function toString(value) { + return value == null ? '' : baseToString(value); + } + + /*------------------------------------------------------------------------*/ + + /** + * Assigns own enumerable string keyed properties of source objects to the + * destination object. Source objects are applied from left to right. + * Subsequent sources overwrite property assignments of previous sources. + * + * **Note:** This method mutates `object` and is loosely based on + * [`Object.assign`](https://mdn.io/Object/assign). + * + * @static + * @memberOf _ + * @since 0.10.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @see _.assignIn + * @example + * + * function Foo() { + * this.a = 1; + * } + * + * function Bar() { + * this.c = 3; + * } + * + * Foo.prototype.b = 2; + * Bar.prototype.d = 4; + * + * _.assign({ 'a': 0 }, new Foo, new Bar); + * // => { 'a': 1, 'c': 3 } + */ + var assign = createAssigner(function(object, source) { + if (isPrototype(source) || isArrayLike(source)) { + copyObject(source, keys(source), object); + return; + } + for (var key in source) { + if (hasOwnProperty.call(source, key)) { + assignValue(object, key, source[key]); + } + } + }); + + /** + * This method is like `_.assign` except that it iterates over own and + * inherited source properties. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @alias extend + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @see _.assign + * @example + * + * function Foo() { + * this.a = 1; + * } + * + * function Bar() { + * this.c = 3; + * } + * + * Foo.prototype.b = 2; + * Bar.prototype.d = 4; + * + * _.assignIn({ 'a': 0 }, new Foo, new Bar); + * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4 } + */ + var assignIn = createAssigner(function(object, source) { + copyObject(source, keysIn(source), object); + }); + + /** + * This method is like `_.assignIn` except that it accepts `customizer` + * which is invoked to produce the assigned values. If `customizer` returns + * `undefined`, assignment is handled by the method instead. The `customizer` + * is invoked with five arguments: (objValue, srcValue, key, object, source). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @alias extendWith + * @category Object + * @param {Object} object The destination object. + * @param {...Object} sources The source objects. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @see _.assignWith + * @example + * + * function customizer(objValue, srcValue) { + * return _.isUndefined(objValue) ? srcValue : objValue; + * } + * + * var defaults = _.partialRight(_.assignInWith, customizer); + * + * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); + * // => { 'a': 1, 'b': 2 } + */ + var assignInWith = createAssigner(function(object, source, srcIndex, customizer) { + copyObject(source, keysIn(source), object, customizer); + }); + + /** + * This method is like `_.assign` except that it accepts `customizer` + * which is invoked to produce the assigned values. If `customizer` returns + * `undefined`, assignment is handled by the method instead. The `customizer` + * is invoked with five arguments: (objValue, srcValue, key, object, source). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} sources The source objects. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @see _.assignInWith + * @example + * + * function customizer(objValue, srcValue) { + * return _.isUndefined(objValue) ? srcValue : objValue; + * } + * + * var defaults = _.partialRight(_.assignWith, customizer); + * + * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); + * // => { 'a': 1, 'b': 2 } + */ + var assignWith = createAssigner(function(object, source, srcIndex, customizer) { + copyObject(source, keys(source), object, customizer); + }); + + /** + * Creates an array of values corresponding to `paths` of `object`. + * + * @static + * @memberOf _ + * @since 1.0.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {...(string|string[])} [paths] The property paths to pick. + * @returns {Array} Returns the picked values. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] }; + * + * _.at(object, ['a[0].b.c', 'a[1]']); + * // => [3, 4] + */ + var at = flatRest(baseAt); + + /** + * Creates an object that inherits from the `prototype` object. If a + * `properties` object is given, its own enumerable string keyed properties + * are assigned to the created object. + * + * @static + * @memberOf _ + * @since 2.3.0 + * @category Object + * @param {Object} prototype The object to inherit from. + * @param {Object} [properties] The properties to assign to the object. + * @returns {Object} Returns the new object. + * @example + * + * function Shape() { + * this.x = 0; + * this.y = 0; + * } + * + * function Circle() { + * Shape.call(this); + * } + * + * Circle.prototype = _.create(Shape.prototype, { + * 'constructor': Circle + * }); + * + * var circle = new Circle; + * circle instanceof Circle; + * // => true + * + * circle instanceof Shape; + * // => true + */ + function create(prototype, properties) { + var result = baseCreate(prototype); + return properties == null ? result : baseAssign(result, properties); + } + + /** + * Assigns own and inherited enumerable string keyed properties of source + * objects to the destination object for all destination properties that + * resolve to `undefined`. Source objects are applied from left to right. + * Once a property is set, additional values of the same property are ignored. + * + * **Note:** This method mutates `object`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @see _.defaultsDeep + * @example + * + * _.defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); + * // => { 'a': 1, 'b': 2 } + */ + var defaults = baseRest(function(object, sources) { + object = Object(object); + + var index = -1; + var length = sources.length; + var guard = length > 2 ? sources[2] : undefined; + + if (guard && isIterateeCall(sources[0], sources[1], guard)) { + length = 1; + } + + while (++index < length) { + var source = sources[index]; + var props = keysIn(source); + var propsIndex = -1; + var propsLength = props.length; + + while (++propsIndex < propsLength) { + var key = props[propsIndex]; + var value = object[key]; + + if (value === undefined || + (eq(value, objectProto[key]) && !hasOwnProperty.call(object, key))) { + object[key] = source[key]; + } + } + } + + return object; + }); + + /** + * This method is like `_.defaults` except that it recursively assigns + * default properties. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 3.10.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @see _.defaults + * @example + * + * _.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } }); + * // => { 'a': { 'b': 2, 'c': 3 } } + */ + var defaultsDeep = baseRest(function(args) { + args.push(undefined, customDefaultsMerge); + return apply(mergeWith, undefined, args); + }); + + /** + * This method is like `_.find` except that it returns the key of the first + * element `predicate` returns truthy for instead of the element itself. + * + * @static + * @memberOf _ + * @since 1.1.0 + * @category Object + * @param {Object} object The object to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {string|undefined} Returns the key of the matched element, + * else `undefined`. + * @example + * + * var users = { + * 'barney': { 'age': 36, 'active': true }, + * 'fred': { 'age': 40, 'active': false }, + * 'pebbles': { 'age': 1, 'active': true } + * }; + * + * _.findKey(users, function(o) { return o.age < 40; }); + * // => 'barney' (iteration order is not guaranteed) + * + * // The `_.matches` iteratee shorthand. + * _.findKey(users, { 'age': 1, 'active': true }); + * // => 'pebbles' + * + * // The `_.matchesProperty` iteratee shorthand. + * _.findKey(users, ['active', false]); + * // => 'fred' + * + * // The `_.property` iteratee shorthand. + * _.findKey(users, 'active'); + * // => 'barney' + */ + function findKey(object, predicate) { + return baseFindKey(object, getIteratee(predicate, 3), baseForOwn); + } + + /** + * This method is like `_.findKey` except that it iterates over elements of + * a collection in the opposite order. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Object + * @param {Object} object The object to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {string|undefined} Returns the key of the matched element, + * else `undefined`. + * @example + * + * var users = { + * 'barney': { 'age': 36, 'active': true }, + * 'fred': { 'age': 40, 'active': false }, + * 'pebbles': { 'age': 1, 'active': true } + * }; + * + * _.findLastKey(users, function(o) { return o.age < 40; }); + * // => returns 'pebbles' assuming `_.findKey` returns 'barney' + * + * // The `_.matches` iteratee shorthand. + * _.findLastKey(users, { 'age': 36, 'active': true }); + * // => 'barney' + * + * // The `_.matchesProperty` iteratee shorthand. + * _.findLastKey(users, ['active', false]); + * // => 'fred' + * + * // The `_.property` iteratee shorthand. + * _.findLastKey(users, 'active'); + * // => 'pebbles' + */ + function findLastKey(object, predicate) { + return baseFindKey(object, getIteratee(predicate, 3), baseForOwnRight); + } + + /** + * Iterates over own and inherited enumerable string keyed properties of an + * object and invokes `iteratee` for each property. The iteratee is invoked + * with three arguments: (value, key, object). Iteratee functions may exit + * iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @since 0.3.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns `object`. + * @see _.forInRight + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forIn(new Foo, function(value, key) { + * console.log(key); + * }); + * // => Logs 'a', 'b', then 'c' (iteration order is not guaranteed). + */ + function forIn(object, iteratee) { + return object == null + ? object + : baseFor(object, getIteratee(iteratee, 3), keysIn); + } + + /** + * This method is like `_.forIn` except that it iterates over properties of + * `object` in the opposite order. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns `object`. + * @see _.forIn + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forInRight(new Foo, function(value, key) { + * console.log(key); + * }); + * // => Logs 'c', 'b', then 'a' assuming `_.forIn` logs 'a', 'b', then 'c'. + */ + function forInRight(object, iteratee) { + return object == null + ? object + : baseForRight(object, getIteratee(iteratee, 3), keysIn); + } + + /** + * Iterates over own enumerable string keyed properties of an object and + * invokes `iteratee` for each property. The iteratee is invoked with three + * arguments: (value, key, object). Iteratee functions may exit iteration + * early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @since 0.3.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns `object`. + * @see _.forOwnRight + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forOwn(new Foo, function(value, key) { + * console.log(key); + * }); + * // => Logs 'a' then 'b' (iteration order is not guaranteed). + */ + function forOwn(object, iteratee) { + return object && baseForOwn(object, getIteratee(iteratee, 3)); + } + + /** + * This method is like `_.forOwn` except that it iterates over properties of + * `object` in the opposite order. + * + * @static + * @memberOf _ + * @since 2.0.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns `object`. + * @see _.forOwn + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forOwnRight(new Foo, function(value, key) { + * console.log(key); + * }); + * // => Logs 'b' then 'a' assuming `_.forOwn` logs 'a' then 'b'. + */ + function forOwnRight(object, iteratee) { + return object && baseForOwnRight(object, getIteratee(iteratee, 3)); + } + + /** + * Creates an array of function property names from own enumerable properties + * of `object`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to inspect. + * @returns {Array} Returns the function names. + * @see _.functionsIn + * @example + * + * function Foo() { + * this.a = _.constant('a'); + * this.b = _.constant('b'); + * } + * + * Foo.prototype.c = _.constant('c'); + * + * _.functions(new Foo); + * // => ['a', 'b'] + */ + function functions(object) { + return object == null ? [] : baseFunctions(object, keys(object)); + } + + /** + * Creates an array of function property names from own and inherited + * enumerable properties of `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to inspect. + * @returns {Array} Returns the function names. + * @see _.functions + * @example + * + * function Foo() { + * this.a = _.constant('a'); + * this.b = _.constant('b'); + * } + * + * Foo.prototype.c = _.constant('c'); + * + * _.functionsIn(new Foo); + * // => ['a', 'b', 'c'] + */ + function functionsIn(object) { + return object == null ? [] : baseFunctions(object, keysIn(object)); + } + + /** + * Gets the value at `path` of `object`. If the resolved value is + * `undefined`, the `defaultValue` is returned in its place. + * + * @static + * @memberOf _ + * @since 3.7.0 + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to get. + * @param {*} [defaultValue] The value returned for `undefined` resolved values. + * @returns {*} Returns the resolved value. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }] }; + * + * _.get(object, 'a[0].b.c'); + * // => 3 + * + * _.get(object, ['a', '0', 'b', 'c']); + * // => 3 + * + * _.get(object, 'a.b.c', 'default'); + * // => 'default' + */ + function get(object, path, defaultValue) { + var result = object == null ? undefined : baseGet(object, path); + return result === undefined ? defaultValue : result; + } + + /** + * Checks if `path` is a direct property of `object`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @returns {boolean} Returns `true` if `path` exists, else `false`. + * @example + * + * var object = { 'a': { 'b': 2 } }; + * var other = _.create({ 'a': _.create({ 'b': 2 }) }); + * + * _.has(object, 'a'); + * // => true + * + * _.has(object, 'a.b'); + * // => true + * + * _.has(object, ['a', 'b']); + * // => true + * + * _.has(other, 'a'); + * // => false + */ + function has(object, path) { + return object != null && hasPath(object, path, baseHas); + } + + /** + * Checks if `path` is a direct or inherited property of `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @returns {boolean} Returns `true` if `path` exists, else `false`. + * @example + * + * var object = _.create({ 'a': _.create({ 'b': 2 }) }); + * + * _.hasIn(object, 'a'); + * // => true + * + * _.hasIn(object, 'a.b'); + * // => true + * + * _.hasIn(object, ['a', 'b']); + * // => true + * + * _.hasIn(object, 'b'); + * // => false + */ + function hasIn(object, path) { + return object != null && hasPath(object, path, baseHasIn); + } + + /** + * Creates an object composed of the inverted keys and values of `object`. + * If `object` contains duplicate values, subsequent values overwrite + * property assignments of previous values. + * + * @static + * @memberOf _ + * @since 0.7.0 + * @category Object + * @param {Object} object The object to invert. + * @returns {Object} Returns the new inverted object. + * @example + * + * var object = { 'a': 1, 'b': 2, 'c': 1 }; + * + * _.invert(object); + * // => { '1': 'c', '2': 'b' } + */ + var invert = createInverter(function(result, value, key) { + if (value != null && + typeof value.toString != 'function') { + value = nativeObjectToString.call(value); + } + + result[value] = key; + }, constant(identity)); + + /** + * This method is like `_.invert` except that the inverted object is generated + * from the results of running each element of `object` thru `iteratee`. The + * corresponding inverted value of each inverted key is an array of keys + * responsible for generating the inverted value. The iteratee is invoked + * with one argument: (value). + * + * @static + * @memberOf _ + * @since 4.1.0 + * @category Object + * @param {Object} object The object to invert. + * @param {Function} [iteratee=_.identity] The iteratee invoked per element. + * @returns {Object} Returns the new inverted object. + * @example + * + * var object = { 'a': 1, 'b': 2, 'c': 1 }; + * + * _.invertBy(object); + * // => { '1': ['a', 'c'], '2': ['b'] } + * + * _.invertBy(object, function(value) { + * return 'group' + value; + * }); + * // => { 'group1': ['a', 'c'], 'group2': ['b'] } + */ + var invertBy = createInverter(function(result, value, key) { + if (value != null && + typeof value.toString != 'function') { + value = nativeObjectToString.call(value); + } + + if (hasOwnProperty.call(result, value)) { + result[value].push(key); + } else { + result[value] = [key]; + } + }, getIteratee); + + /** + * Invokes the method at `path` of `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path of the method to invoke. + * @param {...*} [args] The arguments to invoke the method with. + * @returns {*} Returns the result of the invoked method. + * @example + * + * var object = { 'a': [{ 'b': { 'c': [1, 2, 3, 4] } }] }; + * + * _.invoke(object, 'a[0].b.c.slice', 1, 3); + * // => [2, 3] + */ + var invoke = baseRest(baseInvoke); + + /** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ + function keys(object) { + return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); + } + + /** + * Creates an array of the own and inherited enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keysIn(new Foo); + * // => ['a', 'b', 'c'] (iteration order is not guaranteed) + */ + function keysIn(object) { + return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object); + } + + /** + * The opposite of `_.mapValues`; this method creates an object with the + * same values as `object` and keys generated by running each own enumerable + * string keyed property of `object` thru `iteratee`. The iteratee is invoked + * with three arguments: (value, key, object). + * + * @static + * @memberOf _ + * @since 3.8.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns the new mapped object. + * @see _.mapValues + * @example + * + * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) { + * return key + value; + * }); + * // => { 'a1': 1, 'b2': 2 } + */ + function mapKeys(object, iteratee) { + var result = {}; + iteratee = getIteratee(iteratee, 3); + + baseForOwn(object, function(value, key, object) { + baseAssignValue(result, iteratee(value, key, object), value); + }); + return result; + } + + /** + * Creates an object with the same keys as `object` and values generated + * by running each own enumerable string keyed property of `object` thru + * `iteratee`. The iteratee is invoked with three arguments: + * (value, key, object). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns the new mapped object. + * @see _.mapKeys + * @example + * + * var users = { + * 'fred': { 'user': 'fred', 'age': 40 }, + * 'pebbles': { 'user': 'pebbles', 'age': 1 } + * }; + * + * _.mapValues(users, function(o) { return o.age; }); + * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) + * + * // The `_.property` iteratee shorthand. + * _.mapValues(users, 'age'); + * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) + */ + function mapValues(object, iteratee) { + var result = {}; + iteratee = getIteratee(iteratee, 3); + + baseForOwn(object, function(value, key, object) { + baseAssignValue(result, key, iteratee(value, key, object)); + }); + return result; + } + + /** + * This method is like `_.assign` except that it recursively merges own and + * inherited enumerable string keyed properties of source objects into the + * destination object. Source properties that resolve to `undefined` are + * skipped if a destination value exists. Array and plain object properties + * are merged recursively. Other objects and value types are overridden by + * assignment. Source objects are applied from left to right. Subsequent + * sources overwrite property assignments of previous sources. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 0.5.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @example + * + * var object = { + * 'a': [{ 'b': 2 }, { 'd': 4 }] + * }; + * + * var other = { + * 'a': [{ 'c': 3 }, { 'e': 5 }] + * }; + * + * _.merge(object, other); + * // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] } + */ + var merge = createAssigner(function(object, source, srcIndex) { + baseMerge(object, source, srcIndex); + }); + + /** + * This method is like `_.merge` except that it accepts `customizer` which + * is invoked to produce the merged values of the destination and source + * properties. If `customizer` returns `undefined`, merging is handled by the + * method instead. The `customizer` is invoked with six arguments: + * (objValue, srcValue, key, object, source, stack). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} sources The source objects. + * @param {Function} customizer The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * function customizer(objValue, srcValue) { + * if (_.isArray(objValue)) { + * return objValue.concat(srcValue); + * } + * } + * + * var object = { 'a': [1], 'b': [2] }; + * var other = { 'a': [3], 'b': [4] }; + * + * _.mergeWith(object, other, customizer); + * // => { 'a': [1, 3], 'b': [2, 4] } + */ + var mergeWith = createAssigner(function(object, source, srcIndex, customizer) { + baseMerge(object, source, srcIndex, customizer); + }); + + /** + * The opposite of `_.pick`; this method creates an object composed of the + * own and inherited enumerable property paths of `object` that are not omitted. + * + * **Note:** This method is considerably slower than `_.pick`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The source object. + * @param {...(string|string[])} [paths] The property paths to omit. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.omit(object, ['a', 'c']); + * // => { 'b': '2' } + */ + var omit = flatRest(function(object, paths) { + var result = {}; + if (object == null) { + return result; + } + var isDeep = false; + paths = arrayMap(paths, function(path) { + path = castPath(path, object); + isDeep || (isDeep = path.length > 1); + return path; + }); + copyObject(object, getAllKeysIn(object), result); + if (isDeep) { + result = baseClone(result, CLONE_DEEP_FLAG | CLONE_FLAT_FLAG | CLONE_SYMBOLS_FLAG, customOmitClone); + } + var length = paths.length; + while (length--) { + baseUnset(result, paths[length]); + } + return result; + }); + + /** + * The opposite of `_.pickBy`; this method creates an object composed of + * the own and inherited enumerable string keyed properties of `object` that + * `predicate` doesn't return truthy for. The predicate is invoked with two + * arguments: (value, key). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The source object. + * @param {Function} [predicate=_.identity] The function invoked per property. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.omitBy(object, _.isNumber); + * // => { 'b': '2' } + */ + function omitBy(object, predicate) { + return pickBy(object, negate(getIteratee(predicate))); + } + + /** + * Creates an object composed of the picked `object` properties. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The source object. + * @param {...(string|string[])} [paths] The property paths to pick. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.pick(object, ['a', 'c']); + * // => { 'a': 1, 'c': 3 } + */ + var pick = flatRest(function(object, paths) { + return object == null ? {} : basePick(object, paths); + }); + + /** + * Creates an object composed of the `object` properties `predicate` returns + * truthy for. The predicate is invoked with two arguments: (value, key). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The source object. + * @param {Function} [predicate=_.identity] The function invoked per property. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.pickBy(object, _.isNumber); + * // => { 'a': 1, 'c': 3 } + */ + function pickBy(object, predicate) { + if (object == null) { + return {}; + } + var props = arrayMap(getAllKeysIn(object), function(prop) { + return [prop]; + }); + predicate = getIteratee(predicate); + return basePickBy(object, props, function(value, path) { + return predicate(value, path[0]); + }); + } + + /** + * This method is like `_.get` except that if the resolved value is a + * function it's invoked with the `this` binding of its parent object and + * its result is returned. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to resolve. + * @param {*} [defaultValue] The value returned for `undefined` resolved values. + * @returns {*} Returns the resolved value. + * @example + * + * var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] }; + * + * _.result(object, 'a[0].b.c1'); + * // => 3 + * + * _.result(object, 'a[0].b.c2'); + * // => 4 + * + * _.result(object, 'a[0].b.c3', 'default'); + * // => 'default' + * + * _.result(object, 'a[0].b.c3', _.constant('default')); + * // => 'default' + */ + function result(object, path, defaultValue) { + path = castPath(path, object); + + var index = -1, + length = path.length; + + // Ensure the loop is entered when path is empty. + if (!length) { + length = 1; + object = undefined; + } + while (++index < length) { + var value = object == null ? undefined : object[toKey(path[index])]; + if (value === undefined) { + index = length; + value = defaultValue; + } + object = isFunction(value) ? value.call(object) : value; + } + return object; + } + + /** + * Sets the value at `path` of `object`. If a portion of `path` doesn't exist, + * it's created. Arrays are created for missing index properties while objects + * are created for all other missing properties. Use `_.setWith` to customize + * `path` creation. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 3.7.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @returns {Object} Returns `object`. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }] }; + * + * _.set(object, 'a[0].b.c', 4); + * console.log(object.a[0].b.c); + * // => 4 + * + * _.set(object, ['x', '0', 'y', 'z'], 5); + * console.log(object.x[0].y.z); + * // => 5 + */ + function set(object, path, value) { + return object == null ? object : baseSet(object, path, value); + } + + /** + * This method is like `_.set` except that it accepts `customizer` which is + * invoked to produce the objects of `path`. If `customizer` returns `undefined` + * path creation is handled by the method instead. The `customizer` is invoked + * with three arguments: (nsValue, key, nsObject). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * var object = {}; + * + * _.setWith(object, '[0][1]', 'a', Object); + * // => { '0': { '1': 'a' } } + */ + function setWith(object, path, value, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return object == null ? object : baseSet(object, path, value, customizer); + } + + /** + * Creates an array of own enumerable string keyed-value pairs for `object` + * which can be consumed by `_.fromPairs`. If `object` is a map or set, its + * entries are returned. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @alias entries + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the key-value pairs. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.toPairs(new Foo); + * // => [['a', 1], ['b', 2]] (iteration order is not guaranteed) + */ + var toPairs = createToPairs(keys); + + /** + * Creates an array of own and inherited enumerable string keyed-value pairs + * for `object` which can be consumed by `_.fromPairs`. If `object` is a map + * or set, its entries are returned. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @alias entriesIn + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the key-value pairs. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.toPairsIn(new Foo); + * // => [['a', 1], ['b', 2], ['c', 3]] (iteration order is not guaranteed) + */ + var toPairsIn = createToPairs(keysIn); + + /** + * An alternative to `_.reduce`; this method transforms `object` to a new + * `accumulator` object which is the result of running each of its own + * enumerable string keyed properties thru `iteratee`, with each invocation + * potentially mutating the `accumulator` object. If `accumulator` is not + * provided, a new object with the same `[[Prototype]]` will be used. The + * iteratee is invoked with four arguments: (accumulator, value, key, object). + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @since 1.3.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [accumulator] The custom accumulator value. + * @returns {*} Returns the accumulated value. + * @example + * + * _.transform([2, 3, 4], function(result, n) { + * result.push(n *= n); + * return n % 2 == 0; + * }, []); + * // => [4, 9] + * + * _.transform({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) { + * (result[value] || (result[value] = [])).push(key); + * }, {}); + * // => { '1': ['a', 'c'], '2': ['b'] } + */ + function transform(object, iteratee, accumulator) { + var isArr = isArray(object), + isArrLike = isArr || isBuffer(object) || isTypedArray(object); + + iteratee = getIteratee(iteratee, 4); + if (accumulator == null) { + var Ctor = object && object.constructor; + if (isArrLike) { + accumulator = isArr ? new Ctor : []; + } + else if (isObject(object)) { + accumulator = isFunction(Ctor) ? baseCreate(getPrototype(object)) : {}; + } + else { + accumulator = {}; + } + } + (isArrLike ? arrayEach : baseForOwn)(object, function(value, index, object) { + return iteratee(accumulator, value, index, object); + }); + return accumulator; + } + + /** + * Removes the property at `path` of `object`. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to unset. + * @returns {boolean} Returns `true` if the property is deleted, else `false`. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 7 } }] }; + * _.unset(object, 'a[0].b.c'); + * // => true + * + * console.log(object); + * // => { 'a': [{ 'b': {} }] }; + * + * _.unset(object, ['a', '0', 'b', 'c']); + * // => true + * + * console.log(object); + * // => { 'a': [{ 'b': {} }] }; + */ + function unset(object, path) { + return object == null ? true : baseUnset(object, path); + } + + /** + * This method is like `_.set` except that accepts `updater` to produce the + * value to set. Use `_.updateWith` to customize `path` creation. The `updater` + * is invoked with one argument: (value). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.6.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {Function} updater The function to produce the updated value. + * @returns {Object} Returns `object`. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }] }; + * + * _.update(object, 'a[0].b.c', function(n) { return n * n; }); + * console.log(object.a[0].b.c); + * // => 9 + * + * _.update(object, 'x[0].y.z', function(n) { return n ? n + 1 : 0; }); + * console.log(object.x[0].y.z); + * // => 0 + */ + function update(object, path, updater) { + return object == null ? object : baseUpdate(object, path, castFunction(updater)); + } + + /** + * This method is like `_.update` except that it accepts `customizer` which is + * invoked to produce the objects of `path`. If `customizer` returns `undefined` + * path creation is handled by the method instead. The `customizer` is invoked + * with three arguments: (nsValue, key, nsObject). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.6.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {Function} updater The function to produce the updated value. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * var object = {}; + * + * _.updateWith(object, '[0][1]', _.constant('a'), Object); + * // => { '0': { '1': 'a' } } + */ + function updateWith(object, path, updater, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return object == null ? object : baseUpdate(object, path, castFunction(updater), customizer); + } + + /** + * Creates an array of the own enumerable string keyed property values of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property values. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.values(new Foo); + * // => [1, 2] (iteration order is not guaranteed) + * + * _.values('hi'); + * // => ['h', 'i'] + */ + function values(object) { + return object == null ? [] : baseValues(object, keys(object)); + } + + /** + * Creates an array of the own and inherited enumerable string keyed property + * values of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property values. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.valuesIn(new Foo); + * // => [1, 2, 3] (iteration order is not guaranteed) + */ + function valuesIn(object) { + return object == null ? [] : baseValues(object, keysIn(object)); + } + + /*------------------------------------------------------------------------*/ + + /** + * Clamps `number` within the inclusive `lower` and `upper` bounds. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Number + * @param {number} number The number to clamp. + * @param {number} [lower] The lower bound. + * @param {number} upper The upper bound. + * @returns {number} Returns the clamped number. + * @example + * + * _.clamp(-10, -5, 5); + * // => -5 + * + * _.clamp(10, -5, 5); + * // => 5 + */ + function clamp(number, lower, upper) { + if (upper === undefined) { + upper = lower; + lower = undefined; + } + if (upper !== undefined) { + upper = toNumber(upper); + upper = upper === upper ? upper : 0; + } + if (lower !== undefined) { + lower = toNumber(lower); + lower = lower === lower ? lower : 0; + } + return baseClamp(toNumber(number), lower, upper); + } + + /** + * Checks if `n` is between `start` and up to, but not including, `end`. If + * `end` is not specified, it's set to `start` with `start` then set to `0`. + * If `start` is greater than `end` the params are swapped to support + * negative ranges. + * + * @static + * @memberOf _ + * @since 3.3.0 + * @category Number + * @param {number} number The number to check. + * @param {number} [start=0] The start of the range. + * @param {number} end The end of the range. + * @returns {boolean} Returns `true` if `number` is in the range, else `false`. + * @see _.range, _.rangeRight + * @example + * + * _.inRange(3, 2, 4); + * // => true + * + * _.inRange(4, 8); + * // => true + * + * _.inRange(4, 2); + * // => false + * + * _.inRange(2, 2); + * // => false + * + * _.inRange(1.2, 2); + * // => true + * + * _.inRange(5.2, 4); + * // => false + * + * _.inRange(-3, -2, -6); + * // => true + */ + function inRange(number, start, end) { + start = toFinite(start); + if (end === undefined) { + end = start; + start = 0; + } else { + end = toFinite(end); + } + number = toNumber(number); + return baseInRange(number, start, end); + } + + /** + * Produces a random number between the inclusive `lower` and `upper` bounds. + * If only one argument is provided a number between `0` and the given number + * is returned. If `floating` is `true`, or either `lower` or `upper` are + * floats, a floating-point number is returned instead of an integer. + * + * **Note:** JavaScript follows the IEEE-754 standard for resolving + * floating-point values which can produce unexpected results. + * + * @static + * @memberOf _ + * @since 0.7.0 + * @category Number + * @param {number} [lower=0] The lower bound. + * @param {number} [upper=1] The upper bound. + * @param {boolean} [floating] Specify returning a floating-point number. + * @returns {number} Returns the random number. + * @example + * + * _.random(0, 5); + * // => an integer between 0 and 5 + * + * _.random(5); + * // => also an integer between 0 and 5 + * + * _.random(5, true); + * // => a floating-point number between 0 and 5 + * + * _.random(1.2, 5.2); + * // => a floating-point number between 1.2 and 5.2 + */ + function random(lower, upper, floating) { + if (floating && typeof floating != 'boolean' && isIterateeCall(lower, upper, floating)) { + upper = floating = undefined; + } + if (floating === undefined) { + if (typeof upper == 'boolean') { + floating = upper; + upper = undefined; + } + else if (typeof lower == 'boolean') { + floating = lower; + lower = undefined; + } + } + if (lower === undefined && upper === undefined) { + lower = 0; + upper = 1; + } + else { + lower = toFinite(lower); + if (upper === undefined) { + upper = lower; + lower = 0; + } else { + upper = toFinite(upper); + } + } + if (lower > upper) { + var temp = lower; + lower = upper; + upper = temp; + } + if (floating || lower % 1 || upper % 1) { + var rand = nativeRandom(); + return nativeMin(lower + (rand * (upper - lower + freeParseFloat('1e-' + ((rand + '').length - 1)))), upper); + } + return baseRandom(lower, upper); + } + + /*------------------------------------------------------------------------*/ + + /** + * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the camel cased string. + * @example + * + * _.camelCase('Foo Bar'); + * // => 'fooBar' + * + * _.camelCase('--foo-bar--'); + * // => 'fooBar' + * + * _.camelCase('__FOO_BAR__'); + * // => 'fooBar' + */ + var camelCase = createCompounder(function(result, word, index) { + word = word.toLowerCase(); + return result + (index ? capitalize(word) : word); + }); + + /** + * Converts the first character of `string` to upper case and the remaining + * to lower case. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to capitalize. + * @returns {string} Returns the capitalized string. + * @example + * + * _.capitalize('FRED'); + * // => 'Fred' + */ + function capitalize(string) { + return upperFirst(toString(string).toLowerCase()); + } + + /** + * Deburrs `string` by converting + * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) + * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A) + * letters to basic Latin letters and removing + * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to deburr. + * @returns {string} Returns the deburred string. + * @example + * + * _.deburr('déjà vu'); + * // => 'deja vu' + */ + function deburr(string) { + string = toString(string); + return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); + } + + /** + * Checks if `string` ends with the given target string. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to inspect. + * @param {string} [target] The string to search for. + * @param {number} [position=string.length] The position to search up to. + * @returns {boolean} Returns `true` if `string` ends with `target`, + * else `false`. + * @example + * + * _.endsWith('abc', 'c'); + * // => true + * + * _.endsWith('abc', 'b'); + * // => false + * + * _.endsWith('abc', 'b', 2); + * // => true + */ + function endsWith(string, target, position) { + string = toString(string); + target = baseToString(target); + + var length = string.length; + position = position === undefined + ? length + : baseClamp(toInteger(position), 0, length); + + var end = position; + position -= target.length; + return position >= 0 && string.slice(position, end) == target; + } + + /** + * Converts the characters "&", "<", ">", '"', and "'" in `string` to their + * corresponding HTML entities. + * + * **Note:** No other characters are escaped. To escape additional + * characters use a third-party library like [_he_](https://mths.be/he). + * + * Though the ">" character is escaped for symmetry, characters like + * ">" and "/" don't need escaping in HTML and have no special meaning + * unless they're part of a tag or unquoted attribute value. See + * [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands) + * (under "semi-related fun fact") for more details. + * + * When working with HTML you should always + * [quote attribute values](http://wonko.com/post/html-escaping) to reduce + * XSS vectors. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category String + * @param {string} [string=''] The string to escape. + * @returns {string} Returns the escaped string. + * @example + * + * _.escape('fred, barney, & pebbles'); + * // => 'fred, barney, & pebbles' + */ + function escape(string) { + string = toString(string); + return (string && reHasUnescapedHtml.test(string)) + ? string.replace(reUnescapedHtml, escapeHtmlChar) + : string; + } + + /** + * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+", + * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to escape. + * @returns {string} Returns the escaped string. + * @example + * + * _.escapeRegExp('[lodash](https://lodash.com/)'); + * // => '\[lodash\]\(https://lodash\.com/\)' + */ + function escapeRegExp(string) { + string = toString(string); + return (string && reHasRegExpChar.test(string)) + ? string.replace(reRegExpChar, '\\$&') + : string; + } + + /** + * Converts `string` to + * [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the kebab cased string. + * @example + * + * _.kebabCase('Foo Bar'); + * // => 'foo-bar' + * + * _.kebabCase('fooBar'); + * // => 'foo-bar' + * + * _.kebabCase('__FOO_BAR__'); + * // => 'foo-bar' + */ + var kebabCase = createCompounder(function(result, word, index) { + return result + (index ? '-' : '') + word.toLowerCase(); + }); + + /** + * Converts `string`, as space separated words, to lower case. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the lower cased string. + * @example + * + * _.lowerCase('--Foo-Bar--'); + * // => 'foo bar' + * + * _.lowerCase('fooBar'); + * // => 'foo bar' + * + * _.lowerCase('__FOO_BAR__'); + * // => 'foo bar' + */ + var lowerCase = createCompounder(function(result, word, index) { + return result + (index ? ' ' : '') + word.toLowerCase(); + }); + + /** + * Converts the first character of `string` to lower case. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the converted string. + * @example + * + * _.lowerFirst('Fred'); + * // => 'fred' + * + * _.lowerFirst('FRED'); + * // => 'fRED' + */ + var lowerFirst = createCaseFirst('toLowerCase'); + + /** + * Pads `string` on the left and right sides if it's shorter than `length`. + * Padding characters are truncated if they can't be evenly divided by `length`. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to pad. + * @param {number} [length=0] The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padded string. + * @example + * + * _.pad('abc', 8); + * // => ' abc ' + * + * _.pad('abc', 8, '_-'); + * // => '_-abc_-_' + * + * _.pad('abc', 3); + * // => 'abc' + */ + function pad(string, length, chars) { + string = toString(string); + length = toInteger(length); + + var strLength = length ? stringSize(string) : 0; + if (!length || strLength >= length) { + return string; + } + var mid = (length - strLength) / 2; + return ( + createPadding(nativeFloor(mid), chars) + + string + + createPadding(nativeCeil(mid), chars) + ); + } + + /** + * Pads `string` on the right side if it's shorter than `length`. Padding + * characters are truncated if they exceed `length`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to pad. + * @param {number} [length=0] The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padded string. + * @example + * + * _.padEnd('abc', 6); + * // => 'abc ' + * + * _.padEnd('abc', 6, '_-'); + * // => 'abc_-_' + * + * _.padEnd('abc', 3); + * // => 'abc' + */ + function padEnd(string, length, chars) { + string = toString(string); + length = toInteger(length); + + var strLength = length ? stringSize(string) : 0; + return (length && strLength < length) + ? (string + createPadding(length - strLength, chars)) + : string; + } + + /** + * Pads `string` on the left side if it's shorter than `length`. Padding + * characters are truncated if they exceed `length`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to pad. + * @param {number} [length=0] The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padded string. + * @example + * + * _.padStart('abc', 6); + * // => ' abc' + * + * _.padStart('abc', 6, '_-'); + * // => '_-_abc' + * + * _.padStart('abc', 3); + * // => 'abc' + */ + function padStart(string, length, chars) { + string = toString(string); + length = toInteger(length); + + var strLength = length ? stringSize(string) : 0; + return (length && strLength < length) + ? (createPadding(length - strLength, chars) + string) + : string; + } + + /** + * Converts `string` to an integer of the specified radix. If `radix` is + * `undefined` or `0`, a `radix` of `10` is used unless `value` is a + * hexadecimal, in which case a `radix` of `16` is used. + * + * **Note:** This method aligns with the + * [ES5 implementation](https://es5.github.io/#x15.1.2.2) of `parseInt`. + * + * @static + * @memberOf _ + * @since 1.1.0 + * @category String + * @param {string} string The string to convert. + * @param {number} [radix=10] The radix to interpret `value` by. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {number} Returns the converted integer. + * @example + * + * _.parseInt('08'); + * // => 8 + * + * _.map(['6', '08', '10'], _.parseInt); + * // => [6, 8, 10] + */ + function parseInt(string, radix, guard) { + if (guard || radix == null) { + radix = 0; + } else if (radix) { + radix = +radix; + } + return nativeParseInt(toString(string).replace(reTrimStart, ''), radix || 0); + } + + /** + * Repeats the given string `n` times. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to repeat. + * @param {number} [n=1] The number of times to repeat the string. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {string} Returns the repeated string. + * @example + * + * _.repeat('*', 3); + * // => '***' + * + * _.repeat('abc', 2); + * // => 'abcabc' + * + * _.repeat('abc', 0); + * // => '' + */ + function repeat(string, n, guard) { + if ((guard ? isIterateeCall(string, n, guard) : n === undefined)) { + n = 1; + } else { + n = toInteger(n); + } + return baseRepeat(toString(string), n); + } + + /** + * Replaces matches for `pattern` in `string` with `replacement`. + * + * **Note:** This method is based on + * [`String#replace`](https://mdn.io/String/replace). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to modify. + * @param {RegExp|string} pattern The pattern to replace. + * @param {Function|string} replacement The match replacement. + * @returns {string} Returns the modified string. + * @example + * + * _.replace('Hi Fred', 'Fred', 'Barney'); + * // => 'Hi Barney' + */ + function replace() { + var args = arguments, + string = toString(args[0]); + + return args.length < 3 ? string : string.replace(args[1], args[2]); + } + + /** + * Converts `string` to + * [snake case](https://en.wikipedia.org/wiki/Snake_case). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the snake cased string. + * @example + * + * _.snakeCase('Foo Bar'); + * // => 'foo_bar' + * + * _.snakeCase('fooBar'); + * // => 'foo_bar' + * + * _.snakeCase('--FOO-BAR--'); + * // => 'foo_bar' + */ + var snakeCase = createCompounder(function(result, word, index) { + return result + (index ? '_' : '') + word.toLowerCase(); + }); + + /** + * Splits `string` by `separator`. + * + * **Note:** This method is based on + * [`String#split`](https://mdn.io/String/split). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to split. + * @param {RegExp|string} separator The separator pattern to split by. + * @param {number} [limit] The length to truncate results to. + * @returns {Array} Returns the string segments. + * @example + * + * _.split('a-b-c', '-', 2); + * // => ['a', 'b'] + */ + function split(string, separator, limit) { + if (limit && typeof limit != 'number' && isIterateeCall(string, separator, limit)) { + separator = limit = undefined; + } + limit = limit === undefined ? MAX_ARRAY_LENGTH : limit >>> 0; + if (!limit) { + return []; + } + string = toString(string); + if (string && ( + typeof separator == 'string' || + (separator != null && !isRegExp(separator)) + )) { + separator = baseToString(separator); + if (!separator && hasUnicode(string)) { + return castSlice(stringToArray(string), 0, limit); + } + } + return string.split(separator, limit); + } + + /** + * Converts `string` to + * [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage). + * + * @static + * @memberOf _ + * @since 3.1.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the start cased string. + * @example + * + * _.startCase('--foo-bar--'); + * // => 'Foo Bar' + * + * _.startCase('fooBar'); + * // => 'Foo Bar' + * + * _.startCase('__FOO_BAR__'); + * // => 'FOO BAR' + */ + var startCase = createCompounder(function(result, word, index) { + return result + (index ? ' ' : '') + upperFirst(word); + }); + + /** + * Checks if `string` starts with the given target string. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to inspect. + * @param {string} [target] The string to search for. + * @param {number} [position=0] The position to search from. + * @returns {boolean} Returns `true` if `string` starts with `target`, + * else `false`. + * @example + * + * _.startsWith('abc', 'a'); + * // => true + * + * _.startsWith('abc', 'b'); + * // => false + * + * _.startsWith('abc', 'b', 1); + * // => true + */ + function startsWith(string, target, position) { + string = toString(string); + position = position == null + ? 0 + : baseClamp(toInteger(position), 0, string.length); + + target = baseToString(target); + return string.slice(position, position + target.length) == target; + } + + /** + * Creates a compiled template function that can interpolate data properties + * in "interpolate" delimiters, HTML-escape interpolated data properties in + * "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data + * properties may be accessed as free variables in the template. If a setting + * object is given, it takes precedence over `_.templateSettings` values. + * + * **Note:** In the development build `_.template` utilizes + * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) + * for easier debugging. + * + * For more information on precompiling templates see + * [lodash's custom builds documentation](https://lodash.com/custom-builds). + * + * For more information on Chrome extension sandboxes see + * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval). + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category String + * @param {string} [string=''] The template string. + * @param {Object} [options={}] The options object. + * @param {RegExp} [options.escape=_.templateSettings.escape] + * The HTML "escape" delimiter. + * @param {RegExp} [options.evaluate=_.templateSettings.evaluate] + * The "evaluate" delimiter. + * @param {Object} [options.imports=_.templateSettings.imports] + * An object to import into the template as free variables. + * @param {RegExp} [options.interpolate=_.templateSettings.interpolate] + * The "interpolate" delimiter. + * @param {string} [options.sourceURL='lodash.templateSources[n]'] + * The sourceURL of the compiled template. + * @param {string} [options.variable='obj'] + * The data object variable name. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Function} Returns the compiled template function. + * @example + * + * // Use the "interpolate" delimiter to create a compiled template. + * var compiled = _.template('hello <%= user %>!'); + * compiled({ 'user': 'fred' }); + * // => 'hello fred!' + * + * // Use the HTML "escape" delimiter to escape data property values. + * var compiled = _.template('<%- value %>'); + * compiled({ 'value': ' + diff --git a/e2e/smoke-tests/fix-jsdom-environment.ts b/e2e/smoke-tests/fix-jsdom-environment.ts new file mode 100644 index 00000000000..c2885ab9c45 --- /dev/null +++ b/e2e/smoke-tests/fix-jsdom-environment.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import JSDOMEnvironment from 'jest-environment-jsdom'; + +/** + * JSDOMEnvironment patch to polyfill missing APIs with Node APIs. + */ +// https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string +export default class FixJSDOMEnvironment extends JSDOMEnvironment { + constructor(...args: ConstructorParameters) { + super(...args); + + // Fetch + // FIXME: https://github.com/jsdom/jsdom/issues/1724 + this.global.fetch = fetch; + this.global.Headers = Headers; + this.global.Request = Request; + this.global.Response = Response; + + // Util + this.global.TextEncoder = TextEncoder; + } +} diff --git a/e2e/smoke-tests/jest.config.ts b/e2e/smoke-tests/jest.config.ts new file mode 100644 index 00000000000..7d86672c999 --- /dev/null +++ b/e2e/smoke-tests/jest.config.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Config } from 'jest'; + +const config: Config = { + verbose: true, + testEnvironment: './fix-jsdom-environment.ts', + globals: { + FIREBASE_APPCHECK_DEBUG_TOKEN: process.env.APP_CHECK_DEBUG_TOKEN + } +}; + +export default config; diff --git a/e2e/smoke-tests/package.json b/e2e/smoke-tests/package.json new file mode 100644 index 00000000000..c957f018b5a --- /dev/null +++ b/e2e/smoke-tests/package.json @@ -0,0 +1,40 @@ +{ + "name": "firebase-smoke-test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "yarn jest", + "test:compat": "yarn jest tests/compat.test.ts", + "test:modular": "yarn jest tests/modular.test.ts", + "watch": "webpack --watch", + "build": "webpack", + "start:modular": "webpack serve --config-name modular", + "start:compat": "webpack serve --config-name compat", + "build:modular": "webpack --config-name modular", + "build:compat": "webpack --config-name compat" + }, + "author": "", + "license": "ISC", + "dependencies": { + "firebase": "^11.8.0-20250512211235" + }, + "devDependencies": { + "@babel/core": "7.26.8", + "@babel/preset-env": "7.26.8", + "@babel/preset-typescript": "7.26.0", + "@types/jest": "29.5.14", + "babel-jest": "29.7.0", + "babel-loader": "8.4.1", + "jest": "29.7.0", + "jest-environment-jsdom": "29.7.0", + "ts-node": "10.9.2", + "typescript": "5.5.4", + "webpack": "5.98.0", + "webpack-cli": "5.1.4", + "webpack-dev-server": "5.2.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/e2e/smoke-tests/sample-apps/compat.js b/e2e/smoke-tests/sample-apps/compat.js new file mode 100644 index 00000000000..e77b7d087d0 --- /dev/null +++ b/e2e/smoke-tests/sample-apps/compat.js @@ -0,0 +1,271 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import firebase from 'firebase/compat/app'; +import 'firebase/compat/auth'; +import 'firebase/compat/analytics'; +import 'firebase/compat/app-check'; +import 'firebase/compat/functions'; +import 'firebase/compat/storage'; +import 'firebase/compat/firestore'; +import 'firebase/compat/messaging'; +import 'firebase/compat/performance'; +import 'firebase/compat/database'; +/** + * The config file should look like: + * + * // A config for a project + * export const config = { + * apiKey: ************, + * authDomain: ************, + * databaseURL: ************, + * projectId: ************, + * storageBucket: ************, + * messagingSenderId: ************, + * appId: ************, + * measurementId: ************ + * }; + * + * // A user account with read/write privileges in that project + * // for storage, database, firestore + * export const testAccount = { + * email: ************, + * password: ************ + * } + */ +import { config, testAccount } from '../firebase-config'; + +/** + * Quick sample app to debug and explore basic Firebase API usage/problems. + * + * In order for app check to work, add the app check debug token to + * build/index.html and uncomment the line. + */ + +/** + * Auth smoke test. + * + * Login with email and password. Account must exist. Should set up + * test project rules to only allow read/writes from this account + * (and other test accounts), to properly test rules. + * + * Logout after all tests are done. + */ +async function authLogin() { + const cred = await firebase + .auth() + .signInWithEmailAndPassword(testAccount.email, testAccount.password); + console.log('[AUTH] Logged in with test account', cred.user.email); + return cred; +} +async function authLogout() { + console.log('[AUTH] Logging out user'); + return firebase.auth().signOut(); +} + +/** + * Functions smoke test. + * + * Call a deployed function. + * This cloud function must be deployed in this project first. See + * e2e/smoke-test/README.md for more info. + */ +async function callFunctions() { + console.log('[FUNCTIONS] start'); + const functions = firebase.functions(); + const callTest = functions.httpsCallable('callTest'); + try { + const result = await callTest({ data: 'blah' }); + console.log('[FUNCTIONS] result:', result.data); + } catch (e) { + if (e.message.includes('Unauthenticated')) { + console.warn( + 'Functions blocked by App Check. ' + + 'Activate app check with a live sitekey to allow Functions calls' + ); + } else { + throw e; + } + } +} + +/** + * Storage smoke test. + * Create, read, delete. + */ +async function callStorage() { + console.log('[STORAGE] start'); + const storage = firebase.storage(); + const storageRef = storage.ref('/test.txt'); + await storageRef.putString('efg'); + await new Promise(resolve => setTimeout(resolve, 1000)); + const url = await storageRef.getDownloadURL(); + console.log('[STORAGE] download url', url); + const response = await fetch(url); + const data = await response.text(); + console.log("[STORAGE] Returned data (should be 'efg'):", data); + await storageRef.delete(); +} + +/** + * Firestore smoke test. + * Create 2 docs, test query filter. + * Create, update, delete a doc with `onSnapshot` monitoring changes. + */ +async function callFirestore() { + console.log('[FIRESTORE] start'); + const firestore = firebase.firestore(); + await firestore.collection('testCollection').doc('trueDoc').set({ + testbool: true + }); + await firestore.collection('testCollection').doc('falseDoc').set({ + testbool: false + }); + const trueDocs = await firestore + .collection('testCollection') + .where('testbool', '==', true) + .get(); + trueDocs.docs.forEach(doc => + console.log('[FIRESTORE] Filter test, expect one doc', doc.data()) + ); + await firestore.collection('testCollection').doc('trueDoc').delete(); + await firestore.collection('testCollection').doc('falseDoc').delete(); + const testDocRef = firestore.doc('testCollection/testDoc'); + console.log('[FIRESTORE] Doc creation and updating'); + testDocRef.onSnapshot(snap => { + if (snap.exists) { + console.log('[FIRESTORE] SNAPSHOT:', snap.data()); + } else { + console.log("[FIRESTORE] Snapshot doesn't exist"); + } + }); + console.log('[FIRESTORE] creating (expect to see snapshot data)'); + await testDocRef.set({ word: 'hi', number: 14 }); + console.log('[FIRESTORE] updating (expect to see snapshot data change)'); + await testDocRef.update({ word: 'bye', newProp: ['a'] }); + console.log("[FIRESTORE] deleting (expect to see snapshot doesn't exist)"); + await testDocRef.delete(); +} + +/** + * Database smoke test. + * Create, update, delete a doc with `on` monitoring changes. + */ +async function callDatabase() { + console.log('[DATABASE] start'); + const db = firebase.database(); + const ref = db.ref('abc/def'); + ref.on('value', snap => { + if (snap.exists()) { + console.log(`[DATABASE] value: ${JSON.stringify(snap.val())}`); + } else { + console.log("[DATABASE] Snapshot doesn't exist"); + } + }); + console.log('[DATABASE] creating (expect to see snapshot data)'); + await ref.set({ text: 'string 123 xyz' }); + console.log('[DATABASE] updating (expect to see snapshot data change)'); + await ref.update({ number: 987 }); + console.log("[DATABASE] deleting (expect to see snapshot doesn't exist)"); + await ref.remove(); + ref.off(); +} + +/** + * Messaging smoke test. + * Call getToken(), it may be blocked if user does not click "Allow" for + * notification permissions, or has blocked it on this same host in the past. + */ +async function callMessaging() { + console.log('[MESSAGING] start'); + const messaging = firebase.messaging(); + + return messaging + .getToken() + .then(token => console.log(`[MESSAGING] Got token: ${token}`)) + .catch(e => { + if (e.message.includes('messaging/permission-blocked')) { + console.log('[MESSAGING] Permission blocked (expected on localhost)'); + } else { + throw e; + } + }); +} + +/** + * Analytics smoke test. + * Just make sure some functions can be called without obvious errors. + */ +function callAnalytics() { + console.log('[ANALYTICS] start'); + firebase.analytics.isSupported(); + firebase.analytics().logEvent('begin_checkout'); + console.log('[ANALYTICS] logged event'); +} + +/** + * App Check smoke test. + * Just make sure some functions can be called without obvious errors. + */ +function callAppCheck() { + console.log('[APP CHECK] start'); + firebase + .appCheck() + .activate({ getToken: () => Promise.resolve({ token: 'abcd' }) }); + console.log('[APP CHECK] activated'); +} + +/** + * Analytics smoke test. + * Just make sure some functions can be called without obvious errors. + */ +function callPerformance() { + console.log('[PERFORMANCE] start'); + const performance = firebase.performance(); + const trace = performance.trace('test'); + trace.start(); + trace.stop(); + trace.putAttribute('testattr', 'perftestvalue'); + console.log( + "[PERFORMANCE] trace (should be 'perftestvalue')", + trace.getAttribute('testattr') + ); +} + +/** + * Run smoke tests for all products. + * Comment out any products you want to ignore. + */ +async function main() { + console.log('FIREBASE VERSION', firebase.SDK_VERSION); + const app = firebase.initializeApp(config); + firebase.setLogLevel('warn'); + + callAppCheck(); + await authLogin(); + await callStorage(); + await callFirestore(); + await callDatabase(); + await callMessaging(); + callAnalytics(); + callPerformance(); + await callFunctions(); + await authLogout(); + console.log('DONE'); +} + +main(); diff --git a/e2e/smoke-tests/sample-apps/modular.js b/e2e/smoke-tests/sample-apps/modular.js new file mode 100644 index 00000000000..20ff0ce7bd2 --- /dev/null +++ b/e2e/smoke-tests/sample-apps/modular.js @@ -0,0 +1,359 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { initializeApp, setLogLevel, SDK_VERSION } from 'firebase/app'; +import { getAuth, signInWithEmailAndPassword, signOut } from 'firebase/auth'; +import { + getAnalytics, + isSupported as analyticsIsSupported, + logEvent +} from 'firebase/analytics'; +import { initializeAppCheck, CustomProvider } from 'firebase/app-check'; +import { + getFunctions, + httpsCallable, + httpsCallableFromURL +} from 'firebase/functions'; +import { + getStorage, + ref, + uploadString, + getDownloadURL, + deleteObject +} from 'firebase/storage'; +import { + getFirestore, + collection, + setDoc, + doc, + updateDoc, + deleteDoc, + getDocs, + query, + where, + onSnapshot +} from 'firebase/firestore'; +import { getMessaging, getToken } from 'firebase/messaging'; +import { getPerformance, trace as perfTrace } from 'firebase/performance'; +import { + getDatabase, + ref as dbRef, + set, + update, + remove, + onValue, + off +} from 'firebase/database'; +import { getGenerativeModel, getAI, VertexAIBackend } from 'firebase/ai'; +import { getDataConnect, DataConnect } from 'firebase/data-connect'; + +/** + * The config file should look like: + * + * // A config for a project + * export const config = { + * apiKey: ************, + * authDomain: ************, + * databaseURL: ************, + * projectId: ************, + * storageBucket: ************, + * messagingSenderId: ************, + * appId: ************, + * measurementId: ************ + * }; + * + * // A user account with read/write privileges in that project + * // for storage, database, firestore + * export const testAccount = { + * email: ************, + * password: ************ + * } + */ +import { config, testAccount } from '../firebase-config'; + +/** + * Quick sample app to debug and explore basic Firebase API usage/problems. + * + * In order for app check to work, add the app check debug token to + * build/index.html and uncomment the line. + */ + +/** + * Auth smoke test. + * + * Login with email and password. Account must exist. Should set up + * test project rules to only allow read/writes from this account + * (and other test accounts), to properly test rules. + * + * Logout after all tests are done. + */ +async function authLogin(app) { + const auth = getAuth(app); + const cred = await signInWithEmailAndPassword( + auth, + testAccount.email, + testAccount.password + ); + console.log('[AUTH] Logged in with test account', cred.user.email); + return cred; +} +async function authLogout(app) { + console.log('[AUTH] Logging out user'); + return signOut(getAuth(app)); +} + +/** + * Functions smoke test. + * + * Call a deployed function. + * This cloud function must be deployed in this project first. See + * e2e/smoke-tests/README.md for more info. + */ +async function callFunctions(app) { + console.log('[FUNCTIONS] start'); + const functions = getFunctions(app); + let callTest = httpsCallable(functions, 'callTest'); + try { + const result = await callTest({ data: 'blah' }); + console.log('[FUNCTIONS] result (by name):', result.data); + } catch (e) { + if (e.message.includes('Unauthenticated')) { + console.warn( + 'Functions blocked by App Check. ' + + 'Activate app check with a live sitekey to allow Functions calls' + ); + } else { + throw e; + } + } + callTest = httpsCallableFromURL( + functions, + `https://us-central-${app.options.projectId}.cloudfunctions.net/callTest` + ); + try { + const result = await callTest({ data: 'blah' }); + console.log('[FUNCTIONS] result (by URL):', result.data); + } catch (e) { + if (e.message.includes('Unauthenticated')) { + console.warn( + 'Functions blocked by App Check. ' + + 'Activate app check with a live sitekey to allow Functions calls' + ); + } else { + throw e; + } + } +} + +/** + * Storage smoke test. + * Create, read, delete. + */ +async function callStorage(app) { + console.log('[STORAGE] start'); + const storage = getStorage(app); + const storageRef = ref(storage, '/test.txt'); + await uploadString(storageRef, 'efg'); + await new Promise(resolve => setTimeout(resolve, 1000)); + const url = await getDownloadURL(storageRef); + console.log('[STORAGE] download url', url); + const response = await fetch(url); + const data = await response.text(); + console.log("[STORAGE] Returned data (should be 'efg'):", data); + await deleteObject(storageRef); +} + +/** + * Firestore smoke test. + * Create 2 docs, test query filter. + * Create, update, delete a doc with `onSnapshot` monitoring changes. + */ +async function callFirestore(app) { + console.log('[FIRESTORE] start'); + const firestore = getFirestore(app); + setDoc(doc(collection(firestore, 'testCollection'), 'trueDoc'), { + testbool: true + }); + setDoc(doc(collection(firestore, 'testCollection'), 'falseDoc'), { + testbool: false + }); + const trueDocs = await getDocs( + query( + collection(firestore, 'testCollection'), + where('testbool', '==', true) + ) + ); + trueDocs.docs.forEach(doc => + console.log('[FIRESTORE] Filter test, expect one doc', doc.data()) + ); + await deleteDoc(doc(collection(firestore, 'testCollection'), 'trueDoc')); + await deleteDoc(doc(firestore, 'testCollection/falseDoc')); + const testDocRef = doc(firestore, 'testCollection/testDoc'); + console.log('[FIRESTORE] Doc creation and updating'); + onSnapshot(testDocRef, snap => { + if (snap.exists) { + console.log('[FIRESTORE] SNAPSHOT:', snap.data()); + } else { + console.log("[FIRESTORE] Snapshot doesn't exist"); + } + }); + console.log('[FIRESTORE] creating (expect to see snapshot data)'); + await setDoc(testDocRef, { word: 'hi', number: 14 }); + console.log('[FIRESTORE] updating (expect to see snapshot data change)'); + await updateDoc(testDocRef, { word: 'bye', newProp: ['a'] }); + console.log("[FIRESTORE] deleting (expect to see snapshot doesn't exist)"); + await deleteDoc(testDocRef); +} + +/** + * Database smoke test. + * Create, update, delete a doc with `on` monitoring changes. + */ +async function callDatabase(app) { + console.log('[DATABASE] start'); + const db = getDatabase(app); + const ref = dbRef(db, 'abc/def'); + onValue(ref, snap => { + if (snap.exists()) { + console.log(`[DATABASE] value: ${JSON.stringify(snap.val())}`); + } else { + console.log("[DATABASE] Snapshot doesn't exist"); + } + }); + console.log('[DATABASE] creating (expect to see snapshot data)'); + await set(ref, { text: 'string 123 xyz' }); + console.log('[DATABASE] updating (expect to see snapshot data change)'); + await update(ref, { number: 987 }); + console.log("[DATABASE] deleting (expect to see snapshot doesn't exist)"); + await remove(ref); + off(ref); +} + +/** + * Messaging smoke test. + * Call getToken(), it may be blocked if user does not click "Allow" for + * notification permissions, or has blocked it on this same host in the past. + */ +async function callMessaging(app) { + console.log('[MESSAGING] start'); + const messaging = getMessaging(app); + try { + const token = await getToken(messaging); + console.log(`[MESSAGING] Got token: ${token}`); + } catch (e) { + if (e.message.includes('messaging/permission-blocked')) { + console.log('[MESSAGING] Permission blocked (expected on localhost)'); + } else { + throw e; + } + } +} + +/** + * Analytics smoke test. + * Just make sure some functions can be called without obvious errors. + */ +function callAnalytics(app) { + console.log('[ANALYTICS] start'); + analyticsIsSupported(); + const analytics = getAnalytics(app); + logEvent(analytics, 'begin_checkout'); + console.log('[ANALYTICS] logged event'); +} + +/** + * App Check smoke test. + * Just make sure some functions can be called without obvious errors. + */ +function callAppCheck(app) { + console.log('[APP CHECK] start'); + initializeAppCheck(app, { + provider: new CustomProvider({ + getToken: () => Promise.resolve({ token: 'abcd' }) + }) + }); + console.log('[APP CHECK] initialized'); +} + +/** + * Performance smoke test. + * Just make sure some functions can be called without obvious errors. + */ +function callPerformance(app) { + console.log('[PERFORMANCE] start'); + const performance = getPerformance(app); + const trace = perfTrace(performance, 'test'); + trace.start(); + trace.stop(); + trace.putAttribute('testattr', 'perftestvalue'); + console.log( + "[PERFORMANCE] trace (should be 'perftestvalue')", + trace.getAttribute('testattr') + ); +} + +/** + * AI smoke test. + * Just make sure some functions can be called without obvious errors. + */ +async function callAI(app) { + console.log('[AI] start'); + const ai = getAI(app, { backend: new VertexAIBackend() }); + const model = getGenerativeModel(ai, { model: 'gemini-2.5-flash' }); + const result = await model.countTokens('abcdefg'); + console.log(`[AI] counted tokens: ${result.totalTokens}`); +} + +/** + * DataConnect smoke test. + * Just make sure some functions can be called without obvious errors. + */ +function callDataConnect(app) { + console.log('[DATACONNECT] start'); + getDataConnect(app, { + location: 'a-location', + connector: 'a-connector', + service: 'service' + }); + console.log('[DATACONNECT] initialized'); +} + +/** + * Run smoke tests for all products. + * Comment out any products you want to ignore. + */ +async function main() { + console.log('FIREBASE VERSION', SDK_VERSION); + const app = initializeApp(config); + setLogLevel('warn'); + + callAppCheck(app); + await authLogin(app); + await callStorage(app); + await callFirestore(app); + await callDatabase(app); + await callMessaging(app); + callAnalytics(app); + callPerformance(app); + await callFunctions(app); + await callAI(app); + callDataConnect(app); + await authLogout(app); + console.log('DONE'); +} + +main(); diff --git a/e2e/smoke-tests/tests/compat.test.ts b/e2e/smoke-tests/tests/compat.test.ts new file mode 100644 index 00000000000..661cd886a14 --- /dev/null +++ b/e2e/smoke-tests/tests/compat.test.ts @@ -0,0 +1,214 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import firebase from 'firebase/compat/app'; +import 'firebase/compat/auth'; +import 'firebase/compat/analytics'; +import 'firebase/compat/app-check'; +import 'firebase/compat/functions'; +import 'firebase/compat/storage'; +import 'firebase/compat/firestore'; +import 'firebase/compat/messaging'; +import 'firebase/compat/performance'; +import 'firebase/compat/database'; +import { config, testAccount } from '../firebase-config'; +import 'jest'; + +describe('COMPAT', () => { + let app: firebase.app.App; + beforeAll(() => { + console.log('FIREBASE VERSION', firebase.SDK_VERSION); + app = firebase.initializeApp(config); + firebase.setLogLevel('warn'); + }); + + afterAll(async () => { + await firebase.auth().signOut(); + await app.delete(); + }); + + describe('AUTH', () => { + let auth: firebase.auth.Auth; + it('init auth', () => { + auth = firebase.auth(); + }); + it('signInWithEmailAndPassword()', async () => { + const cred = await auth.signInWithEmailAndPassword( + testAccount.email, + testAccount.password + ); + console.log('Logged in with test account', cred.user?.email); + expect(cred.user?.email).toBe(testAccount.email); + }); + }); + + describe('APP CHECK', () => { + let appCheck: firebase.appCheck.AppCheck; + it('init appCheck', () => { + // @ts-ignore + appCheck = firebase.appCheck(); + }); + it('activate()', async () => { + // Test uses debug token, any string is fine here. + appCheck.activate('asdf'); + }); + }); + + describe('FUNCTIONS', () => { + let functions: firebase.functions.Functions; + it('init functions', () => { + functions = firebase.functions(); + }); + it('httpsCallable()', async () => { + const callTest = functions.httpsCallable('callTest'); + const result = await callTest({ data: 'blah' }); + expect(result.data.word).toBe('hellooo'); + }); + }); + + describe('STORAGE', () => { + let storage: firebase.storage.Storage; + let storageRef: firebase.storage.Reference; + let url: string; + it('init storage', () => { + storage = firebase.storage(); + }); + it('putString()', async () => { + storageRef = storage.ref('/test-compat.txt'); + await storageRef.putString('efg'); + }); + it('getDownloadUrl()', async () => { + url = await storageRef.getDownloadURL(); + expect(url).toMatch(/test-compat\.txt/); + }); + it('fetch and check uploaded data', async () => { + const response = await fetch(url); + const data = await response.text(); + expect(data).toBe('efg'); + await storageRef.delete(); + }); + }); + + describe('FIRESTORE', () => { + let firestore: firebase.firestore.Firestore; + it('init firestore', () => { + firestore = firebase.firestore(); + // @ts-ignore Super hacky way to deactive useFetchStreams + // which don't work in jsdom + firestore._delegate._settings.useFetchStreams = false; + }); + it('set(), get(), where()', async () => { + await firestore.collection('testCollection').doc('trueDoc').set({ + testbool: true + }); + await firestore.collection('testCollection').doc('falseDoc').set({ + testbool: false + }); + const trueDocs = await firestore + .collection('testCollection') + .where('testbool', '==', true) + .get(); + expect(trueDocs.docs.length).toBe(1); + await firestore.collection('testCollection').doc('trueDoc').delete(); + await firestore.collection('testCollection').doc('falseDoc').delete(); + }); + it('onSnapshot() reflects CRUD operations', async () => { + const testDocRef = firestore.doc('testCollection/testDoc'); + let expectedSnap: any = {}; + testDocRef.onSnapshot(snap => { + expect(snap.exists).toBe(expectedSnap.exists); + if (snap.exists) { + expect(snap.data()).toEqual(expectedSnap.data); + } + }); + expectedSnap = { exists: true, data: { word: 'hi', number: 14 } }; + await testDocRef.set({ word: 'hi', number: 14 }); + expectedSnap = { + exists: true, + data: { word: 'bye', number: 14, newProp: ['a'] } + }; + await testDocRef.update({ word: 'bye', newProp: ['a'] }); + expectedSnap = { exists: false }; + await testDocRef.delete(); + }); + }); + + describe('DATABASE', () => { + let db: firebase.database.Database; + it('init database', () => { + db = firebase.database(); + }); + it('on() reflects CRUD operations', async () => { + const ref = db.ref('abc/def'); + let expectedValue: any = {}; + ref.on('value', snap => { + if (snap.exists()) { + expect(snap.val()).toEqual(expectedValue); + } else { + expect(expectedValue).toBeNull; + } + }); + expectedValue = { text: 'string 123 xyz' }; + await ref.set({ text: 'string 123 xyz' }); + expectedValue.number = 987; + await ref.update({ number: 987 }); + expectedValue = null; + await ref.remove(); + ref.off(); + }); + }); + + describe('MESSAGING', () => { + it('init messaging', () => { + // @ts-ignore Stub missing browser APIs that FCM depends on + window.indexedDB = { open: () => Promise.resolve() }; + // @ts-ignore Stub missing browser APIs that FCM depends on + navigator.serviceWorker = { addEventListener: () => {} }; + firebase.messaging(); + // @ts-ignore + delete window.indexedDB; + // @ts-ignore + delete navigator.serviceWorker; + }); + }); + + describe('ANALYTICS', () => { + let analytics: firebase.analytics.Analytics; + it('analytics.isSupported() (static method)', () => + firebase.analytics.isSupported()); + it('init analytics', () => { + analytics = firebase.analytics(); + }); + it("logEvent doesn't error", () => { + analytics.logEvent('begin_checkout'); + }); + }); + + describe('PERFORMANCE', () => { + let performance: firebase.performance.Performance; + it('init performance', () => { + performance = firebase.performance(); + }); + it('trace()', () => { + const trace = performance.trace('test'); + trace.start(); + trace.stop(); + trace.putAttribute('testattr', 'perftestvalue'); + expect(trace.getAttribute('testattr')).toBe('perftestvalue'); + }); + }); +}); diff --git a/e2e/smoke-tests/tests/modular.test.ts b/e2e/smoke-tests/tests/modular.test.ts new file mode 100644 index 00000000000..536a271ca5e --- /dev/null +++ b/e2e/smoke-tests/tests/modular.test.ts @@ -0,0 +1,337 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + initializeApp, + setLogLevel, + SDK_VERSION, + FirebaseApp, + deleteApp +} from 'firebase/app'; +import { + Auth, + getAuth, + initializeAuth, + signInWithEmailAndPassword, + signOut +} from 'firebase/auth'; +import { + initializeAppCheck, + AppCheck, + ReCaptchaV3Provider, + getToken +} from 'firebase/app-check'; +import { + getAnalytics, + logEvent, + isSupported as analyticsIsSupported, + Analytics +} from 'firebase/analytics'; +import { + getDatabase, + ref as dbRef, + onValue, + off, + update, + remove, + set, + Database +} from 'firebase/database'; +import { + collection, + getFirestore, + doc, + setDoc, + updateDoc, + onSnapshot, + getDocs, + where, + query, + deleteDoc, + Firestore, + initializeFirestore +} from 'firebase/firestore'; +import { + Functions, + getFunctions, + httpsCallable, + httpsCallableFromURL +} from 'firebase/functions'; +import { getMessaging } from 'firebase/messaging'; +import { + FirebasePerformance, + getPerformance, + trace as perfTrace +} from 'firebase/performance'; +import { + getStorage, + FirebaseStorage, + ref as storageRef, + uploadString, + getDownloadURL, + StorageReference, + deleteObject +} from 'firebase/storage'; +import { getGenerativeModel, getAI, AI, VertexAIBackend } from 'firebase/ai'; +import { getDataConnect, DataConnect } from 'firebase/data-connect'; +// @ts-ignore +import { config, testAccount } from '../firebase-config'; +import 'jest'; + +describe('MODULAR', () => { + let app: FirebaseApp; + beforeAll(() => { + console.log('FIREBASE VERSION', SDK_VERSION); + app = initializeApp(config); + setLogLevel('warn'); + }); + + afterAll(() => { + signOut(getAuth(app)); + deleteApp(app); + }); + + describe('AUTH', () => { + let auth: Auth; + it('initializeAuth()', () => { + auth = initializeAuth(app); + }); + it('getAuth()', () => { + auth = getAuth(app); + }); + it('signInWithEmailAndPassword()', async () => { + const cred = await signInWithEmailAndPassword( + auth, + testAccount.email, + testAccount.password + ); + console.log('Logged in with test account', cred.user.email); + expect(cred.user.email).toBe(testAccount.email); + }); + }); + + describe('APP CHECK', () => { + let appCheck: AppCheck; + it('init appCheck', () => { + // Test uses debug token, any string is fine here. + appCheck = initializeAppCheck(app, { + provider: new ReCaptchaV3Provider('fsad') + }); + }); + it('getToken()', async () => { + await getToken(appCheck); + }); + }); + + describe('FUNCTIONS', () => { + let functions: Functions; + it('getFunctions()', () => { + functions = getFunctions(app); + }); + it('httpsCallable()', async () => { + const callTest = httpsCallable<{ data: string }, { word: string }>( + functions, + 'callTest' + ); + const result = await callTest({ data: 'blah' }); + expect(result.data.word).toBe('hellooo'); + // This takes a while. Extend timeout past default (2000) + }); + it('httpsCallableFromURL()', async () => { + const callTest = httpsCallableFromURL<{ data: string }, { word: string }>( + functions, + `https://us-central1-${app.options.projectId}.cloudfunctions.net/callTest` + ); + const result = await callTest({ data: 'blah' }); + expect(result.data.word).toBe('hellooo'); + // This takes a while. Extend timeout past default (2000) + }); + }); + + describe('STORAGE', () => { + let storage: FirebaseStorage; + let sRef: StorageReference; + let url: string; + it('getStorage()', () => { + storage = getStorage(app); + }); + it('uploadString()', async () => { + sRef = storageRef(storage, '/test-exp.txt'); + await uploadString(sRef, 'exp-efg'); + }); + it('getDownloadURL()', async () => { + url = await getDownloadURL(sRef); + expect(url).toMatch(/test-exp\.txt/); + }); + it('fetch uploaded data', async () => { + const response = await fetch(url); + const data = await response.text(); + expect(data).toBe('exp-efg'); + await deleteObject(sRef); + }); + }); + + describe('FIRESTORE', () => { + let firestore: Firestore; + it('initializeFirestore()', () => { + // fetch streams doesn't work in Jest. + // @ts-ignore I think the option is private so TS doesn't like it. + firestore = initializeFirestore(app, { useFetchStreams: false }); + }); + it('getFirestore()', () => { + firestore = getFirestore(app); + }); + it('setDoc(), getDocs(), query(), where()', async () => { + firestore = getFirestore(app); + await setDoc(doc(firestore, 'testCollection/trueDoc'), { + testbool: true + }); + // Reference doc a different way. + await setDoc(doc(collection(firestore, 'testCollection'), 'falseDoc'), { + testbool: false + }); + const trueDocs = await getDocs( + query( + collection(firestore, 'testCollection'), + where('testbool', '==', true) + ) + ); + expect(trueDocs.docs.length).toBe(1); + await deleteDoc(doc(collection(firestore, 'testCollection'), 'trueDoc')); + + await deleteDoc(doc(firestore, 'testCollection/falseDoc')); + }); + it('onSnapshot() reflects CRUD operations', async () => { + firestore = getFirestore(app); + const testDocRef = doc(firestore, 'testCollection/testDoc'); + let expectedData: any = {}; + const unsub = onSnapshot(testDocRef, snap => { + if (snap.exists()) { + expect(snap.data()).toEqual(expectedData); + } else { + expect(expectedData).toBeNull; + } + }); + expectedData = { word: 'hi', number: 14 }; + await setDoc(testDocRef, { word: 'hi', number: 14 }); + expectedData = { word: 'bye', number: 14, newProp: ['a'] }; + await updateDoc(testDocRef, { word: 'bye', newProp: ['a'] }); + expectedData = null; + await deleteDoc(testDocRef); + unsub(); + }); + }); + + describe('DATABASE', () => { + let db: Database; + it('getDatabase', () => { + db = getDatabase(app); + }); + it('onValue() reflects CRUD operations', async () => { + const ref = dbRef(db, 'abc/def'); + let expectedValue: any = {}; + onValue(ref, snap => { + if (snap.exists()) { + expect(snap.val()).toEqual(expectedValue); + } else { + expect(expectedValue).toBeNull; + } + }); + expectedValue = { text: 'string 123 xyz' }; + await set(ref, { text: 'string 123 xyz' }); + expectedValue.number = 987; + await update(ref, { number: 987 }); + expectedValue = null; + await remove(ref); + off(ref); + }); + }); + + describe('MESSAGING', () => { + it('getMessaging()', () => { + // @ts-ignore Stub missing browser APIs that FCM depends on + window.indexedDB = { open: () => Promise.resolve() }; + // @ts-ignore Stub missing browser APIs that FCM depends on + navigator.serviceWorker = { addEventListener: () => {} }; + getMessaging(app); + // @ts-ignore + delete window.indexedDB; + // @ts-ignore + delete navigator.serviceWorker; + }); + }); + + describe('ANALYTICS', () => { + let analytics: Analytics; + it('analyticsIsSupported()', () => { + analyticsIsSupported(); + }); + it('getAnalytics()', () => { + const warn = jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); + analytics = getAnalytics(app); + expect(warn).toHaveBeenCalledWith( + expect.stringMatching('@firebase/analytics'), + expect.stringMatching(/IndexedDB unavailable/) + ); + warn.mockRestore(); + }); + it("logEvent() doesn't error", () => { + logEvent(analytics, 'begin_checkout'); + }); + }); + + describe('PERFORMANCE', () => { + let performance: FirebasePerformance; + it('getPerformance()', () => { + performance = getPerformance(app); + }); + it('trace()', () => { + const trace = perfTrace(performance, 'test'); + trace.start(); + trace.stop(); + trace.putAttribute('testattr', 'perftestvalue'); + expect(trace.getAttribute('testattr')).toBe('perftestvalue'); + }); + }); + + describe('AI', () => { + let ai: AI; + it('getVertexAI()', () => { + ai = getAI(app, { backend: new VertexAIBackend() }); + }); + it('getGenerativeModel() and countTokens()', async () => { + const model = getGenerativeModel(ai, { model: 'gemini-2.5-flash' }); + expect(model.model).toMatch(/gemini-2.5-flash$/); + const result = await model.countTokens('abcdefg'); + expect(result.totalTokens).toBeTruthy; + }); + }); + + describe('DATA CONNECT', () => { + let dataConnect: DataConnect; + it('getDataConnect()', () => { + dataConnect = getDataConnect(app, { + location: 'a-location', + connector: 'a-connector', + service: 'service' + }); + }); + it('dataConnect.getSettings()', () => { + expect(dataConnect.getSettings().location).toBe('a-location'); + }); + }); +}); diff --git a/e2e/smoke-tests/tsconfig.json b/e2e/smoke-tests/tsconfig.json new file mode 100644 index 00000000000..c09f694edf7 --- /dev/null +++ b/e2e/smoke-tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "typeRoots": ["./node_modules/@types"] + } +} diff --git a/e2e/smoke-tests/webpack.config.js b/e2e/smoke-tests/webpack.config.js new file mode 100644 index 00000000000..b2e4c25f62e --- /dev/null +++ b/e2e/smoke-tests/webpack.config.js @@ -0,0 +1,96 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This config is used for the sample app. The tests do not use webpack. + */ +var path = require('path'); + +module.exports = [ + { + name: 'compat', + mode: 'development', + entry: './sample-apps/compat.js', + output: { + path: path.resolve(__dirname, 'build'), + filename: 'app.bundle.js' + }, + module: { + rules: [ + { + test: /\.js$/, + loader: 'babel-loader', + options: { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: '10' + } + } + ] + ] + } + } + ] + }, + stats: { + colors: true + }, + devtool: 'source-map', + devServer: { + static: './build' + } + }, + { + name: 'modular', + mode: 'development', + entry: './sample-apps/modular.js', + output: { + path: path.resolve(__dirname, 'build'), + filename: 'app.bundle.js' + }, + module: { + rules: [ + { + test: /\.js$/, + loader: 'babel-loader', + options: { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: '10' + } + } + ] + ] + } + } + ] + }, + stats: { + colors: true + }, + devtool: 'source-map', + devServer: { + static: './build' + } + } +]; diff --git a/e2e/smoke-tests/yarn.lock b/e2e/smoke-tests/yarn.lock new file mode 100644 index 00000000000..b9a6d8e4451 --- /dev/null +++ b/e2e/smoke-tests/yarn.lock @@ -0,0 +1,5790 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz" + integrity sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.9": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.0.tgz" + integrity sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA== + +"@babel/compat-data@^7.26.5", "@babel/compat-data@^7.26.8": + version "7.26.8" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz" + integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ== + +"@babel/core@7.26.8": + version "7.26.8" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz" + integrity sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.8" + "@babel/helper-compilation-targets" "^7.26.5" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.7" + "@babel/parser" "^7.26.8" + "@babel/template" "^7.26.8" + "@babel/traverse" "^7.26.8" + "@babel/types" "^7.26.8" + "@types/gensync" "^1.0.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz" + integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.26.0" + "@babel/generator" "^7.26.0" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.0" + "@babel/parser" "^7.26.0" + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.26.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.25.9", "@babel/generator@^7.26.0": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.26.0.tgz" + integrity sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w== + dependencies: + "@babel/parser" "^7.26.0" + "@babel/types" "^7.26.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/generator@^7.26.8", "@babel/generator@^7.26.9": + version "7.26.9" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz" + integrity sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg== + dependencies: + "@babel/parser" "^7.26.9" + "@babel/types" "^7.26.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/generator@^7.7.2": + version "7.26.3" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz" + integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== + dependencies: + "@babel/parser" "^7.26.3" + "@babel/types" "^7.26.3" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz" + integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== + dependencies: + "@babel/types" "^7.25.9" + +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz" + integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== + dependencies: + "@babel/compat-data" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-compilation-targets@^7.26.5": + version "7.26.5" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz" + integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA== + dependencies: + "@babel/compat-data" "^7.26.5" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz" + integrity sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/traverse" "^7.25.9" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz" + integrity sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + regexpu-core "^6.1.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.2": + version "0.6.2" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz" + integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-define-polyfill-provider@^0.6.3": + version "0.6.3" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz" + integrity sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-member-expression-to-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz" + integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-optimise-call-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz" + integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== + dependencies: + "@babel/types" "^7.25.9" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.8.0": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz" + integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== + +"@babel/helper-plugin-utils@^7.26.5": + version "7.26.5" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz" + integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg== + +"@babel/helper-remap-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz" + integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-wrap-function" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-replace-supers@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz" + integrity sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/helper-simple-access@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz" + integrity sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz" + integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helper-wrap-function@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz" + integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== + dependencies: + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helpers@^7.26.0": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz" + integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== + dependencies: + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.0" + +"@babel/helpers@^7.26.7": + version "7.26.9" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz" + integrity sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA== + dependencies: + "@babel/template" "^7.26.9" + "@babel/types" "^7.26.9" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.26.3": + version "7.26.3" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz" + integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== + dependencies: + "@babel/types" "^7.26.3" + +"@babel/parser@^7.25.9", "@babel/parser@^7.26.0": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.26.0.tgz" + integrity sha512-aP8x5pIw3xvYr/sXT+SEUwyhrXT8rUJRZltK/qN3Db80dcKpTett8cJxHyjk+xYSVXvNnl2SfcJVjbwxpOSscA== + dependencies: + "@babel/types" "^7.26.0" + +"@babel/parser@^7.26.8", "@babel/parser@^7.26.9": + version "7.26.9" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz" + integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A== + dependencies: + "@babel/types" "^7.26.9" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz" + integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz" + integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz" + integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz" + integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz" + integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-import-assertions@^7.26.0": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz" + integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-import-attributes@^7.24.7", "@babel/plugin-syntax-import-attributes@^7.26.0": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz" + integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.25.9", "@babel/plugin-syntax-jsx@^7.7.2": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz" + integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.25.9", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz" + integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz" + integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-async-generator-functions@^7.26.8": + version "7.26.8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz" + integrity sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg== + dependencies: + "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-remap-async-to-generator" "^7.25.9" + "@babel/traverse" "^7.26.8" + +"@babel/plugin-transform-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz" + integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" + +"@babel/plugin-transform-block-scoped-functions@^7.26.5": + version "7.26.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz" + integrity sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ== + dependencies: + "@babel/helper-plugin-utils" "^7.26.5" + +"@babel/plugin-transform-block-scoping@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz" + integrity sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-class-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz" + integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-class-static-block@^7.26.0": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz" + integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-classes@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz" + integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/traverse" "^7.25.9" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz" + integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/template" "^7.25.9" + +"@babel/plugin-transform-destructuring@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz" + integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-dotall-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz" + integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-duplicate-keys@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz" + integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz" + integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-dynamic-import@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz" + integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-exponentiation-operator@^7.26.3": + version "7.26.3" + resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz" + integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-export-namespace-from@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz" + integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-for-of@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz" + integrity sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-function-name@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz" + integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== + dependencies: + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-transform-json-strings@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz" + integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz" + integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-logical-assignment-operators@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz" + integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-member-expression-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz" + integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-modules-amd@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz" + integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== + dependencies: + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-modules-commonjs@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz" + integrity sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg== + dependencies: + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-simple-access" "^7.25.9" + +"@babel/plugin-transform-modules-commonjs@^7.26.3": + version "7.26.3" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz" + integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== + dependencies: + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-modules-systemjs@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz" + integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== + dependencies: + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-transform-modules-umd@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz" + integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== + dependencies: + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz" + integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-new-target@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz" + integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.26.6": + version "7.26.6" + resolved "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz" + integrity sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw== + dependencies: + "@babel/helper-plugin-utils" "^7.26.5" + +"@babel/plugin-transform-numeric-separator@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz" + integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-object-rest-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz" + integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== + dependencies: + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + +"@babel/plugin-transform-object-super@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz" + integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + +"@babel/plugin-transform-optional-catch-binding@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz" + integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz" + integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-parameters@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz" + integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-private-methods@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz" + integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-private-property-in-object@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz" + integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-property-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz" + integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-regenerator@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz" + integrity sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-regexp-modifiers@^7.26.0": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz" + integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-reserved-words@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz" + integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-shorthand-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz" + integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz" + integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-sticky-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz" + integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-template-literals@^7.26.8": + version "7.26.8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz" + integrity sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q== + dependencies: + "@babel/helper-plugin-utils" "^7.26.5" + +"@babel/plugin-transform-typeof-symbol@^7.26.7": + version "7.26.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz" + integrity sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw== + dependencies: + "@babel/helper-plugin-utils" "^7.26.5" + +"@babel/plugin-transform-typescript@^7.25.9": + version "7.26.3" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz" + integrity sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-syntax-typescript" "^7.25.9" + +"@babel/plugin-transform-unicode-escapes@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz" + integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-property-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz" + integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz" + integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-sets-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz" + integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/preset-env@7.26.8": + version "7.26.8" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.8.tgz" + integrity sha512-um7Sy+2THd697S4zJEfv/U5MHGJzkN2xhtsR3T/SWRbVSic62nbISh51VVfU9JiO/L/Z97QczHTaFVkOU8IzNg== + dependencies: + "@babel/compat-data" "^7.26.8" + "@babel/helper-compilation-targets" "^7.26.5" + "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.26.0" + "@babel/plugin-syntax-import-attributes" "^7.26.0" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.25.9" + "@babel/plugin-transform-async-generator-functions" "^7.26.8" + "@babel/plugin-transform-async-to-generator" "^7.25.9" + "@babel/plugin-transform-block-scoped-functions" "^7.26.5" + "@babel/plugin-transform-block-scoping" "^7.25.9" + "@babel/plugin-transform-class-properties" "^7.25.9" + "@babel/plugin-transform-class-static-block" "^7.26.0" + "@babel/plugin-transform-classes" "^7.25.9" + "@babel/plugin-transform-computed-properties" "^7.25.9" + "@babel/plugin-transform-destructuring" "^7.25.9" + "@babel/plugin-transform-dotall-regex" "^7.25.9" + "@babel/plugin-transform-duplicate-keys" "^7.25.9" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-dynamic-import" "^7.25.9" + "@babel/plugin-transform-exponentiation-operator" "^7.26.3" + "@babel/plugin-transform-export-namespace-from" "^7.25.9" + "@babel/plugin-transform-for-of" "^7.25.9" + "@babel/plugin-transform-function-name" "^7.25.9" + "@babel/plugin-transform-json-strings" "^7.25.9" + "@babel/plugin-transform-literals" "^7.25.9" + "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" + "@babel/plugin-transform-member-expression-literals" "^7.25.9" + "@babel/plugin-transform-modules-amd" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.26.3" + "@babel/plugin-transform-modules-systemjs" "^7.25.9" + "@babel/plugin-transform-modules-umd" "^7.25.9" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-new-target" "^7.25.9" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.26.6" + "@babel/plugin-transform-numeric-separator" "^7.25.9" + "@babel/plugin-transform-object-rest-spread" "^7.25.9" + "@babel/plugin-transform-object-super" "^7.25.9" + "@babel/plugin-transform-optional-catch-binding" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + "@babel/plugin-transform-private-methods" "^7.25.9" + "@babel/plugin-transform-private-property-in-object" "^7.25.9" + "@babel/plugin-transform-property-literals" "^7.25.9" + "@babel/plugin-transform-regenerator" "^7.25.9" + "@babel/plugin-transform-regexp-modifiers" "^7.26.0" + "@babel/plugin-transform-reserved-words" "^7.25.9" + "@babel/plugin-transform-shorthand-properties" "^7.25.9" + "@babel/plugin-transform-spread" "^7.25.9" + "@babel/plugin-transform-sticky-regex" "^7.25.9" + "@babel/plugin-transform-template-literals" "^7.26.8" + "@babel/plugin-transform-typeof-symbol" "^7.26.7" + "@babel/plugin-transform-unicode-escapes" "^7.25.9" + "@babel/plugin-transform-unicode-property-regex" "^7.25.9" + "@babel/plugin-transform-unicode-regex" "^7.25.9" + "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.11.0" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.40.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-typescript@7.26.0": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz" + integrity sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.25.9" + "@babel/plugin-transform-typescript" "^7.25.9" + +"@babel/runtime@^7.8.4": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.25.9", "@babel/template@^7.3.3": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/template@^7.26.8", "@babel/template@^7.26.9": + version "7.26.9" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz" + integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/parser" "^7.26.9" + "@babel/types" "^7.26.9" + +"@babel/traverse@^7.25.9": + version "7.25.9" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz" + integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/generator" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/template" "^7.25.9" + "@babel/types" "^7.25.9" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/traverse@^7.26.8": + version "7.26.9" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz" + integrity sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.9" + "@babel/parser" "^7.26.9" + "@babel/template" "^7.26.9" + "@babel/types" "^7.26.9" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.26.3", "@babel/types@^7.3.3": + version "7.26.3" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz" + integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.4.4": + version "7.26.0" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz" + integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@babel/types@^7.26.8", "@babel/types@^7.26.9": + version "7.26.9" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz" + integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@firebase/ai@1.3.0-20250512211235": + version "1.3.0-20250512211235" + resolved "https://registry.npmjs.org/@firebase/ai/-/ai-1.3.0-20250512211235.tgz" + integrity sha512-L/3vFDzzmAScgRVcZXiqyQHLXTjyBvfC4bVHvsHGlUyBYjlS+xcPUFdNHFowM84r6qyp/X94pO58ERgRO/CUCg== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/component" "0.6.14" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.11.1" + tslib "^2.1.0" + +"@firebase/analytics-compat@0.2.20-20250512211235": + version "0.2.20-20250512211235" + resolved "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.20-20250512211235.tgz#725ebf8e94ad9aba4ed22f79206c1a2420190a82" + integrity sha512-2MmqqNlHlbs6454pOnKnNbtz8cF54UYgCiFWc0tXV6VNrSAGAyWgY3Ujve5+ayuSmzMuTaLNKk8kOPC90nNmCA== + dependencies: + "@firebase/analytics" "0.10.14-20250512211235" + "@firebase/analytics-types" "0.8.3" + "@firebase/component" "0.6.15-20250512211235" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/analytics-types@0.8.3": + version "0.8.3" + resolved "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz" + integrity sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg== + +"@firebase/analytics@0.10.14-20250512211235": + version "0.10.14-20250512211235" + resolved "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.14-20250512211235.tgz#0c9fb8e352e0e641168d1968e616a0c775a4392f" + integrity sha512-yPo0D9Ec5jSWRmI6JYt8PCzTVUHtTvnmhN7s09Wh2ckrbv7kgejIAYOjKOK30Umro+teA6wTsY07Q27C1dOM4A== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/installations" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/app-check-compat@0.3.23-20250512211235": + version "0.3.23-20250512211235" + resolved "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.23-20250512211235.tgz#f060c925f2bcd936299504a5f34bc81ea872388f" + integrity sha512-iI2mGM/f8z4aQhoB57HnErh8vCqyQJFVQS7USLy6dUyL15FcIe5HQ88N4oD9glagu4IDRlZ+Rohlrw9ObbyNxA== + dependencies: + "@firebase/app-check" "0.10.0-20250512211235" + "@firebase/app-check-types" "0.5.3" + "@firebase/component" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/app-check-interop-types@0.3.3": + version "0.3.3" + resolved "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz" + integrity sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A== + +"@firebase/app-check-types@0.5.3": + version "0.5.3" + resolved "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz" + integrity sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng== + +"@firebase/app-check@0.10.0-20250512211235": + version "0.10.0-20250512211235" + resolved "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.10.0-20250512211235.tgz#304ae95ed148d0837f64b94527d6b2b68966b117" + integrity sha512-zFbNyt3j6ixg6/Gh3WdA+FvMhUxIRASEq/NRC+Jtofm5la5ZJ88wdLDJ/kSHtdMINQfKuneHbJJ832u5BTqpYQ== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/app-compat@0.4.0-20250512211235": + version "0.4.0-20250512211235" + resolved "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.0-20250512211235.tgz#4ae94412aa00afcf4663919af564e321ea17b335" + integrity sha512-epnyKsA97L0FMigDHWOcgJ2TeMnlpuyiGIVZdnQiElKfRs3UFgDvBb6AK5dHgT4REe7ZbXqGa/8TSUUGFhEcmw== + dependencies: + "@firebase/app" "0.13.0-20250512211235" + "@firebase/component" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/app-types@0.9.3": + version "0.9.3" + resolved "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz" + integrity sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw== + +"@firebase/app@0.13.0-20250512211235": + version "0.13.0-20250512211235" + resolved "https://registry.npmjs.org/@firebase/app/-/app-0.13.0-20250512211235.tgz#b8028efcd45ac07b3304ac361008f08b0cfa445f" + integrity sha512-9qcJX9fKMFuZf5+8BxK0Dg1gJU+x4shoH+VFiKnZ7ri6sJ0OP67Y4PnHDYdzKmx7/HJt0KSyZMDUX/hT2Wbl/g== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/auth-compat@0.5.23-20250512211235": + version "0.5.23-20250512211235" + resolved "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.23-20250512211235.tgz#130b8583e137bae9b7e288e8991583ff04b1ab25" + integrity sha512-dWBbbYAiNmrD6+P5m2TgQ9GWPIffjedQ9HRXnPyqKytTiEQzxvYBd91NS3y3YI6bBg1RWAWccMnwtQVjl1eK6A== + dependencies: + "@firebase/auth" "1.10.3-20250512211235" + "@firebase/auth-types" "0.13.0" + "@firebase/component" "0.6.15-20250512211235" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/auth-interop-types@0.2.4": + version "0.2.4" + resolved "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz" + integrity sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA== + +"@firebase/auth-types@0.13.0": + version "0.13.0" + resolved "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz" + integrity sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg== + +"@firebase/auth@1.10.3-20250512211235": + version "1.10.3-20250512211235" + resolved "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.3-20250512211235.tgz#47f55c228e4eb441050914c6e545ccb1f021a712" + integrity sha512-f7Lov3vogDMyroncY7OipDmNL5bqZcDxdIVe1qN/LEaaKygG2wPxKMa+LjSFwxwo/oGp/i6R4ixT8otMkOL2MQ== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/component@0.6.14": + version "0.6.14" + resolved "https://registry.npmjs.org/@firebase/component/-/component-0.6.14.tgz" + integrity sha512-kf/zAT8GQJ9nYoHuj0mv7twp1QzifKYrO+GsmsVHHM+Hi9KkmI7E3B3J0CtihHpb34vinl4gbJrYJ2p2wfvc9A== + dependencies: + "@firebase/util" "1.11.1" + tslib "^2.1.0" + +"@firebase/component@0.6.15-20250512211235": + version "0.6.15-20250512211235" + resolved "https://registry.npmjs.org/@firebase/component/-/component-0.6.15-20250512211235.tgz#91f4f234904480f34d26aa41922b8cf4603944c2" + integrity sha512-8U//EzcIE6frUcWUMcmrVzWBAXLBZt2eGWwt8Of1y+yU6t3Zwwu+1JpCz0hbiHpzXnkuTFjPwWA6fKgZQTk0MQ== + dependencies: + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/data-connect@0.3.6-20250512211235": + version "0.3.6-20250512211235" + resolved "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.6-20250512211235.tgz#f20ee3c0cc576f000244ad769e1813151e2aa3cd" + integrity sha512-ZMs7lX9/Sd7XNRaylht535sY1mFUasLCY0toKQWF4KIAdUeij0TNzqWpeULNqNJXtczu+eRrG3Nct1rY4JtiiQ== + dependencies: + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/database-compat@2.0.7-20250512211235": + version "2.0.7-20250512211235" + resolved "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.7-20250512211235.tgz#0d78c287cab62583d46525c690469576787aca22" + integrity sha512-8bt/9/EXqxce32RDg254yfkEUsG9x9h3K6+IFdsCeLhPH9mQLosrcRBSHusHGX65oNbY1ATgxkt/+v+uhoj+nw== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/database" "1.0.16-20250512211235" + "@firebase/database-types" "1.0.12-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/database-types@1.0.12-20250512211235": + version "1.0.12-20250512211235" + resolved "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.12-20250512211235.tgz#4b89e43e99e0fb621fc4abcec792d027c0669f44" + integrity sha512-FsZ/VeSyNLaJWQGLP0Yy955Zonlm/WSPhpdOpof84ToKAekUQobxlI08skPe+Gk2dAUnKrdqfGJPO9pagsByYA== + dependencies: + "@firebase/app-types" "0.9.3" + "@firebase/util" "1.12.0-20250512211235" + +"@firebase/database@1.0.16-20250512211235": + version "1.0.16-20250512211235" + resolved "https://registry.npmjs.org/@firebase/database/-/database-1.0.16-20250512211235.tgz#223f45e9010574d361cbaa53e56b51dab17a804d" + integrity sha512-l2jVH/uCzbuAhCYk37+YWFHE2nIkGyTiFL3RthkDvoT+KHDzvaIxnWmjnbAgrQEYSHaimziiw0DmpQM+Safb0g== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/firestore-compat@0.3.48-20250512211235": + version "0.3.48-20250512211235" + resolved "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.48-20250512211235.tgz#547d62fd41bc15495c26f9675d9ed24ff3c3f50c" + integrity sha512-8nWD3QRkN9rUoApULVfBMg112QNYstUWRm58cMwcdp0Wkx6+RV+9YraDYzg7GmsEklL4CnGF36HGNrtzraxOTg== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/firestore" "4.7.13-20250512211235" + "@firebase/firestore-types" "3.0.3" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/firestore-types@3.0.3": + version "3.0.3" + resolved "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz" + integrity sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q== + +"@firebase/firestore@4.7.13-20250512211235": + version "4.7.13-20250512211235" + resolved "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.13-20250512211235.tgz#cdebb6b7ddba2b7738896458327ae064bb6b540e" + integrity sha512-m3xEnbYrsgp58a5if00Vqrrglm/22yU7amZ3CjwqUD+/M6TFSf1a9lvMqF9S33AuPbwHDxUb3hJ4ogpTQ+uSjQ== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + "@firebase/webchannel-wrapper" "1.0.3" + "@grpc/grpc-js" "~1.9.0" + "@grpc/proto-loader" "^0.7.8" + tslib "^2.1.0" + +"@firebase/functions-compat@0.3.22-20250512211235": + version "0.3.22-20250512211235" + resolved "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.22-20250512211235.tgz#2ce718de863e7a26a9ec2f7e19ab6a15cb375306" + integrity sha512-/CrVsUlcVKoCtuzpYzIfBYjdiCBtpMkDIY3M2/MIKv5gwB90ydX4E2OP/E57Ne85eiYfezYoGurt27OXSno/Fg== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/functions" "0.12.5-20250512211235" + "@firebase/functions-types" "0.6.3" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/functions-types@0.6.3": + version "0.6.3" + resolved "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz" + integrity sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg== + +"@firebase/functions@0.12.5-20250512211235": + version "0.12.5-20250512211235" + resolved "https://registry.npmjs.org/@firebase/functions/-/functions-0.12.5-20250512211235.tgz#17696d8918136e319459628a43ea45e08e9bfa39" + integrity sha512-3BTjMHjx4ulr22+G1/ut8ZQW6OTGmpS5LC+2MhTl/W8OgSYP+3Hc844OhhlDq4kBjcWO752PG2+I1JwJNK6rPA== + dependencies: + "@firebase/app-check-interop-types" "0.3.3" + "@firebase/auth-interop-types" "0.2.4" + "@firebase/component" "0.6.15-20250512211235" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/installations-compat@0.2.15-20250512211235": + version "0.2.15-20250512211235" + resolved "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.15-20250512211235.tgz#5c91a48c29c358164fe34f254f1509b7404373ca" + integrity sha512-i7sV28ZLE8p65lHyy/vni93aiIKIyo7PuOUNyaMnfdam++Sv2mBkNj0koo9B6LidDaWxZ3gZJ2bTxSyPOOukYg== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/installations" "0.6.15-20250512211235" + "@firebase/installations-types" "0.5.3" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/installations-types@0.5.3": + version "0.5.3" + resolved "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz" + integrity sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA== + +"@firebase/installations@0.6.15-20250512211235": + version "0.6.15-20250512211235" + resolved "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.15-20250512211235.tgz#c0b5b07feda48a92b72e7d919a2a2f524a632b35" + integrity sha512-RinZjbNpImsz0JGi6u6B6Yb7ezrE6ej2pdBT7K3ju1cScpBmKRBeWSl7AevD5PEH+W9E5i6U2VX+q2gycI3ZRA== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/util" "1.12.0-20250512211235" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/logger@0.4.4": + version "0.4.4" + resolved "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz" + integrity sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g== + dependencies: + tslib "^2.1.0" + +"@firebase/messaging-compat@0.2.19-20250512211235": + version "0.2.19-20250512211235" + resolved "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.19-20250512211235.tgz#1980cc4c931085742e568596d7b28af0853aa020" + integrity sha512-vTL0hMyPosU/WRvZ1xmi01aURJyjUI3zeBByYnnwSUJN01eF0YX7eM1RId/HSFcHoT/HgfsHFvINUFjy58TVUQ== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/messaging" "0.12.19-20250512211235" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/messaging-interop-types@0.2.3": + version "0.2.3" + resolved "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz" + integrity sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q== + +"@firebase/messaging@0.12.19-20250512211235": + version "0.12.19-20250512211235" + resolved "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.19-20250512211235.tgz#f225f480879c8a550d2927d314ae62e17f30355f" + integrity sha512-h4fraiJuJrLSOSI6WIFlbV1u6L6/8zPfTzcNxqyjCIVj5eFVpgoLIW5lQ2O+G6h6RQiWG29AJkuWGnp26BOJ6Q== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/installations" "0.6.15-20250512211235" + "@firebase/messaging-interop-types" "0.2.3" + "@firebase/util" "1.12.0-20250512211235" + idb "7.1.1" + tslib "^2.1.0" + +"@firebase/performance-compat@0.2.17-20250512211235": + version "0.2.17-20250512211235" + resolved "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.17-20250512211235.tgz#ead4a1201effd57aef5a8e1a4353196d6bde9b41" + integrity sha512-5uUlGWbEyoTzGa0JaS6OIUl5T69VwK4gYRAjcET79kuVSZU85l4M1gsDbXqhl/YH1o/gpQEPmm1T1UrK9bRQbw== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/performance" "0.7.4-20250512211235" + "@firebase/performance-types" "0.2.3" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/performance-types@0.2.3": + version "0.2.3" + resolved "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz" + integrity sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ== + +"@firebase/performance@0.7.4-20250512211235": + version "0.7.4-20250512211235" + resolved "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.4-20250512211235.tgz#fadf55c6e8072320e8660e1b8dd97e3d3561650f" + integrity sha512-defCXX20kxX05/3b4qI+V2PreeH/qwn1Egvp82geS+hGyK6cDRFGgV7q94tIcc6idQJoRhsSW32ABKdI99XpWg== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/installations" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + web-vitals "^4.2.4" + +"@firebase/remote-config-compat@0.2.15-20250512211235": + version "0.2.15-20250512211235" + resolved "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.15-20250512211235.tgz#cb391f23f46aea88e3df0a7f1edb15d24000e965" + integrity sha512-DiHaFVywKBG1GGziN/NA5k8qkvJ68gX+AFe63yQOmzAfvH59W+dbxAgSG116pGrfF3xDH/e/lho4Cs3M6tbL8g== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/remote-config" "0.6.2-20250512211235" + "@firebase/remote-config-types" "0.4.0" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/remote-config-types@0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz" + integrity sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg== + +"@firebase/remote-config@0.6.2-20250512211235": + version "0.6.2-20250512211235" + resolved "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.2-20250512211235.tgz#c19a7057fdb4493efc70c065cebd85b0841744ee" + integrity sha512-FR/IDz/AkQiSXXZw80OJEl/MT++URfbrrUDev3p1Dgc5yuz2hGCReuiq/YYVTA3n/yppwY+BnRdVidV9dRaHSQ== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/installations" "0.6.15-20250512211235" + "@firebase/logger" "0.4.4" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/storage-compat@0.3.19-20250512211235": + version "0.3.19-20250512211235" + resolved "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.19-20250512211235.tgz#d81a8cd4fb7c5222d11e8c4f67f83f48c8d064f6" + integrity sha512-Qz3kRo4qc6wRSDq/sCRT9ITqT/Of4puErdhideN+GBi40m42bLWOqBkxpkzPmHphOmFNwKUBA8/9Cksr/fj6IA== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/storage" "0.13.9-20250512211235" + "@firebase/storage-types" "0.8.3" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/storage-types@0.8.3": + version "0.8.3" + resolved "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz" + integrity sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg== + +"@firebase/storage@0.13.9-20250512211235": + version "0.13.9-20250512211235" + resolved "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.9-20250512211235.tgz#1d809b25797bd118ee9b83ac220d8a6ae0eafed8" + integrity sha512-68vuBOmYVUu93Tbri++ubk8ECASRQWYnB1cvPpfBT0WoLSgdgxj+2F/+buXqzFZT/3ae8FBcEdgK6cD0gcT7tA== + dependencies: + "@firebase/component" "0.6.15-20250512211235" + "@firebase/util" "1.12.0-20250512211235" + tslib "^2.1.0" + +"@firebase/util@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@firebase/util/-/util-1.11.1.tgz" + integrity sha512-RXg4WE8C2LUrvoV/TMGRTu223zZf9Dq9MR8yHZio9nF9TpLnpCPURw9VWWB2WATDl6HfIdWfl2x2SJYtHkN4hw== + dependencies: + tslib "^2.1.0" + +"@firebase/util@1.12.0-20250512211235": + version "1.12.0-20250512211235" + resolved "https://registry.npmjs.org/@firebase/util/-/util-1.12.0-20250512211235.tgz" + integrity sha512-o7CkpIAymb6irDYCoK0wKZiEMMgHX4oGdsXEXvsIPBWjoZsUavF6HH4iYVGslL4KkasEEjJiLNiomxxGbJPqKg== + dependencies: + tslib "^2.1.0" + +"@firebase/webchannel-wrapper@1.0.3": + version "1.0.3" + resolved "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz" + integrity sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ== + +"@grpc/grpc-js@~1.9.0": + version "1.9.15" + resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz" + integrity sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.8": + version "0.7.13" + resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz" + integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": + version "0.1.3" + resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + dependencies: + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jsonjoy.com/base64@^1.1.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/json-pack@^1.0.3": + version "1.1.0" + resolved "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.0.tgz" + integrity sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg== + dependencies: + "@jsonjoy.com/base64" "^1.1.1" + "@jsonjoy.com/util" "^1.1.2" + hyperdyperid "^1.2.0" + thingies "^1.20.0" + +"@jsonjoy.com/util@^1.1.2", "@jsonjoy.com/util@^1.3.0": + version "1.5.0" + resolved "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz" + integrity sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA== + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/babel__core@^7.1.14": + version "7.20.5" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.8" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz" + integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.20.6" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz" + integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== + dependencies: + "@babel/types" "^7.20.7" + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.13": + version "3.5.13" + resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.5.4": + version "1.5.4" + resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.7": + version "3.7.7" + resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "9.6.1" + resolved "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" + integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.6": + version "1.0.7" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": + version "5.0.1" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz" + integrity sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.19.6" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz" + integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "5.0.0" + resolved "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz" + integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/gensync@^1.0.0": + version "1.0.4" + resolved "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz" + integrity sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA== + +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.15" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz" + integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.6" + resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.4" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@29.5.14": + version "29.5.14" + resolved "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz" + integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/jsdom@^20.0.0": + version "20.0.1" + resolved "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz" + integrity sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" + +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "22.8.0" + resolved "https://registry.npmjs.org/@types/node/-/node-22.8.0.tgz" + integrity sha512-84rafSBHC/z1i1E3p0cJwKA+CfYDNSXX9WSZBRopjIzLET8oNt6ht2tei4C7izwDeEiLLfdeSVBv1egOH916hg== + dependencies: + undici-types "~6.19.8" + +"@types/qs@*": + version "6.9.16" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz" + integrity sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/retry@0.12.2": + version "0.12.2" + resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz" + integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.4": + version "1.9.4" + resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.15.5": + version "1.15.7" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/sockjs@^0.3.36": + version "0.3.36" + resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.3" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + +"@types/ws@^8.5.10": + version "8.5.12" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + dependencies: + "@types/node" "*" + +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^17.0.8": + version "17.0.33" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz" + integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== + dependencies: + "@types/yargs-parser" "*" + +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" + +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" + +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.1": + version "2.1.1" + resolved "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.2": + version "2.0.2" + resolved "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.5": + version "2.0.5" + resolved "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abab@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz" + integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== + dependencies: + acorn "^8.1.0" + acorn-walk "^8.0.2" + +acorn-walk@^8.0.2, acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +acorn@^8.1.0, acorn@^8.11.0, acorn@^8.4.1, acorn@^8.8.1: + version "8.14.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + +acorn@^8.14.0: + version "8.14.1" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + +acorn@^8.8.2: + version "8.13.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz" + integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== + +agent-base@6: + version "6.0.2" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +babel-jest@29.7.0, babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-loader@8.4.1: + version "8.4.1" + resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz" + integrity sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^2.0.4" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.11" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz" + integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.6.2" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.11.0: + version "0.11.1" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz" + integrity sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.3" + core-js-compat "^3.40.0" + +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.2" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz" + integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.2" + +babel-preset-current-node-syntax@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz" + integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.24.0: + version "4.24.2" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz" + integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== + dependencies: + caniuse-lite "^1.0.30001669" + electron-to-chromium "^1.5.41" + node-releases "^2.0.18" + update-browserslist-db "^1.1.1" + +browserslist@^4.24.4: + version "4.24.4" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== + dependencies: + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001669: + version "1.0.30001669" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz" + integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w== + +caniuse-lite@^1.0.30001688: + version "1.0.30001702" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz" + integrity sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +cjs-module-lexer@^1.0.0: + version "1.4.1" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz" + integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== + +core-js-compat@^3.40.0: + version "3.41.0" + resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz" + integrity sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A== + dependencies: + browserslist "^4.24.4" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +data-urls@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz" + integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== + dependencies: + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: + version "4.3.7" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +decimal.js@^10.4.2: + version "10.4.3" + resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + +dedent@^1.0.0: + version "1.5.3" + resolved "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz" + integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +default-browser-id@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz" + integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== + +default-browser@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz" + integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.41: + version "1.5.45" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.45.tgz" + integrity sha512-vOzZS6uZwhhbkZbcRyiy99Wg+pYFV5hk+5YaECvx0+Z31NR3Tt5zS6dze2OepT6PCTzVzT0dIJItti+uAW5zmw== + +electron-to-chromium@^1.5.73: + version "1.5.112" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.112.tgz" + integrity sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +enhanced-resolve@^5.17.1: + version "5.18.1" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz" + integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +envinfo@^7.7.3: + version "7.14.0" + resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz" + integrity sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.6.0" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz" + integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escodegen@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + +express@^4.21.2: + version "4.21.2" + resolved "https://registry.npmjs.org/express/-/express-4.21.2.tgz" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.7.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.3.1" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.12" + proxy-addr "~2.0.7" + qs "6.13.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.2" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-uri@^3.0.1: + version "3.0.3" + resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz" + integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +faye-websocket@0.11.4, faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +firebase@^11.8.0-20250512211235: + version "11.8.0-20250512211235" + resolved "https://registry.npmjs.org/firebase/-/firebase-11.8.0-20250512211235.tgz" + integrity sha512-0juBB1XpC52s4BF/6OwJd7ka8imyXIIbsnjp6yVr1n8W0GpYIpvXzKP0oahiV2Wiv9l0DfVRZpsMB0OJvk9nQw== + dependencies: + "@firebase/ai" "1.3.0-20250512211235" + "@firebase/analytics" "0.10.14-20250512211235" + "@firebase/analytics-compat" "0.2.20-20250512211235" + "@firebase/app" "0.13.0-20250512211235" + "@firebase/app-check" "0.10.0-20250512211235" + "@firebase/app-check-compat" "0.3.23-20250512211235" + "@firebase/app-compat" "0.4.0-20250512211235" + "@firebase/app-types" "0.9.3" + "@firebase/auth" "1.10.3-20250512211235" + "@firebase/auth-compat" "0.5.23-20250512211235" + "@firebase/data-connect" "0.3.6-20250512211235" + "@firebase/database" "1.0.16-20250512211235" + "@firebase/database-compat" "2.0.7-20250512211235" + "@firebase/firestore" "4.7.13-20250512211235" + "@firebase/firestore-compat" "0.3.48-20250512211235" + "@firebase/functions" "0.12.5-20250512211235" + "@firebase/functions-compat" "0.3.22-20250512211235" + "@firebase/installations" "0.6.15-20250512211235" + "@firebase/installations-compat" "0.2.15-20250512211235" + "@firebase/messaging" "0.12.19-20250512211235" + "@firebase/messaging-compat" "0.2.19-20250512211235" + "@firebase/performance" "0.7.4-20250512211235" + "@firebase/performance-compat" "0.2.17-20250512211235" + "@firebase/remote-config" "0.6.2-20250512211235" + "@firebase/remote-config-compat" "0.2.15-20250512211235" + "@firebase/storage" "0.13.9-20250512211235" + "@firebase/storage-compat" "0.3.19-20250512211235" + "@firebase/util" "1.12.0-20250512211235" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +follow-redirects@^1.0.0: + version "1.15.9" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + +form-data@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +http-proxy-middleware@^2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +idb@7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.1.0: + version "2.2.0" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-network-error@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz" + integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + 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== + dependencies: + isobject "^3.0.1" + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== + dependencies: + is-inside-container "^1.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-instrument@^6.0.0: + version "6.0.3" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== + dependencies: + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.7" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + dependencies: + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + +jest-environment-jsdom@29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz" + integrity sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/jsdom" "^20.0.0" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + jsdom "^20.0.0" + +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsdom@^20.0.0: + version "20.0.3" + resolved "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz" + integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== + dependencies: + abab "^2.0.6" + acorn "^8.8.1" + acorn-globals "^7.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.2" + decimal.js "^10.4.2" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.2" + parse5 "^7.1.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + ws "^8.11.0" + xml-name-validator "^4.0.0" + +jsesc@^3.0.2, jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json5@^2.1.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +launch-editor@^2.6.1: + version "2.9.1" + resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz" + integrity sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +long@^5.0.0: + version "5.2.3" + resolved "https://registry.npmjs.org/long/-/long-5.2.3.tgz" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +make-dir@^3.0.2, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^4.6.0: + version "4.14.0" + resolved "https://registry.npmjs.org/memfs/-/memfs-4.14.0.tgz" + integrity sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA== + dependencies: + "@jsonjoy.com/json-pack" "^1.0.3" + "@jsonjoy.com/util" "^1.3.0" + tree-dump "^1.0.1" + tslib "^2.0.0" + +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimalistic-assert@^1.0.0: + 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@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-forge@^1: + version "1.3.1" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nwsapi@^2.2.2: + version "2.2.16" + resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz" + integrity sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1, on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^10.0.3: + version "10.1.0" + resolved "https://registry.npmjs.org/open/-/open-10.1.0.tgz" + integrity sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw== + dependencies: + default-browser "^5.2.1" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + is-wsl "^3.1.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-retry@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz" + integrity sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA== + dependencies: + "@types/retry" "0.12.2" + is-network-error "^1.0.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse5@^7.0.0, parse5@^7.1.1: + version "7.2.1" + resolved "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz" + integrity sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ== + dependencies: + entities "^4.5.0" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== + +picocolors@^1.0.0, picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.6" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pretty-format@^29.0.0, pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +protobufjs@^7.2.5: + version "7.4.0" + resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz" + integrity sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +psl@^1.1.33: + version "1.15.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz" + integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== + dependencies: + punycode "^2.3.1" + +punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +pure-rand@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + +qs@6.13.0: + version "6.13.0" + resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-is@^18.0.0: + version "18.3.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + 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" + +readable-stream@^3.0.6: + version "3.6.2" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + +regexpu-core@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz" + integrity sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.11.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.11.0: + version "0.11.1" + resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz" + integrity sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ== + dependencies: + jsesc "~3.0.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz" + integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== + +resolve@^1.14.2, resolve@^1.20.0: + version "1.22.8" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +run-applescript@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz" + integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0, schema-utils@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +schema-utils@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz" + integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.4.1: + version "2.4.1" + resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.5.3, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.19.0: + version "0.19.0" + resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.11: + version "5.3.14" + resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" + +terser@^5.31.1: + version "5.39.0" + resolved "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz" + integrity sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +thingies@^1.20.0: + version "1.21.0" + resolved "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz" + integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tough-cookie@^4.1.2: + version "4.1.4" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + +tree-dump@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz" + integrity sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ== + +ts-node@10.9.2: + version "10.9.2" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tslib@^2.0.0, tslib@^2.1.0: + version "2.8.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz" + integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== + +type-detect@4.0.8: + 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== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@5.5.4: + version "5.5.4" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + +undici-types@~6.19.8: + version "6.19.8" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.2.0" + resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz" + integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz" + integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-to-istanbul@^9.0.1: + version "9.3.0" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz" + integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== + dependencies: + xml-name-validator "^4.0.0" + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +web-vitals@^4.2.4: + version "4.2.4" + resolved "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz" + integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw== + +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +webpack-cli@5.1.4: + version "5.1.4" + resolved "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz" + integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.1" + "@webpack-cli/info" "^2.0.2" + "@webpack-cli/serve" "^2.0.5" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^7.4.2: + version "7.4.2" + resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz" + integrity sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA== + dependencies: + colorette "^2.0.10" + memfs "^4.6.0" + mime-types "^2.1.31" + on-finished "^2.4.1" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz" + integrity sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA== + dependencies: + "@types/bonjour" "^3.5.13" + "@types/connect-history-api-fallback" "^1.5.4" + "@types/express" "^4.17.21" + "@types/serve-index" "^1.9.4" + "@types/serve-static" "^1.15.5" + "@types/sockjs" "^0.3.36" + "@types/ws" "^8.5.10" + ansi-html-community "^0.0.8" + bonjour-service "^1.2.1" + chokidar "^3.6.0" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + express "^4.21.2" + graceful-fs "^4.2.6" + http-proxy-middleware "^2.0.7" + ipaddr.js "^2.1.0" + launch-editor "^2.6.1" + open "^10.0.3" + p-retry "^6.2.0" + schema-utils "^4.2.0" + selfsigned "^2.4.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^7.4.2" + ws "^8.18.0" + +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.98.0: + version "5.98.0" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz#44ae19a8f2ba97537978246072fb89d10d1fbd17" + integrity sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA== + dependencies: + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.6" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.14.0" + browserslist "^4.24.0" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^4.3.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.11" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +ws@^8.11.0, ws@^8.18.0: + version "8.18.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.3.1, yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/e2e/template/README.md b/e2e/template/README.md new file mode 100644 index 00000000000..ef2b2deb997 --- /dev/null +++ b/e2e/template/README.md @@ -0,0 +1,3 @@ +# E2E Test Package Template + +This is a template for an E2E test package. It contains one small dependency (date-fns) not used in the global monorepo workspace to confirm the dependencies in this directory do not affect the global workspace. \ No newline at end of file diff --git a/e2e/template/index.js b/e2e/template/index.js new file mode 100644 index 00000000000..1e3a1a5d9af --- /dev/null +++ b/e2e/template/index.js @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +console.log('hello'); diff --git a/e2e/template/package.json b/e2e/template/package.json new file mode 100644 index 00000000000..c07c4c5c080 --- /dev/null +++ b/e2e/template/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-project", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "date-fns": "4.1.0" + } +} diff --git a/e2e/template/yarn.lock b/e2e/template/yarn.lock new file mode 100644 index 00000000000..4d7e448a31b --- /dev/null +++ b/e2e/template/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +date-fns@4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" + integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== diff --git a/integration/compat-interop/analytics.test.ts b/integration/compat-interop/analytics.test.ts new file mode 100644 index 00000000000..51055ae0fa1 --- /dev/null +++ b/integration/compat-interop/analytics.test.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getModularInstance } from '@firebase/util'; +import { expect } from 'chai'; +import { getAnalytics } from '@firebase/analytics'; +import firebase from '@firebase/app-compat'; +import '@firebase/analytics-compat'; + +import { TEST_PROJECT_CONFIG } from './util'; + +firebase.initializeApp(TEST_PROJECT_CONFIG); + +const compatAnalytics = firebase.analytics(); +const modularAnalytics = getAnalytics(); + +describe('Analytics compat interop', () => { + it('Analytics compat instance references modular Analytics instance', () => { + expect(getModularInstance(compatAnalytics)).to.equal(modularAnalytics); + }); +}); diff --git a/integration/compat-interop/app.test.ts b/integration/compat-interop/app.test.ts new file mode 100644 index 00000000000..ef2196a7809 --- /dev/null +++ b/integration/compat-interop/app.test.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getModularInstance } from '@firebase/util'; +import { expect } from 'chai'; +import { getApp, getApps } from '@firebase/app'; +import firebase from '@firebase/app-compat'; + +import { TEST_PROJECT_CONFIG } from './util'; +//TODO: add Storage, Firestore and Database tests once v8 is removed. Currently it's too difficult to set them up in integration tests. +describe('App compat interop', () => { + afterEach(() => { + const deletePromises = []; + for (const app of firebase.apps) { + deletePromises.push(app.delete()); + } + + return Promise.all(deletePromises); + }); + + it('App compat instance references modular App instance', () => { + const compatApp = firebase.initializeApp(TEST_PROJECT_CONFIG); + const modularApp = getApp(); + expect(getModularInstance(compatApp)).to.equal(modularApp); + }); + + it('deleting compat app deletes modular app', async () => { + const compatApp = firebase.initializeApp(TEST_PROJECT_CONFIG); + expect(firebase.apps.length).to.equal(1); + expect(getApps().length).to.equal(1); + + await compatApp.delete(); + expect(firebase.apps.length).to.equal(0); + expect(getApps().length).to.equal(0); + }); +}); diff --git a/integration/compat-interop/auth.test.ts b/integration/compat-interop/auth.test.ts new file mode 100644 index 00000000000..5cc6950850d --- /dev/null +++ b/integration/compat-interop/auth.test.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getModularInstance } from '@firebase/util'; +import { expect } from 'chai'; +import { getAuth, signOut } from '@firebase/auth'; +import firebase from '@firebase/app-compat'; +import '@firebase/auth-compat'; + +import { TEST_PROJECT_CONFIG } from './util'; + +firebase.initializeApp(TEST_PROJECT_CONFIG); + +const compatAuth = firebase.auth(); +const modularAuth = getAuth(); + +describe('Auth compat interop', () => { + it('Auth compat instance references modular Auth instance', () => { + expect(getModularInstance(compatAuth)).to.equal(modularAuth); + }); + + it('Auth compat and modular Auth share the same user state', async () => { + expect(compatAuth.currentUser).to.equal(null); + expect(modularAuth.currentUser).to.equal(null); + const userCred = await compatAuth.signInAnonymously(); + expect(userCred.user?.uid).to.equal(modularAuth.currentUser?.uid); + expect(await userCred.user?.getIdToken()).to.equal( + await modularAuth.currentUser?.getIdToken() + ); + + await signOut(modularAuth); + expect(compatAuth.currentUser).to.equal(null); + expect(modularAuth.currentUser).to.equal(null); + }); +}); diff --git a/integration/compat-interop/functions.test.ts b/integration/compat-interop/functions.test.ts new file mode 100644 index 00000000000..00de38c268e --- /dev/null +++ b/integration/compat-interop/functions.test.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getModularInstance } from '@firebase/util'; +import { expect } from 'chai'; +import { getFunctions } from '@firebase/functions'; +import firebase from '@firebase/app-compat'; +import '@firebase/functions-compat'; + +import { TEST_PROJECT_CONFIG } from './util'; + +firebase.initializeApp(TEST_PROJECT_CONFIG); + +const compatFunction = firebase.functions(); +const modularFunctions = getFunctions(); + +describe('Functions compat interop', () => { + it('Functions compat instance references modular Functions instance', () => { + expect(getModularInstance(compatFunction)).to.equal(modularFunctions); + }); +}); diff --git a/integration/compat-interop/karma.conf.js b/integration/compat-interop/karma.conf.js new file mode 100644 index 00000000000..6dde3b61344 --- /dev/null +++ b/integration/compat-interop/karma.conf.js @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const karma = require('karma'); +const karmaBase = require('../../config/karma.base'); + +const files = ['*.test.*']; + +module.exports = function (config) { + const karmaConfig = Object.assign({}, karmaBase, { + // files to load into karma + files: files, + preprocessors: { '**/*.ts': ['webpack', 'sourcemap'] }, + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'] + }); + + config.set(karmaConfig); +}; + +module.exports.files = files; diff --git a/integration/compat-interop/messaging.test.ts b/integration/compat-interop/messaging.test.ts new file mode 100644 index 00000000000..a4937a7985f --- /dev/null +++ b/integration/compat-interop/messaging.test.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getModularInstance } from '@firebase/util'; +import { expect } from 'chai'; +import { getMessaging } from '@firebase/messaging'; +import firebase from '@firebase/app-compat'; +import '@firebase/messaging-compat'; + +import { TEST_PROJECT_CONFIG } from './util'; + +firebase.initializeApp(TEST_PROJECT_CONFIG); + +const compatMessaging = firebase.messaging(); +const modularMessaging = getMessaging(); + +describe('Messaging compat interop', () => { + it('Messaging compat instance references modular Messaging instance', () => { + expect(getModularInstance(compatMessaging)).to.equal(modularMessaging); + }); +}); diff --git a/integration/compat-interop/package.json b/integration/compat-interop/package.json new file mode 100644 index 00000000000..9660e71e652 --- /dev/null +++ b/integration/compat-interop/package.json @@ -0,0 +1,32 @@ +{ + "name": "firebase-compat-interop-test", + "private": true, + "version": "0.1.0", + "scripts": { + "test": "karma start", + "test:ci": "node ../../scripts/run_tests_in_ci.js -s test", + "test:debug": "karma start --browsers Chrome --auto-watch" + }, + "dependencies": { + "@firebase/app": "0.14.5", + "@firebase/app-compat": "0.5.5", + "@firebase/analytics": "0.10.19", + "@firebase/analytics-compat": "0.2.25", + "@firebase/auth": "1.11.1", + "@firebase/auth-compat": "0.6.1", + "@firebase/functions": "0.13.1", + "@firebase/functions-compat": "0.4.1", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-compat": "0.2.20" + }, + "devDependencies": { + "typescript": "5.5.4" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/integration/compat-interop/performance.test.ts b/integration/compat-interop/performance.test.ts new file mode 100644 index 00000000000..47f357fdade --- /dev/null +++ b/integration/compat-interop/performance.test.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getModularInstance } from '@firebase/util'; +import { expect } from 'chai'; +import { getPerformance } from '@firebase/performance'; +import firebase from '@firebase/app-compat'; +import '@firebase/performance-compat'; + +import { TEST_PROJECT_CONFIG } from './util'; + +firebase.initializeApp(TEST_PROJECT_CONFIG); + +const compatPerf = firebase.performance(); +const modularPerf = getPerformance(); + +describe('Performance compat interop', () => { + it('Performance compat instance references modular Performance instance', () => { + expect(getModularInstance(compatPerf)).to.equal(modularPerf); + }); + + it('Performance compat and modular Performance instance share the same configuration', () => { + expect(compatPerf.dataCollectionEnabled).to.equal(true); + expect(compatPerf.instrumentationEnabled).to.equal(true); + expect(modularPerf.dataCollectionEnabled).to.equal(true); + expect(modularPerf.instrumentationEnabled).to.equal(true); + + // change settings on the compat instance + compatPerf.dataCollectionEnabled = false; + compatPerf.instrumentationEnabled = false; + + expect(compatPerf.dataCollectionEnabled).to.equal(false); + expect(compatPerf.instrumentationEnabled).to.equal(false); + expect(modularPerf.dataCollectionEnabled).to.equal(false); + expect(modularPerf.instrumentationEnabled).to.equal(false); + + // change settings on the modular instance + modularPerf.dataCollectionEnabled = true; + modularPerf.instrumentationEnabled = true; + + expect(compatPerf.dataCollectionEnabled).to.equal(true); + expect(compatPerf.instrumentationEnabled).to.equal(true); + expect(modularPerf.dataCollectionEnabled).to.equal(true); + expect(modularPerf.instrumentationEnabled).to.equal(true); + }); +}); diff --git a/integration/compat-interop/remote-config.test.ts b/integration/compat-interop/remote-config.test.ts new file mode 100644 index 00000000000..9f7983c106e --- /dev/null +++ b/integration/compat-interop/remote-config.test.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getModularInstance } from '@firebase/util'; +import { expect } from 'chai'; +import { getRemoteConfig } from '@firebase/remote-config'; +import firebase from '@firebase/app-compat'; +import '@firebase/remote-config-compat'; + +import { TEST_PROJECT_CONFIG } from './util'; + +firebase.initializeApp(TEST_PROJECT_CONFIG); + +const compatRC = firebase.remoteConfig(); +const modularRC = getRemoteConfig(); + +describe('RC compat interop', () => { + it('RC compat instance references modular RC instance', () => { + expect(getModularInstance(compatRC)).to.equal(modularRC); + }); +}); diff --git a/integration/compat-interop/tsconfig.json b/integration/compat-interop/tsconfig.json new file mode 100644 index 00000000000..c986fcaf8a7 --- /dev/null +++ b/integration/compat-interop/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compileOnSave": false, + "compilerOptions": { + "allowJs": true, + "declaration": false, + "module": "commonjs", + "moduleResolution": "node", + "noImplicitAny": true, + "outDir": "dist", + "target": "es2020", + "sourceMap": true, + "esModuleInterop": true + }, + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/integration/compat-interop/util.ts b/integration/compat-interop/util.ts new file mode 100644 index 00000000000..68734591c6e --- /dev/null +++ b/integration/compat-interop/util.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const TEST_PROJECT_CONFIG = require('../../config/project.json'); diff --git a/integration/compat-typings/package.json b/integration/compat-typings/package.json new file mode 100644 index 00000000000..6ad4d2e1298 --- /dev/null +++ b/integration/compat-typings/package.json @@ -0,0 +1,18 @@ +{ + "name": "firebase-compat-typings-test", + "private": true, + "version": "0.1.0", + "scripts": { + "test": "tsc", + "test:ci": "node ../../scripts/run_tests_in_ci.js -s test" + }, + "dependencies": { + "firebase": "*" + }, + "devDependencies": { + "typescript": "5.5.4" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/integration/compat-typings/tsconfig.json b/integration/compat-typings/tsconfig.json new file mode 100644 index 00000000000..e8b1681f407 --- /dev/null +++ b/integration/compat-typings/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compileOnSave": false, + "compilerOptions": { + "outDir": "dist" + }, + "include": ["typings.ts"], + "exclude": [ + "dist" + ] +} diff --git a/integration/compat-typings/typings.ts b/integration/compat-typings/typings.ts new file mode 100644 index 00000000000..171d0a74a0f --- /dev/null +++ b/integration/compat-typings/typings.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import firebase from 'firebase/compat'; +import { FirebaseAuth, User } from '@firebase/auth-types'; +import { FirebaseAnalytics } from '@firebase/analytics-types'; +import { FirebaseApp } from '@firebase/app-compat'; +import { + FirebaseFirestore, + DocumentReference, + CollectionReference +} from '@firebase/firestore-types'; +import { FirebaseFunctions } from '@firebase/functions-types'; +import { FirebaseInstallations } from '@firebase/installations-types'; +// Get type directly from messaging package, messaging-compat does not implement +// the current messaging API. +import { MessagingCompat } from '../../packages/messaging-compat/src/messaging-compat'; +import { FirebasePerformance } from '@firebase/performance-types'; +import { RemoteConfig } from '@firebase/remote-config-types'; +import { + FirebaseStorage, + Reference as StorageReference +} from '@firebase/storage-types'; +import { FirebaseDatabase, Reference, Query } from '@firebase/database-types'; + +/** + * Test namespaced types from compat/index.d.ts against the types used in + * implementation of the actual compat services. In almost all cases the services + * implement types found in the corresponding -types package. The only exceptions + * are + * - messaging, which has changed its public API in the compat version + * - app-compat, which defines its FirebaseApp type in its own package + */ + +const compatTypes: { + auth: FirebaseAuth; + analytics: FirebaseAnalytics; + app: FirebaseApp; + firestore: FirebaseFirestore; + functions: FirebaseFunctions; + installations?: FirebaseInstallations; + messaging: MessagingCompat; + performance: FirebasePerformance; + remoteConfig: RemoteConfig; + storage: FirebaseStorage; + storageReference: StorageReference; + firestoreDocumentReference: DocumentReference; + firestoreCollectionReference: CollectionReference; + authUser: User; + database: FirebaseDatabase; + databaseReference: Reference; + databaseQuery: Query; +} = { + auth: firebase.auth(), + analytics: firebase.analytics(), + app: firebase.initializeApp({}), + firestore: firebase.firestore(), + functions: firebase.functions(), + installations: {} as firebase.installations.Installations, + messaging: firebase.messaging(), + performance: firebase.performance(), + remoteConfig: firebase.remoteConfig(), + storage: firebase.storage(), + storageReference: {} as firebase.storage.Reference, + firestoreDocumentReference: {} as firebase.firestore.DocumentReference, + firestoreCollectionReference: {} as firebase.firestore.CollectionReference, + authUser: {} as firebase.User, + database: {} as firebase.database.Database, + databaseReference: {} as firebase.database.Reference, + databaseQuery: {} as firebase.database.Query +}; diff --git a/integration/firebase/karma.conf.js b/integration/firebase/karma.conf.js deleted file mode 100644 index c1210c70fe0..00000000000 --- a/integration/firebase/karma.conf.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const karma = require('karma'); -const path = require('path'); -const karmaBase = require('../../config/karma.base'); - -const files = ['test/**/*.test.*']; - -module.exports = function (config) { - const karmaConfig = Object.assign({}, karmaBase, { - // files to load into karma - files: files, - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'] - }); - - config.set(karmaConfig); -}; - -module.exports.files = files; diff --git a/integration/firebase/package.json b/integration/firebase/package.json deleted file mode 100644 index 9c008310d16..00000000000 --- a/integration/firebase/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "firebase-namespace-integration-test", - "private": true, - "version": "0.2.1", - "scripts": { - "test": "karma start --single-run", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test" - }, - "devDependencies": { - "firebase": "8.2.3", - "@types/chai": "4.2.14", - "@types/mocha": "7.0.2", - "chai": "4.2.0", - "karma": "5.2.3", - "karma-babel-preprocessor": "8.0.1", - "karma-chrome-launcher": "3.1.0", - "karma-firefox-launcher": "2.1.0", - "karma-mocha": "2.0.1", - "karma-spec-reporter": "0.0.32", - "karma-typescript": "5.2.0", - "mocha": "7.2.0", - "npm-run-all": "4.1.5", - "typescript": "4.0.5" - } -} diff --git a/integration/firebase/test/namespace.test.ts b/integration/firebase/test/namespace.test.ts deleted file mode 100644 index 42d4eec6390..00000000000 --- a/integration/firebase/test/namespace.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import firebase from 'firebase'; -import * as namespaceDefinition from './namespaceDefinition.json'; -import validateNamespace from './validator'; - -firebase.initializeApp({ - apiKey: 'test-api-key', - authDomain: 'test-project-name.firebaseapp.com', - databaseURL: 'https://test-project-name.firebaseio.com', - projectId: 'test-project-name', - storageBucket: 'test-project-name.appspot.com', - messagingSenderId: '012345678910', - appId: 'myAppId' -}); - -describe('Firebase Namespace Validation', function () { - validateNamespace(namespaceDefinition, firebase); -}); diff --git a/integration/firebase/test/namespaceDefinition.json b/integration/firebase/test/namespaceDefinition.json deleted file mode 100644 index 3f4bb783cc9..00000000000 --- a/integration/firebase/test/namespaceDefinition.json +++ /dev/null @@ -1,240 +0,0 @@ -{ - "initializeApp": { - "__type": "function", - "__args": 2 - }, - "app": { - "__type": "function", - "__args": 1, - "App": { - "__type": "function" - }, - "__return": { - "name": { - "__type": "string" - }, - "options": { - "__type": "object" - }, - "delete": { - "__type": "function" - }, - "INTERNAL": { - "getToken": { - "__type": "function" - }, - "addAuthTokenListener": { - "__type": "function" - }, - "removeAuthTokenListener": { - "__type": "function" - } - } - } - }, - "SDK_VERSION": { - "__type": "string" - }, - "apps": { - "__type": "array" - }, - "INTERNAL": { - "__type": "object", - "registerComponent": { - "__type": "function" - }, - "extendNamespace": { - "__type": "function" - }, - "createFirebaseNamespace": { - "__type": "function" - }, - "createSubscribe": { - "__type": "function" - }, - "removeApp": { - "__type": "function" - }, - "components": { - "__type": "Map" - }, - "ErrorFactory": { - "__type": "function" - }, - "deepExtend": { - "__type": "function" - } - }, - "auth": { - "__type": "function", - "Auth": { - "__type": "function" - }, - "Error": { - "__type": "function" - }, - "EmailAuthProvider": { - "__type": "function" - }, - "FacebookAuthProvider": { - "__type": "function" - }, - "GithubAuthProvider": { - "__type": "function" - }, - "GoogleAuthProvider": { - "__type": "function" - }, - "TwitterAuthProvider": { - "__type": "function" - }, - "__return": { - "app": { - "__type": "object" - }, - "INTERNAL": { - "delete": { - "__type": "function" - } - } - } - }, - "database": { - "__type": "function", - "Database": { - "__type": "function" - }, - "Query": { - "__type": "function" - }, - "Reference": { - "__type": "function" - }, - "enableLogging": { - "__type": "function" - }, - "ServerValue": { - "__type": "object", - "TIMESTAMP": { - "__type": "object" - } - }, - "__return": { - "app": { - "__type": "object" - }, - "INTERNAL": { - "delete": { - "__type": "function" - } - }, - "ref": { - "__type": "function" - }, - "refFromURL": { - "__type": "function" - }, - "goOnline": { - "__type": "function" - }, - "goOffline": { - "__type": "function" - } - } - }, - "storage": { - "__type": "function", - "Storage": { - "__type": "function" - }, - "Reference": { - "__type": "function" - }, - "TaskEvent": { - "__type": "object", - "STATE_CHANGED": { - "__type": "string" - } - }, - "TaskState": { - "__type": "object", - "RUNNING": { - "__type": "string" - }, - "PAUSED": { - "__type": "string" - }, - "SUCCESS": { - "__type": "string" - }, - "CANCELED": { - "__type": "string" - }, - "ERROR": { - "__type": "string" - } - }, - "StringFormat": { - "__type": "object", - "RAW": { - "__type": "string" - }, - "BASE64": { - "__type": "string" - }, - "BASE64URL": { - "__type": "string" - }, - "DATA_URL": { - "__type": "string" - } - }, - "__return": { - "app": { - "__type": "object" - }, - "INTERNAL": { - "delete": { - "__type": "function" - } - }, - "ref": { - "__type": "function" - }, - "refFromURL": { - "__type": "function" - } - } - }, - "messaging": { - "__type": "function", - "__return": { - "app": { - "__type": "object" - }, - "INTERNAL": { - "delete": { - "__type": "function" - } - }, - "getToken": { - "__type": "function" - }, - "onMessage": { - "__type": "function" - }, - "onTokenRefresh": { - "__type": "function" - }, - "requestPermission": { - "__type": "function" - }, - "deleteToken": { - "__type": "function" - }, - "setBackgroundMessageHandler": { - "__type": "function" - } - } - } -} \ No newline at end of file diff --git a/integration/firebase/test/typings.d.ts b/integration/firebase/test/typings.d.ts deleted file mode 100644 index b59c269ac31..00000000000 --- a/integration/firebase/test/typings.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -declare module '*.json' { - const value: any; - export default value; -} diff --git a/integration/firebase/test/validator.js b/integration/firebase/test/validator.js deleted file mode 100644 index 9f4fdc42c18..00000000000 --- a/integration/firebase/test/validator.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -let validatedVersion = false; - -function validateNamespace(definition, candidate) { - const __expect = require('chai').expect; - const keys = Object.keys(definition).filter(key => !~key.indexOf('__')); - - if (!validatedVersion) { - describe('Firebase SDK Version', function () { - it('Should be properly defined', function () { - __expect(candidate.SDK_VERSION).to.equal( - require('../../../packages/firebase/package.json').version - ); - }); - }); - validatedVersion = true; - } - - // Validate Keys - keys.forEach(key => { - /** - * This object that we capture here could potentially - * contain multiple layers of APIs. We recursively - * spin up the validations as needed but defer them - * till after we have validated the current API - */ - const definitionChunk = definition[key]; - const candidateChunk = candidate[key]; - - /** - * Grab all of the keys that aren't meta properties and capture - * them for more testing later - */ - const internalKeys = Object.keys(definitionChunk).filter( - iKey => !~iKey.indexOf('__') - ); - const returnKeys = Object.keys(definitionChunk).filter( - iKey => ~iKey.indexOf('__return') - ); - - describe(`${key}`, function () { - /** - * Tests of the actual API - */ - if (definitionChunk.__type) { - it(`Should be a \`${definitionChunk.__type}\``, function () { - __expect(candidateChunk).to.be.a(definitionChunk.__type); - }); - } - - /** - * If both the definition and candidate pieces are truthy - * then we can continue validation of the nested layers - */ - if (definitionChunk && candidateChunk) { - validateNamespace(definitionChunk, candidateChunk); - } - - /** - * Keys marked with `__return` allow us to validate the - * return value of a specific part of the API - * - * e.g. - * { - * ... - * app: { - * __return: { - * - * } - * } - * } - */ - if ( - definitionChunk.__type === 'function' && - definitionChunk.__return && - typeof candidateChunk === 'function' - ) { - try { - candidateChunk(); - } catch (e) { - it(`Throws because current browser is unsupported`, () => { - __expect(e.code).to.have.string('unsupported-browser'); - }); - return; - } - validateNamespace(definitionChunk.__return, candidateChunk()); - } - }); - }); -} - -module.exports = validateNamespace; -module.exports.default = validateNamespace; diff --git a/integration/firebase/tsconfig.json b/integration/firebase/tsconfig.json deleted file mode 100644 index 4da78214594..00000000000 --- a/integration/firebase/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compileOnSave": false, - "compilerOptions": { - "allowJs": true, - "declaration": false, - "module": "commonjs", - "moduleResolution": "node", - "noImplicitAny": true, - "outDir": "dist", - "target": "ES5", - "sourceMap": true, - "esModuleInterop": true - }, - "exclude": [ - "node_modules", - "dist" - ] -} diff --git a/integration/firestore/firebase_export.ts b/integration/firestore/firebase_export.ts index 317a53fac29..4646e11fe2c 100644 --- a/integration/firestore/firebase_export.ts +++ b/integration/firestore/firebase_export.ts @@ -15,46 +15,39 @@ * limitations under the License. */ -import firebase from '@firebase/app'; -import '@firebase/firestore'; -import '@firebase/firestore/bundle'; -import { FirebaseApp } from '@firebase/app-types'; -import { Settings, FirebaseFirestore } from '@firebase/firestore-types'; +import { FirebaseApp, initializeApp } from '@firebase/app'; +import { + Firestore, + FirestoreSettings, + initializeFirestore +} from '@firebase/firestore'; // This file replaces "packages/firestore/test/integration/util/firebase_export" // and depends on the minified sources. let appCount = 0; -export function newTestFirestore( - projectId: string, - nameOrApp?: string | FirebaseApp, - settings?: Settings -): FirebaseFirestore { - if (nameOrApp === undefined) { - nameOrApp = 'test-app-' + appCount++; - } - const app = - typeof nameOrApp === 'string' - ? firebase.initializeApp({ apiKey: 'fake-api-key', projectId }, nameOrApp) - : nameOrApp; - - const firestore = firebase.firestore(app); - if (settings) { - firestore.settings(settings); +export function newTestApp(projectId: string, appName?: string): FirebaseApp { + if (appName === undefined) { + appName = 'test-app-' + appCount++; } - return firestore; + return initializeApp( + { + apiKey: 'fake-api-key', + projectId + }, + appName + ); } -export function usesFunctionalApi(): false { - return false; +export function newTestFirestore( + app: FirebaseApp, + settings?: FirestoreSettings, + dbName?: string +): Firestore { + return initializeFirestore(app, settings || {}, dbName); } -const Firestore = firebase.firestore.Firestore; -const FieldPath = firebase.firestore.FieldPath; -const Timestamp = firebase.firestore.Timestamp; -const GeoPoint = firebase.firestore.GeoPoint; -const FieldValue = firebase.firestore.FieldValue; -const Blob = firebase.firestore.Blob; +export * from '@firebase/firestore'; -export { Firestore, FieldValue, FieldPath, Timestamp, Blob, GeoPoint }; +export type PrivateSettings = Record; diff --git a/integration/firestore/firebase_export_memory.ts b/integration/firestore/firebase_export_memory.ts deleted file mode 100644 index 98e6ce3ae78..00000000000 --- a/integration/firestore/firebase_export_memory.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import firebase from '@firebase/app'; -import '@firebase/firestore/memory'; -import '@firebase/firestore/memory-bundle'; -import { FirebaseApp } from '@firebase/app-types'; -import { Settings, FirebaseFirestore } from '@firebase/firestore-types'; - -// This file replaces "packages/firestore/test/integration/util/firebase_export" -// and depends on the minified sources. - -let appCount = 0; - -export function newTestFirestore( - projectId: string, - nameOrApp?: string | FirebaseApp, - settings?: Settings -): FirebaseFirestore { - if (nameOrApp === undefined) { - nameOrApp = 'test-app-' + appCount++; - } - const app = - typeof nameOrApp === 'string' - ? firebase.initializeApp({ apiKey: 'fake-api-key', projectId }, nameOrApp) - : nameOrApp; - - const firestore = firebase.firestore(app); - if (settings) { - firestore.settings(settings); - } - return firestore; -} - -export function usesFunctionalApi(): false { - return false; -} - -const Firestore = firebase.firestore.Firestore; -const FieldPath = firebase.firestore.FieldPath; -const Timestamp = firebase.firestore.Timestamp; -const GeoPoint = firebase.firestore.GeoPoint; -const FieldValue = firebase.firestore.FieldValue; -const Blob = firebase.firestore.Blob; - -export { Firestore, FieldValue, FieldPath, Timestamp, Blob, GeoPoint }; diff --git a/integration/firestore/gulpfile.js b/integration/firestore/gulpfile.js index 83b3b13f86f..57dc674606b 100644 --- a/integration/firestore/gulpfile.js +++ b/integration/firestore/gulpfile.js @@ -42,9 +42,11 @@ function copyTests() { .src( [ testBase + '/integration/api/*.ts', + testBase + '/integration/util/composite_index_test_helper.ts', testBase + '/integration/util/events_accumulator.ts', testBase + '/integration/util/helpers.ts', testBase + '/integration/util/settings.ts', + testBase + '/integration/util/testing_hooks_util.ts', testBase + '/util/equality_matcher.ts', testBase + '/util/promise.ts' ], @@ -53,27 +55,18 @@ function copyTests() { .pipe( replace( /** - * This regex is designed to match the following statement used in our - * firestore integration test suites: - * - * import * as firebaseExport from '../../util/firebase_export'; - * - * It will handle variations in whitespace, single/double quote - * differences, as well as different paths to a valid firebase_export + * This regex is designed to match the Firebase import in our + * integration tests. */ - /import\s+\* as firebaseExport\s+from\s+('|")[^\1]+firebase_export\1;?/, - `import * as firebaseExport from '${resolve( - __dirname, - isPersistenceEnabled() - ? './firebase_export' - : './firebase_export_memory' - )}'; + /\s+from '\.(\.\/util)?\/firebase_export';/, + ` from '${resolve(__dirname, './firebase_export')}'; - if (typeof process === 'undefined') { - process = { env: { INCLUDE_FIRESTORE_PERSISTENCE: '${isPersistenceEnabled()}' } } as any; - } else { - process.env.INCLUDE_FIRESTORE_PERSISTENCE = '${isPersistenceEnabled()}'; - }` +if (typeof process === 'undefined') { + process = { env: { INCLUDE_FIRESTORE_PERSISTENCE: '${isPersistenceEnabled()}' } } as any; +} else { + process.env.INCLUDE_FIRESTORE_PERSISTENCE = '${isPersistenceEnabled()}'; +} +` ) ) .pipe( diff --git a/integration/firestore/package.json b/integration/firestore/package.json index 92251bf7b1d..26e7f66f193 100644 --- a/integration/firestore/package.json +++ b/integration/firestore/package.json @@ -6,29 +6,26 @@ "build:deps": "lerna run --scope @firebase/'{app,firestore}' --include-dependencies build", "build:persistence": "INCLUDE_FIRESTORE_PERSISTENCE=true gulp compile-tests", "build:memory": "INCLUDE_FIRESTORE_PERSISTENCE=false gulp compile-tests", - "test": "yarn build:memory; karma start --single-run; yarn build:persistence; karma start --single-run;", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test", - "test:persistence": " yarn build:persistence; karma start --single-run", + "karma:singlerun": "karma start", + "prettier": "prettier --write '*.js' '*.ts'", + "test:persistence": " yarn build:persistence; karma start", "test:persistence:debug": "yarn build:persistence; karma start --auto-watch --browsers Chrome", - "test:memory": "yarn build:memory; karma start --single-run", + "test:memory": "yarn build:memory; karma start", "test:memory:debug": "yarn build:memory; karma start --auto-watch --browsers Chrome" }, + "dependencies": { + "@firebase/app": "0.14.5", + "@firebase/firestore": "4.9.2" + }, "devDependencies": { - "@firebase/app": "0.6.13", - "@firebase/firestore": "2.1.2", - "@types/mocha": "7.0.2", + "@types/mocha": "9.1.1", "gulp": "4.0.2", - "gulp-filter": "6.0.0", - "gulp-replace": "1.0.0", - "karma": "5.2.3", - "karma-chrome-launcher": "3.1.0", - "karma-firefox-launcher": "2.1.0", - "karma-mocha": "2.0.1", - "karma-spec-reporter": "0.0.32", - "mocha": "7.2.0", - "ts-loader": "8.0.12", - "typescript": "4.0.5", - "webpack": "4.44.2", - "webpack-stream": "6.1.1" + "gulp-filter": "7.0.0", + "gulp-replace": "1.1.4", + "typescript": "5.5.4", + "webpack-stream": "7.0.0" + }, + "engines": { + "node": ">=20.0.0" } } diff --git a/integration/messaging/package.json b/integration/messaging/package.json index 50f8639f578..1c633bede91 100644 --- a/integration/messaging/package.json +++ b/integration/messaging/package.json @@ -9,13 +9,15 @@ "test:manual": "mocha --exit" }, "devDependencies": { - "firebase": "8.2.3", - "chai": "4.2.0", - "chromedriver": "86.0.0", - "express": "4.17.1", - "geckodriver": "1.21.1", - "mocha": "7.2.0", - "node-fetch": "2.6.1", - "selenium-assistant": "6.1.0" + "firebase": "12.5.0", + "chai": "4.5.0", + "chromedriver": "119.0.1", + "express": "4.21.2", + "geckodriver": "2.0.4", + "mocha": "9.2.2", + "selenium-assistant": "6.1.1" + }, + "engines": { + "node": ">=20.0.0" } } diff --git a/integration/messaging/test/static/default-sw/index.html b/integration/messaging/test/static/default-sw/index.html index 6da882bc5dd..45ceb59919c 100644 --- a/integration/messaging/test/static/default-sw/index.html +++ b/integration/messaging/test/static/default-sw/index.html @@ -1,3 +1,19 @@ + + FCM Demo diff --git a/integration/messaging/test/static/valid-manifest/index.html b/integration/messaging/test/static/valid-manifest/index.html index dc73c40350f..3f482bab7af 100644 --- a/integration/messaging/test/static/valid-manifest/index.html +++ b/integration/messaging/test/static/valid-manifest/index.html @@ -1,3 +1,19 @@ + + FCM Demo diff --git a/integration/messaging/test/static/valid-vapid-key-modern-sw/index.html b/integration/messaging/test/static/valid-vapid-key-modern-sw/index.html index 269a41f0d91..8d7d93c9f5a 100644 --- a/integration/messaging/test/static/valid-vapid-key-modern-sw/index.html +++ b/integration/messaging/test/static/valid-vapid-key-modern-sw/index.html @@ -1,3 +1,19 @@ + + FCM Demo diff --git a/integration/messaging/test/static/valid-vapid-key/index.html b/integration/messaging/test/static/valid-vapid-key/index.html index c6a663497a2..b8d32ec7f00 100644 --- a/integration/messaging/test/static/valid-vapid-key/index.html +++ b/integration/messaging/test/static/valid-vapid-key/index.html @@ -1,3 +1,19 @@ + + FCM Demo diff --git a/integration/messaging/test/test-deleteToken.js b/integration/messaging/test/test-deleteToken.js deleted file mode 100644 index cb6aa341088..00000000000 --- a/integration/messaging/test/test-deleteToken.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const expect = require('chai').expect; -const testServer = require('./utils/test-server'); -const deleteToken = require('./utils/deleteToken'); -const clearAppForTest = require('./utils/clearAppForTest'); -const retrieveToken = require('./utils/retrieveToken'); -const seleniumAssistant = require('selenium-assistant'); -const createPermittedWebDriver = require('./utils/createPermittedWebDriver'); - -const TEST_SUITE_TIMEOUT_MS = 100000; -const TEST_DOMAIN = 'valid-vapid-key'; - -describe('Firebase Messaging Integration Tests > get and delete token', function () { - this.timeout(TEST_SUITE_TIMEOUT_MS); - this.retries(3); - let globalWebDriver; - - before(async function () { - await testServer.start(); - }); - - after(async function () { - await testServer.stop(); - }); - - const availableBrowsers = seleniumAssistant.getLocalBrowsers(); - availableBrowsers.forEach(assistantBrowser => { - // TODO: enable testing for firefox - if (assistantBrowser.getId() !== 'chrome') { - return; - } - - describe(`Testing browser: ${assistantBrowser.getPrettyName()} : ${TEST_DOMAIN}`, function () { - before(async function () { - // Use one webDriver per browser instead of one per test to speed up test. - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - - await globalWebDriver.get( - `${testServer.serverAddress}/${TEST_DOMAIN}/` - ); - }); - - after(async function () { - await seleniumAssistant.killWebDriver(globalWebDriver); - }); - - afterEach(async function () { - await clearAppForTest(globalWebDriver); - }); - - it(`Test app can delete a valid token`, async function () { - const token = await retrieveToken(globalWebDriver); - expect(token).to.exist; - try { - await deleteToken(globalWebDriver, token); - console.log('Token deletion succeed.'); - } catch (e) { - console.log('Error trying to delete FCM token: ', e); - fail(); - } - }); - }); - }); -}); diff --git a/integration/messaging/test/test-receive-background.js b/integration/messaging/test/test-receive-background.js new file mode 100644 index 00000000000..258bff32051 --- /dev/null +++ b/integration/messaging/test/test-receive-background.js @@ -0,0 +1,157 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const testServer = require('./utils/test-server'); +const sendMessage = require('./utils/sendMessage'); +const retrieveToken = require('./utils/retrieveToken'); +const seleniumAssistant = require('selenium-assistant'); +const getReceivedBackgroundMessages = require('./utils/getReceivedBackgroundMessages'); +const createPermittedWebDriver = require('./utils/createPermittedWebDriver'); +const checkSendResponse = require('./utils/checkSendResponse'); +const checkMessageReceived = require('./utils/checkMessageReceived'); +const openNewTab = require('./utils/openNewTab'); + +const TEST_DOMAINS = ['valid-vapid-key-modern-sw']; + +// 4 minutes. The fact that the flow includes making a request to the Send Service, +// storing/retrieving form indexedDb asynchronously makes these test units to have a execution time +// variance. Therefore, allowing these units to have a longer time to work is crucial. +const TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS = 240000; + +// 1 minute. Wait for object store to be created and received message to be stored in idb. This +// waiting time MUST be longer than the wait time for adding to db in the sw. +const WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS = 60000; + +const wait = ms => new Promise(res => setTimeout(res, ms)); + +// 50% of integration test run time is spent in testing receiving in background. Running these +// slower tests last so other tests can fail quickly if they do fail. +require('./test-token-delete'); +require('./test-token-update'); +require('./test-useValidManifest'); +require('./test-useDefaultServiceWorker'); +require('./test-receive-foreground'); + +describe('Firebase Messaging Integration Tests > Test Background Receive', function () { + this.retries(2); + let globalWebDriver; + + before(async function () { + await testServer.start(); + }); + + after(async function () { + await testServer.stop(); + }); + + // TODO: enable testing for firefox + seleniumAssistant.getLocalBrowsers().forEach(assistantBrowser => { + if (assistantBrowser.getId() !== 'chrome') { + return; + } + + TEST_DOMAINS.forEach(domain => { + describe(`Testing browser: ${assistantBrowser.getPrettyName()} : ${domain}`, function () { + before(async function () { + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + }); + + it('Background app can receive a {} empty message from sw', async function () { + this.timeout(TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); + + // Clearing the cache and db data by killing the previously instantiated driver. Note that + // ideally this call is placed inside the after/before hooks. However, Mocha forbids + // operations longer than 2s in hooks. Hence, this clearing call needs to be inside the + // test unit. + await seleniumAssistant.killWebDriver(globalWebDriver); + + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + + prepareBackgroundApp(globalWebDriver, domain); + + checkSendResponse( + await sendMessage({ + to: await retrieveToken(globalWebDriver) + }) + ); + + await wait( + WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS + ); + + checkMessageReceived( + await getReceivedBackgroundMessages(globalWebDriver), + /* expectedNotificationPayload= */ null, + /* expectedDataPayload= */ null, + /* isLegacyPayload= */ false + ); + }); + + it('Background app can receive a {"data"} message frow sw', async function () { + this.timeout(TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); + + await seleniumAssistant.killWebDriver(globalWebDriver); + + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + + prepareBackgroundApp(globalWebDriver, domain); + + checkSendResponse( + await sendMessage({ + to: await retrieveToken(globalWebDriver), + data: getTestDataPayload() + }) + ); + + await wait( + WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS + ); + + checkMessageReceived( + await getReceivedBackgroundMessages(globalWebDriver), + /* expectedNotificationPayload= */ null, + /* expectedDataPayload= */ getTestDataPayload() + ); + }); + }); + }); + }); +}); + +async function prepareBackgroundApp(globalWebDriver, domain) { + await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); + // TODO: remove the try/catch block once the underlying bug has been resolved. Shift window focus + // away from app window so that background messages can be received/processed + try { + await openNewTab(globalWebDriver); + } catch (err) { + // ChromeDriver seems to have an open bug which throws "JavascriptError: javascript error: + // circular reference". Nevertheless, a new tab can still be opened. Hence, just catch and + // continue here. + console.log('FCM (ignored on purpose): ' + err); + } +} + +function getTestDataPayload() { + return { hello: 'world' }; +} diff --git a/integration/messaging/test/test-receive-foreground.js b/integration/messaging/test/test-receive-foreground.js new file mode 100644 index 00000000000..8c23a07fae6 --- /dev/null +++ b/integration/messaging/test/test-receive-foreground.js @@ -0,0 +1,177 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const testServer = require('./utils/test-server'); +const sendMessage = require('./utils/sendMessage'); +const retrieveToken = require('./utils/retrieveToken'); +const seleniumAssistant = require('selenium-assistant'); +const checkSendResponse = require('./utils/checkSendResponse'); +const getReceivedForegroundMessages = require('./utils/getReceivedForegroundMessages'); +const checkMessageReceived = require('./utils/checkMessageReceived'); +const createPermittedWebDriver = require('./utils/createPermittedWebDriver'); + +// Only testing 'valid-vapid-key' because 'valid-vapid-key-modern-sw' has the same behavior +const TEST_DOMAINS = ['valid-vapid-key']; +const TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS = 120000; + +// Getting and deleting token is the entry step of using FM SDK. Let it run first and fail quickly. +require('./test-token-delete'); + +describe('Firebase Messaging Integration Tests > Test Foreground Receive', function () { + this.retries(2); + let globalWebDriver; + + before(async function () { + await testServer.start(); + }); + + after(async function () { + await testServer.stop(); + }); + + // TODO: enable testing for firefox + seleniumAssistant.getLocalBrowsers().forEach(assistantBrowser => { + if (assistantBrowser.getId() !== 'chrome') { + return; + } + + TEST_DOMAINS.forEach(domain => { + describe(`Testing browser: ${assistantBrowser.getPrettyName()} : ${domain}`, function () { + before(async function () { + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + }); + }); + + it('Foreground app can receive a {} empty message in onMessage', async function () { + this.timeout(TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); + + await seleniumAssistant.killWebDriver(globalWebDriver); + + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + + await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); + + let token = await retrieveToken(globalWebDriver); + checkSendResponse( + await sendMessage({ + to: token + }) + ); + + await checkMessageReceived( + await getReceivedForegroundMessages(globalWebDriver), + /* expectedNotificationPayload= */ null, + /* expectedDataPayload= */ null + ); + }); + + it('Foreground app can receive a {"notification"} message in onMessage', async function () { + this.timeout(TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); + + await seleniumAssistant.killWebDriver(globalWebDriver); + + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + + await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); + + checkSendResponse( + await sendMessage({ + to: await retrieveToken(globalWebDriver), + notification: getTestNotificationPayload() + }) + ); + + await checkMessageReceived( + await getReceivedForegroundMessages(globalWebDriver), + /* expectedNotificationPayload= */ getTestNotificationPayload(), + /* expectedDataPayload= */ null + ); + }); + + it('Foreground app can receive a {"data"} message in onMessage', async function () { + this.timeout(TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); + + await seleniumAssistant.killWebDriver(globalWebDriver); + + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + + await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); + + checkSendResponse( + await sendMessage({ + to: await retrieveToken(globalWebDriver), + data: getTestDataPayload() + }) + ); + + await checkMessageReceived( + await getReceivedForegroundMessages(globalWebDriver), + /* expectedNotificationPayload= */ null, + /* expectedDataPayload= */ getTestDataPayload() + ); + }); + + it('Foreground app can receive a {"notification", "data"} message in onMessage', async function () { + this.timeout(TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); + + await seleniumAssistant.killWebDriver(globalWebDriver); + + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + + await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); + + checkSendResponse( + await sendMessage({ + to: await retrieveToken(globalWebDriver), + data: getTestDataPayload(), + notification: getTestNotificationPayload() + }) + ); + + await checkMessageReceived( + await getReceivedForegroundMessages(globalWebDriver), + /* expectedNotificationPayload= */ getTestNotificationPayload(), + /* expectedDataPayload= */ getTestDataPayload() + ); + }); + }); + }); +}); + +function getTestDataPayload() { + return { hello: 'world' }; +} + +function getTestNotificationPayload() { + return { + title: 'test title', + body: 'test body', + icon: '/test/icon.png', + click_action: '/', + tag: 'test-tag' + }; +} diff --git a/integration/messaging/test/test-send.js b/integration/messaging/test/test-send.js deleted file mode 100644 index 93ead480819..00000000000 --- a/integration/messaging/test/test-send.js +++ /dev/null @@ -1,302 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const expect = require('chai').expect; -const testServer = require('./utils/test-server'); -const sendMessage = require('./utils/sendMessage'); -const retrieveToken = require('./utils/retrieveToken'); -const seleniumAssistant = require('selenium-assistant'); -const getReceivedBackgroundMessages = require('./utils/getReceivedBackgroundMessages'); -const getReceivedForegroundMessages = require('./utils/getReceivedForegroundMessages'); -const openNewTab = require('./utils/openNewTab'); -const createPermittedWebDriver = require('./utils/createPermittedWebDriver'); - -const TEST_DOMAINS = ['valid-vapid-key', 'valid-vapid-key-modern-sw']; -const TEST_PROJECT_SENDER_ID = '750970317741'; -const DEFAULT_COLLAPSE_KEY_VALUE = 'do_not_collapse'; -const FIELD_FROM = 'from'; -const FIELD_COLLAPSE_KEY_LEGACY = 'collapse_key'; -const FIELD_COLLAPSE_KEY = 'collapseKey'; - -const FIELD_DATA = 'data'; -const FIELD_NOTIFICATION = 'notification'; - -// 4 minutes. The fact that the flow includes making a request to the Send Service, -// storing/retrieving form indexedDb asynchronously makes these test units to have a execution time -// variance. Therefore, allowing these units to have a longer time to work is crucial. -const TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS = 240000; - -const TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS = 120000; - -// 1 minute. Wait for object store to be created and received message to be stored in idb. This -// waiting time MUST be longer than the wait time for adding to db in the sw. -const WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS = 60000; - -const wait = ms => new Promise(res => setTimeout(res, ms)); - -describe('Starting Integration Test > Sending and Receiving ', function () { - this.retries(3); - let globalWebDriver; - - before(async function () { - await testServer.start(); - }); - - after(async function () { - await testServer.stop(); - }); - - // TODO: enable testing for firefox - seleniumAssistant.getLocalBrowsers().forEach(assistantBrowser => { - if (assistantBrowser.getId() !== 'chrome') { - return; - } - - TEST_DOMAINS.forEach(domain => { - describe(`Testing browser: ${assistantBrowser.getPrettyName()} : ${domain}`, function () { - before(async function () { - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - }); - - it('Background app can receive a {} empty message from sw', async function () { - this.timeout(TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); - - // Clearing the cache and db data by killing the previously instantiated driver. Note that - // ideally this call is placed inside the after/before hooks. However, Mocha forbids - // operations longer than 2s in hooks. Hence, this clearing call needs to be inside the - // test unit. - await seleniumAssistant.killWebDriver(globalWebDriver); - - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - - prepareBackgroundApp(globalWebDriver, domain); - - checkSendResponse( - await sendMessage({ - to: await retrieveToken(globalWebDriver) - }) - ); - - await wait( - WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS - ); - - checkMessageReceived( - await getReceivedBackgroundMessages(globalWebDriver), - /* expectedNotificationPayload= */ null, - /* expectedDataPayload= */ null, - /* isLegacyPayload= */ false - ); - }); - - it('Background app can receive a {"data"} message frow sw', async function () { - this.timeout(TIMEOUT_BACKGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); - - await seleniumAssistant.killWebDriver(globalWebDriver); - - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - - prepareBackgroundApp(globalWebDriver, domain); - - checkSendResponse( - await sendMessage({ - to: await retrieveToken(globalWebDriver), - data: getTestDataPayload() - }) - ); - - await wait( - WAIT_TIME_BEFORE_RETRIEVING_BACKGROUND_MESSAGES_MILLISECONDS - ); - - checkMessageReceived( - await getReceivedBackgroundMessages(globalWebDriver), - /* expectedNotificationPayload= */ null, - /* expectedDataPayload= */ getTestDataPayload() - ); - }); - }); - - it('Foreground app can receive a {} empty message in onMessage', async function () { - this.timeout(TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); - - await seleniumAssistant.killWebDriver(globalWebDriver); - - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - - await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); - - let token = await retrieveToken(globalWebDriver); - checkSendResponse( - await sendMessage({ - to: token - }) - ); - - await checkMessageReceived( - await getReceivedForegroundMessages(globalWebDriver), - /* expectedNotificationPayload= */ null, - /* expectedDataPayload= */ null - ); - }); - - it('Foreground app can receive a {"notification"} message in onMessage', async function () { - this.timeout(TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); - - await seleniumAssistant.killWebDriver(globalWebDriver); - - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - - await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); - - checkSendResponse( - await sendMessage({ - to: await retrieveToken(globalWebDriver), - notification: getTestNotificationPayload() - }) - ); - - await checkMessageReceived( - await getReceivedForegroundMessages(globalWebDriver), - /* expectedNotificationPayload= */ getTestNotificationPayload(), - /* expectedDataPayload= */ null - ); - }); - - it('Foreground app can receive a {"data"} message in onMessage', async function () { - this.timeout(TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); - - await seleniumAssistant.killWebDriver(globalWebDriver); - - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - - await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); - - checkSendResponse( - await sendMessage({ - to: await retrieveToken(globalWebDriver), - data: getTestDataPayload() - }) - ); - - await checkMessageReceived( - await getReceivedForegroundMessages(globalWebDriver), - /* expectedNotificationPayload= */ null, - /* expectedDataPayload= */ getTestDataPayload() - ); - }); - - it('Foreground app can receive a {"notification", "data"} message in onMessage', async function () { - this.timeout(TIMEOUT_FOREGROUND_MESSAGE_TEST_UNIT_MILLISECONDS); - - await seleniumAssistant.killWebDriver(globalWebDriver); - - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - - await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); - - checkSendResponse( - await sendMessage({ - to: await retrieveToken(globalWebDriver), - data: getTestDataPayload(), - notification: getTestNotificationPayload() - }) - ); - - await checkMessageReceived( - await getReceivedForegroundMessages(globalWebDriver), - /* expectedNotificationPayload= */ getTestNotificationPayload(), - /* expectedDataPayload= */ getTestDataPayload() - ); - }); - }); - }); -}); - -function checkMessageReceived( - receivedMessages, - expectedNotificationPayload, - expectedDataPayload -) { - expect(receivedMessages).to.exist; - - const message = receivedMessages[0]; - - expect(message[FIELD_FROM]).to.equal(TEST_PROJECT_SENDER_ID); - const collapseKey = !!message[FIELD_COLLAPSE_KEY_LEGACY] - ? message[FIELD_COLLAPSE_KEY_LEGACY] - : message[FIELD_COLLAPSE_KEY]; - expect(collapseKey).to.equal(DEFAULT_COLLAPSE_KEY_VALUE); - - if (expectedNotificationPayload) { - expect(message[FIELD_NOTIFICATION]).to.deep.equal( - getTestNotificationPayload() - ); - } - - if (expectedDataPayload) { - expect(message[FIELD_DATA]).to.deep.equal(getTestDataPayload()); - } -} - -function checkSendResponse(response) { - expect(response).to.exist; - expect(response.success).to.equal(1); -} - -function getTestNotificationPayload() { - return { - title: 'test title', - body: 'test body', - icon: '/test/icon.png', - click_action: '/', - tag: 'test-tag' - }; -} - -function getTestDataPayload() { - return { hello: 'world' }; -} - -async function prepareBackgroundApp(globalWebDriver, domain) { - await globalWebDriver.get(`${testServer.serverAddress}/${domain}/`); - - // TODO: remove the try/catch block once the underlying bug has been resolved. Shift window focus - // away from app window so that background messages can be received/processed - try { - await openNewTab(globalWebDriver); - } catch (err) { - // ChromeDriver seems to have an open bug which throws "JavascriptError: javascript error: - // circular reference". Nevertheless, a new tab can still be opened. Hence, just catch and - // continue here. - console.log('FCM (ignored on purpose): ' + err); - } -} diff --git a/integration/messaging/test/test-token-delete.js b/integration/messaging/test/test-token-delete.js new file mode 100644 index 00000000000..d3f0fca908e --- /dev/null +++ b/integration/messaging/test/test-token-delete.js @@ -0,0 +1,82 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const expect = require('chai').expect; +const testServer = require('./utils/test-server'); +const deleteToken = require('./utils/deleteToken'); +const clearAppForTest = require('./utils/clearAppForTest'); +const retrieveToken = require('./utils/retrieveToken'); +const seleniumAssistant = require('selenium-assistant'); +const createPermittedWebDriver = require('./utils/createPermittedWebDriver'); + +const TEST_SUITE_TIMEOUT_MS = 20000; +const TEST_DOMAIN = 'valid-vapid-key'; + +describe('Firebase Messaging Integration Tests > get and delete token', function () { + this.timeout(TEST_SUITE_TIMEOUT_MS); + this.retries(2); + let globalWebDriver; + + before(async function () { + await testServer.start(); + }); + + after(async function () { + await testServer.stop(); + }); + + const availableBrowsers = seleniumAssistant.getLocalBrowsers(); + availableBrowsers.forEach(assistantBrowser => { + // TODO: enable testing for firefox + if (assistantBrowser.getId() !== 'chrome') { + return; + } + + describe(`Testing browser: ${assistantBrowser.getPrettyName()} : ${TEST_DOMAIN}`, function () { + before(async function () { + // Use one webDriver per browser instead of one per test to speed up test. + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + + await globalWebDriver.get( + `${testServer.serverAddress}/${TEST_DOMAIN}/` + ); + }); + + after(async function () { + await seleniumAssistant.killWebDriver(globalWebDriver); + }); + + afterEach(async function () { + await clearAppForTest(globalWebDriver); + }); + + it(`Test app can delete a valid token`, async function () { + const token = await retrieveToken(globalWebDriver); + expect(token).to.exist; + try { + await deleteToken(globalWebDriver, token); + console.log('Token deletion succeed.'); + } catch (e) { + console.log('Error trying to delete FCM token: ', e); + fail(); + } + }); + }); + }); +}); diff --git a/integration/messaging/test/test-token-update.js b/integration/messaging/test/test-token-update.js new file mode 100644 index 00000000000..d2fc33782c1 --- /dev/null +++ b/integration/messaging/test/test-token-update.js @@ -0,0 +1,88 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const seleniumAssistant = require('selenium-assistant'); +const expect = require('chai').expect; +const testServer = require('./utils/test-server'); +const retrieveToken = require('./utils/retrieveToken'); +const clearAppForTest = require('./utils/clearAppForTest'); +const createPermittedWebDriver = require('./utils/createPermittedWebDriver'); +const timeForward = require('./utils/forwardTime'); +const triggerGetToken = require('./utils/triggerGetToken'); +const getErrors = require('./utils/getErrors'); + +const TEST_SUITE_TIMEOUT_MS = 70000; +const TEST_DOMAIN = 'valid-vapid-key'; + +// Getting and deleting token is the entry step of using FM SDK. Let it run first and fail quickly. +require('./test-token-delete'); + +describe('Firebase Messaging Integration Tests > update a token', function () { + this.timeout(TEST_SUITE_TIMEOUT_MS); + this.retries(2); + + let globalWebDriver; + + before(async function () { + await testServer.start(); + }); + + after(async function () { + await testServer.stop(); + }); + + const availableBrowsers = seleniumAssistant.getLocalBrowsers(); + // TODO: enable testing for edge and firefox if applicable + availableBrowsers.forEach(assistantBrowser => { + if (assistantBrowser.getId() !== 'chrome') { + return; + } + + describe(`Testing browser: ${assistantBrowser.getPrettyName()} : ${TEST_DOMAIN}`, function () { + before(async function () { + // Use one webDriver per browser instead of one per test to speed up test. + globalWebDriver = createPermittedWebDriver( + /* browser= */ assistantBrowser.getId() + ); + await globalWebDriver.get( + `${testServer.serverAddress}/${TEST_DOMAIN}/` + ); + }); + + after(async function () { + await seleniumAssistant.killWebDriver(globalWebDriver); + }); + + afterEach(async function () { + await clearAppForTest(globalWebDriver); + }); + + it(`should update a token`, async function () { + const token = await retrieveToken(globalWebDriver); + expect(token).to.exist; + + // roll the clock forward > 7days + await timeForward(globalWebDriver); + const updatedToken = await triggerGetToken(globalWebDriver); + const errors = await getErrors(globalWebDriver); + expect(errors).to.exist; + expect(errors.length).to.equal(0); + expect(updatedToken).to.exist; + }); + }); + }); +}); diff --git a/integration/messaging/test/test-updateToken.js b/integration/messaging/test/test-updateToken.js deleted file mode 100644 index 2de606be2a9..00000000000 --- a/integration/messaging/test/test-updateToken.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const seleniumAssistant = require('selenium-assistant'); -const expect = require('chai').expect; -const testServer = require('./utils/test-server'); -const retrieveToken = require('./utils/retrieveToken'); -const clearAppForTest = require('./utils/clearAppForTest'); -const createPermittedWebDriver = require('./utils/createPermittedWebDriver'); -const timeForward = require('./utils/forwardTime'); -const triggerGetToken = require('./utils/triggerGetToken'); -const getErrors = require('./utils/getErrors'); - -const TEST_SUITE_TIMEOUT_MS = 70000; -const TEST_DOMAIN = 'valid-vapid-key'; - -describe('Firebase Messaging Integration Tests > update a token', function () { - this.timeout(TEST_SUITE_TIMEOUT_MS); - this.retries(3); - - let globalWebDriver; - - before(async function () { - await testServer.start(); - }); - - after(async function () { - await testServer.stop(); - }); - - const availableBrowsers = seleniumAssistant.getLocalBrowsers(); - // TODO: enable testing for edge and firefox if applicable - availableBrowsers.forEach(assistantBrowser => { - if (assistantBrowser.getId() !== 'chrome') { - return; - } - - describe(`Testing browser: ${assistantBrowser.getPrettyName()} : ${TEST_DOMAIN}`, function () { - before(async function () { - // Use one webDriver per browser instead of one per test to speed up test. - globalWebDriver = createPermittedWebDriver( - /* browser= */ assistantBrowser.getId() - ); - await globalWebDriver.get( - `${testServer.serverAddress}/${TEST_DOMAIN}/` - ); - }); - - after(async function () { - await seleniumAssistant.killWebDriver(globalWebDriver); - }); - - afterEach(async function () { - await clearAppForTest(globalWebDriver); - }); - - it(`should update a token`, async function () { - const token = await retrieveToken(globalWebDriver); - expect(token).to.exist; - - // roll the clock forward > 7days - await timeForward(globalWebDriver); - const updatedToken = await triggerGetToken(globalWebDriver); - const errors = await getErrors(globalWebDriver); - expect(errors).to.exist; - expect(errors.length).to.equal(0); - expect(updatedToken).to.exist; - }); - }); - }); -}); diff --git a/integration/messaging/test/test-useDefaultServiceWorker.js b/integration/messaging/test/test-useDefaultServiceWorker.js index 0d29f22b0ab..0e3a903042f 100644 --- a/integration/messaging/test/test-useDefaultServiceWorker.js +++ b/integration/messaging/test/test-useDefaultServiceWorker.js @@ -24,11 +24,12 @@ const createPermittedWebDriver = require('./utils/createPermittedWebDriver'); const TEST_DOMAIN = 'default-sw'; const TEST_SUITE_TIMEOUT_MS = 70000; +// Getting and deleting token is the entry step of using FM SDK. Let it run first and fail quickly. +require('./test-token-delete'); + describe(`Firebase Messaging Integration Tests > Use 'firebase-messaging-sw.js' by default`, function () { this.timeout(TEST_SUITE_TIMEOUT_MS); - this.retries(3); - let globalWebDriver; before(async function () { diff --git a/integration/messaging/test/test-useValidManifest.js b/integration/messaging/test/test-useValidManifest.js index da2a8c3f15f..4c566fc9eb4 100644 --- a/integration/messaging/test/test-useValidManifest.js +++ b/integration/messaging/test/test-useValidManifest.js @@ -24,11 +24,12 @@ const createPermittedWebDriver = require('./utils/createPermittedWebDriver'); const TEST_DOMAIN = 'valid-manifest'; const TEST_SUITE_TIMEOUT_MS = 70000; +// Getting and deleting token is the entry step of using FM SDK. Let it run first and fail quickly. +require('./test-token-delete'); + describe(`Firebase Messaging Integration Tests > Use 'use valid manifest`, function () { this.timeout(TEST_SUITE_TIMEOUT_MS); - this.retries(3); - let globalWebDriver; before(async function () { diff --git a/integration/messaging/test/utils/checkMessageReceived.js b/integration/messaging/test/utils/checkMessageReceived.js new file mode 100644 index 00000000000..c407ae6b9b8 --- /dev/null +++ b/integration/messaging/test/utils/checkMessageReceived.js @@ -0,0 +1,66 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const expect = require('chai').expect; + +const TEST_PROJECT_SENDER_ID = '750970317741'; +const DEFAULT_COLLAPSE_KEY_VALUE = 'do_not_collapse'; +const FIELD_FROM = 'from'; +const FIELD_COLLAPSE_KEY_LEGACY = 'collapse_key'; +const FIELD_COLLAPSE_KEY = 'collapseKey'; + +const FIELD_DATA = 'data'; +const FIELD_NOTIFICATION = 'notification'; + +module.exports = ( + receivedMessages, + expectedNotificationPayload, + expectedDataPayload +) => { + expect(receivedMessages).to.exist; + + const message = receivedMessages[0]; + + expect(message[FIELD_FROM]).to.equal(TEST_PROJECT_SENDER_ID); + const collapseKey = !!message[FIELD_COLLAPSE_KEY_LEGACY] + ? message[FIELD_COLLAPSE_KEY_LEGACY] + : message[FIELD_COLLAPSE_KEY]; + expect(collapseKey).to.equal(DEFAULT_COLLAPSE_KEY_VALUE); + + if (expectedNotificationPayload) { + expect(message[FIELD_NOTIFICATION]).to.deep.equal( + getTestNotificationPayload() + ); + } + + if (expectedDataPayload) { + expect(message[FIELD_DATA]).to.deep.equal(getTestDataPayload()); + } +}; + +function getTestNotificationPayload() { + return { + title: 'test title', + body: 'test body', + icon: '/test/icon.png', + click_action: '/', + tag: 'test-tag' + }; +} + +function getTestDataPayload() { + return { hello: 'world' }; +} diff --git a/integration/messaging/test/utils/checkSendResponse.js b/integration/messaging/test/utils/checkSendResponse.js new file mode 100644 index 00000000000..5c43f78ef8a --- /dev/null +++ b/integration/messaging/test/utils/checkSendResponse.js @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const expect = require('chai').expect; + +module.exports = response => { + expect(response).to.exist; + expect(response.success).to.equal(1); +}; diff --git a/integration/messaging/test/utils/getReceivedBackgroundMessages.js b/integration/messaging/test/utils/getReceivedBackgroundMessages.js index 3f20e4cb946..d6af2471545 100644 --- a/integration/messaging/test/utils/getReceivedBackgroundMessages.js +++ b/integration/messaging/test/utils/getReceivedBackgroundMessages.js @@ -35,11 +35,10 @@ module.exports = async webdriver => { const db = dbOpenReq.result; const tx = db.transaction(BACKGROUND_MESSAGES_OBJECT_STORE, 'readonly'); - tx - .objectStore(BACKGROUND_MESSAGES_OBJECT_STORE) - .getAll().onsuccess = e => { - window.backgroundMessages = e.target.result; - }; + tx.objectStore(BACKGROUND_MESSAGES_OBJECT_STORE).getAll().onsuccess = + e => { + window.backgroundMessages = e.target.result; + }; }; }); diff --git a/integration/messaging/test/utils/sendMessage.js b/integration/messaging/test/utils/sendMessage.js index 9d3b93986f1..6393e90364c 100644 --- a/integration/messaging/test/utils/sendMessage.js +++ b/integration/messaging/test/utils/sendMessage.js @@ -15,7 +15,6 @@ * limitations under the License. */ -const fetch = require('node-fetch'); const FCM_SEND_ENDPOINT = 'https://fcm.googleapis.com/fcm/send'; // Rotatable fcm server key. It's generally a bad idea to expose server keys. The reason is to // simplify testing process (no need to implement server side decryption of git secret). The diff --git a/lerna.json b/lerna.json index 9192331da41..1d51eb2d1c0 100644 --- a/lerna.json +++ b/lerna.json @@ -3,7 +3,6 @@ "npmClient": "yarn", "packages": [ "packages/*", - "packages-exp/*", "integration/*", "repo-scripts/*" ], diff --git a/package.json b/package.json index aecb4ef4140..39455ef1161 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", "engines": { - "node": "^8.13.0 || >=10.10.0" + "node": ">=20.0.0" }, "homepage": "https://github.com/firebase/firebase-js-sdk", "keywords": [ @@ -21,146 +21,143 @@ "performance" ], "scripts": { - "dev": "lerna run --parallel --scope @firebase/* --scope firebase --scope rxfire dev", - "build": "lerna run --scope @firebase/app-exp build:deps && lerna run --scope @firebase/* --scope firebase --scope rxfire --ignore @firebase/app-exp build", - "build:exp": "lerna run --scope @firebase/*-exp --scope @firebase/*-compat --scope firebase-exp build", - "build:release": "lerna run --scope @firebase/app-exp build:deps && lerna run --scope @firebase/* --scope firebase --scope rxfire --ignore @firebase/*-exp --ignore @firebase/*-compat build", + "dev": "lerna run --parallel --scope @firebase/* --scope firebase dev", + "build": "lerna run --scope @firebase/* --scope firebase build", "build:changed": "ts-node-script scripts/ci-test/build_changed.ts", - "link:packages": "lerna exec --scope @firebase/* --scope firebase --scope rxfire -- yarn link", + "release:prepare": "lerna run --scope @firebase/* add-compat-overloads && lerna run --scope @firebase/* typings:public", + "link:packages": "lerna exec --scope @firebase/* --scope firebase -- yarn link", "stage:packages": "./scripts/prepublish.sh", "repl": "node tools/repl.js", "release": "ts-node-script scripts/release/cli.ts", - "release:exp": "ts-node-script scripts/exp/release.ts", "pretest": "node tools/pretest.js", - "test": "lerna run --concurrency 4 --stream test", - "test:ci": "lerna run --concurrency 4 test:ci", - "test:release": "lerna run --concurrency 4 --ignore @firebase/*-exp --ignore firebase-exp --ignore @firebase/*-compat test:ci", - "test:exp": "lerna run --concurrency 4 --scope @firebase/*-exp --scope firebase-exp --scope @firebase/*-compat --stream test", + "test": "lerna run --ignore firebase-messaging-integration-test --stream test", + "test:ci": "lerna run --ignore firebase-messaging-integration-test test:ci", "pretest:coverage": "mkdirp coverage", "ci:coverage": "lcov-result-merger 'packages/**/lcov.info' 'lcov-all.info'", "test:coverage": "lcov-result-merger 'packages/**/lcov.info' | coveralls", "test:changed": "ts-node-script scripts/ci-test/test_changed.ts", "test:setup": "node tools/config.js", "test:saucelabs": "node scripts/run_saucelabs.js", - "docgen:js": "node scripts/docgen/generate-docs.js --api js", - "docgen:node": "node scripts/docgen/generate-docs.js --api node", - "docgen": "yarn docgen:js; yarn docgen:node", - "prettier": "prettier --config .prettierrc --write '**/*.{ts,js}'", - "lint": "lerna run --scope @firebase/* --scope rxfire lint", - "lint:fix": "lerna run --scope @firebase/* --scope rxfire lint:fix", + "trusted-type-check": "lerna run --scope @firebase/* trusted-type-check --no-bail", + "docgen": "ts-node-script scripts/docgen/docgen.ts", + "docgen:compat": "node scripts/docgen-compat/generate-docs.js --api js", + "docgen:all": "yarn docgen devsite && yarn docgen toc", + "lint": "lerna run --scope @firebase/* lint", + "lint:fix": "lerna run --scope @firebase/* lint:fix", "size-report": "ts-node-script scripts/size_report/report_binary_size.ts", "modular-export-size-report": "ts-node-script scripts/size_report/report_modular_export_binary_size.ts", - "api-report": "lerna run --scope @firebase/*-exp --scope @firebase/firestore api-report", - "docgen:exp": "ts-node-script scripts/exp/docgen.ts", - "postinstall": "yarn --cwd repo-scripts/changelog-generator build", - "sa": "ts-node-script repo-scripts/size-analysis/cli.ts" + "api-report": "lerna run --scope @firebase/* api-report", + "postinstall": "patch-package && yarn --cwd repo-scripts/changelog-generator build", + "sa": "ts-node-script repo-scripts/size-analysis/cli.ts", + "api-documenter-devsite": "ts-node-script repo-scripts/api-documenter/src/start.ts", + "format": "ts-node ./scripts/format/format.ts" }, "repository": { "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" + "url": "git+https://github.com/firebase/firebase-js-sdk.git" }, "workspaces": [ "packages/*", - "packages-exp/*", "integration/*", "repo-scripts/*" ], "devDependencies": { - "@babel/core": "7.12.10", - "@babel/plugin-transform-modules-commonjs": "7.12.1", - "@babel/preset-env": "7.12.11", - "@changesets/changelog-github": "0.2.7", - "@changesets/cli": "2.12.0", - "@types/chai": "4.2.14", - "@types/chai-as-promised": "7.1.3", - "@types/child-process-promise": "2.2.1", - "@types/clone": "2.1.0", - "@types/eslint": "7.2.6", - "@types/inquirer": "7.3.1", - "@types/listr": "0.14.2", - "@types/long": "4.0.1", - "@types/mocha": "7.0.2", - "@types/mz": "2.7.3", - "@types/node": "12.19.11", - "@types/sinon": "9.0.10", - "@types/sinon-chai": "3.2.5", - "@types/tmp": "0.2.0", - "@types/yargs": "15.0.12", - "@typescript-eslint/eslint-plugin": "4.11.1", - "@typescript-eslint/eslint-plugin-tslint": "4.11.1", - "@typescript-eslint/parser": "4.11.1", - "api-documenter-me": "0.1.0", - "api-extractor-me": "0.1.1", - "babel-loader": "8.2.2", - "chai": "4.2.0", - "chai-as-promised": "7.1.1", - "chalk": "4.1.0", + "@babel/core": "7.26.8", + "@babel/plugin-transform-modules-commonjs": "7.26.3", + "@babel/preset-env": "7.26.8", + "@babel/preset-typescript": "7.26.0", + "@babel/register": "7.25.9", + "@changesets/changelog-github": "0.5.0", + "@changesets/cli": "2.27.12", + "@types/chai": "4.3.20", + "@types/chai-as-promised": "7.1.8", + "@types/child-process-promise": "2.2.6", + "@types/clone": "2.1.4", + "@types/eslint": "7.29.0", + "@types/inquirer": "8.2.10", + "@types/js-yaml": "4.0.9", + "@types/listr": "0.14.9", + "@types/long": "4.0.2", + "@types/mocha": "9.1.1", + "@types/mz": "2.7.8", + "@types/node": "18.19.83", + "@types/request": "2.48.12", + "@types/sinon": "9.0.11", + "@types/sinon-chai": "3.2.12", + "@types/tmp": "0.2.6", + "@types/trusted-types": "2.0.7", + "@types/yargs": "17.0.33", + "@typescript-eslint/eslint-plugin": "7.18.0", + "@typescript-eslint/eslint-plugin-tslint": "7.0.2", + "@typescript-eslint/parser": "7.18.0", + "api-documenter-me": "0.1.1", + "api-extractor-me": "0.1.2", + "babel-loader": "8.4.1", + "chai": "4.5.0", + "chai-as-promised": "7.1.2", + "chalk": "4.1.2", "child-process-promise": "2.2.1", "clone": "2.1.2", - "coveralls": "3.1.0", - "del": "6.0.0", - "dependency-graph": "0.9.0", - "eslint": "7.16.0", - "eslint-plugin-import": "2.22.1", - "eslint-plugin-unused-imports": "1.0.1", - "express": "4.17.1", + "coveralls": "3.1.1", + "del": "6.1.1", + "dependency-graph": "0.11.0", + "eslint": "8.57.1", + "eslint-plugin-import": "2.31.0", + "eslint-plugin-unused-imports": "3.2.0", + "express": "4.21.2", "find-free-port": "2.0.0", - "firebase-functions": "3.13.0", - "firebase-tools": "9.1.0", - "git-rev-sync": "3.0.1", - "glob": "7.1.6", - "http-server": "0.12.3", - "husky": "4.3.6", - "indexeddbshim": "7.0.0", - "inquirer": "7.3.3", - "istanbul-instrumenter-loader": "3.0.1", - "js-yaml": "3.14.1", - "karma": "5.2.3", - "karma-chrome-launcher": "3.1.0", + "firebase-tools": "13.30.0", + "glob": "7.2.3", + "http-server": "14.1.1", + "indexeddbshim": "10.1.0", + "inquirer": "8.2.6", + "js-yaml": "4.1.0", + "karma": "6.4.4", + "karma-chrome-launcher": "3.2.0", "karma-cli": "2.0.0", - "karma-coverage-istanbul-reporter": "2.1.1", - "karma-firefox-launcher": "2.1.0", + "karma-coverage-istanbul-reporter": "3.0.3", + "karma-firefox-launcher": "2.1.3", "karma-mocha": "2.0.1", "karma-mocha-reporter": "2.2.5", - "karma-safari-launcher": "1.0.0", - "karma-sourcemap-loader": "0.3.8", - "karma-spec-reporter": "0.0.32", - "karma-summary-reporter": "1.9.0", - "karma-webpack": "4.0.2", - "lcov-result-merger": "3.1.0", - "lerna": "3.22.1", + "karma-sourcemap-loader": "0.4.0", + "karma-spec-reporter": "0.0.36", + "karma-summary-reporter": "3.1.1", + "karma-webkit-launcher": "2.6.0", + "karma-webpack": "5.0.0", + "lcov-result-merger": "3.3.0", + "lerna": "4.0.0", "listr": "0.14.3", - "lodash": "4.17.20", + "lodash": "4.17.21", "long": "3.2.0", "merge2": "1.4.1", "mkdirp": "1.0.4", - "mocha": "7.2.0", + "mocha": "9.2.2", "mz": "2.7.0", - "npm-run-all": "4.1.5", - "npm-run-path": "4.0.1", + "node-polyfill-webpack-plugin": "2.0.1", + "npm-run-all2": "5.0.2", "nyc": "15.1.0", - "ora": "5.1.0", - "prettier": "2.2.1", + "ora": "5.4.1", + "patch-package": "7.0.2", + "playwright": "1.51.1", + "postinstall-postinstall": "2.1.0", + "prettier": "2.8.8", "protractor": "5.4.2", - "rxjs": "6.6.3", - "semver": "7.3.4", - "simple-git": "2.31.0", - "sinon": "9.2.2", - "sinon-chai": "3.5.0", + "request": "2.88.2", + "semver": "7.7.1", + "simple-git": "3.27.0", + "sinon": "9.2.4", + "sinon-chai": "3.7.0", "source-map-loader": "1.1.3", - "terser": "5.5.1", - "ts-loader": "8.0.12", - "ts-node": "9.1.1", + "sqlite3": "5.1.7", + "terser": "5.38.1", + "ts-loader": "9.5.2", + "ts-node": "10.9.2", + "tsec": "0.2.8", "tslint": "6.1.3", "typedoc": "0.16.11", - "typescript": "4.0.5", + "typescript": "5.5.4", "watch": "1.0.2", - "webpack": "4.44.2", - "yargs": "16.2.0" - }, - "husky": { - "hooks": { - "pre-commit": "node tools/gitHooks/precommit.js" - } + "webpack": "5.98.0", + "yargs": "17.7.2" } } diff --git a/packages-exp/app-compat/README.md b/packages-exp/app-compat/README.md deleted file mode 100644 index bb7e8ac3386..00000000000 --- a/packages-exp/app-compat/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @firebase/app-compat - -This is the compat package that recreates the v7 APIs. - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/app-compat/package.json b/packages-exp/app-compat/package.json deleted file mode 100644 index b98fdb489d8..00000000000 --- a/packages-exp/app-compat/package.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "@firebase/app-compat", - "version": "0.0.900", - "description": "The primary entrypoint to the Firebase JS SDK", - "author": "Firebase (https://firebase.google.com/)", - "private": true, - "main": "dist/index.cjs.js", - "browser": "dist/index.esm5.js", - "module": "dist/index.esm5.js", - "esm2017": "dist/index.esm2017.js", - "lite": "dist/index.lite.js", - "lite-esm2017": "dist/index.lite.esm2017.js", - "files": ["dist"], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c", - "build:release": "rollup -c rollup.config.release.js", - "build:deps": "lerna run --scope @firebase/app-compat --include-dependencies build", - "dev": "rollup -c -w", - "test": "run-p lint test:all", - "test:all": "run-p test:browser test:node", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", - "test:browser": "karma start --single-run", - "test:browser:debug": "karma start --browsers Chrome --auto-watch", - "test:node": "TS_NODE_FILES=true TS_NODE_CACHE=NO TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha test/**/*.test.* src/**/*.test.ts --config ../../config/mocharc.node.js" - }, - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-exp": "0.0.900", - "@firebase/util": "0.3.4", - "@firebase/logger": "0.2.6", - "@firebase/component": "0.1.21", - "tslib": "^1.11.1", - "dom-storage": "2.1.0", - "xmlhttprequest": "1.8.0" - }, - "devDependencies": { - "rollup": "2.35.1", - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-replace": "2.2.0", - "rollup-plugin-typescript2": "0.29.0", - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages-exp/app-compat", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "dist/src/index.d.ts", - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/app-compat/rollup.config.js b/packages-exp/app-compat/rollup.config.js deleted file mode 100644 index 5e4e3657a39..00000000000 --- a/packages-exp/app-compat/rollup.config.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import typescriptPlugin from 'rollup-plugin-typescript2'; -import json from '@rollup/plugin-json'; -import typescript from 'typescript'; -import pkg from './package.json'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - abortOnError: false - }), - json() -]; - -const es5Builds = [ - { - input: 'src/index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } - ], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - { - input: 'src/index.lite.ts', - output: { - file: pkg.lite, - format: 'es', - sourcemap: true - }, - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - abortOnError: false, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - } - }), - json({ - preferConst: true - }) -]; - -const es2017Builds = [ - /** - * Browser Builds - */ - { - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - plugins: es2017BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - { - input: 'src/index.lite.ts', - output: { - file: pkg['lite-esm2017'], - format: 'es', - sourcemap: true - }, - plugins: es2017BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/app-compat/rollup.config.release.js b/packages-exp/app-compat/rollup.config.release.js deleted file mode 100644 index a38c8a03d41..00000000000 --- a/packages-exp/app-compat/rollup.config.release.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import typescriptPlugin from 'rollup-plugin-typescript2'; -import json from '@rollup/plugin-json'; -import typescript from 'typescript'; -import pkg from './package.json'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - clean: true, - abortOnError: false, - transformers: [importPathTransformer] - }), - json() -]; - -const es5Builds = [ - { - input: 'src/index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } - ], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - treeshake: { - moduleSideEffects: false - } - }, - { - input: 'src/index.lite.ts', - output: { - file: pkg.lite, - format: 'es', - sourcemap: true - }, - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - treeshake: { - moduleSideEffects: false - } - } -]; - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - clean: true, - abortOnError: false, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - transformers: [importPathTransformer] - }), - json({ - preferConst: true - }) -]; - -const es2017Builds = [ - /** - * Browser Builds - */ - { - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - plugins: es2017BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - treeshake: { - moduleSideEffects: false - } - }, - { - input: 'src/index.lite.ts', - output: { - file: pkg['lite-esm2017'], - format: 'es', - sourcemap: true - }, - plugins: es2017BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), - treeshake: { - moduleSideEffects: false - } - } -]; - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/app-compat/src/firebaseApp.ts b/packages-exp/app-compat/src/firebaseApp.ts deleted file mode 100644 index 19fe04d0cd0..00000000000 --- a/packages-exp/app-compat/src/firebaseApp.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp, FirebaseOptions } from '@firebase/app-types'; -import { - _FirebaseNamespace, - FirebaseService -} from '@firebase/app-types/private'; -import { - Component, - ComponentType, - Name, - ComponentContainer -} from '@firebase/component'; -import { _FirebaseAppInternal } from '@firebase/app-types-exp'; -import { - deleteApp, - _addComponent, - _addOrOverwriteComponent, - _DEFAULT_ENTRY_NAME -} from '@firebase/app-exp'; - -/** - * Global context object for a collection of services using - * a shared authentication state. - */ -export class FirebaseAppImpl implements FirebaseApp { - private readonly container: ComponentContainer; - - constructor( - private readonly app: _FirebaseAppInternal, - private readonly firebase: _FirebaseNamespace - ) { - // add itself to container - // TODO: change the component name to 'app-compat' before the official release - _addComponent(app, new Component('app', () => this, ComponentType.PUBLIC)); - this.container = app.container; - } - - get automaticDataCollectionEnabled(): boolean { - return this.app.automaticDataCollectionEnabled; - } - - set automaticDataCollectionEnabled(val) { - this.app.automaticDataCollectionEnabled = val; - } - - get name(): string { - return this.app.name; - } - - get options(): FirebaseOptions { - return this.app.options; - } - - delete(): Promise { - return new Promise(resolve => { - this.app.checkDestroyed(); - resolve(); - }).then(() => { - this.firebase.INTERNAL.removeApp(this.name); - return deleteApp(this.app); - }); - } - - /** - * Return a service instance associated with this app (creating it - * on demand), identified by the passed instanceIdentifier. - * - * NOTE: Currently storage and functions are the only ones that are leveraging this - * functionality. They invoke it by calling: - * - * ```javascript - * firebase.app().storage('STORAGE BUCKET ID') - * ``` - * - * The service name is passed to this already - * @internal - */ - _getService( - name: string, - instanceIdentifier: string = _DEFAULT_ENTRY_NAME - ): FirebaseService { - this.app.checkDestroyed(); - - // getImmediate will always succeed because _getService is only called for registered components. - return (this.app.container.getProvider(name as Name).getImmediate({ - identifier: instanceIdentifier - }) as unknown) as FirebaseService; - } - - /** - * Remove a service instance from the cache, so we will create a new instance for this service - * when people try to get it again. - * - * NOTE: currently only firestore uses this functionality to support firestore shutdown. - * - * @param name The service name - * @param instanceIdentifier instance identifier in case multiple instances are allowed - * @internal - */ - _removeServiceInstance( - name: string, - instanceIdentifier: string = _DEFAULT_ENTRY_NAME - ): void { - this.app.container - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .getProvider(name as any) - .clearInstance(instanceIdentifier); - } - - /** - * @param component the component being added to this app's container - * @internal - */ - _addComponent(component: Component): void { - _addComponent(this.app, component); - } - - _addOrOverwriteComponent(component: Component): void { - _addOrOverwriteComponent(this.app, component); - } -} - -// TODO: investigate why the following needs to be commented out -// Prevent dead-code elimination of these methods w/o invalid property -// copying. -// (FirebaseAppImpl.prototype.name && FirebaseAppImpl.prototype.options) || -// FirebaseAppImpl.prototype.delete || -// console.log('dc'); diff --git a/packages-exp/app-compat/src/firebaseNamespace.ts b/packages-exp/app-compat/src/firebaseNamespace.ts deleted file mode 100644 index a57ab0bd155..00000000000 --- a/packages-exp/app-compat/src/firebaseNamespace.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseNamespace } from '@firebase/app-types'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { createSubscribe, deepExtend, ErrorFactory } from '@firebase/util'; -import { FirebaseAppImpl } from './firebaseApp'; -import { createFirebaseNamespaceCore } from './firebaseNamespaceCore'; - -/** - * Return a firebase namespace object. - * - * In production, this will be called exactly once and the result - * assigned to the 'firebase' global. It may be called multiple times - * in unit tests. - */ -export function createFirebaseNamespace(): FirebaseNamespace { - const namespace = createFirebaseNamespaceCore(FirebaseAppImpl); - (namespace as _FirebaseNamespace).INTERNAL = { - ...(namespace as _FirebaseNamespace).INTERNAL, - createFirebaseNamespace, - extendNamespace, - createSubscribe, - ErrorFactory, - deepExtend - }; - - /** - * Patch the top-level firebase namespace with additional properties. - * - * firebase.INTERNAL.extendNamespace() - */ - function extendNamespace(props: { [prop: string]: unknown }): void { - deepExtend(namespace, props); - } - - return namespace; -} - -export const firebase = createFirebaseNamespace(); diff --git a/packages-exp/app-compat/src/firebaseNamespaceCore.ts b/packages-exp/app-compat/src/firebaseNamespaceCore.ts deleted file mode 100644 index ae9a2ddd5f8..00000000000 --- a/packages-exp/app-compat/src/firebaseNamespaceCore.ts +++ /dev/null @@ -1,207 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - FirebaseApp, - FirebaseOptions, - FirebaseNamespace -} from '@firebase/app-types'; // TODO: create @firebase/app-types-compat before the official release -import { - _FirebaseNamespace, - FirebaseService, - FirebaseServiceNamespace -} from '@firebase/app-types/private'; -import * as modularAPIs from '@firebase/app-exp'; -import { _FirebaseAppInternal } from '@firebase/app-types-exp'; -import { Component, ComponentType } from '@firebase/component'; - -import { deepExtend, contains } from '@firebase/util'; -import { FirebaseAppImpl } from './firebaseApp'; -import { ERROR_FACTORY, AppError } from './errors'; -import { FirebaseAppLiteImpl } from './lite/firebaseAppLite'; - -/** - * Because auth can't share code with other components, we attach the utility functions - * in an internal namespace to share code. - * This function return a firebase namespace object without - * any utility functions, so it can be shared between the regular firebaseNamespace and - * the lite version. - */ -export function createFirebaseNamespaceCore( - firebaseAppImpl: typeof FirebaseAppImpl | typeof FirebaseAppLiteImpl -): FirebaseNamespace { - const apps: { [name: string]: FirebaseApp } = {}; - // // eslint-disable-next-line @typescript-eslint/no-explicit-any - // const components = new Map>(); - - // A namespace is a plain JavaScript Object. - const namespace: FirebaseNamespace = { - // Hack to prevent Babel from modifying the object returned - // as the firebase namespace. - // @ts-ignore - __esModule: true, - initializeApp: initializeAppCompat, - // @ts-ignore - app, - registerVersion: modularAPIs.registerVersion, - setLogLevel: modularAPIs.setLogLevel, - onLog: modularAPIs.onLog, - // @ts-ignore - apps: null, - SDK_VERSION: modularAPIs.SDK_VERSION, - INTERNAL: { - registerComponent: registerComponentCompat, - removeApp, - useAsService, - modularAPIs - } - }; - - // Inject a circular default export to allow Babel users who were previously - // using: - // - // import firebase from 'firebase'; - // which becomes: var firebase = require('firebase').default; - // - // instead of - // - // import * as firebase from 'firebase'; - // which becomes: var firebase = require('firebase'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (namespace as any)['default'] = namespace; - - // firebase.apps is a read-only getter. - Object.defineProperty(namespace, 'apps', { - get: getApps - }); - - /** - * Called by App.delete() - but before any services associated with the App - * are deleted. - */ - function removeApp(name: string): void { - delete apps[name]; - } - - /** - * Get the App object for a given name (or DEFAULT). - */ - function app(name?: string): FirebaseApp { - name = name || modularAPIs._DEFAULT_ENTRY_NAME; - if (!contains(apps, name)) { - throw ERROR_FACTORY.create(AppError.NO_APP, { appName: name }); - } - return apps[name]; - } - - // @ts-ignore - app['App'] = firebaseAppImpl; - - /** - * Create a new App instance (name must be unique). - */ - function initializeAppCompat( - options: FirebaseOptions, - rawConfig = {} - ): FirebaseApp { - const app = modularAPIs.initializeApp( - options, - rawConfig - ) as _FirebaseAppInternal; - const appCompat = new firebaseAppImpl(app, namespace as _FirebaseNamespace); - apps[app.name] = appCompat; - return appCompat; - } - - /* - * Return an array of all the non-deleted FirebaseApps. - */ - function getApps(): FirebaseApp[] { - // Make a copy so caller cannot mutate the apps list. - return Object.keys(apps).map(name => apps[name]); - } - - function registerComponentCompat( - component: Component - ): FirebaseServiceNamespace | null { - const componentName = component.name; - if ( - modularAPIs._registerComponent(component) && - component.type === ComponentType.PUBLIC - ) { - // create service namespace for public components - // The Service namespace is an accessor function ... - const serviceNamespace = ( - appArg: FirebaseApp = app() - ): FirebaseService => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (typeof (appArg as any)[componentName] !== 'function') { - // Invalid argument. - // This happens in the following case: firebase.storage('gs:/') - throw ERROR_FACTORY.create(AppError.INVALID_APP_ARGUMENT, { - appName: componentName - }); - } - - // Forward service instance lookup to the FirebaseApp. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (appArg as any)[componentName](); - }; - - // ... and a container for service-level properties. - if (component.serviceProps !== undefined) { - deepExtend(serviceNamespace, component.serviceProps); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (namespace as any)[componentName] = serviceNamespace; - - // Patch the FirebaseAppImpl prototype - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (firebaseAppImpl.prototype as any)[componentName] = - // TODO: The eslint disable can be removed and the 'ignoreRestArgs' - // option added to the no-explicit-any rule when ESlint releases it. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function (...args: any) { - const serviceFxn = this._getService.bind(this, componentName); - return serviceFxn.apply( - this, - component.multipleInstances ? args : [] - ); - }; - } - - return component.type === ComponentType.PUBLIC - ? // eslint-disable-next-line @typescript-eslint/no-explicit-any - (namespace as any)[componentName] - : null; - } - - // Map the requested service to a registered service name - // (used to map auth to serverAuth service when needed). - function useAsService(app: FirebaseApp, name: string): string | null { - if (name === 'serverAuth') { - return null; - } - - const useService = name; - - return useService; - } - - return namespace; -} diff --git a/packages-exp/app-compat/src/index.lite.ts b/packages-exp/app-compat/src/index.lite.ts deleted file mode 100644 index c411c425822..00000000000 --- a/packages-exp/app-compat/src/index.lite.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createFirebaseNamespaceLite } from './lite/firebaseNamespaceLite'; -import { registerCoreComponents } from './registerCoreComponents'; - -export const firebase = createFirebaseNamespaceLite(); - -registerCoreComponents('lite'); - -// eslint-disable-next-line import/no-default-export -export default firebase; diff --git a/packages-exp/app-compat/src/index.ts b/packages-exp/app-compat/src/index.ts deleted file mode 100644 index aa65496a4ef..00000000000 --- a/packages-exp/app-compat/src/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseNamespace } from '@firebase/app-types'; -import { isBrowser } from '@firebase/util'; -import { firebase as firebaseNamespace } from './firebaseNamespace'; -import { logger } from './logger'; -import { registerCoreComponents } from './registerCoreComponents'; - -// Firebase Lite detection -// eslint-disable-next-line @typescript-eslint/no-explicit-any -if (isBrowser() && (self as any).firebase !== undefined) { - logger.warn(` - Warning: Firebase is already defined in the global scope. Please make sure - Firebase library is only loaded once. - `); - - // eslint-disable-next-line - const sdkVersion = ((self as any).firebase as FirebaseNamespace).SDK_VERSION; - if (sdkVersion && sdkVersion.indexOf('LITE') >= 0) { - logger.warn(` - Warning: You are trying to load Firebase while using Firebase Performance standalone script. - You should load Firebase Performance with this instance of Firebase to avoid loading duplicate code. - `); - } -} - -export const firebase = firebaseNamespace; - -registerCoreComponents(); - -// eslint-disable-next-line import/no-default-export -export default firebase; diff --git a/packages-exp/app-compat/src/lite/firebaseAppLite.ts b/packages-exp/app-compat/src/lite/firebaseAppLite.ts deleted file mode 100644 index cff6a40bf46..00000000000 --- a/packages-exp/app-compat/src/lite/firebaseAppLite.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp, FirebaseOptions } from '@firebase/app-types'; -import { - _FirebaseNamespace, - FirebaseService -} from '@firebase/app-types/private'; -import { - deleteApp, - _addComponent, - _DEFAULT_ENTRY_NAME -} from '@firebase/app-exp'; -import { _FirebaseAppInternal } from '@firebase/app-types-exp'; -import { Component, ComponentType, Name } from '@firebase/component'; - -/** - * Global context object for a collection of services using - * a shared authentication state. - */ -export class FirebaseAppLiteImpl implements FirebaseApp { - constructor( - private readonly app: _FirebaseAppInternal, - private readonly firebase: _FirebaseNamespace - ) { - // add itself to container - _addComponent(app, new Component('app', () => this, ComponentType.PUBLIC)); - } - - get automaticDataCollectionEnabled(): boolean { - return this.app.automaticDataCollectionEnabled; - } - - set automaticDataCollectionEnabled(val) { - this.automaticDataCollectionEnabled = val; - } - - get name(): string { - return this.app.name; - } - - get options(): FirebaseOptions { - return this.app.options; - } - - delete(): Promise { - this.firebase.INTERNAL.removeApp(this.name); - return deleteApp(this.app); - } - - /** - * Return a service instance associated with this app (creating it - * on demand), identified by the passed instanceIdentifier. - * - * NOTE: Currently storage is the only one that is leveraging this - * functionality. They invoke it by calling: - * - * ```javascript - * firebase.app().storage('STORAGE BUCKET ID') - * ``` - * - * The service name is passed to this already - * @internal - */ - _getService( - name: string, - instanceIdentifier: string = _DEFAULT_ENTRY_NAME - ): FirebaseService { - this.app.checkDestroyed(); - - // getImmediate will always succeed because _getService is only called for registered components. - return (this.app.container.getProvider(name as Name).getImmediate({ - identifier: instanceIdentifier - }) as unknown) as FirebaseService; - } -} diff --git a/packages-exp/app-compat/src/lite/firebaseNamespaceLite.ts b/packages-exp/app-compat/src/lite/firebaseNamespaceLite.ts deleted file mode 100644 index 5904e46ac60..00000000000 --- a/packages-exp/app-compat/src/lite/firebaseNamespaceLite.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseNamespace } from '@firebase/app-types'; -import { - _FirebaseNamespace, - FirebaseServiceNamespace, - FirebaseService -} from '@firebase/app-types/private'; -import { FirebaseAppLiteImpl } from './firebaseAppLite'; -import { createFirebaseNamespaceCore } from '../firebaseNamespaceCore'; -import { Component, ComponentType } from '@firebase/component'; - -export function createFirebaseNamespaceLite(): FirebaseNamespace { - const namespace = createFirebaseNamespaceCore(FirebaseAppLiteImpl); - - namespace.SDK_VERSION = `${namespace.SDK_VERSION}_LITE`; - - const registerComponent = (namespace as _FirebaseNamespace).INTERNAL - .registerComponent; - (namespace as _FirebaseNamespace).INTERNAL.registerComponent = registerComponentForLite; - - /** - * This is a special implementation, so it only works with performance. - * only allow performance SDK to register. - */ - function registerComponentForLite( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - component: Component - ): FirebaseServiceNamespace | null { - // only allow performance to register with firebase lite - if ( - component.type === ComponentType.PUBLIC && - component.name !== 'performance' && - component.name !== 'installations' - ) { - throw Error(`${name} cannot register with the standalone perf instance`); - } - - return registerComponent(component); - } - - return namespace; -} diff --git a/packages-exp/app-compat/src/registerCoreComponents.ts b/packages-exp/app-compat/src/registerCoreComponents.ts deleted file mode 100644 index 3b2eab206b7..00000000000 --- a/packages-exp/app-compat/src/registerCoreComponents.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { registerVersion } from '@firebase/app-exp'; - -import { name, version } from '../package.json'; - -export function registerCoreComponents(variant?: string): void { - // Register `app` package. - registerVersion(name, version, variant); -} diff --git a/packages-exp/app-compat/test/firebaseAppCompat.test.ts b/packages-exp/app-compat/test/firebaseAppCompat.test.ts deleted file mode 100644 index e0d55213d44..00000000000 --- a/packages-exp/app-compat/test/firebaseAppCompat.test.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import './setup'; -import { expect } from 'chai'; -import { stub } from 'sinon'; -import { FirebaseNamespace, FirebaseOptions } from '@firebase/app-types'; -import { _FirebaseApp, _FirebaseNamespace } from '@firebase/app-types/private'; -import { _components, _clearComponents } from '@firebase/app-exp'; -import { ComponentType } from '@firebase/component'; - -import { createFirebaseNamespace } from '../src/firebaseNamespace'; -import { createFirebaseNamespaceLite } from '../src/lite/firebaseNamespaceLite'; - -import { createTestComponent, TestService } from './util'; - -executeFirebaseTests(); -executeFirebaseLiteTests(); - -function executeFirebaseTests(): void { - firebaseAppTests('Firebase App Tests', createFirebaseNamespace); - - describe('Firebase Service Registration', () => { - const firebase: FirebaseNamespace = createFirebaseNamespace(); - - afterEach(() => { - const deleteTasks = []; - for (const app of firebase.apps) { - deleteTasks.push(app.delete()); - } - return Promise.all(deleteTasks); - }); - - it('will do nothing if registerComponent is called again with the same name', () => { - const registerStub = stub( - (firebase as _FirebaseNamespace).INTERNAL, - 'registerComponent' - ).callThrough(); - - const testComponent = createTestComponent('test'); - - (firebase as _FirebaseNamespace).INTERNAL.registerComponent( - testComponent - ); - firebase.initializeApp({}); - const serviceNamespace = (firebase as any).test; - - (firebase as _FirebaseNamespace).INTERNAL.registerComponent( - testComponent - ); - - const serviceNamespace2 = (firebase as any).test; - - expect(serviceNamespace).to.eq(serviceNamespace2); - expect(registerStub).to.have.not.thrown(); - }); - - it('returns cached service instances', () => { - firebase.initializeApp({}); - (firebase as _FirebaseNamespace).INTERNAL.registerComponent( - createTestComponent('test') - ); - - const service = (firebase as any).test(); - - expect(service).to.eq((firebase as any).test()); - }); - - it(`creates a new instance of a service after removing the existing instance`, () => { - const app = firebase.initializeApp({}); - (firebase as _FirebaseNamespace).INTERNAL.registerComponent( - createTestComponent('test') - ); - - const service = (firebase as any).test(); - - expect(service).to.eq((firebase as any).test()); - - (app as _FirebaseApp)._removeServiceInstance('test'); - - expect(service, (firebase as any).test()); - }); - - it(`creates a new instance of a service after removing the existing instance - for service that supports multiple instances`, () => { - const app = firebase.initializeApp({}); - (firebase as _FirebaseNamespace).INTERNAL.registerComponent( - createTestComponent('multiInstance', true) - ); - - // default instance - const instance1 = (firebase.app() as any).multiInstance(); - const serviceIdentifier = 'custom instance identifier'; - const instance2 = (firebase.app() as any).multiInstance( - serviceIdentifier - ); - - (app as _FirebaseApp)._removeServiceInstance( - 'multiInstance', - serviceIdentifier - ); - - // default instance should not be changed - expect(instance1).to.eq((firebase.app() as any).multiInstance()); - - expect(instance2).to.not.eq( - (firebase.app() as any).multiInstance(serviceIdentifier) - ); - }); - }); - - describe('Firebase Version Registration', () => { - const firebase: FirebaseNamespace = createFirebaseNamespace(); - - afterEach(() => { - _clearComponents(); - }); - - it('will register an official version component without warnings', () => { - const warnStub = stub(console, 'warn'); - const initialSize = _components.size; - - firebase.registerVersion('@firebase/analytics', '1.2.3'); - expect(_components.get('fire-analytics-version')).to.exist; - expect(_components.size).to.equal(initialSize + 1); - - expect(warnStub.called).to.be.false; - }); - - it('will register an arbitrary version component without warnings', () => { - const warnStub = stub(console, 'warn'); - const initialSize = _components.size; - - firebase.registerVersion('angularfire', '1.2.3'); - expect(_components.get('angularfire-version')).to.exist; - expect(_components.size).to.equal(initialSize + 1); - - expect(warnStub.called).to.be.false; - }); - - it('will do nothing if registerVersion() is given illegal characters', () => { - const warnStub = stub(console, 'warn'); - const initialSize = _components.size; - - firebase.registerVersion('remote config', '1.2.3'); - expect(warnStub.args[0][1]).to.include('library name "remote config"'); - expect(_components.size).to.equal(initialSize); - - firebase.registerVersion('remote-config', '1.2/3'); - expect(warnStub.args[1][1]).to.include('version name "1.2/3"'); - expect(_components.size).to.equal(initialSize); - }); - }); -} - -function executeFirebaseLiteTests(): void { - firebaseAppTests('Firebase App Lite Tests', createFirebaseNamespaceLite); - - describe('Firebase Lite Service Registration', () => { - const firebase: FirebaseNamespace = createFirebaseNamespaceLite(); - - afterEach(() => { - const deleteTasks = []; - for (const app of firebase.apps) { - deleteTasks.push(app.delete()); - } - return Promise.all(deleteTasks); - }); - - it('allows Performance service to register', () => { - (firebase as _FirebaseNamespace).INTERNAL.registerComponent( - createTestComponent('performance') - ); - const app = firebase.initializeApp({}); - const perf = (app as any).performance(); - expect(perf).to.be.instanceof(TestService); - }); - - it('allows Installations service to register', () => { - (firebase as _FirebaseNamespace).INTERNAL.registerComponent( - createTestComponent('installations') - ); - const app = firebase.initializeApp({}); - const perf = (app as any).installations(); - expect(perf).to.be.instanceof(TestService); - }); - - it('does NOT allow services other than Performance and installations to register', () => { - expect(() => - (firebase as _FirebaseNamespace).INTERNAL.registerComponent( - createTestComponent('auth') - ) - ).to.throw(); - }); - - it('allows any private component to register', () => { - expect(() => - (firebase as _FirebaseNamespace).INTERNAL.registerComponent( - createTestComponent('auth-internal', false, ComponentType.PRIVATE) - ) - ).to.not.throw(); - }); - }); -} - -function firebaseAppTests( - testName: string, - firebaseNamespaceFactory: () => FirebaseNamespace -): void { - describe(testName, () => { - const firebase: FirebaseNamespace = firebaseNamespaceFactory(); - - afterEach(() => { - const deleteTasks = []; - for (const app of firebase.apps) { - deleteTasks.push(app.delete()); - } - return Promise.all(deleteTasks); - }); - - it('has no initial apps.', () => { - expect(firebase.apps.length).to.eq(0); - }); - - it('Can get app via firebase namespace.', () => { - const app = firebase.initializeApp({}); - expect(app).to.be.not.null; - }); - - it('can initialize DEFAULT App.', () => { - const app = firebase.initializeApp({}); - expect(firebase.apps.length).to.eq(1); - expect(app).to.eq(firebase.apps[0]); - expect(app.name).to.eq('[DEFAULT]'); - expect(firebase.app()).to.eq(app); - expect(firebase.app('[DEFAULT]')).to.eq(app); - }); - - it('can get options of App.', () => { - const options: FirebaseOptions = { projectId: 'projectId' }; - const app = firebase.initializeApp(options); - expect(app.options).to.deep.eq(options); - }); - - it('can delete App.', async () => { - const app = firebase.initializeApp({}); - expect(firebase.apps.length).to.eq(1); - await app.delete(); - expect(firebase.apps.length).to.eq(0); - }); - - it('can create named App.', () => { - const app = firebase.initializeApp({}, 'my-app'); - expect(firebase.apps.length).to.eq(1); - expect(app.name).to.eq('my-app'); - expect(firebase.app('my-app')).to.eq(app); - }); - - it('can create named App and DEFAULT app.', () => { - firebase.initializeApp({}, 'my-app'); - expect(firebase.apps.length).to.eq(1); - firebase.initializeApp({}); - expect(firebase.apps.length).to.eq(2); - }); - - it('duplicate DEFAULT initialize is an error.', () => { - firebase.initializeApp({}); - expect(() => firebase.initializeApp({})).throws(/\[DEFAULT\].*exists/i); - }); - - it('duplicate named App initialize is an error.', () => { - firebase.initializeApp({}, 'abc'); - - expect(() => firebase.initializeApp({}, 'abc')).throws(/'abc'.*exists/i); - }); - - it('automaticDataCollectionEnabled is `false` by default', () => { - const app = firebase.initializeApp({}, 'my-app'); - expect(app.automaticDataCollectionEnabled).to.eq(false); - }); - - it('automaticDataCollectionEnabled can be set via the config object', () => { - const app = firebase.initializeApp( - {}, - { automaticDataCollectionEnabled: true } - ); - expect(app.automaticDataCollectionEnabled).to.eq(true); - }); - - it('Modifying options object does not change options.', () => { - const options: FirebaseOptions = { - appId: 'original', - measurementId: 'someId' - }; - firebase.initializeApp(options); - options.appId = 'changed'; - delete options.measurementId; - expect(firebase.app().options).to.deep.eq({ - appId: 'original', - measurementId: 'someId' - }); - }); - - it('Error to use app after it is deleted.', async () => { - const app = firebase.initializeApp({}); - await app.delete(); - expect(() => console.log(app.name)).throws(/already.*deleted/); - }); - - it('OK to create same-name app after it is deleted.', async () => { - const app = firebase.initializeApp({}, 'app-name'); - await app.delete(); - - const app2 = firebase.initializeApp({}, 'app-name'); - expect(app).to.not.eq(app2, 'Expect new instance.'); - // But original app id still orphaned. - expect(() => console.log(app.name)).throws(/already.*deleted/); - }); - - it('OK to use Object.prototype member names as app name.', () => { - const app = firebase.initializeApp({}, 'toString'); - expect(firebase.apps.length).to.eq(1); - expect(app.name).to.eq('toString'); - expect(firebase.app('toString')).to.eq(app); - }); - - it('Error to get uninitialized app using Object.prototype member name.', () => { - expect(() => firebase.app('toString')).throws(/'toString'.*created/i); - }); - - describe('Check for bad app names', () => { - const tests = ['', 123, false]; - for (const data of tests) { - it("where name == '" + data + "'", () => { - expect(() => firebase.initializeApp({}, data as string)).throws( - /Illegal app name/i - ); - }); - } - }); - - describe('Check for bad app names, passed as an object', () => { - const tests = ['', 123, false, null]; - for (const name of tests) { - it("where name == '" + name + "'", () => { - expect(() => - firebase.initializeApp({}, { name: name as string }) - ).throws(/Illegal app name/i); - }); - } - }); - }); -} diff --git a/packages-exp/app-compat/test/setup.ts b/packages-exp/app-compat/test/setup.ts deleted file mode 100644 index b1e3136529f..00000000000 --- a/packages-exp/app-compat/test/setup.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { use } from 'chai'; -import { restore } from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -use(sinonChai); - -afterEach(async () => { - restore(); -}); diff --git a/packages-exp/app-compat/test/util.ts b/packages-exp/app-compat/test/util.ts deleted file mode 100644 index 3161ca8bdb6..00000000000 --- a/packages-exp/app-compat/test/util.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseService } from '@firebase/app-types/private'; -import { FirebaseApp } from '@firebase/app-types'; -import { ComponentType, Component } from '@firebase/component'; - -export class TestService implements FirebaseService { - constructor(private app_: FirebaseApp, public instanceIdentifier?: string) {} - - get app(): FirebaseApp { - return this.app_; - } - - delete(): Promise { - return new Promise((resolve: (v?: void) => void) => { - setTimeout(() => resolve(), 10); - }); - } -} - -export function createTestComponent( - name: string, - multiInstances = false, - type = ComponentType.PUBLIC -): Component { - const component = new Component( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - name as any, - container => new TestService(container.getProvider('app').getImmediate()), - type - ); - component.setMultipleInstances(multiInstances); - return component; -} diff --git a/packages-exp/app-exp/README.md b/packages-exp/app-exp/README.md deleted file mode 100644 index efe4a954881..00000000000 --- a/packages-exp/app-exp/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @firebase/app-exp - -This package coordinates the communication between the different Firebase components - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/app-exp/api-extractor.json b/packages-exp/app-exp/api-extractor.json deleted file mode 100644 index cb3fc26988e..00000000000 --- a/packages-exp/app-exp/api-extractor.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/dist/packages-exp/app-exp/src/index.d.ts", - "dtsRollup": { - "enabled": true, - "untrimmedFilePath": "/dist/.d.ts", - "publicTrimmedFilePath": "/dist/-public.d.ts" - } -} \ No newline at end of file diff --git a/packages-exp/app-exp/karma.conf.js b/packages-exp/app-exp/karma.conf.js deleted file mode 100644 index c0917db53d1..00000000000 --- a/packages-exp/app-exp/karma.conf.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const karmaBase = require('../../config/karma.base'); - -const files = ['src/**/*.test.ts']; - -module.exports = function (config) { - const karmaConfig = Object.assign({}, karmaBase, { - // files to load into karma - files: files, - preprocessors: { '**/*.ts': ['webpack', 'sourcemap'] }, - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'] - }); - - config.set(karmaConfig); -}; - -module.exports.files = files; diff --git a/packages-exp/app-exp/package.json b/packages-exp/app-exp/package.json deleted file mode 100644 index 1a130a20f1a..00000000000 --- a/packages-exp/app-exp/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@firebase/app-exp", - "version": "0.0.900", - "private": true, - "description": "FirebaseApp", - "author": "Firebase (https://firebase.google.com/)", - "main": "dist/index.cjs.js", - "browser": "dist/index.esm5.js", - "module": "dist/index.esm5.js", - "esm2017": "dist/index.esm2017.js", - "files": ["dist"], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c && yarn api-report", - "build:release": "rollup -c rollup.config.release.js && yarn api-report && yarn typings:public", - "build:deps": "lerna run --scope @firebase/app-exp --include-dependencies build", - "dev": "rollup -c -w", - "test": "run-p lint test:all", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", - "test:all": "run-p test:browser test:node", - "test:browser": "karma start --single-run", - "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha src/**/*.test.ts --config ../../config/mocharc.node.js", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn build && yarn doc", - "typings:public": "node ./use_typings.js --public", - "typings:internal": "node ./use_typings.js" - }, - "dependencies": { - "@firebase/app-types-exp": "0.0.900", - "@firebase/util": "0.3.4", - "@firebase/logger": "0.2.6", - "@firebase/component": "0.1.21", - "tslib": "^1.11.1" - }, - "license": "Apache-2.0", - "devDependencies": { - "rollup": "2.35.1", - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-replace": "2.2.0", - "rollup-plugin-typescript2": "0.29.0", - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages-exp/app-exp", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "./dist/app-exp.d.ts", - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/app-exp/rollup.config.js b/packages-exp/app-exp/rollup.config.js deleted file mode 100644 index 5dc6ca47633..00000000000 --- a/packages-exp/app-exp/rollup.config.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import json from '@rollup/plugin-json'; -import pkg from './package.json'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - } - }), - json({ - preferConst: true - }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/app-exp/rollup.config.release.js b/packages-exp/app-exp/rollup.config.release.js deleted file mode 100644 index 1b5cca8c1fc..00000000000 --- a/packages-exp/app-exp/rollup.config.release.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import json from '@rollup/plugin-json'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - clean: true, - transformers: [importPathTransformer] - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins, - treeshake: { - moduleSideEffects: false - } -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - clean: true, - transformers: [importPathTransformer] - }), - json({ - preferConst: true - }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins, - treeshake: { - moduleSideEffects: false - } -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/app-exp/rollup.shared.js b/packages-exp/app-exp/rollup.shared.js deleted file mode 100644 index 08de1c1476f..00000000000 --- a/packages-exp/app-exp/rollup.shared.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import pkg from './package.json'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -export const es5BuildsNoPlugin = [ - /** - * Browser Builds - */ - { - input: 'src/index.ts', - output: [{ file: pkg.browser, format: 'es', sourcemap: true }], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - /** - * Node.js Build - */ - { - input: 'src/index.ts', - output: [{ file: pkg.main, format: 'cjs', sourcemap: true }], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -export const es2017BuildsNoPlugin = [ - /** - * Browser Builds - */ - { - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; diff --git a/packages-exp/app-exp/src/api.test.ts b/packages-exp/app-exp/src/api.test.ts deleted file mode 100644 index 6a45fbc81a5..00000000000 --- a/packages-exp/app-exp/src/api.test.ts +++ /dev/null @@ -1,317 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { stub, spy } from 'sinon'; -import '../test/setup'; -import { - initializeApp, - getApps, - deleteApp, - getApp, - registerVersion, - setLogLevel, - onLog -} from './api'; -import { DEFAULT_ENTRY_NAME } from './constants'; -import { - _FirebaseAppInternal, - _FirebaseService -} from '@firebase/app-types-exp'; -import { - _clearComponents, - _components, - _registerComponent, - _getProvider -} from './internal'; -import { createTestComponent } from '../test/util'; -import { Component, ComponentType } from '@firebase/component'; -import { Logger } from '@firebase/logger'; - -declare module '@firebase/component' { - interface NameServiceMapping { - 'test-shell': void; - } -} - -describe('API tests', () => { - afterEach(() => { - for (const app of getApps()) { - deleteApp(app).catch(() => {}); - } - }); - - describe('initializeApp', () => { - it('creats DEFAULT App', () => { - const app = initializeApp({}); - expect(app.name).to.equal(DEFAULT_ENTRY_NAME); - }); - - it('creates named App', () => { - const appName = 'MyApp'; - const app = initializeApp({}, appName); - expect(app.name).to.equal(appName); - }); - - it('creates named and DEFAULT App', () => { - const appName = 'MyApp'; - const app1 = initializeApp({}); - const app2 = initializeApp({}, appName); - - expect(app1.name).to.equal(DEFAULT_ENTRY_NAME); - expect(app2.name).to.equal(appName); - }); - - it('throws when creating duplicate DEDAULT Apps', () => { - initializeApp({}); - expect(() => initializeApp({})).throws(/\[DEFAULT\].*exists/i); - }); - - it('throws when creating duplicate named Apps', () => { - const appName = 'MyApp'; - initializeApp({}, appName); - expect(() => initializeApp({}, appName)).throws(/'MyApp'.*exists/i); - }); - - it('takes an object as the second parameter to create named App', () => { - const appName = 'MyApp'; - const app = initializeApp({}, { name: appName }); - expect(app.name).to.equal(appName); - }); - - it('takes an object as the second parameter to create named App', () => { - const appName = 'MyApp'; - const app = initializeApp({}, { name: appName }); - expect(app.name).to.equal(appName); - }); - - it('sets automaticDataCollectionEnabled', () => { - const app = initializeApp({}, { automaticDataCollectionEnabled: true }); - expect(app.automaticDataCollectionEnabled).to.be.true; - }); - - it('adds registered components to App', () => { - _clearComponents(); - const comp1 = createTestComponent('test1'); - const comp2 = createTestComponent('test2'); - _registerComponent(comp1); - _registerComponent(comp2); - - const app = initializeApp({}) as _FirebaseAppInternal; - // -1 here to not count the FirebaseApp provider that's added during initializeApp - expect(app.container.getProviders().length - 1).to.equal( - _components.size - ); - }); - }); - - describe('getApp', () => { - it('retrieves DEFAULT App', () => { - const app = initializeApp({}); - expect(getApp()).to.equal(app); - }); - - it('retrives named App', () => { - const appName = 'MyApp'; - const app = initializeApp({}, appName); - expect(getApp(appName)).to.equal(app); - }); - - it('throws retrieving a non existing App', () => { - expect(() => getApp('RandomName')).throws(/No Firebase App 'RandomName'/); - }); - }); - - describe('getApps', () => { - it('retrives all Apps that have been created', () => { - const app1 = initializeApp({}); - const app2 = initializeApp({}, 'App2'); - - const apps = getApps(); - - expect(apps.length).to.equal(2); - expect(apps[0]).to.equal(app1); - expect(apps[1]).to.equal(app2); - }); - - it('does NOT return deleted Apps', () => { - const app1 = initializeApp({}); - const app2 = initializeApp({}, 'App2'); - - deleteApp(app1).catch(() => {}); - - const apps = getApps(); - - expect(apps.length).to.equal(1); - expect(apps[0]).to.equal(app2); - }); - }); - - describe('deleteApp', () => { - it('marks an App as deleted', async () => { - const app = initializeApp({}); - expect((app as _FirebaseAppInternal).isDeleted).to.be.false; - - await deleteApp(app).catch(() => {}); - expect((app as _FirebaseAppInternal).isDeleted).to.be.true; - }); - - it('removes App from the cache', () => { - const app = initializeApp({}); - expect(getApps().length).to.equal(1); - - deleteApp(app).catch(() => {}); - expect(getApps().length).to.equal(0); - }); - - it('waits for all services being deleted', async () => { - _clearComponents(); - let count = 0; - const comp1 = new Component( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - 'test1' as any, - _container => - ({ - _delete: async () => { - await Promise.resolve(); - expect(count).to.equal(0); - count++; - } - } as _FirebaseService), - ComponentType.PUBLIC - ); - _registerComponent(comp1); - - const app = initializeApp({}); - // create service instance - const test1Provider = _getProvider(app, 'test1' as any); - test1Provider.getImmediate(); - - await deleteApp(app); - expect(count).to.equal(1); - }); - }); - - describe('registerVersion', () => { - afterEach(() => { - _clearComponents(); - }); - - it('will register an official version component without warnings', () => { - const warnStub = stub(console, 'warn'); - const initialSize = _components.size; - - registerVersion('@firebase/analytics', '1.2.3'); - expect(_components.get('fire-analytics-version')).to.exist; - expect(_components.size).to.equal(initialSize + 1); - - expect(warnStub.called).to.be.false; - }); - - it('will register an arbitrary version component without warnings', () => { - const warnStub = stub(console, 'warn'); - const initialSize = _components.size; - - registerVersion('angularfire', '1.2.3'); - expect(_components.get('angularfire-version')).to.exist; - expect(_components.size).to.equal(initialSize + 1); - - expect(warnStub.called).to.be.false; - }); - - it('will do nothing if registerVersion() is given illegal characters', () => { - const warnStub = stub(console, 'warn'); - const initialSize = _components.size; - - registerVersion('remote config', '1.2.3'); - expect(warnStub.args[0][1]).to.include('library name "remote config"'); - expect(_components.size).to.equal(initialSize); - - registerVersion('remote-config', '1.2/3'); - expect(warnStub.args[1][1]).to.include('version name "1.2/3"'); - expect(_components.size).to.equal(initialSize); - }); - }); - - describe('User Log Methods', () => { - describe('Integration Tests', () => { - beforeEach(() => { - _clearComponents(); - }); - - it(`respects log level set through setLogLevel()`, () => { - const warnSpy = spy(console, 'warn'); - const infoSpy = spy(console, 'info'); - const logSpy = spy(console, 'log'); - const app = initializeApp({}); - _registerComponent( - new Component( - 'test-shell', - () => { - const logger = new Logger('@firebase/logger-test'); - logger.warn('hello'); - expect(warnSpy.called).to.be.true; - setLogLevel('warn'); - logger.info('hi'); - expect(infoSpy.called).to.be.false; - logger.log('hi'); - expect(logSpy.called).to.be.false; - logSpy.resetHistory(); - infoSpy.resetHistory(); - setLogLevel('debug'); - logger.info('hi'); - expect(infoSpy.called).to.be.true; - logger.log('hi'); - expect(logSpy.called).to.be.true; - return {}; - }, - ComponentType.PUBLIC - ) - ); - - _getProvider(app, 'test-shell').getImmediate(); - }); - - it(`correctly triggers callback given to onLog()`, () => { - const infoSpy = spy(console, 'info'); - let result: any = null; - // Note: default log level is INFO. - const app = initializeApp({}); - _registerComponent( - new Component( - 'test-shell', - () => { - const logger = new Logger('@firebase/logger-test'); - onLog(logData => { - result = logData; - }); - logger.info('hi'); - expect(result.level).to.equal('info'); - expect(result.message).to.equal('hi'); - expect(result.args).to.deep.equal(['hi']); - expect(result.type).to.equal('@firebase/logger-test'); - expect(infoSpy.called).to.be.true; - return {}; - }, - ComponentType.PUBLIC - ) - ); - _getProvider(app, 'test-shell').getImmediate(); - }); - }); - }); -}); diff --git a/packages-exp/app-exp/src/api.ts b/packages-exp/app-exp/src/api.ts deleted file mode 100644 index 064babe8c4e..00000000000 --- a/packages-exp/app-exp/src/api.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - FirebaseApp, - FirebaseOptions, - FirebaseAppConfig, - _FirebaseAppInternal -} from '@firebase/app-types-exp'; -import { DEFAULT_ENTRY_NAME, PLATFORM_LOG_STRING } from './constants'; -import { ERROR_FACTORY, AppError } from './errors'; -import { - ComponentContainer, - Component, - Name, - ComponentType -} from '@firebase/component'; -import { version } from '../../firebase-exp/package.json'; -import { FirebaseAppImpl } from './firebaseApp'; -import { _apps, _components, _registerComponent } from './internal'; -import { logger } from './logger'; -import { - LogLevelString, - setLogLevel as setLogLevelImpl, - LogCallback, - LogOptions, - setUserLogHandler -} from '@firebase/logger'; - -/** - * The current SDK version. - * - * @public - */ -export const SDK_VERSION = version; - -/** - * Creates and initializes a FirebaseApp instance. - * - * See - * {@link - * https://firebase.google.com/docs/web/setup#add_firebase_to_your_app - * | Add Firebase to your app} and - * {@link - * https://firebase.google.com/docs/web/setup#multiple-projects - * | Initialize multiple projects} for detailed documentation. - * - * @example - * ```javascript - * - * // Initialize default app - * // Retrieve your own options values by adding a web app on - * // https://console.firebase.google.com - * initializeApp({ - * apiKey: "AIza....", // Auth / General Use - * authDomain: "YOUR_APP.firebaseapp.com", // Auth with popup/redirect - * databaseURL: "https://YOUR_APP.firebaseio.com", // Realtime Database - * storageBucket: "YOUR_APP.appspot.com", // Storage - * messagingSenderId: "123456789" // Cloud Messaging - * }); - * ``` - * - * @example - * ```javascript - * - * // Initialize another app - * const otherApp = initializeApp({ - * databaseURL: "https://.firebaseio.com", - * storageBucket: ".appspot.com" - * }, "otherApp"); - * ``` - * - * @param options - Options to configure the app's services. - * @param name - Optional name of the app to initialize. If no name - * is provided, the default is `"[DEFAULT]"`. - * - * @returns The initialized app. - * - * @public - */ -export function initializeApp( - options: FirebaseOptions, - name?: string -): FirebaseApp; -/** - * Creates and initializes a FirebaseApp instance. - * - * @param options - Options to configure the app's services. - * @param config - FirebaseApp Configuration - * - * @public - */ -export function initializeApp( - options: FirebaseOptions, - config?: FirebaseAppConfig -): FirebaseApp; -export function initializeApp( - options: FirebaseOptions, - rawConfig = {} -): FirebaseApp { - if (typeof rawConfig !== 'object') { - const name = rawConfig; - rawConfig = { name }; - } - - const config: Required = { - name: DEFAULT_ENTRY_NAME, - automaticDataCollectionEnabled: false, - ...rawConfig - }; - const name = config.name; - - if (typeof name !== 'string' || !name) { - throw ERROR_FACTORY.create(AppError.BAD_APP_NAME, { - appName: String(name) - }); - } - - if (_apps.has(name)) { - throw ERROR_FACTORY.create(AppError.DUPLICATE_APP, { appName: name }); - } - - const container = new ComponentContainer(name); - for (const component of _components.values()) { - container.addComponent(component); - } - - const newApp = new FirebaseAppImpl(options, config, container); - - _apps.set(name, newApp); - - return newApp; -} - -/** - * Retrieves a FirebaseApp instance. - * - * When called with no arguments, the default app is returned. When an app name - * is provided, the app corresponding to that name is returned. - * - * An exception is thrown if the app being retrieved has not yet been - * initialized. - * - * @example - * ```javascript - * // Return the default app - * const app = getApp(); - * ``` - * - * @example - * ```javascript - * // Return a named app - * const otherApp = getApp("otherApp"); - * ``` - * - * @param name - Optional name of the app to return. If no name is - * provided, the default is `"[DEFAULT]"`. - * - * @returns The app corresponding to the provided app name. - * If no app name is provided, the default app is returned. - * - * @public - */ -export function getApp(name: string = DEFAULT_ENTRY_NAME): FirebaseApp { - const app = _apps.get(name); - if (!app) { - throw ERROR_FACTORY.create(AppError.NO_APP, { appName: name }); - } - - return app; -} - -/** - * A (read-only) array of all initialized apps. - * @public - */ -export function getApps(): FirebaseApp[] { - return Array.from(_apps.values()); -} - -/** - * Renders this app unusable and frees the resources of all associated - * services. - * - * @example - * ```javascript - * deleteApp(app) - * .then(function() { - * console.log("App deleted successfully"); - * }) - * .catch(function(error) { - * console.log("Error deleting app:", error); - * }); - * ``` - * - * @public - */ -export async function deleteApp(app: FirebaseApp): Promise { - const name = app.name; - if (_apps.has(name)) { - _apps.delete(name); - await Promise.all( - (app as _FirebaseAppInternal).container - .getProviders() - .map(provider => provider.delete()) - ); - (app as _FirebaseAppInternal).isDeleted = true; - } -} - -/** - * Registers a library's name and version for platform logging purposes. - * @param library - Name of 1p or 3p library (e.g. firestore, angularfire) - * @param version - Current version of that library. - * @param variant - Bundle variant, e.g., node, rn, etc. - * - * @public - */ -export function registerVersion( - libraryKeyOrName: string, - version: string, - variant?: string -): void { - // TODO: We can use this check to whitelist strings when/if we set up - // a good whitelist system. - let library = PLATFORM_LOG_STRING[libraryKeyOrName] ?? libraryKeyOrName; - if (variant) { - library += `-${variant}`; - } - const libraryMismatch = library.match(/\s|\//); - const versionMismatch = version.match(/\s|\//); - if (libraryMismatch || versionMismatch) { - const warning = [ - `Unable to register library "${library}" with version "${version}":` - ]; - if (libraryMismatch) { - warning.push( - `library name "${library}" contains illegal characters (whitespace or "/")` - ); - } - if (libraryMismatch && versionMismatch) { - warning.push('and'); - } - if (versionMismatch) { - warning.push( - `version name "${version}" contains illegal characters (whitespace or "/")` - ); - } - logger.warn(warning.join(' ')); - return; - } - _registerComponent( - new Component( - `${library}-version` as Name, - () => ({ library, version }), - ComponentType.VERSION - ) - ); -} - -/** - * Sets log handler for all Firebase SDKs. - * @param logCallback - An optional custom log handler that executes user code whenever - * the Firebase SDK makes a logging call. - * - * @public - */ -export function onLog( - logCallback: LogCallback | null, - options?: LogOptions -): void { - if (logCallback !== null && typeof logCallback !== 'function') { - throw ERROR_FACTORY.create(AppError.INVALID_LOG_ARGUMENT, { - appName: name - }); - } - setUserLogHandler(logCallback, options); -} - -/** - * Sets log level for all Firebase SDKs. - * - * All of the log types above the current log level are captured (i.e. if - * you set the log level to `info`, errors are logged, but `debug` and - * `verbose` logs are not). - * - * @public - */ -export function setLogLevel(logLevel: LogLevelString): void { - setLogLevelImpl(logLevel); -} diff --git a/packages-exp/app-exp/src/constants.ts b/packages-exp/app-exp/src/constants.ts deleted file mode 100644 index 61517c49794..00000000000 --- a/packages-exp/app-exp/src/constants.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { name as appName } from '../package.json'; -import { name as appCompatName } from '../../app-compat/package.json'; -import { name as analyticsName } from '../../../packages/analytics/package.json'; -import { name as authName } from '../../../packages/auth/package.json'; -import { name as databaseName } from '../../../packages/database/package.json'; -import { name as functionsName } from '../../../packages-exp/functions-exp/package.json'; -import { name as functionsCompatName } from '../../../packages-exp/functions-compat/package.json'; -import { name as installationsName } from '../../../packages/installations/package.json'; -import { name as messagingName } from '../../../packages/messaging/package.json'; -import { name as performanceName } from '../../../packages/performance/package.json'; -import { name as remoteConfigName } from '../../../packages/remote-config/package.json'; -import { name as storageName } from '../../../packages/storage/package.json'; -import { name as firestoreName } from '../../../packages/firestore/package.json'; -import { name as packageName } from '../../../packages-exp/firebase-exp/package.json'; - -/** - * The default app name - * - * @internal - */ -export const DEFAULT_ENTRY_NAME = '[DEFAULT]'; - -export const PLATFORM_LOG_STRING = { - [appName]: 'fire-core', - [appCompatName]: 'fire-core-compat', - [analyticsName]: 'fire-analytics', - [authName]: 'fire-auth', - [databaseName]: 'fire-rtdb', - [functionsName]: 'fire-fn', - [functionsCompatName]: 'fire-fn-compat', - [installationsName]: 'fire-iid', - [messagingName]: 'fire-fcm', - [performanceName]: 'fire-perf', - [remoteConfigName]: 'fire-rc', - [storageName]: 'fire-gcs', - [firestoreName]: 'fire-fst', - 'fire-js': 'fire-js', // Platform identifier for JS SDK. - [packageName]: 'fire-js-all' -} as const; diff --git a/packages-exp/app-exp/src/errors.ts b/packages-exp/app-exp/src/errors.ts deleted file mode 100644 index 82179836359..00000000000 --- a/packages-exp/app-exp/src/errors.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ErrorFactory, ErrorMap } from '@firebase/util'; - -export const enum AppError { - NO_APP = 'no-app', - BAD_APP_NAME = 'bad-app-name', - DUPLICATE_APP = 'duplicate-app', - APP_DELETED = 'app-deleted', - INVALID_APP_ARGUMENT = 'invalid-app-argument', - INVALID_LOG_ARGUMENT = 'invalid-log-argument' -} - -const ERRORS: ErrorMap = { - [AppError.NO_APP]: - "No Firebase App '{$appName}' has been created - " + - 'call Firebase App.initializeApp()', - [AppError.BAD_APP_NAME]: "Illegal App name: '{$appName}", - [AppError.DUPLICATE_APP]: "Firebase App named '{$appName}' already exists", - [AppError.APP_DELETED]: "Firebase App named '{$appName}' already deleted", - [AppError.INVALID_APP_ARGUMENT]: - 'firebase.{$appName}() takes either no argument or a ' + - 'Firebase App instance.', - [AppError.INVALID_LOG_ARGUMENT]: - 'First argument to `onLog` must be null or a function.' -}; - -type ErrorParams = { [key in AppError]: { appName: string } }; - -export const ERROR_FACTORY = new ErrorFactory( - 'app', - 'Firebase', - ERRORS -); diff --git a/packages-exp/app-exp/src/firebaseApp.test.ts b/packages-exp/app-exp/src/firebaseApp.test.ts deleted file mode 100644 index 12e56b41ff7..00000000000 --- a/packages-exp/app-exp/src/firebaseApp.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import '../test/setup'; -import { FirebaseAppImpl } from './firebaseApp'; -import { ComponentContainer } from '@firebase/component'; -import { _FirebaseAppInternal } from '@firebase/app-types-exp'; - -describe('FirebaseAppNext', () => { - it('has various accessors', () => { - const options = { - apiKey: 'APIKEY' - }; - const app = new FirebaseAppImpl( - options, - { name: 'test', automaticDataCollectionEnabled: false }, - new ComponentContainer('test') - ); - - expect(app.automaticDataCollectionEnabled).to.be.false; - expect(app.name).to.equal('test'); - expect(app.options).to.deep.equal(options); - }); - - it('deep-copies options', () => { - const options = { - apiKey: 'APIKEY' - }; - const app = new FirebaseAppImpl( - options, - { name: 'test', automaticDataCollectionEnabled: false }, - new ComponentContainer('test') - ); - - expect(app.options).to.not.equal(options); - expect(app.options).to.deep.equal(options); - }); - - it('sets automaticDataCollectionEnabled', () => { - const app = new FirebaseAppImpl( - {}, - { name: 'test', automaticDataCollectionEnabled: false }, - new ComponentContainer('test') - ); - - expect(app.automaticDataCollectionEnabled).to.be.false; - app.automaticDataCollectionEnabled = true; - expect(app.automaticDataCollectionEnabled).to.be.true; - }); - - it('throws accessing any property after being deleted', () => { - const app = new FirebaseAppImpl( - {}, - { name: 'test', automaticDataCollectionEnabled: false }, - new ComponentContainer('test') - ); - - expect(() => app.name).to.not.throw(); - ((app as unknown) as _FirebaseAppInternal).isDeleted = true; - - expect(() => { - app.name; - }).throws("Firebase App named 'test' already deleted"); - expect(() => app.options).throws( - "Firebase App named 'test' already deleted" - ); - expect(() => app.automaticDataCollectionEnabled).throws( - "Firebase App named 'test' already deleted" - ); - }); -}); diff --git a/packages-exp/app-exp/src/firebaseApp.ts b/packages-exp/app-exp/src/firebaseApp.ts deleted file mode 100644 index 3bff806191e..00000000000 --- a/packages-exp/app-exp/src/firebaseApp.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - FirebaseApp, - FirebaseOptions, - FirebaseAppConfig -} from '@firebase/app-types-exp'; -import { - ComponentContainer, - Component, - ComponentType -} from '@firebase/component'; -import { ERROR_FACTORY, AppError } from './errors'; - -export class FirebaseAppImpl implements FirebaseApp { - private readonly options_: FirebaseOptions; - private readonly name_: string; - private automaticDataCollectionEnabled_: boolean; - private isDeleted = false; - private readonly container: ComponentContainer; - - constructor( - options: FirebaseOptions, - config: Required, - container: ComponentContainer - ) { - this.options_ = { ...options }; - this.name_ = config.name; - this.automaticDataCollectionEnabled_ = - config.automaticDataCollectionEnabled; - this.container = container; - this.container.addComponent( - new Component('app-exp', () => this, ComponentType.PUBLIC) - ); - } - - get automaticDataCollectionEnabled(): boolean { - this.checkDestroyed(); - return this.automaticDataCollectionEnabled_; - } - - set automaticDataCollectionEnabled(val: boolean) { - this.checkDestroyed(); - this.automaticDataCollectionEnabled_ = val; - } - - get name(): string { - this.checkDestroyed(); - return this.name_; - } - - get options(): FirebaseOptions { - this.checkDestroyed(); - return this.options_; - } - - /** - * This function will throw an Error if the App has already been deleted - - * use before performing API actions on the App. - */ - private checkDestroyed(): void { - if (this.isDeleted) { - throw ERROR_FACTORY.create(AppError.APP_DELETED, { appName: this.name_ }); - } - } -} diff --git a/packages-exp/app-exp/src/index.ts b/packages-exp/app-exp/src/index.ts deleted file mode 100644 index fc54eb62ee6..00000000000 --- a/packages-exp/app-exp/src/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Firebase App - * - * @remarks This package coordinates the communication between the different Firebase components - * @packageDocumentation - */ - -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { registerCoreComponents } from './registerCoreComponents'; - -export * from './api'; -export * from './internal'; - -registerCoreComponents(); diff --git a/packages-exp/app-exp/src/internal.ts b/packages-exp/app-exp/src/internal.ts deleted file mode 100644 index 8421ef9f482..00000000000 --- a/packages-exp/app-exp/src/internal.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _FirebaseAppInternal, FirebaseApp } from '@firebase/app-types-exp'; -import { Component, Provider, Name } from '@firebase/component'; -import { logger } from './logger'; -import { DEFAULT_ENTRY_NAME } from './constants'; - -/** - * @internal - */ -export const _apps = new Map(); - -/** - * Registered components. - * - * @internal - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const _components = new Map>(); - -/** - * @param component - the component being added to this app's container - * - * @internal - */ -export function _addComponent(app: FirebaseApp, component: Component): void { - try { - (app as _FirebaseAppInternal).container.addComponent(component); - } catch (e) { - logger.debug( - `Component ${component.name} failed to register with FirebaseApp ${app.name}`, - e - ); - } -} - -/** - * - * @internal - */ -export function _addOrOverwriteComponent( - app: FirebaseApp, - component: Component -): void { - (app as _FirebaseAppInternal).container.addOrOverwriteComponent(component); -} - -/** - * - * @param component - the component to register - * @returns whether or not the component is registered successfully - * - * @internal - */ -export function _registerComponent(component: Component): boolean { - const componentName = component.name; - if (_components.has(componentName)) { - logger.debug( - `There were multiple attempts to register component ${componentName}.` - ); - - return false; - } - - _components.set(componentName, component); - - // add the component to existing app instances - for (const app of _apps.values()) { - _addComponent(app as _FirebaseAppInternal, component); - } - - return true; -} - -/** - * - * @param app - FirebaseApp instance - * @param name - service name - * - * @returns the provider for the service with the matching name - * - * @internal - */ -export function _getProvider( - app: FirebaseApp, - name: T -): Provider { - return (app as _FirebaseAppInternal).container.getProvider(name); -} - -/** - * - * @param app - FirebaseApp instance - * @param name - service name - * @param instanceIdentifier - service instance identifier in case the service supports multiple instances - * - * @internal - */ -export function _removeServiceInstance( - app: FirebaseApp, - name: T, - instanceIdentifier: string = DEFAULT_ENTRY_NAME -): void { - _getProvider(app, name).clearInstance(instanceIdentifier); -} - -/** - * Test only - * - * @internal - */ -export function _clearComponents(): void { - _components.clear(); -} - -/** - * Exported in order to be used in app-compat package - */ -export { DEFAULT_ENTRY_NAME as _DEFAULT_ENTRY_NAME }; diff --git a/packages-exp/app-exp/src/logger.ts b/packages-exp/app-exp/src/logger.ts deleted file mode 100644 index 6e38f59a3f8..00000000000 --- a/packages-exp/app-exp/src/logger.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Logger } from '@firebase/logger'; - -export const logger = new Logger('@firebase/app'); diff --git a/packages-exp/app-exp/src/platformLoggerService.ts b/packages-exp/app-exp/src/platformLoggerService.ts deleted file mode 100644 index 54048b714fb..00000000000 --- a/packages-exp/app-exp/src/platformLoggerService.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - ComponentContainer, - ComponentType, - Provider, - Name -} from '@firebase/component'; - -export class PlatformLoggerService { - constructor(private readonly container: ComponentContainer) {} - // In initial implementation, this will be called by installations on - // auth token refresh, and installations will send this string. - getPlatformInfoString(): string { - const providers = this.container.getProviders(); - // Loop through providers and get library/version pairs from any that are - // version components. - return providers - .map(provider => { - if (isVersionServiceProvider(provider)) { - const service = provider.getImmediate(); - return `${service.library}/${service.version}`; - } else { - return null; - } - }) - .filter(logString => logString) - .join(' '); - } -} -/** - * - * @param provider check if this provider provides a VersionService - * - * NOTE: Using Provider<'app-version'> is a hack to indicate that the provider - * provides VersionService. The provider is not necessarily a 'app-version' - * provider. - */ -function isVersionServiceProvider( - provider: Provider -): provider is Provider<'app-version'> { - const component = provider.getComponent(); - return component?.type === ComponentType.VERSION; -} diff --git a/packages-exp/app-exp/src/registerCoreComponents.ts b/packages-exp/app-exp/src/registerCoreComponents.ts deleted file mode 100644 index 732be496a1e..00000000000 --- a/packages-exp/app-exp/src/registerCoreComponents.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Component, ComponentType } from '@firebase/component'; -import { PlatformLoggerService } from './platformLoggerService'; -import { name, version } from '../package.json'; -import { _registerComponent } from './internal'; -import { registerVersion } from './api'; - -export function registerCoreComponents(variant?: string): void { - _registerComponent( - new Component( - 'platform-logger', - container => new PlatformLoggerService(container), - ComponentType.PRIVATE - ) - ); - - // Register `app` package. - registerVersion(name, version, variant); - // Register platform SDK identifier (no version). - registerVersion('fire-js', ''); -} diff --git a/packages-exp/app-exp/test/setup.ts b/packages-exp/app-exp/test/setup.ts deleted file mode 100644 index b1e3136529f..00000000000 --- a/packages-exp/app-exp/test/setup.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { use } from 'chai'; -import { restore } from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -use(sinonChai); - -afterEach(async () => { - restore(); -}); diff --git a/packages-exp/app-exp/test/util.ts b/packages-exp/app-exp/test/util.ts deleted file mode 100644 index bc41c08fbca..00000000000 --- a/packages-exp/app-exp/test/util.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp, _FirebaseService } from '@firebase/app-types-exp'; -import { ComponentType, Component } from '@firebase/component'; - -export class TestService implements _FirebaseService { - constructor(private app_: FirebaseApp, public instanceIdentifier?: string) {} - - get app(): FirebaseApp { - return this.app_; - } - - _delete(): Promise { - return new Promise((resolve: (v?: void) => void) => { - setTimeout(() => resolve(), 10); - }); - } -} - -export function createTestComponent( - name: string, - multiInstances = false, - type = ComponentType.PUBLIC -): Component { - const component = new Component( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - name as any, - container => - new TestService(container.getProvider('app-exp').getImmediate()), - type - ); - component.setMultipleInstances(multiInstances); - return component; -} diff --git a/packages-exp/app-exp/tsconfig.json b/packages-exp/app-exp/tsconfig.json deleted file mode 100644 index 735ea623511..00000000000 --- a/packages-exp/app-exp/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "downlevelIteration": true - }, - "exclude": [ - "dist/**/*" - ] -} \ No newline at end of file diff --git a/packages-exp/app-exp/use_typings.js b/packages-exp/app-exp/use_typings.js deleted file mode 100644 index 1a299535a16..00000000000 --- a/packages-exp/app-exp/use_typings.js +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const { writeFileSync } = require('fs'); -const { argv } = require('yargs'); - -const path = require('path'); -const packageJsonPath = path.resolve(__dirname, './package.json'); - -// point typings field to the public d.ts file in package.json -const TYPINGS_PATH = argv.public - ? './dist/app-exp-public.d.ts' - : './dist/app-exp.d.ts'; -console.log( - `Updating the packages-exp/app-exp typings field to the ${ - argv.public ? 'public' : 'internal' - } d.ts file ${TYPINGS_PATH}` -); -const packageJson = require(packageJsonPath); -packageJson.typings = TYPINGS_PATH; - -writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, { - encoding: 'utf-8' -}); diff --git a/packages-exp/app-types-exp/README.md b/packages-exp/app-types-exp/README.md deleted file mode 100644 index c607c8aadf6..00000000000 --- a/packages-exp/app-types-exp/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @firebase/app-types-exp - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/app-types-exp/api-extractor.json b/packages-exp/app-types-exp/api-extractor.json deleted file mode 100644 index 42f37a88c4b..00000000000 --- a/packages-exp/app-types-exp/api-extractor.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/index.d.ts" -} \ No newline at end of file diff --git a/packages-exp/app-types-exp/index.d.ts b/packages-exp/app-types-exp/index.d.ts deleted file mode 100644 index d74be71601c..00000000000 --- a/packages-exp/app-types-exp/index.d.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ComponentContainer } from '@firebase/component'; - -/** - * A FirebaseApp holds the initialization information for a collection of - * services. - * - * Do not call this constructor directly. Instead, use - * {@link @firebase/app-exp#(initializeApp:1) | initializeApp()} to create an app. - * - * @public - */ -export interface FirebaseApp { - /** - * The (read-only) name for this app. - * - * The default app's name is `"[DEFAULT]"`. - * - * @example - * ```javascript - * // The default app's name is "[DEFAULT]" - * const app = initializeApp(defaultAppConfig); - * console.log(app.name); // "[DEFAULT]" - * ``` - * - * @example - * ```javascript - * // A named app's name is what you provide to initializeApp() - * const otherApp = initializeApp(otherAppConfig, "other"); - * console.log(otherApp.name); // "other" - * ``` - */ - readonly name: string; - - /** - * The (read-only) configuration options for this app. These are the original - * parameters given in {@link @firebase/app-exp#(initializeApp:1) | initializeApp()}. - * - * @example - * ```javascript - * const app = initializeApp(config); - * console.log(app.options.databaseURL === config.databaseURL); // true - * ``` - */ - readonly options: FirebaseOptions; - - /** - * The settable config flag for GDPR opt-in/opt-out - */ - automaticDataCollectionEnabled: boolean; -} - -/** - * @internal - */ -export interface _FirebaseAppInternal extends FirebaseApp { - container: ComponentContainer; - isDeleted: boolean; - checkDestroyed(): void; -} - -/** - * @public - */ -export interface FirebaseOptions { - apiKey?: string; - authDomain?: string; - databaseURL?: string; - projectId?: string; - storageBucket?: string; - messagingSenderId?: string; - appId?: string; - measurementId?: string; -} - -/** - * @public - */ -export interface FirebaseAppConfig { - name?: string; - automaticDataCollectionEnabled?: boolean; -} - -/** - * @public - */ -export interface PlatformLoggerService { - getPlatformInfoString(): string; -} - -/** - * @internal - */ -export interface _FirebaseService { - app: FirebaseApp; - /** - * Delete the service and free it's resources - called from - * {@link @firebase/app-exp#deleteApp | deleteApp()} - */ - _delete(): Promise; -} - -export interface VersionService { - library: string; - version: string; -} - -declare module '@firebase/component' { - interface NameServiceMapping { - 'app-exp': FirebaseApp; - 'app-version': VersionService; - 'platform-logger': PlatformLoggerService; - } -} diff --git a/packages-exp/app-types-exp/package.json b/packages-exp/app-types-exp/package.json deleted file mode 100644 index 03c1d353d3f..00000000000 --- a/packages-exp/app-types-exp/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@firebase/app-types-exp", - "version": "0.0.900", - "private": true, - "description": "@firebase/app Types", - "author": "Firebase (https://firebase.google.com/)", - "license": "Apache-2.0", - "scripts": { - "test": "tsc", - "test:ci": "node ../../scripts/run_tests_in_ci.js", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn api-report && yarn doc" - }, - "files": [ - "index.d.ts", - "private.d.ts" - ], - "repository": { - "directory": "packages-exp/app-types-exp", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "devDependencies": { - "typescript": "4.0.5" - } -} diff --git a/packages-exp/app-types-exp/tsconfig.json b/packages-exp/app-types-exp/tsconfig.json deleted file mode 100644 index 9a785433d90..00000000000 --- a/packages-exp/app-types-exp/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "noEmit": true - }, - "exclude": [ - "dist/**/*" - ] -} diff --git a/packages-exp/auth-compat-exp/README.md b/packages-exp/auth-compat-exp/README.md deleted file mode 100644 index ed7faf71ab8..00000000000 --- a/packages-exp/auth-compat-exp/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @firebase/auth-compat-exp - -This is a compatability layer to for the Firebase Authentication SDK - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** \ No newline at end of file diff --git a/packages-exp/auth-compat-exp/demo/.gitignore b/packages-exp/auth-compat-exp/demo/.gitignore deleted file mode 100644 index 25ca4250c97..00000000000 --- a/packages-exp/auth-compat-exp/demo/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -src/config.js -.firebaserc -public/config.js -public/service-worker.* -public/web-worker.* -public/index.* diff --git a/packages-exp/auth-compat-exp/demo/README.md b/packages-exp/auth-compat-exp/demo/README.md deleted file mode 100644 index 8d19e0a1af2..00000000000 --- a/packages-exp/auth-compat-exp/demo/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Firebase-Auth for web - Auth Demo (Auth Compatibility Layer) - -## Prerequisite - -You need to have created a Firebase Project in the -[Firebase Console](https://firebase.google.com/console/) as well as configured a web app. - -## Installation -Make sure you run `yarn` to install all dependencies in the root directory. - -Enable the Auth providers you would like to offer your users in the console, under -Auth > Sign-in methods. - -Run: - -```bash -git clone https://github.com/firebase/firebase-js-sdk.git -cd firebase-js-sdk/packages-exp/auth-compat-exp/demo -``` - -This will clone the repository in the current directory. - -If you want to be able to deploy the demo app to one of your own Firebase Hosting instance, -configure it using the following command: - -```bash -firebase use --add -``` - -Select the project you have created in the prerequisite, and type in `default` or -any other name as the alias to use for this project. - -Copy `public/sample-config.js` to `public/config.js`: - -```bash -cp public/sample-config.js public/config.js -``` - -Then copy and paste the Web snippet config found in the console (either by clicking "Add Firebase to -your web app" button in your Project overview, or clicking the "Web setup" button in the Auth page) -in the `config.js` file. - -In the `functions` folder you'll need to install the admin SDK: - -```bash -cd functions -yarn install -``` - -## Deploy - -Before deploying, you may need to build the auth-exp package: -```bash -yarn build:deps -``` - -You'll also need to build a fully resolved firebase-app.js and firebase-auth.js from auth-compat-exp: - -```bash -yarn build -``` - -This can take some time, and you only need to do it if you've modified the auth-exp or auth-compta-exp packages. - -To run the app locally, simply issue the following command in the `auth-compat-exp/demo` directory: - -```bash -yarn run demo -``` - -This will compile all the files needed to run Firebase Auth, and start a Firebase server locally at -[http://localhost:5000](http://localhost:5000). - diff --git a/packages-exp/auth-compat-exp/demo/functions/package.json b/packages-exp/auth-compat-exp/demo/functions/package.json deleted file mode 100644 index 02654130eda..00000000000 --- a/packages-exp/auth-compat-exp/demo/functions/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "functions", - "description": "Cloud Functions for Firebase", - "scripts": { - "serve": "firebase serve --only functions", - "shell": "firebase experimental:functions:shell", - "start": "yarn shell", - "deploy": "firebase deploy --only functions", - "logs": "firebase functions:log" - }, - "dependencies": { - "firebase-admin": "8.13.0", - "firebase-functions": "3.13.0" - }, - "private": true -} diff --git a/packages-exp/auth-compat-exp/demo/functions/yarn.lock b/packages-exp/auth-compat-exp/demo/functions/yarn.lock deleted file mode 100644 index 0193d674cbb..00000000000 --- a/packages-exp/auth-compat-exp/demo/functions/yarn.lock +++ /dev/null @@ -1,1730 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@firebase/app-types@0.6.1": - version "0.6.1" - resolved "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz#dcbd23030a71c0c74fc95d4a3f75ba81653850e9" - integrity sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg== - -"@firebase/auth-interop-types@0.1.5": - version "0.1.5" - resolved "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz#9fc9bd7c879f16b8d1bb08373a0f48c3a8b74557" - integrity sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw== - -"@firebase/component@0.1.19": - version "0.1.19" - resolved "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz#bd2ac601652c22576b574c08c40da245933dbac7" - integrity sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ== - dependencies: - "@firebase/util" "0.3.2" - tslib "^1.11.1" - -"@firebase/database-types@0.5.2": - version "0.5.2" - resolved "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.2.tgz#23bec8477f84f519727f165c687761e29958b63c" - integrity sha512-ap2WQOS3LKmGuVFKUghFft7RxXTyZTDr0Xd8y2aqmWsbJVjgozi0huL/EUMgTjGFrATAjcf2A7aNs8AKKZ2a8g== - dependencies: - "@firebase/app-types" "0.6.1" - -"@firebase/database@^0.6.0": - version "0.6.13" - resolved "https://registry.npmjs.org/@firebase/database/-/database-0.6.13.tgz#b96fe0c53757dd6404ee085fdcb45c0f9f525c17" - integrity sha512-NommVkAPzU7CKd1gyehmi3lz0K78q0KOfiex7Nfy7MBMwknLm7oNqKovXSgQV1PCLvKXvvAplDSFhDhzIf9obA== - dependencies: - "@firebase/auth-interop-types" "0.1.5" - "@firebase/component" "0.1.19" - "@firebase/database-types" "0.5.2" - "@firebase/logger" "0.2.6" - "@firebase/util" "0.3.2" - faye-websocket "0.11.3" - tslib "^1.11.1" - -"@firebase/logger@0.2.6": - version "0.2.6" - resolved "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz#3aa2ca4fe10327cabf7808bd3994e88db26d7989" - integrity sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw== - -"@firebase/util@0.3.2": - version "0.3.2" - resolved "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz#87de27f9cffc2324651cabf6ec133d0a9eb21b52" - integrity sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g== - dependencies: - tslib "^1.11.1" - -"@google-cloud/common@^2.1.1": - version "2.4.0" - resolved "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz#2783b7de8435024a31453510f2dab5a6a91a4c82" - integrity sha512-zWFjBS35eI9leAHhjfeOYlK5Plcuj/77EzstnrJIZbKgF/nkqjcQuGiMCpzCwOfPyUbz8ZaEOYgbHa759AKbjg== - dependencies: - "@google-cloud/projectify" "^1.0.0" - "@google-cloud/promisify" "^1.0.0" - arrify "^2.0.0" - duplexify "^3.6.0" - ent "^2.2.0" - extend "^3.0.2" - google-auth-library "^5.5.0" - retry-request "^4.0.0" - teeny-request "^6.0.0" - -"@google-cloud/firestore@^3.0.0": - version "3.8.6" - resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-3.8.6.tgz#9e6dea57323a5824563430a759244825fb01d834" - integrity sha512-ox80NbrM1MLJgvAAUd1quFLx/ie/nSjrk1PtscSicpoYDlKb9e6j7pHrVpbopBMyliyfNl3tLJWaDh+x+uCXqw== - dependencies: - deep-equal "^2.0.0" - functional-red-black-tree "^1.0.1" - google-gax "^1.15.3" - readable-stream "^3.4.0" - through2 "^3.0.0" - -"@google-cloud/paginator@^2.0.0": - version "2.0.3" - resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz#c7987ad05d1c3ebcef554381be80e9e8da4e4882" - integrity sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg== - dependencies: - arrify "^2.0.0" - extend "^3.0.2" - -"@google-cloud/projectify@^1.0.0": - version "1.0.4" - resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz#28daabebba6579ed998edcadf1a8f3be17f3b5f0" - integrity sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg== - -"@google-cloud/promisify@^1.0.0": - version "1.0.4" - resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz#ce86ffa94f9cfafa2e68f7b3e4a7fad194189723" - integrity sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ== - -"@google-cloud/storage@^4.1.2": - version "4.7.0" - resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.7.0.tgz#a7466086a83911c7979cc238d00a127ffb645615" - integrity sha512-f0guAlbeg7Z0m3gKjCfBCu7FG9qS3M3oL5OQQxlvGoPtK7/qg3+W+KQV73O2/sbuS54n0Kh2mvT5K2FWzF5vVQ== - dependencies: - "@google-cloud/common" "^2.1.1" - "@google-cloud/paginator" "^2.0.0" - "@google-cloud/promisify" "^1.0.0" - arrify "^2.0.0" - compressible "^2.0.12" - concat-stream "^2.0.0" - date-and-time "^0.13.0" - duplexify "^3.5.0" - extend "^3.0.2" - gaxios "^3.0.0" - gcs-resumable-upload "^2.2.4" - hash-stream-validation "^0.2.2" - mime "^2.2.0" - mime-types "^2.0.8" - onetime "^5.1.0" - p-limit "^2.2.0" - pumpify "^2.0.0" - readable-stream "^3.4.0" - snakeize "^0.1.0" - stream-events "^1.0.1" - through2 "^3.0.0" - xdg-basedir "^4.0.0" - -"@grpc/grpc-js@~1.0.3": - version "1.0.5" - resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.5.tgz#09948c0810e62828fdd61455b2eb13d7879888b0" - integrity sha512-Hm+xOiqAhcpT9RYM8lc15dbQD7aQurM7ZU8ulmulepiPlN7iwBXXwP3vSBUimoFoApRqz7pSIisXU8pZaCB4og== - dependencies: - semver "^6.2.0" - -"@grpc/proto-loader@^0.5.1": - version "0.5.5" - resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz#6725e7a1827bdf8e92e29fbf4e9ef0203c0906a9" - integrity sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ== - dependencies: - lodash.camelcase "^4.3.0" - protobufjs "^6.8.6" - -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= - -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - -"@types/body-parser@*": - version "1.19.0" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" - integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.33" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" - integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== - dependencies: - "@types/node" "*" - -"@types/express-serve-static-core@*": - version "4.17.13" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" - integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - -"@types/express@4.17.3": - version "4.17.3" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz#38e4458ce2067873b09a73908df488870c303bd9" - integrity sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "*" - "@types/serve-static" "*" - -"@types/fs-extra@^8.0.1": - version "8.1.1" - resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" - integrity sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w== - dependencies: - "@types/node" "*" - -"@types/long@^4.0.0", "@types/long@^4.0.1": - version "4.0.1" - resolved "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" - integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== - -"@types/mime@*": - version "2.0.3" - resolved "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" - integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== - -"@types/node@*": - version "14.11.2" - resolved "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" - integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== - -"@types/node@^13.7.0": - version "13.13.21" - resolved "https://registry.npmjs.org/@types/node/-/node-13.13.21.tgz#e48d3c2e266253405cf404c8654d1bcf0d333e5c" - integrity sha512-tlFWakSzBITITJSxHV4hg4KvrhR/7h3xbJdSFbYJBVzKubrASbnnIFuSgolUh7qKGo/ZeJPKUfbZ0WS6Jp14DQ== - -"@types/node@^8.10.59": - version "8.10.64" - resolved "https://registry.npmjs.org/@types/node/-/node-8.10.64.tgz#0dddc4c53ca4819a32b7478232d8b446ca90e1c6" - integrity sha512-/EwBIb+imu8Qi/A3NF9sJ9iuKo7yV+pryqjmeRqaU0C4wBAOhas5mdvoYeJ5PCKrh6thRSJHdoasFqh3BQGILA== - -"@types/qs@*": - version "6.9.5" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" - integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== - -"@types/range-parser@*": - version "1.2.3" - resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" - integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== - -"@types/serve-static@*": - version "1.13.5" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" - integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== - dependencies: - "@types/express-serve-static-core" "*" - "@types/mime" "*" - -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -agent-base@6: - version "6.0.1" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" - integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== - dependencies: - debug "4" - -array-filter@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" - integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -arrify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" - integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== - -available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" - integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== - dependencies: - array-filter "^1.0.0" - -base64-js@^1.3.0: - version "1.3.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== - -bignumber.js@^9.0.0: - version "9.0.1" - resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" - integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== - -body-parser@1.19.0: - version "1.19.0" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -compressible@^2.0.12: - version "2.0.18" - resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -concat-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" - integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.0.2" - typedarray "^0.0.6" - -configstore@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cors@^2.8.5: - version "2.8.5" - resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - -date-and-time@^0.13.0: - version "0.13.1" - resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-0.13.1.tgz#d12ba07ac840d5b112dc4c83f8a03e8a51f78dd6" - integrity sha512-/Uge9DJAT+s+oAcDxtBhyR8+sKjUnZbYmyhbmWjTHNtX7B7oWD8YyYdeXcBRbwSj6hVvj+IQegJam7m7czhbFw== - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.1.1: - version "4.2.0" - resolved "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== - dependencies: - ms "2.1.2" - -deep-equal@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0" - integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA== - dependencies: - es-abstract "^1.17.5" - es-get-iterator "^1.1.0" - is-arguments "^1.0.4" - is-date-object "^1.0.2" - is-regex "^1.0.5" - isarray "^2.0.5" - object-is "^1.1.2" - object-keys "^1.1.1" - object.assign "^4.1.0" - regexp.prototype.flags "^1.3.0" - side-channel "^1.0.2" - which-boxed-primitive "^1.0.1" - which-collection "^1.0.1" - which-typed-array "^1.1.2" - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -dicer@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" - integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== - dependencies: - streamsearch "0.1.2" - -dot-prop@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.0.0" - -duplexify@^3.5.0, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -duplexify@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz#7027dc374f157b122a8ae08c2d3ea4d2d953aa61" - integrity sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA== - dependencies: - end-of-stream "^1.4.1" - inherits "^2.0.3" - readable-stream "^3.1.1" - stream-shift "^1.0.0" - -ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: - version "1.0.11" - resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -ent@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" - integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= - -es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.7" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" - integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: - version "1.18.0-next.1" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-get-iterator@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" - integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== - dependencies: - es-abstract "^1.17.4" - has-symbols "^1.0.1" - is-arguments "^1.0.4" - is-map "^2.0.1" - is-set "^2.0.1" - is-string "^1.0.5" - isarray "^2.0.5" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -express@^4.17.1: - version "4.17.1" - resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" - content-type "~1.0.4" - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" - range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -fast-text-encoding@^1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" - integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== - -faye-websocket@0.11.3: - version "0.11.3" - resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" - integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== - dependencies: - websocket-driver ">=0.5.1" - -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -firebase-admin@8.13.0: - version "8.13.0" - resolved "https://registry.npmjs.org/firebase-admin/-/firebase-admin-8.13.0.tgz#997d34ae8357d7dc162ba622148bbebcf7f2e923" - integrity sha512-krXj5ncWMJBhCpXSn9UFY6zmDWjFjqgx+1e9ATXKFYndEjmKtNBuJzqdrAdDh7aTUR7X6+0TPx4Hbc08kd0lwQ== - dependencies: - "@firebase/database" "^0.6.0" - "@types/node" "^8.10.59" - dicer "^0.3.0" - jsonwebtoken "^8.5.1" - node-forge "^0.7.6" - optionalDependencies: - "@google-cloud/firestore" "^3.0.0" - "@google-cloud/storage" "^4.1.2" - -firebase-functions@3.13.0: - version "3.13.0" - resolved "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.13.0.tgz#66278dbeb45f343a179814f2b1d95b383beec5e7" - integrity sha512-tnltJL5KlGtbeBD9scsVjoKTSTMeo6EAy1gsdOfRlrwAu6idgLRKYVdmw0YymS8N7SwJ3CXo+3fuvSSihKhXbA== - dependencies: - "@types/express" "4.17.3" - cors "^2.8.5" - express "^4.17.1" - lodash "^4.17.14" - -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -gaxios@^2.0.0, gaxios@^2.1.0: - version "2.3.4" - resolved "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz#eea99353f341c270c5f3c29fc46b8ead56f0a173" - integrity sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA== - dependencies: - abort-controller "^3.0.0" - extend "^3.0.2" - https-proxy-agent "^5.0.0" - is-stream "^2.0.0" - node-fetch "^2.3.0" - -gaxios@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz#11b6f0e8fb08d94a10d4d58b044ad3bec6dd486a" - integrity sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q== - dependencies: - abort-controller "^3.0.0" - extend "^3.0.2" - https-proxy-agent "^5.0.0" - is-stream "^2.0.0" - node-fetch "^2.3.0" - -gcp-metadata@^3.4.0: - version "3.5.0" - resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz#6d28343f65a6bbf8449886a0c0e4a71c77577055" - integrity sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA== - dependencies: - gaxios "^2.1.0" - json-bigint "^0.3.0" - -gcs-resumable-upload@^2.2.4: - version "2.3.3" - resolved "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.3.tgz#02c616ed17eff6676e789910aeab3907d412c5f8" - integrity sha512-sf896I5CC/1AxeaGfSFg3vKMjUq/r+A3bscmVzZm10CElyRanN0XwPu/MxeIO4LSP+9uF6yKzXvNsaTsMXUG6Q== - dependencies: - abort-controller "^3.0.0" - configstore "^5.0.0" - gaxios "^2.0.0" - google-auth-library "^5.0.0" - pumpify "^2.0.0" - stream-events "^1.0.4" - -google-auth-library@^5.0.0, google-auth-library@^5.5.0: - version "5.10.1" - resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz#504ec75487ad140e68dd577c21affa363c87ddff" - integrity sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg== - dependencies: - arrify "^2.0.0" - base64-js "^1.3.0" - ecdsa-sig-formatter "^1.0.11" - fast-text-encoding "^1.0.0" - gaxios "^2.1.0" - gcp-metadata "^3.4.0" - gtoken "^4.1.0" - jws "^4.0.0" - lru-cache "^5.0.0" - -google-gax@^1.15.3: - version "1.15.3" - resolved "https://registry.npmjs.org/google-gax/-/google-gax-1.15.3.tgz#e88cdcbbd19c7d88cc5fd7d7b932c4d1979a5aca" - integrity sha512-3JKJCRumNm3x2EksUTw4P1Rad43FTpqrtW9jzpf3xSMYXx+ogaqTM1vGo7VixHB4xkAyATXVIa3OcNSh8H9zsQ== - dependencies: - "@grpc/grpc-js" "~1.0.3" - "@grpc/proto-loader" "^0.5.1" - "@types/fs-extra" "^8.0.1" - "@types/long" "^4.0.0" - abort-controller "^3.0.0" - duplexify "^3.6.0" - google-auth-library "^5.0.0" - is-stream-ended "^0.1.4" - lodash.at "^4.6.0" - lodash.has "^4.5.2" - node-fetch "^2.6.0" - protobufjs "^6.8.9" - retry-request "^4.0.0" - semver "^6.0.0" - walkdir "^0.4.0" - -google-p12-pem@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz#036462394e266472632a78b685f0cc3df4ef337b" - integrity sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg== - dependencies: - node-forge "^0.9.0" - -graceful-fs@^4.1.2: - version "4.2.4" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -gtoken@^4.1.0: - version "4.1.4" - resolved "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz#925ff1e7df3aaada06611d30ea2d2abf60fcd6a7" - integrity sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA== - dependencies: - gaxios "^2.1.0" - google-p12-pem "^2.0.0" - jws "^4.0.0" - mime "^2.2.0" - -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-stream-validation@^0.2.2: - version "0.2.4" - resolved "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" - integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-parser-js@>=0.5.1: - version "0.5.2" - resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" - integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== - -http-proxy-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== - -is-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" - integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== - -is-boolean-object@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" - integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== - -is-callable@^1.1.4, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== - -is-date-object@^1.0.1, is-date-object@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -is-map@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" - integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== - -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= - -is-number-object@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-regex@^1.0.5, is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -is-set@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" - integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== - -is-stream-ended@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" - integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== - -is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - -is-string@^1.0.4, is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== - -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - -is-typed-array@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" - integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== - dependencies: - available-typed-arrays "^1.0.0" - es-abstract "^1.17.4" - foreach "^2.0.5" - has-symbols "^1.0.1" - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== - -is-weakset@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" - integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -json-bigint@^0.3.0: - version "0.3.1" - resolved "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz#0c1729d679f580d550899d6a2226c228564afe60" - integrity sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ== - dependencies: - bignumber.js "^9.0.0" - -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jwa@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" - integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - -jws@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" - integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== - dependencies: - jwa "^2.0.0" - safe-buffer "^5.0.1" - -lodash.at@^4.6.0: - version "4.6.0" - resolved "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz#93cdce664f0a1994ea33dd7cd40e23afd11b0ff8" - integrity sha1-k83OZk8KGZTqM9181A4jr9EbD/g= - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - -lodash.has@^4.5.2: - version "4.5.2" - resolved "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" - integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI= - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - -lodash@^4.17.14: - version "4.17.20" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -long@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== - -lru-cache@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -"mime-db@>= 1.43.0 < 2": - version "1.45.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== - -mime-types@^2.0.8, mime-types@~2.1.24: - version "2.1.27" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.2.0: - version "2.4.6" - resolved "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" - integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@2.1.2, ms@^2.1.1: - version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.6.0: - version "2.6.1" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-forge@^0.7.6: - version "0.7.6" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" - integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== - -node-forge@^0.9.0: - version "0.9.2" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.2.tgz#b35a44c28889b2ea55cabf8c79e3563f9676190a" - integrity sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw== - -object-assign@^4: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== - -object-is@^1.1.2: - version "1.1.3" - resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81" - integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0, object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^5.1.0: - version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -protobufjs@^6.8.6, protobufjs@^6.8.9: - version "6.10.1" - resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz#e6a484dd8f04b29629e9053344e3970cccf13cd2" - integrity sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.1" - "@types/node" "^13.7.0" - long "^4.0.0" - -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" - integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw== - dependencies: - duplexify "^4.1.1" - inherits "^2.0.3" - pump "^3.0.0" - -qs@6.7.0: - version "6.7.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - -"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^2.0.0: - version "2.3.7" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - 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" - -regexp.prototype.flags@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -retry-request@^4.0.0: - version "4.1.3" - resolved "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" - integrity sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ== - dependencies: - debug "^4.1.1" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -semver@^5.6.0: - version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.2.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -send@0.17.1: - version "0.17.1" - resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.7.2" - mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -side-channel@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3" - integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g== - dependencies: - es-abstract "^1.18.0-next.0" - object-inspect "^1.8.0" - -signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -snakeize@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d" - integrity sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0= - -"statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -stream-events@^1.0.1, stream-events@^1.0.4, stream-events@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" - integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== - dependencies: - stubs "^3.0.0" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -streamsearch@0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" - integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= - -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -stubs@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" - integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls= - -teeny-request@^6.0.0: - version "6.0.3" - resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.3.tgz#b617f9d5b7ba95c76a3f257f6ba2342b70228b1f" - integrity sha512-TZG/dfd2r6yeji19es1cUIwAlVD8y+/svB1kAC2Y0bjEyysrfbO8EZvJBRwIE6WkwmUoB7uvWLwTIhJbMXZ1Dw== - dependencies: - http-proxy-agent "^4.0.0" - https-proxy-agent "^5.0.0" - node-fetch "^2.2.0" - stream-events "^1.0.5" - uuid "^7.0.0" - -through2@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" - integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ== - dependencies: - inherits "^2.0.4" - readable-stream "2 || 3" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -tslib@^1.11.1: - version "1.13.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== - -type-is@~1.6.17, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^7.0.0: - version "7.0.3" - resolved "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" - integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== - -vary@^1, vary@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -walkdir@^0.4.0: - version "0.4.1" - resolved "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" - integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ== - -websocket-driver@>=0.5.1: - version "0.7.4" - resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -which-boxed-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" - integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== - dependencies: - is-bigint "^1.0.0" - is-boolean-object "^1.0.0" - is-number-object "^1.0.3" - is-string "^1.0.4" - is-symbol "^1.0.2" - -which-collection@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== - dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" - -which-typed-array@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" - integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== - dependencies: - available-typed-arrays "^1.0.2" - es-abstract "^1.17.5" - foreach "^2.0.5" - function-bind "^1.1.1" - has-symbols "^1.0.1" - is-typed-array "^1.1.3" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/packages-exp/auth-compat-exp/demo/package.json b/packages-exp/auth-compat-exp/demo/package.json deleted file mode 100644 index 39e9d42b1d8..00000000000 --- a/packages-exp/auth-compat-exp/demo/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@firebase/auth-compat-exp-demo", - "version": "0.1.0", - "private": true, - "description": "Demo for Auth SDK Compatibility Layer", - "author": "Firebase (https://firebase.google.com/)", - "browser": "public/index.js", - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../../.gitignore'", - "demo": "rollup -c && firebase serve", - "build": "rollup -c", - "build:deps": "lerna run --scope @firebase/'{app-compat,app-exp,auth-compat-exp}' --include-dependencies build", - "dev": "rollup -c -w" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x", - "@firebase/auth-types": "0.x", - "@firebase/auth-exp": "0.x" - }, - "dependencies": { - "@firebase/logger": "^0.2.2", - "@firebase/util": "^0.3.0", - "lerna": "^3.22.1", - "tslib": "1.14.1" - }, - "license": "Apache-2.0", - "devDependencies": { - "rollup": "1.32.1", - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-replace": "2.2.0", - "@rollup/plugin-commonjs": "15.1.0", - "rollup-plugin-license": "0.14.0", - "@rollup/plugin-node-resolve": "9.0.0", - "rollup-plugin-sourcemaps": "0.6.3", - "rollup-plugin-typescript2": "0.29.0", - "rollup-plugin-uglify": "6.0.4", - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages-exp/auth-compat-exp/demo", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "dist/index.d.ts", - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/auth-compat-exp/demo/public/index.html b/packages-exp/auth-compat-exp/demo/public/index.html deleted file mode 100644 index 3d518d16854..00000000000 --- a/packages-exp/auth-compat-exp/demo/public/index.html +++ /dev/null @@ -1,767 +0,0 @@ - - - - - Headless App - - - - - - - - - - - - - - - - -
- - - - - - - -
-
- - -
-
- - -
-
- - - [anonymous] / - uid: - -
-
- - / - - - - - - - -
- - - -
-
-
-
- - -
- -
-
-
- - - -
-
- -
Development mode APIs
-
-
- - -
- -
- - -
Web Worker Testing
-
- -
- -
Service Worker Testing
-
- -
- - -
Auth State Persistence
-
- - -
- - -
Language code
-
- - - -
- - -
Sign Up
-
- - - -
- - - -
Sign In
-
- - - -
-
- - -
- -
- - - - - -
- -
- - - - - -
- - -
Sign In with Email Link
-
- - - -
-
- - -
- - -
Password Reset
-
- - -
-
- - - - -
- -
Fetch Sign In Methods
-
- - -
- - -
Update Current User
-
- -
-
- -
-
-
- -
Update Profile
-
- - -
-
- - -
-
- - - -
- - - -
Linking/Unlinking
- -
- - - -
-
- - - - - -
- -
- - - - - - - -
- -
- - - - - -
- -
- - -
- - -
Enroll Second Factor
- -
-
-
- - - - - - -
-
-
- - -
Other Actions
- -
- - -
- - - - - - - -
Delete account
- -
- -
-
Web
-
- -
-
Android
-
-
- -
- - -
- -
-
-
iOS
-
-
- -
-
-
-
- - -
- -
-
-
-

-              
-            
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages-exp/auth-compat-exp/demo/public/manifest.json b/packages-exp/auth-compat-exp/demo/public/manifest.json deleted file mode 100644 index 646c61a9320..00000000000 --- a/packages-exp/auth-compat-exp/demo/public/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "Firebse Auth Test App", - "short_name": "FirebaseAuthTest", - "start_url": "/", - "display": "standalone", - "background_color": "#fff", - "lang": "en-US", - "description": "Test app to test all functionality for Firebase Auth.", - "prefer_related_applications": false, - "theme_color": "#fff", - "scope": "/", - "orientation": "portrait-primary" -} diff --git a/packages-exp/auth-compat-exp/demo/public/script.js b/packages-exp/auth-compat-exp/demo/public/script.js deleted file mode 100644 index 02244f53999..00000000000 --- a/packages-exp/auth-compat-exp/demo/public/script.js +++ /dev/null @@ -1,1863 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Common javascript for the application. - */ - -var app = null; -var auth = null; -var tempApp = null; -var tempAuth = null; -var currentTab = null; -var lastUser = null; -var applicationVerifier = null; -var multiFactorErrorResolver = null; -var selectedMultiFactorHint = null; -var recaptchaSize = 'normal'; - -// Fix for IE8 when developer's console is not opened. -if (!window.console) { - window.console = { - log: function () {}, - error: function () {} - }; -} - -// The corresponding Font Awesome icons for each provider. -var providersIcons = { - 'google.com': 'fa-google', - 'facebook.com': 'fa-facebook-official', - 'twitter.com': 'fa-twitter-square', - 'github.com': 'fa-github', - 'yahoo.com': 'fa-yahoo', - 'phone': 'fa-phone' -}; - -/** - * Logs the message in the console and on the log window in the app - * using the level given. - * @param {?Object} message Object or message to log. - * @param {string} level The level of log (log, error, debug). - * @private - */ -function logAtLevel_(message, level) { - if (message != null) { - var messageDiv = $('
'); - messageDiv.addClass(level); - if (typeof message === 'object') { - messageDiv.text(JSON.stringify(message, null, ' ')); - } else { - messageDiv.text(message); - } - $('.logs').append(messageDiv); - } - console[level](message); -} - -/** - * Logs info level. - * @param {string} message Object or message to log. - */ -function log(message) { - logAtLevel_(message, 'log'); -} - -/** - * Clear the logs. - */ -function clearLogs() { - $('.logs').text(''); -} - -/** - * Displays for a few seconds a box with a specific message and then fades - * it out. - * @param {string} message Small message to display. - * @param {string} cssClass The class(s) to give the alert box. - * @private - */ -function alertMessage_(message, cssClass) { - var alertBox = $('
') - .addClass(cssClass) - .css('display', 'none') - .text(message); - // When modals are visible, display the alert in the modal layer above the - // grey background. - var visibleModal = $('.modal.in'); - if (visibleModal.size() > 0) { - // Check first if the model has an overlaying-alert. If not, append the - // overlaying-alert container. - if (visibleModal.find('.overlaying-alert').size() == 0) { - var $overlayingAlert = $( - '
' - ); - visibleModal.append($overlayingAlert); - } - visibleModal.find('.overlaying-alert').prepend(alertBox); - } else { - $('#alert-messages').prepend(alertBox); - } - alertBox.fadeIn({ - complete: function () { - setTimeout(function () { - alertBox.slideUp(400, function () { - // On completion, remove the alert element from the DOM. - alertBox.remove(); - }); - }, 3000); - } - }); -} - -/** - * Alerts a small success message in a overlaying alert box. - * @param {string} message Small message to display. - */ -function alertSuccess(message) { - alertMessage_(message, 'alert alert-success'); -} - -/** - * Alerts a small error message in a overlaying alert box. - * @param {string} message Small message to display. - */ -function alertError(message) { - alertMessage_(message, 'alert alert-danger'); -} - -/** - * Returns the active user (i.e. currentUser or lastUser). - * @return {!firebase.User} - */ -function activeUser() { - var type = $('input[name=toggle-user-selection]:checked').val(); - if (type == 'lastUser') { - return lastUser; - } else { - return auth.currentUser; - } -} - -/** - * Refreshes the current user data in the UI, displaying a user info box if - * a user is signed in, or removing it. - */ -function refreshUserData() { - if (activeUser()) { - var user = activeUser(); - $('.profile').show(); - $('body').addClass('user-info-displayed'); - $('div.profile-email,span.profile-email').text(user.email || 'No Email'); - $('div.profile-phone,span.profile-phone').text( - user.phoneNumber || 'No Phone' - ); - $('div.profile-uid,span.profile-uid').text(user.uid); - $('div.profile-name,span.profile-name').text(user.displayName || 'No Name'); - $('input.profile-name').val(user.displayName); - $('input.photo-url').val(user.photoURL); - if (user.photoURL != null) { - var photoURL = user.photoURL; - // Append size to the photo URL for Google hosted images to avoid requesting - // the image with its original resolution (using more bandwidth than needed) - // when it is going to be presented in smaller size. - if ( - photoURL.indexOf('googleusercontent.com') != -1 || - photoURL.indexOf('ggpht.com') != -1 - ) { - photoURL = photoURL + '?sz=' + $('img.profile-image').height(); - } - $('img.profile-image').attr('src', photoURL).show(); - } else { - $('img.profile-image').hide(); - } - $('.profile-email-verified').toggle(user.emailVerified); - $('.profile-email-not-verified').toggle(!user.emailVerified); - $('.profile-anonymous').toggle(user.isAnonymous); - // Display/Hide providers icons. - $('.profile-providers').empty(); - if (user['providerData'] && user['providerData'].length) { - var providersCount = user['providerData'].length; - for (var i = 0; i < providersCount; i++) { - addProviderIcon(user['providerData'][i]['providerId']); - } - } - // Show enrolled second factors if available for the active user. - showMultiFactorStatus(user); - // Change color. - if (user == auth.currentUser) { - $('#user-info').removeClass('last-user'); - $('#user-info').addClass('current-user'); - } else { - $('#user-info').removeClass('current-user'); - $('#user-info').addClass('last-user'); - } - } else { - $('.profile').slideUp(); - $('body').removeClass('user-info-displayed'); - $('input.profile-data').val(''); - } -} - -/** - * Sets last signed in user and updates UI. - * @param {?firebase.User} user The last signed in user. - */ -function setLastUser(user) { - lastUser = user; - if (user) { - // Displays the toggle. - $('#toggle-user').show(); - $('#toggle-user-placeholder').hide(); - } else { - $('#toggle-user').hide(); - $('#toggle-user-placeholder').show(); - } -} - -/** - * Add a provider icon to the profile info. - * @param {string} providerId The providerId of the provider. - */ -function addProviderIcon(providerId) { - var pElt = $('') - .addClass('fa ' + providersIcons[providerId]) - .attr('title', providerId) - .data({ - 'toggle': 'tooltip', - 'placement': 'bottom' - }); - $('.profile-providers').append(pElt); - pElt.tooltip(); -} - -/** - * Updates the active user's multi-factor enrollment status. - * @param {!firebase.User} activeUser The corresponding user. - */ -function showMultiFactorStatus(activeUser) { - var enrolledFactors = - (activeUser.multiFactor && activeUser.multiFactor.enrolledFactors) || []; - var $listGroup = $('#user-info .dropdown-menu.enrolled-second-factors'); - // Hide the drop down menu initially. - $listGroup.empty().parent().hide(); - if (enrolledFactors.length) { - // If enrolled factors are available, show the drop down menu. - $listGroup.parent().show(); - // Populate the enrolled factors. - showMultiFactors( - $listGroup, - enrolledFactors, - // On row click, do nothing. This is needed to prevent the drop down - // menu from closing. - function (e) { - e.preventDefault(); - e.stopPropagation(); - }, - // On delete click unenroll the selected factor. - function (e) { - e.preventDefault(); - // Get the corresponding second factor index. - var index = parseInt($(this).attr('data-index'), 10); - // Get the second factor info. - var info = enrolledFactors[index]; - // Get the display name. If not available, use uid. - var label = info && (info.displayName || info.uid); - if (label) { - $('#enrolled-factors-drop-down').removeClass('open'); - activeUser.multiFactor.unenroll(info).then(function () { - refreshUserData(); - alertSuccess('Multi-factor successfully unenrolled.'); - }, onAuthError); - } - } - ); - } -} - -/** - * Updates the UI when the user is successfully authenticated. - * @param {!firebase.User} user User authenticated. - */ -function onAuthSuccess(user) { - console.log(user); - alertSuccess('User authenticated, id: ' + user.uid); - refreshUserData(); -} - -/** - * Displays an error message when the authentication failed. - * @param {!firebase.auth.Error} error Error message to display. - */ -function onAuthError(error) { - logAtLevel_(error, 'error'); - if (error.code == 'auth/multi-factor-auth-required') { - // Handle second factor sign-in. - handleMultiFactorSignIn(error.resolver); - } else { - alertError('Error: ' + error.code); - } -} - -/** - * Changes the UI when the user has been signed out. - */ -function signOut() { - log('User successfully signed out.'); - alertSuccess('User successfully signed out.'); - refreshUserData(); -} - -/** - * Saves the new language code provided in the language code input field. - */ -function onSetLanguageCode() { - var languageCode = $('#language-code').val() || null; - try { - auth.languageCode = languageCode; - alertSuccess('Language code changed to "' + languageCode + '".'); - } catch (error) { - alertError('Error: ' + error.code); - } -} - -/** - * Switches Auth instance language to device language. - */ -function onUseDeviceLanguage() { - auth.useDeviceLanguage(); - $('#language-code').val(auth.languageCode); - alertSuccess('Using device language "' + auth.languageCode + '".'); -} - -/** - * Changes the Auth state persistence to the specified one. - */ -function onSetPersistence() { - var type = $('#persistence-type').val(); - try { - auth.setPersistence(type).then( - function () { - log('Persistence state change to "' + type + '".'); - alertSuccess('Persistence state change to "' + type + '".'); - }, - function (error) { - alertError('Error: ' + error.code); - } - ); - } catch (error) { - alertError('Error: ' + error.code); - } -} - -/** - * Signs up a new user with an email and a password. - */ -function onSignUp() { - var email = $('#signup-email').val(); - var password = $('#signup-password').val(); - auth - .createUserWithEmailAndPassword(email, password) - .then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Signs in a user with an email and a password. - */ -function onSignInWithEmailAndPassword() { - var email = $('#signin-email').val(); - var password = $('#signin-password').val(); - auth - .signInWithEmailAndPassword(email, password) - .then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Signs in a user with an email link. - */ -function onSignInWithEmailLink() { - var email = $('#sign-in-with-email-link-email').val(); - var link = $('#sign-in-with-email-link-link').val() || undefined; - if (auth.isSignInWithEmailLink(link)) { - auth.signInWithEmailLink(email, link).then(onAuthSuccess, onAuthError); - } else { - alertError('Sign in link is invalid'); - } -} - -/** - * Links a user with an email link. - */ -function onLinkWithEmailLink() { - var email = $('#link-with-email-link-email').val(); - var link = $('#link-with-email-link-link').val() || undefined; - var credential = firebase.auth.EmailAuthProvider.credentialWithLink( - email, - link - ); - activeUser() - .linkWithCredential(credential) - .then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Re-authenticate a user with email link credential. - */ -function onReauthenticateWithEmailLink() { - var email = $('#link-with-email-link-email').val(); - var link = $('#link-with-email-link-link').val() || undefined; - var credential = firebase.auth.EmailAuthProvider.credentialWithLink( - email, - link - ); - activeUser() - .reauthenticateWithCredential(credential) - .then(function (result) { - logAdditionalUserInfo(result); - refreshUserData(); - alertSuccess('User reauthenticated!'); - }, onAuthError); -} - -/** - * Signs in with a custom token. - * @param {DOMEvent} event HTML DOM event returned by the listener. - */ -function onSignInWithCustomToken(event) { - // The token can be directly specified on the html element. - var token = $('#user-custom-token').val(); - - auth - .signInWithCustomToken(token) - .then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Signs in anonymously. - */ -function onSignInAnonymously() { - auth.signInAnonymously().then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Signs in with a generic IdP credential. - */ -function onSignInWithGenericIdPCredential() { - var providerId = $('#signin-generic-idp-provider-id').val(); - var idToken = $('#signin-generic-idp-id-token').val() || undefined; - var rawNonce = $('#signin-generic-idp-raw-nonce').val() || undefined; - var accessToken = $('#signin-generic-idp-access-token').val() || undefined; - var provider = new firebase.auth.OAuthProvider(providerId); - auth - .signInWithCredential( - provider.credential({ - idToken: idToken, - accessToken: accessToken, - rawNonce: rawNonce - }) - ) - .then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Initializes the ApplicationVerifier. - * @param {string} submitButtonId The ID of the DOM element of the button to - * which we attach the invisible reCAPTCHA. This is required even in visible - * mode. - */ -function makeApplicationVerifier(submitButtonId) { - var container = - recaptchaSize === 'invisible' ? submitButtonId : 'recaptcha-container'; - applicationVerifier = new firebase.auth.RecaptchaVerifier(container, { - 'size': recaptchaSize - }); -} - -/** - * Clears the ApplicationVerifier. - */ -function clearApplicationVerifier() { - if (applicationVerifier) { - applicationVerifier.clear(); - applicationVerifier = null; - } -} - -/** - * Sends a phone number verification code for sign-in. - */ -function onSignInVerifyPhoneNumber() { - var phoneNumber = $('#signin-phone-number').val(); - var provider = new firebase.auth.PhoneAuthProvider(auth); - // Clear existing reCAPTCHA as an existing reCAPTCHA could be targeted for a - // link/re-auth operation. - clearApplicationVerifier(); - // Initialize a reCAPTCHA application verifier. - makeApplicationVerifier('signin-verify-phone-number'); - provider.verifyPhoneNumber(phoneNumber, applicationVerifier).then( - function (verificationId) { - clearApplicationVerifier(); - $('#signin-phone-verification-id').val(verificationId); - alertSuccess('Phone verification sent!'); - }, - function (error) { - clearApplicationVerifier(); - onAuthError(error); - } - ); -} - -/** - * Confirms a phone number verification for sign-in. - */ -function onSignInConfirmPhoneVerification() { - var verificationId = $('#signin-phone-verification-id').val(); - var verificationCode = $('#signin-phone-verification-code').val(); - var credential = firebase.auth.PhoneAuthProvider.credential( - verificationId, - verificationCode - ); - signInOrLinkCredential(credential); -} - -/** - * Sends a phone number verification code for linking or reauth. - */ -function onLinkReauthVerifyPhoneNumber() { - var phoneNumber = $('#link-reauth-phone-number').val(); - var provider = new firebase.auth.PhoneAuthProvider(auth); - // Clear existing reCAPTCHA as an existing reCAPTCHA could be targeted for a - // sign-in operation. - clearApplicationVerifier(); - // Initialize a reCAPTCHA application verifier. - makeApplicationVerifier('link-reauth-verify-phone-number'); - provider.verifyPhoneNumber(phoneNumber, applicationVerifier).then( - function (verificationId) { - clearApplicationVerifier(); - $('#link-reauth-phone-verification-id').val(verificationId); - alertSuccess('Phone verification sent!'); - }, - function (error) { - clearApplicationVerifier(); - onAuthError(error); - } - ); -} - -/** - * Updates the user's phone number. - */ -function onUpdateConfirmPhoneVerification() { - if (!activeUser()) { - alertError('You need to sign in before linking an account.'); - return; - } - var verificationId = $('#link-reauth-phone-verification-id').val(); - var verificationCode = $('#link-reauth-phone-verification-code').val(); - var credential = firebase.auth.PhoneAuthProvider.credential( - verificationId, - verificationCode - ); - activeUser() - .updatePhoneNumber(credential) - .then(function () { - refreshUserData(); - alertSuccess('Phone number updated!'); - }, onAuthError); -} - -/** - * Confirms a phone number verification for linking. - */ -function onLinkConfirmPhoneVerification() { - var verificationId = $('#link-reauth-phone-verification-id').val(); - var verificationCode = $('#link-reauth-phone-verification-code').val(); - var credential = firebase.auth.PhoneAuthProvider.credential( - verificationId, - verificationCode - ); - signInOrLinkCredential(credential); -} - -/** - * Confirms a phone number verification for reauthentication. - */ -function onReauthConfirmPhoneVerification() { - var verificationId = $('#link-reauth-phone-verification-id').val(); - var verificationCode = $('#link-reauth-phone-verification-code').val(); - var credential = firebase.auth.PhoneAuthProvider.credential( - verificationId, - verificationCode - ); - activeUser() - .reauthenticateWithCredential(credential) - .then(function (result) { - logAdditionalUserInfo(result); - refreshUserData(); - alertSuccess('User reauthenticated!'); - }, onAuthError); -} - -/** - * Sends a phone number verification code for enrolling second factor. - */ -function onStartEnrollWithPhoneMultiFactor() { - var phoneNumber = $('#enroll-mfa-phone-number').val(); - if (!phoneNumber || !activeUser()) { - return; - } - var provider = new firebase.auth.PhoneAuthProvider(auth); - // Clear existing reCAPTCHA as an existing reCAPTCHA could be targeted for a - // sign-in operation. - clearApplicationVerifier(); - // Initialize a reCAPTCHA application verifier. - makeApplicationVerifier('enroll-mfa-verify-phone-number'); - activeUser() - .multiFactor.getSession() - .then(function (multiFactorSession) { - var phoneInfoOptions = { - 'phoneNumber': phoneNumber, - 'session': multiFactorSession - }; - return provider.verifyPhoneNumber(phoneInfoOptions, applicationVerifier); - }) - .then( - function (verificationId) { - clearApplicationVerifier(); - $('#enroll-mfa-phone-verification-id').val(verificationId); - alertSuccess('Phone verification sent!'); - }, - function (error) { - clearApplicationVerifier(); - onAuthError(error); - } - ); -} - -/** - * Confirms a phone number verification for MFA enrollment. - */ -function onFinalizeEnrollWithPhoneMultiFactor() { - var verificationId = $('#enroll-mfa-phone-verification-id').val(); - var verificationCode = $('#enroll-mfa-phone-verification-code').val(); - if (!verificationId || !verificationCode || !activeUser()) { - return; - } - var credential = firebase.auth.PhoneAuthProvider.credential( - verificationId, - verificationCode - ); - var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion( - credential - ); - var displayName = $('#enroll-mfa-phone-display-name').val() || undefined; - - activeUser() - .multiFactor.enroll(multiFactorAssertion, displayName) - .then(function () { - refreshUserData(); - alertSuccess('Phone number enrolled!'); - }, onAuthError); -} - -/** - * Signs in or links a provider's credential, based on current tab opened. - * @param {!firebase.auth.AuthCredential} credential The provider's credential. - */ -function signInOrLinkCredential(credential) { - if (currentTab == '#user-section') { - if (!activeUser()) { - alertError('You need to sign in before linking an account.'); - return; - } - activeUser() - .linkWithCredential(credential) - .then(function (result) { - logAdditionalUserInfo(result); - refreshUserData(); - alertSuccess('Provider linked!'); - }, onAuthError); - } else { - auth - .signInWithCredential(credential) - .then(onAuthUserCredentialSuccess, onAuthError); - } -} - -/** @return {!Object} The Action Code Settings object. */ -function getActionCodeSettings() { - var actionCodeSettings = {}; - var url = $('#continueUrl').val(); - var apn = $('#apn').val(); - var amv = $('#amv').val(); - var ibi = $('#ibi').val(); - var installApp = $('input[name=install-app]:checked').val() == 'Yes'; - var handleCodeInApp = $('input[name=handle-in-app]:checked').val() == 'Yes'; - if (url || apn || ibi) { - actionCodeSettings['url'] = url; - if (apn) { - actionCodeSettings['android'] = { - 'packageName': apn, - 'installApp': !!installApp, - 'minimumVersion': amv || undefined - }; - } - if (ibi) { - actionCodeSettings['iOS'] = { - 'bundleId': ibi - }; - } - actionCodeSettings['handleCodeInApp'] = handleCodeInApp; - } - return actionCodeSettings; -} - -/** Reset action code settings form. */ -function onActionCodeSettingsReset() { - $('#continueUrl').val(''); - $('#apn').val(''); - $('#amv').val(''); - $('#ibi').val(''); -} - -/** - * Changes the user's email. - */ -function onChangeEmail() { - var email = $('#changed-email').val(); - activeUser() - .updateEmail(email) - .then(function () { - refreshUserData(); - alertSuccess('Email changed!'); - }, onAuthError); -} - -/** - * Changes the user's password. - */ -function onChangePassword() { - var password = $('#changed-password').val(); - activeUser() - .updatePassword(password) - .then(function () { - refreshUserData(); - alertSuccess('Password changed!'); - }, onAuthError); -} - -/** - * Changes the user's password. - */ -function onUpdateProfile() { - var displayName = $('#display-name').val(); - var photoURL = $('#photo-url').val(); - activeUser() - .updateProfile({ - 'displayName': displayName, - 'photoURL': photoURL - }) - .then(function () { - refreshUserData(); - alertSuccess('Profile updated!'); - }, onAuthError); -} - -/** - * Sends sign in with email link to the user. - */ -function onSendSignInLinkToEmail() { - var email = $('#sign-in-with-email-link-email').val(); - auth.sendSignInLinkToEmail(email, getActionCodeSettings()).then(function () { - alertSuccess('Email sent!'); - }, onAuthError); -} - -/** - * Sends sign in with email link to the user and pass in current url. - */ -function onSendSignInLinkToEmailCurrentUrl() { - var email = $('#sign-in-with-email-link-email').val(); - var actionCodeSettings = { - 'url': window.location.href, - 'handleCodeInApp': true - }; - - auth.sendSignInLinkToEmail(email, actionCodeSettings).then(function () { - if ('localStorage' in window && window['localStorage'] !== null) { - window.localStorage.setItem( - 'emailForSignIn', - // Save the email and the timestamp. - JSON.stringify({ - email: email, - timestamp: new Date().getTime() - }) - ); - } - alertSuccess('Email sent!'); - }, onAuthError); -} - -/** - * Sends email link to link the user. - */ -function onSendLinkEmailLink() { - var email = $('#link-with-email-link-email').val(); - auth.sendSignInLinkToEmail(email, getActionCodeSettings()).then(function () { - alertSuccess('Email sent!'); - }, onAuthError); -} - -/** - * Sends password reset email to the user. - */ -function onSendPasswordResetEmail() { - var email = $('#password-reset-email').val(); - auth.sendPasswordResetEmail(email, getActionCodeSettings()).then(function () { - alertSuccess('Email sent!'); - }, onAuthError); -} - -/** - * Verifies the password reset code entered by the user. - */ -function onVerifyPasswordResetCode() { - var code = $('#password-reset-code').val(); - auth.verifyPasswordResetCode(code).then(function () { - alertSuccess('Password reset code is valid!'); - }, onAuthError); -} - -/** - * Confirms the password reset with the code and password supplied by the user. - */ -function onConfirmPasswordReset() { - var code = $('#password-reset-code').val(); - var password = $('#password-reset-password').val(); - auth.confirmPasswordReset(code, password).then(function () { - alertSuccess('Password has been changed!'); - }, onAuthError); -} - -/** - * Gets the list of possible sign in methods for the given email address. - */ -function onFetchSignInMethodsForEmail() { - var email = $('#fetch-sign-in-methods-email').val(); - auth.fetchSignInMethodsForEmail(email).then(function (signInMethods) { - log('Sign in methods for ' + email + ' :'); - log(signInMethods); - if (signInMethods.length == 0) { - alertSuccess('Sign In Methods for ' + email + ': N/A'); - } else { - alertSuccess( - 'Sign In Methods for ' + email + ': ' + signInMethods.join(', ') - ); - } - }, onAuthError); -} - -/** - * Fetches and logs the user's providers data. - */ -function onGetProviderData() { - log('Providers data:'); - log(activeUser()['providerData']); -} - -/** - * Links a signed in user with an email and password account. - */ -function onLinkWithEmailAndPassword() { - var email = $('#link-email').val(); - var password = $('#link-password').val(); - activeUser() - .linkWithCredential( - firebase.auth.EmailAuthProvider.credential(email, password) - ) - .then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Links with a generic IdP credential. - */ -function onLinkWithGenericIdPCredential() { - var providerId = $('#link-generic-idp-provider-id').val(); - var idToken = $('#link-generic-idp-id-token').val() || undefined; - var rawNonce = $('#link-generic-idp-raw-nonce').val() || undefined; - var accessToken = $('#link-generic-idp-access-token').val() || undefined; - var provider = new firebase.auth.OAuthProvider(providerId); - activeUser() - .linkWithCredential( - provider.credential({ - idToken: idToken, - accessToken: accessToken, - rawNonce: rawNonce - }) - ) - .then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Unlinks the specified provider. - */ -function onUnlinkProvider() { - var providerId = $('#unlinked-provider-id').val(); - activeUser() - .unlink(providerId) - .then(function (user) { - alertSuccess('Provider unlinked from user.'); - refreshUserData(); - }, onAuthError); -} - -/** - * Sends email verification to the user. - */ -function onSendEmailVerification() { - activeUser() - .sendEmailVerification(getActionCodeSettings()) - .then(function () { - alertSuccess('Email verification sent!'); - }, onAuthError); -} - -/** - * Confirms the email verification code given. - */ -function onApplyActionCode() { - var code = $('#email-verification-code').val(); - auth.applyActionCode(code).then(function () { - alertSuccess('Email successfully verified!'); - refreshUserData(); - }, onAuthError); -} - -/** - * Gets or refreshes the ID token. - * @param {boolean} forceRefresh Whether to force the refresh of the token - * or not. - */ -function getIdToken(forceRefresh) { - if (activeUser() == null) { - alertError('No user logged in.'); - return; - } - if (activeUser().getIdToken) { - activeUser() - .getIdToken(forceRefresh) - .then(alertSuccess, function () { - log('No token'); - }); - } else { - activeUser() - .getToken(forceRefresh) - .then(alertSuccess, function () { - log('No token'); - }); - } -} - -/** - * Gets or refreshes the ID token result. - * @param {boolean} forceRefresh Whether to force the refresh of the token - * or not - */ -function getIdTokenResult(forceRefresh) { - if (activeUser() == null) { - alertError('No user logged in.'); - return; - } - activeUser() - .getIdTokenResult(forceRefresh) - .then(function (idTokenResult) { - alertSuccess(JSON.stringify(idTokenResult)); - }, onAuthError); -} - -/** - * Triggers the retrieval of the ID token result. - */ -function onGetIdTokenResult() { - getIdTokenResult(false); -} - -/** - * Triggers the refresh of the ID token result. - */ -function onRefreshTokenResult() { - getIdTokenResult(true); -} - -/** - * Triggers the retrieval of the ID token. - */ -function onGetIdToken() { - getIdToken(false); -} - -/** - * Triggers the refresh of the ID token. - */ -function onRefreshToken() { - getIdToken(true); -} - -/** - * Signs out the user. - */ -function onSignOut() { - setLastUser(auth.currentUser); - auth.signOut().then(signOut, onAuthError); -} - -/** - * Handles multi-factor sign-in completion. - * @param {!firebase.auth.MultiFactorResolver} resolver The multi-factor error - * resolver. - */ -function handleMultiFactorSignIn(resolver) { - // Save multi-factor error resolver. - multiFactorErrorResolver = resolver; - // Populate 2nd factor options from resolver. - var $listGroup = $('#multiFactorModal div.enrolled-second-factors'); - // Populate the list of 2nd factors in the list group specified. - showMultiFactors( - $listGroup, - multiFactorErrorResolver.hints, - // On row click, select the corresponding second factor to complete - // sign-in with. - function (e) { - e.preventDefault(); - // Remove all other active entries. - $listGroup.find('a').removeClass('active'); - // Mark current entry as active. - $(this).addClass('active'); - // Select current factor. - onSelectMultiFactorHint(parseInt($(this).attr('data-index'), 10)); - }, - // Do not show delete option - null - ); - // Hide phone form (other second factor types could be supported). - $('#multi-factor-phone').addClass('hidden'); - // Show second factor recovery dialog. - $('#multiFactorModal').modal(); -} - -/** - * Displays the list of multi-factors in the provided list group. - * @param {!jQuery} $listGroup The list group where the enrolled - * factors will be displayed. - * @param {!Array} multiFactorInfo The list of - * multi-factors to display. - * @param {?function(!jQuery.Event)} onClick The click handler when a second - * factor is clicked. - * @param {?function(!jQuery.Event)} onDelete The click handler when a second - * factor is delete. If not provided, no delete button is shown. - */ -function showMultiFactors($listGroup, multiFactorInfo, onClick, onDelete) { - // Append entry to list. - $listGroup.empty(); - $.each(multiFactorInfo, function (i) { - // Append entry to list. - var info = multiFactorInfo[i]; - var displayName = info.displayName || 'N/A'; - var $a = $('') - .addClass('list-group-item') - .addClass('list-group-item-action') - // Set index on entry. - .attr('data-index', i) - .appendTo($listGroup); - $a.append($('

').text(info.uid)); - $a.append($('').text(info.factorId)); - $a.append($('

').text(displayName)); - if (info.phoneNumber) { - $a.append($('').text(info.phoneNumber)); - } - // Check if a delete button is to be displayed. - if (onDelete) { - var $deleteBtn = $( - '' + - '' + - '' - ); - // Append delete button to row. - $a.append($deleteBtn); - // Add delete button click handler. - $a.find('button.delete-factor').click(onDelete); - } - // On entry click. - if (onClick) { - $a.click(onClick); - } - }); -} - -/** - * Handles the user selection of second factor to complete sign-in with. - * @param {number} index The selected multi-factor hint index. - */ -function onSelectMultiFactorHint(index) { - // Hide all forms for handling each type of second factors. - // Currently only phone is supported. - $('#multi-factor-phone').addClass('hidden'); - if ( - !multiFactorErrorResolver || - typeof multiFactorErrorResolver.hints[index] === 'undefined' - ) { - return; - } - - if (multiFactorErrorResolver.hints[index].factorId == 'phone') { - // Save selected second factor. - selectedMultiFactorHint = multiFactorErrorResolver.hints[index]; - // Show options for phone 2nd factor. - // Get reCAPTCHA ready. - clearApplicationVerifier(); - makeApplicationVerifier('send-2fa-phone-code'); - // Show sign-in with phone second factor menu. - $('#multi-factor-phone').removeClass('hidden'); - // Clear all input. - $('#multi-factor-sign-in-verification-id').val(''); - $('#multi-factor-sign-in-verification-code').val(''); - } else { - // 2nd factor not found or not supported by app. - alertError('Selected 2nd factor is not supported!'); - } -} - -/** - * Start sign-in with the 2nd factor phone number. - * @param {!jQuery.Event} event The jQuery event object. - */ -function onStartSignInWithPhoneMultiFactor(event) { - event.preventDefault(); - // Make sure a second factor is selected. - if (!selectedMultiFactorHint || !multiFactorErrorResolver) { - return; - } - // Initialize a reCAPTCHA application verifier. - var provider = new firebase.auth.PhoneAuthProvider(auth); - var signInRequest = { - multiFactorHint: selectedMultiFactorHint, - session: multiFactorErrorResolver.session - }; - provider.verifyPhoneNumber(signInRequest, applicationVerifier).then( - function (verificationId) { - clearApplicationVerifier(); - $('#multi-factor-sign-in-verification-id').val(verificationId); - alertSuccess('Phone verification sent!'); - }, - function (error) { - clearApplicationVerifier(); - onAuthError(error); - } - ); -} - -/** - * Completes sign-in with the 2nd factor phone assertion. - * @param {!jQuery.Event} event The jQuery event object. - */ -function onFinalizeSignInWithPhoneMultiFactor(event) { - event.preventDefault(); - var verificationId = $('#multi-factor-sign-in-verification-id').val(); - var code = $('#multi-factor-sign-in-verification-code').val(); - if (!code || !verificationId || !multiFactorErrorResolver) { - return; - } - var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, code); - var assertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred); - multiFactorErrorResolver - .resolveSignIn(assertion) - .then(function (userCredential) { - onAuthUserCredentialSuccess(userCredential); - $('#multiFactorModal').modal('hide'); - }, onAuthError); -} - -/** - * Adds a new row to insert an OAuth custom parameter key/value pair. - * @param {!jQuery.Event} event The jQuery event object. - */ -function onPopupRedirectAddCustomParam(event) { - // Form container. - var html = '

'; - // OAuth parameter key input. - html += - ''; - // OAuth parameter value input. - html += - ''; - // Button to remove current key/value pair. - html += ''; - html += ''; - // Create jQuery node. - var $node = $(html); - // Add button click event listener to remove item. - $node.find('button').on('click', function (e) { - // Remove button click event listener. - $(this).off('click'); - // Get row container and remove it. - $(this).closest('form.customParamItem').remove(); - e.preventDefault(); - }); - // Append constructed row to parameter list container. - $('#popup-redirect-custom-parameters').append($node); -} - -/** - * Performs the corresponding popup/redirect action for a generic provider. - */ -function onPopupRedirectGenericProviderClick() { - var providerId = $('#popup-redirect-generic-providerid').val(); - var provider = new firebase.auth.OAuthProvider(providerId); - signInWithPopupRedirect(provider); -} - -/** - * Performs the corresponding popup/redirect action for a SAML provider. - */ -function onPopupRedirectSamlProviderClick() { - var providerId = $('#popup-redirect-saml-providerid').val(); - var provider = new firebase.auth.SAMLAuthProvider(providerId); - signInWithPopupRedirect(provider); -} - -/** - * Performs the corresponding popup/redirect action based on user's selection. - * @param {!jQuery.Event} event The jQuery event object. - */ -function onPopupRedirectProviderClick(event) { - var providerId = $(event.currentTarget).data('provider'); - var provider = null; - switch (providerId) { - case 'google.com': - provider = new firebase.auth.GoogleAuthProvider(); - break; - case 'facebook.com': - provider = new firebase.auth.FacebookAuthProvider(); - break; - case 'github.com': - provider = new firebase.auth.GithubAuthProvider(); - break; - case 'twitter.com': - provider = new firebase.auth.TwitterAuthProvider(); - break; - default: - return; - } - signInWithPopupRedirect(provider); -} - -/** - * Performs a popup/redirect action based on a given provider and the user's - * selections. - * @param {!firebase.auth.AuthProvider} provider The provider with which to - * sign in. - */ -function signInWithPopupRedirect(provider) { - var action = $('input[name=popup-redirect-action]:checked').val(); - var type = $('input[name=popup-redirect-type]:checked').val(); - var method = null; - var inst = null; - if (action == 'link' || action == 'reauthenticate') { - if (!activeUser()) { - alertError('No user logged in.'); - return; - } - inst = activeUser(); - method = action + 'With'; - } else { - inst = auth; - method = 'signInWith'; - } - if (type == 'popup') { - method += 'Popup'; - } else { - method += 'Redirect'; - } - // Get custom OAuth parameters. - var customParameters = {}; - // For each entry. - $('form.customParamItem').each(function (index) { - // Get parameter key. - var key = $(this).find('input.customParamKey').val(); - // Get parameter value. - var value = $(this).find('input.customParamValue').val(); - // Save to list if valid. - if (key && value) { - customParameters[key] = value; - } - }); - console.log('customParameters: ', customParameters); - // For older jscore versions that do not support this. - if (provider.setCustomParameters) { - // Set custom parameters on current provider. - provider.setCustomParameters(customParameters); - } - - // Add scopes for providers who do have scopes available (i.e. not Twitter). - if (provider.addScope) { - // String.prototype.trim not available in IE8. - var scopes = $.trim($('#scopes').val()).split(/\s*,\s*/); - for (var i = 0; i < scopes.length; i++) { - provider.addScope(scopes[i]); - } - } - console.log('Provider:'); - console.log(provider); - if (type == 'popup') { - inst[method](provider).then(function (response) { - console.log('Popup response:'); - console.log(response); - alertSuccess(action + ' with ' + provider['providerId'] + ' successful!'); - logAdditionalUserInfo(response); - onAuthSuccess(activeUser()); - }, onAuthError); - } else { - try { - inst[method](provider).catch(onAuthError); - } catch (error) { - console.log('Error while calling ' + method); - console.error(error); - } - } -} - -/** - * Displays user credential result. - * @param {!firebase.auth.UserCredential} result The UserCredential result - * object. - */ -function onAuthUserCredentialSuccess(result) { - onAuthSuccess(result.user); - logAdditionalUserInfo(result); -} - -/** - * Displays redirect result. - */ -function onGetRedirectResult() { - auth.getRedirectResult().then(function (response) { - log('Redirect results:'); - if (response.credential) { - log('Credential:'); - log(response.credential); - } else { - log('No credential'); - } - if (response.user) { - log("User's id:"); - log(response.user.uid); - } else { - log('No user'); - } - logAdditionalUserInfo(response); - console.log(response); - }, onAuthError); -} - -/** - * Logs additional user info returned by a sign-in event, if available. - * @param {!Object} response - */ -function logAdditionalUserInfo(response) { - if (response.additionalUserInfo) { - if (response.additionalUserInfo.username) { - log( - response.additionalUserInfo['providerId'] + - ' username: ' + - response.additionalUserInfo.username - ); - } - if (response.additionalUserInfo.profile) { - log(response.additionalUserInfo['providerId'] + ' profile information:'); - log(JSON.stringify(response.additionalUserInfo.profile, null, 2)); - } - if (typeof response.additionalUserInfo.isNewUser !== 'undefined') { - log( - response.additionalUserInfo['providerId'] + - ' isNewUser: ' + - response.additionalUserInfo.isNewUser - ); - } - if (response.credential) { - log('credential: ' + JSON.stringify(response.credential.toJSON())); - } - } -} - -/** - * Deletes the user account. - */ -function onDelete() { - activeUser() - ['delete']() - .then(function () { - log('User successfully deleted.'); - alertSuccess('User successfully deleted.'); - refreshUserData(); - }, onAuthError); -} - -/** - * Gets a specific query parameter from the current URL. - * @param {string} name Name of the parameter. - * @return {string} The query parameter requested. - */ -function getParameterByName(name) { - var url = window.location.href; - name = name.replace(/[\[\]]/g, '\\$&'); - var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); - var results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, ' ')); -} - -/** - * Detects if an action code is passed in the URL, and populates accordingly - * the input field for the confirm email verification process. - */ -function populateActionCodes() { - var emailForSignIn = null; - var signInTime = 0; - if ('localStorage' in window && window['localStorage'] !== null) { - try { - // Try to parse as JSON first using new storage format. - var emailForSignInData = JSON.parse( - window.localStorage.getItem('emailForSignIn') - ); - emailForSignIn = emailForSignInData['email'] || null; - signInTime = emailForSignInData['timestamp'] || 0; - } catch (e) { - // JSON parsing failed. This means the email is stored in the old string - // format. - emailForSignIn = window.localStorage.getItem('emailForSignIn'); - } - if (emailForSignIn) { - // Clear old codes. Old format codes should be cleared immediately. - if (new Date().getTime() - signInTime >= 1 * 24 * 3600 * 1000) { - // Remove email from storage. - window.localStorage.removeItem('emailForSignIn'); - } - } - } - var actionCode = getParameterByName('oobCode'); - if (actionCode != null) { - var mode = getParameterByName('mode'); - if (mode == 'verifyEmail') { - $('#email-verification-code').val(actionCode); - } else if (mode == 'resetPassword') { - $('#password-reset-code').val(actionCode); - } else if (mode == 'signIn') { - if (emailForSignIn) { - $('#sign-in-with-email-link-email').val(emailForSignIn); - $('#sign-in-with-email-link-link').val(window.location.href); - onSignInWithEmailLink(); - // Remove email from storage as the code is only usable once. - window.localStorage.removeItem('emailForSignIn'); - } - } else { - $('#email-verification-code').val(actionCode); - $('#password-reset-code').val(actionCode); - } - } -} - -/** - * Provides basic Database checks for authenticated and unauthenticated access. - * The Database node being tested has the following rule: - * "users": { - * "$user_id": { - * ".read": "$user_id === auth.uid", - * ".write": "$user_id === auth.uid" - * } - * } - * This applies when Real-time database service is available. - */ -function checkDatabaseAuthAccess() { - var randomString = Math.floor(Math.random() * 10000000).toString(); - var dbRef; - var dbPath; - var errMessage; - // Run this check only when Database module is available. - if ( - typeof firebase !== 'undefined' && - typeof firebase.database !== 'undefined' - ) { - if (lastUser && !firebase.auth().currentUser) { - dbPath = 'users/' + lastUser.uid; - // After sign out, confirm read/write access to users/$user_id blocked. - dbRef = firebase.database().ref(dbPath); - dbRef - .set({ - 'test': randomString - }) - .then(function () { - alertError( - 'Error: Unauthenticated write to Database node ' + - dbPath + - ' unexpectedly succeeded!' - ); - }) - .catch(function (error) { - errMessage = error.message.toLowerCase(); - // Permission denied error should be thrown. - if (errMessage.indexOf('permission_denied') == -1) { - alertError('Error: ' + error.code); - return; - } - dbRef - .once('value') - .then(function () { - alertError( - 'Error: Unauthenticated read to Database node ' + - dbPath + - ' unexpectedly succeeded!' - ); - }) - .catch(function (error) { - errMessage = error.message.toLowerCase(); - // Permission denied error should be thrown. - if (errMessage.indexOf('permission_denied') == -1) { - alertError('Error: ' + error.code); - return; - } - log( - 'Unauthenticated read/write to Database node ' + - dbPath + - ' failed as expected!' - ); - }); - }); - } else if (firebase.auth().currentUser) { - dbPath = 'users/' + firebase.auth().currentUser.uid; - // Confirm read/write access to users/$user_id allowed. - dbRef = firebase.database().ref(dbPath); - dbRef - .set({ - 'test': randomString - }) - .then(function () { - return dbRef.once('value'); - }) - .then(function (snapshot) { - if (snapshot.val().test === randomString) { - // read/write successful. - log( - 'Authenticated read/write to Database node ' + - dbPath + - ' succeeded!' - ); - } else { - throw new Error( - 'Authenticated read/write to Database node ' + dbPath + ' failed!' - ); - } - // Clean up: clear that node's content. - return dbRef.remove(); - }) - .catch(function (error) { - alertError('Error: ' + error.code); - }); - } - } -} - -/** Runs all web worker tests if web workers are supported. */ -function onRunWebWorkTests() { - if (!webWorker) { - alertError('Error: Web workers are not supported in the current browser!'); - return; - } - var onError = function (error) { - alertError('Error code: ' + error.code + ' message: ' + error.message); - }; - auth - .signInWithPopup(new firebase.auth.GoogleAuthProvider()) - .then(function (result) { - runWebWorkerTests(result.credential.idToken); - }, onError); -} - -/** Runs service worker tests if supported. */ -function onRunServiceWorkTests() { - $.ajax('/checkIfAuthenticated').then( - function (data, textStatus, jqXHR) { - alertSuccess('User authenticated: ' + data.uid); - }, - function (jqXHR, textStatus, errorThrown) { - alertError(jqXHR.status + ': ' + JSON.stringify(jqXHR.responseJSON)); - } - ); -} - -/** Copy current user of auth to tempAuth. */ -function onCopyActiveUser() { - tempAuth.updateCurrentUser(activeUser()).then( - function () { - alertSuccess('Copied active user to temp Auth'); - }, - function (error) { - alertError('Error: ' + error.code); - } - ); -} - -/** Copy last user to auth. */ -function onCopyLastUser() { - // If last user is null, NULL_USER error will be thrown. - auth.updateCurrentUser(lastUser).then( - function () { - alertSuccess('Copied last user to Auth'); - }, - function (error) { - alertError('Error: ' + error.code); - } - ); -} - -/** Applies selected auth settings change. */ -function onApplyAuthSettingsChange() { - try { - auth.settings.appVerificationDisabledForTesting = - $('input[name=enable-app-verification]:checked').val() == 'No'; - alertSuccess('Auth settings changed'); - } catch (error) { - alertError('Error: ' + error.code); - } -} - -/** - * Initiates the application by setting event listeners on the various buttons. - */ -function initApp() { - log('Initializing app...'); - app = firebase.initializeApp(config); - auth = app.auth(); - - tempApp = firebase.initializeApp( - { - 'apiKey': config['apiKey'], - 'authDomain': config['authDomain'] - }, - auth['app']['name'] + '-temp' - ); - tempAuth = tempApp.auth(); - - // Listen to reCAPTCHA config togglers. - initRecaptchaToggle(function (size) { - clearApplicationVerifier(); - recaptchaSize = size; - }); - - // The action code for email verification or password reset - // can be passed in the url address as a parameter, and for convenience - // this preloads the input field. - populateActionCodes(); - - // Allows to login the user if previously logged in. - if (auth.onIdTokenChanged) { - auth.onIdTokenChanged(function (user) { - refreshUserData(); - if (user) { - user.getIdTokenResult(false).then( - function (idTokenResult) { - log(JSON.stringify(idTokenResult)); - }, - function () { - log('No token.'); - } - ); - } else { - log('No user logged in.'); - } - }); - } - - if (auth.onAuthStateChanged) { - auth.onAuthStateChanged(function (user) { - if (user) { - log('user state change detected: ' + user.uid); - } else { - log('user state change detected: no user'); - } - // Check Database Auth access. - checkDatabaseAuthAccess(); - }); - } - - if (tempAuth.onAuthStateChanged) { - tempAuth.onAuthStateChanged(function (user) { - if (user) { - log('user state change on temp Auth detect: ' + JSON.stringify(user)); - alertSuccess('user state change on temp Auth detect: ' + user.uid); - } - }); - } - - // We check for redirect result to refresh user's data. - auth.getRedirectResult().then(function (response) { - refreshUserData(); - logAdditionalUserInfo(response); - }, onAuthError); - - // Bootstrap tooltips. - $('[data-toggle="tooltip"]').tooltip(); - - // Auto submit the choose library type form. - $('#library-form').on('change', 'input.library-option', function () { - $('#library-form').submit(); - }); - - // To clear the logs in the page. - $('.clear-logs').click(clearLogs); - - // Disables JS forms. - $('form.no-submit').on('submit', function () { - return false; - }); - - // Keeps track of the current tab opened. - $('#tab-menu a').click(function (event) { - currentTab = $(event.currentTarget).attr('href'); - }); - - // Toggles user. - $('input[name=toggle-user-selection]').change(refreshUserData); - - // Actions listeners. - $('#sign-up-with-email-and-password').click(onSignUp); - $('#sign-in-with-email-and-password').click(onSignInWithEmailAndPassword); - $('.sign-in-with-custom-token').click(onSignInWithCustomToken); - $('#sign-in-anonymously').click(onSignInAnonymously); - $('#sign-in-with-generic-idp-credential').click( - onSignInWithGenericIdPCredential - ); - $('#signin-verify-phone-number').click(onSignInVerifyPhoneNumber); - $('#signin-confirm-phone-verification').click( - onSignInConfirmPhoneVerification - ); - // On enter click in verification code, complete phone sign-in. This prevents - // reCAPTCHA from being re-rendered (default behavior on enter). - $('#signin-phone-verification-code').keypress(function (e) { - if (e.which == 13) { - onSignInConfirmPhoneVerification(); - e.preventDefault(); - } - }); - $('#sign-in-with-email-link').click(onSignInWithEmailLink); - $('#link-with-email-link').click(onLinkWithEmailLink); - $('#reauth-with-email-link').click(onReauthenticateWithEmailLink); - - $('#change-email').click(onChangeEmail); - $('#change-password').click(onChangePassword); - $('#update-profile').click(onUpdateProfile); - - $('#send-sign-in-link-to-email').click(onSendSignInLinkToEmail); - $('#send-sign-in-link-to-email-current-url').click( - onSendSignInLinkToEmailCurrentUrl - ); - $('#send-link-email-link').click(onSendLinkEmailLink); - - $('#send-password-reset-email').click(onSendPasswordResetEmail); - $('#verify-password-reset-code').click(onVerifyPasswordResetCode); - $('#confirm-password-reset').click(onConfirmPasswordReset); - - $('#get-provider-data').click(onGetProviderData); - $('#link-with-email-and-password').click(onLinkWithEmailAndPassword); - $('#link-with-generic-idp-credential').click(onLinkWithGenericIdPCredential); - $('#unlink-provider').click(onUnlinkProvider); - $('#link-reauth-verify-phone-number').click(onLinkReauthVerifyPhoneNumber); - $('#update-confirm-phone-verification').click( - onUpdateConfirmPhoneVerification - ); - $('#link-confirm-phone-verification').click(onLinkConfirmPhoneVerification); - $('#reauth-confirm-phone-verification').click( - onReauthConfirmPhoneVerification - ); - // On enter click in verification code, complete phone sign-in. This prevents - // reCAPTCHA from being re-rendered (default behavior on enter). - $('#link-reauth-phone-verification-code').keypress(function (e) { - if (e.which == 13) { - // User first option option as default. - onUpdateConfirmPhoneVerification(); - e.preventDefault(); - } - }); - - $('#send-email-verification').click(onSendEmailVerification); - $('#confirm-email-verification').click(onApplyActionCode); - $('#get-token-result').click(onGetIdTokenResult); - $('#refresh-token-result').click(onRefreshTokenResult); - $('#get-token').click(onGetIdToken); - $('#refresh-token').click(onRefreshToken); - $('#get-token-worker').click(onGetCurrentUserDataFromWebWorker); - $('#sign-out').click(onSignOut); - - $('.popup-redirect-provider').click(onPopupRedirectProviderClick); - $('#popup-redirect-generic').click(onPopupRedirectGenericProviderClick); - $('#popup-redirect-get-redirect-result').click(onGetRedirectResult); - $('#popup-redirect-add-custom-parameter').click( - onPopupRedirectAddCustomParam - ); - $('#popup-redirect-saml').click(onPopupRedirectSamlProviderClick); - - $('#action-code-settings-reset').click(onActionCodeSettingsReset); - - $('#delete').click(onDelete); - - $('#set-persistence').click(onSetPersistence); - - $('#set-language-code').click(onSetLanguageCode); - $('#use-device-language').click(onUseDeviceLanguage); - - $('#fetch-sign-in-methods-for-email').click(onFetchSignInMethodsForEmail); - - $('#run-web-worker-tests').click(onRunWebWorkTests); - $('#run-service-worker-tests').click(onRunServiceWorkTests); - $('#copy-active-user').click(onCopyActiveUser); - $('#copy-last-user').click(onCopyLastUser); - - $('#apply-auth-settings-change').click(onApplyAuthSettingsChange); - - // Multi-factor operations. - // Starts multi-factor sign-in with selected phone number. - $('#send-2fa-phone-code').click(onStartSignInWithPhoneMultiFactor); - // Completes multi-factor sign-in with supplied SMS code. - $('#sign-in-with-phone-multi-factor').click( - onFinalizeSignInWithPhoneMultiFactor - ); - // Starts multi-factor enrollment with phone number. - $('#enroll-mfa-verify-phone-number').click(onStartEnrollWithPhoneMultiFactor); - // Completes multi-factor enrollment with supplied SMS code. - $('#enroll-mfa-confirm-phone-verification').click( - onFinalizeEnrollWithPhoneMultiFactor - ); -} - -$(initApp); diff --git a/packages-exp/auth-compat-exp/demo/public/service-worker.js b/packages-exp/auth-compat-exp/demo/public/service-worker.js deleted file mode 100644 index c7c9a9ab49b..00000000000 --- a/packages-exp/auth-compat-exp/demo/public/service-worker.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Service worker for Firebase Auth test app application. The - * service worker caches all content and only serves cached content in offline - * mode. - */ - -importScripts('/dist/firebase-app.js'); -importScripts('/dist/firebase-auth.js'); -importScripts('config.js'); - -// Initialize the Firebase app in the web worker. -firebase.initializeApp(config); - -var CACHE_NAME = 'cache-v1'; -var urlsToCache = [ - '/', - '/manifest.json', - '/config.js', - '/script.js', - '/common.js', - '/style.css', - '/dist/firebase-app.js', - '/dist/firebase-auth.js', - '/dist/firebase-database.js' -]; - -/** - * Returns a promise that resolves with an ID token if available. - * @return {!Promise} The promise that resolves with an ID token if - * available. Otherwise, the promise resolves with null. - */ -var getIdToken = function () { - return new Promise(function (resolve, reject) { - firebase.auth().onAuthStateChanged(function (user) { - if (user) { - user.getIdToken().then( - function (idToken) { - resolve(idToken); - }, - function (error) { - resolve(null); - } - ); - } else { - resolve(null); - } - }); - }).catch(function (error) { - console.log(error); - }); -}; - -/** - * @param {string} url The URL whose origin is to be returned. - * @return {string} The origin corresponding to given URL. - */ -var getOriginFromUrl = function (url) { - // https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript - var pathArray = url.split('/'); - var protocol = pathArray[0]; - var host = pathArray[2]; - return protocol + '//' + host; -}; - -self.addEventListener('install', function (event) { - // Perform install steps. - event.waitUntil( - caches.open(CACHE_NAME).then(function (cache) { - // Add all URLs of resources we want to cache. - return cache.addAll(urlsToCache).catch(function (error) { - // Suppress error as some of the files may not be available for the - // current page. - }); - }) - ); -}); - -// As this is a test app, let's only return cached data when offline. -self.addEventListener('fetch', function (event) { - var fetchEvent = event; - var requestProcessor = function (idToken) { - var req = event.request; - // For same origin https requests, append idToken to header. - if ( - self.location.origin == getOriginFromUrl(event.request.url) && - (self.location.protocol == 'https:' || - self.location.hostname == 'localhost') && - idToken - ) { - // Clone headers as request headers are immutable. - var headers = new Headers(); - for (var entry of req.headers.entries()) { - headers.append(entry[0], entry[1]); - } - // Add ID token to header. We can't add to Authentication header as it - // will break HTTP basic authentication. - headers.append('x-id-token', idToken); - try { - req = new Request(req.url, { - method: req.method, - headers: headers, - mode: 'same-origin', - credentials: req.credentials, - cache: req.cache, - redirect: req.redirect, - referrer: req.referrer, - body: req.body, - bodyUsed: req.bodyUsed, - context: req.context - }); - } catch (e) { - // This will fail for CORS requests. We just continue with the - // fetch caching logic below and do not pass the ID token. - } - } - return fetch(req) - .then(function (response) { - // Check if we received a valid response. - // If not, just funnel the error response. - if (!response || response.status !== 200 || response.type !== 'basic') { - return response; - } - // If response is valid, clone it and save it to the cache. - var responseToCache = response.clone(); - // Save response to cache. - caches.open(CACHE_NAME).then(function (cache) { - cache.put(fetchEvent.request, responseToCache); - }); - // After caching, return response. - return response; - }) - .catch(function (error) { - // For fetch errors, attempt to retrieve the resource from cache. - return caches.match(fetchEvent.request.clone()); - }) - .catch(function (error) { - // If error getting resource from cache, do nothing. - console.log(error); - }); - }; - // Try to fetch the resource first after checking for the ID token. - event.respondWith(getIdToken().then(requestProcessor, requestProcessor)); -}); - -self.addEventListener('activate', function (event) { - // Update this list with all caches that need to remain cached. - var cacheWhitelist = ['cache-v1']; - event.waitUntil( - caches.keys().then(function (cacheNames) { - return Promise.all( - cacheNames.map(function (cacheName) { - // Check if cache is not whitelisted above. - if (cacheWhitelist.indexOf(cacheName) === -1) { - // If not whitelisted, delete it. - return caches.delete(cacheName); - } - }) - ); - }) - ); -}); diff --git a/packages-exp/auth-compat-exp/demo/public/style.css b/packages-exp/auth-compat-exp/demo/public/style.css deleted file mode 100644 index cdd999f8e8b..00000000000 --- a/packages-exp/auth-compat-exp/demo/public/style.css +++ /dev/null @@ -1,207 +0,0 @@ -/** - * Copyright 2017 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -body { - padding-top: 70px; -} -body.user-info-displayed { - padding-top: 120px; -} - -@media (min-width: 768px) { - body { - padding-bottom: 100px; - } -} -@media (max-width: 767px) { - body { - padding-bottom: 15px; - } -} - -#tab-menu { - margin-bottom: 15px; -} - -#user-info { - display: none; - padding: 10px 15px; - position: fixed; - top: 50px; - width: 100%; - z-index: 1000; -} - -#toggle-user-placeholder { - margin-bottom: 15px; -} - -#user-info.current-user, -#toggle-user-placeholder .current-user, -#toggle-user label.active.current-user { - background-color: #d6e9c6; - border: 1px solid #dff0d8; - color: #3c763d; -} - -#user-info.last-user, -#toggle-user label.active.last-user { - background-color: #d9edf7; - border: 1px solid #bce8f1; - color: #31708f; -} - -#recaptcha-container { - border: 5px solid red; - bottom: 0; - position: fixed; - right: 0; - z-index: 1500; -} - -#recaptcha-container:empty { - border: none; -} - -.logs { - color: #555; - font-family: 'Courier New', Courier; - font-size: 0.9em; - word-wrap: break-word; -} - -.logs > .error { - color: #d9534f; -} - -/* Margin top for small screens when the logs are below the buttons */ -@media (max-width: 767px) { - .logs { - margin-top: 20px; - } -} - -.overlaying-alert { - bottom: 15px; - pointer-events: none; - position: fixed; - width: 100%; - word-wrap: break-word; - z-index: 1010; -} - -.actions { - margin-bottom: 15px; -} - -.group { - border-top: 1px solid #555; - color: #555; - font-size: 0.9em; - font-weight: bold; - letter-spacing: 0.2em; - margin-bottom: 15px; - padding: 2px 0 0 10px; -} - -button + .group, -div + .group, -.btn-block + .group, -.form + .group { - margin-top: 30px; -} - -.form { - text-align: center; -} - -.form-bordered { - border: 1px solid #CCC; - border-radius: 9px; - padding: 5px; -} - -button + .form, -input + .form, -.form + .form { - margin: 15px 0; -} - -.form + button, -.form-control + .btn-block, -.form-control + .form-control { - margin-top: 5px; -} - -/* Bootstrap .hidden adds the !important which invalides jQuery .show() */ -.hidden, -.hide, -.profile, -.overlaying-alert > .alert, -#toggle-user { - display: none; -} - -.profile { - line-height: 30px; -} - -@media (max-width: 767px) { - .profile { - /* Use a smaller line height for small screens so user information doesn't - take up half the screen. */ - line-height: normal; - } -} - -.profile-uid { - font-family: 'Courier New', Courier; -} - -.profile-image { - float: left; - height: 30px; - margin-right: 10px; -} - -.profile-email-not-verified { - color: #d9534f; -} - -.profile-providers { - color: #333; -} - -.profile-providers > i { - margin: 0 5px; -} - -.radio-block { - margin-bottom: 15px; - width: 100%; -} - -.radio-block > label { - width: 50%; -} - -/** Overrides default drop down menu styles for enrolled factors display. */ -.enrolled-second-factors { - left: initial; - min-width: 400px; - right: 0px; - width: 100%; -} diff --git a/packages-exp/auth-compat-exp/demo/public/web-worker.js b/packages-exp/auth-compat-exp/demo/public/web-worker.js deleted file mode 100644 index 9ce860cf0dc..00000000000 --- a/packages-exp/auth-compat-exp/demo/public/web-worker.js +++ /dev/null @@ -1,210 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Web worker for Firebase Auth test app application. The - * web worker tries to run operations on the Auth instance for testing purposes. - */ - -importScripts('/dist/firebase-app.js'); -importScripts('/dist/firebase-auth.js'); -importScripts('config.js'); - -// Initialize the Firebase app in the web worker. -firebase.initializeApp(config); - -/** - * Returns a promise that resolves with an ID token if available. - * @return {!Promise} The promise that resolves with an ID token if - * available. Otherwise, the promise resolves with null. - */ -var getIdToken = function () { - return new Promise(function (resolve, reject) { - firebase.auth().onAuthStateChanged(function (user) { - if (user) { - user.getIdToken().then( - function (idToken) { - resolve(idToken); - }, - function (error) { - resolve(null); - } - ); - } else { - resolve(null); - } - }); - }).catch(function (error) { - console.log(error); - }); -}; - -/** - * Runs various Firebase Auth tests in a web worker environment and confirms the - * expected behavior. This is useful for manual testing in different browsers. - * @param {string} googleIdToken The Google ID token to sign in with. - * @return {!Promise} A promise that resolves when all tests run - * successfully. - */ -var runWorkerTests = function (googleIdToken) { - var inMemoryPersistence = firebase.auth.Auth.Persistence.NONE; - var expectedDisplayName = 'Test User'; - var oauthCredential = firebase.auth.GoogleAuthProvider.credential( - googleIdToken - ); - var provider = new firebase.auth.GoogleAuthProvider(); - var OPERATION_NOT_SUPPORTED_CODE = - 'auth/operation-not-supported-in-this-environment'; - var email = - 'user' + - Math.floor(Math.random() * 10000000000).toString() + - '@example.com'; - var pass = 'password'; - return firebase - .auth() - .setPersistence(inMemoryPersistence) - .then(function () { - firebase.auth().useDeviceLanguage(); - return firebase.auth().signInAnonymously(); - }) - .then(function (result) { - if (!result.user.uid) { - throw new Error('signInAnonymously unexpectedly failed!'); - } - return result.user.updateProfile({ displayName: expectedDisplayName }); - }) - .then(function () { - if (firebase.auth().currentUser.displayName != expectedDisplayName) { - throw new Error('Profile update failed!'); - } - return firebase.auth().currentUser.delete(); - }) - .then(function () { - if (firebase.auth().currentUser) { - throw new Error('currentUser.delete unexpectedly failed!'); - } - return firebase.auth().createUserWithEmailAndPassword(email, pass); - }) - .then(function (result) { - if (result.user.email != email) { - throw new Error('createUserWithEmailAndPassword unexpectedly failed!'); - } - return firebase.auth().fetchProvidersForEmail(email); - }) - .then(function (providers) { - if (providers.length == 0 || providers[0] != 'password') { - throw new Error('fetchProvidersForEmail failed!'); - } - return firebase.auth().signInWithEmailAndPassword(email, pass); - }) - .then(function (result) { - if (result.user.email != email) { - throw new Error('signInWithEmailAndPassword unexpectedly failed!'); - } - return result.user.delete(); - }) - .then(function () { - return firebase - .auth() - .signInWithPopup(provider) - .catch(function (error) { - if (error.code != OPERATION_NOT_SUPPORTED_CODE) { - throw error; - } - }); - }) - .then(function () { - return firebase - .auth() - .signInWithRedirect(provider) - .catch(function (error) { - if (error.code != OPERATION_NOT_SUPPORTED_CODE) { - throw error; - } - }); - }) - .then(function () { - return Promise.resolve() - .then(function () { - return new firebase.auth.RecaptchaVerifier('id'); - }) - .then(function () { - throw new Error( - 'RecaptchaVerifer instantiation succeeded unexpectedly!' - ); - }) - .catch(function (error) { - if (error.code != OPERATION_NOT_SUPPORTED_CODE) { - throw error; - } - }); - }) - .then(function () { - return firebase.auth().signInWithCredential(oauthCredential); - }) - .then(function (result) { - if ( - !result.user || - !result.user.uid || - !result.credential || - !result.additionalUserInfo - ) { - throw new Error('signInWithCredential unexpectedly failed!'); - } - return firebase.auth().signOut(); - }) - .then(function () { - if (firebase.auth().currentUser) { - throw new Error('signOut unexpectedly failed!'); - } - }); -}; - -/** - * Handles the incoming message from the main script. - * @param {!Object} e The message event received. - */ -self.onmessage = function (e) { - if (e.data && e.data.type) { - var result = { type: e.data.type }; - switch (e.data.type) { - case 'GET_USER_INFO': - getIdToken().then(function (idToken) { - result.idToken = idToken; - result.uid = - firebase.auth().currentUser && firebase.auth().currentUser.uid; - self.postMessage(result); - }); - break; - case 'RUN_TESTS': - runWorkerTests(e.data.googleIdToken) - .then(function () { - result.status = 'success'; - self.postMessage(result); - }) - .catch(function (error) { - result.status = 'failure'; - // DataCloneError when postMessaging in IE11 and 10. - result.error = error.code ? error : error.message; - self.postMessage(result); - }); - break; - default: - self.postMessage({}); - } - } -}; diff --git a/packages-exp/auth-compat-exp/demo/rollup.config.js b/packages-exp/auth-compat-exp/demo/rollup.config.js deleted file mode 100644 index 0316182cc7f..00000000000 --- a/packages-exp/auth-compat-exp/demo/rollup.config.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -import resolveModule from '@rollup/plugin-node-resolve'; -import sourcemaps from 'rollup-plugin-sourcemaps'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; - -const plugins = [ - sourcemaps(), - resolveModule(), - typescriptPlugin({ - typescript, - include: ['../**/*.ts'] - }), - json(), - commonjs() -]; - -/** - * Individual Component Builds - */ -const umdBuilds = [ - /** - * App UMD Builds - */ - { - input: '../../firebase-exp/compat/app/index.ts', - output: { - file: 'public/dist/firebase-app.js', - sourcemap: true, - format: 'umd', - name: 'firebase' - }, - plugins: [...plugins] - }, - { - input: `../index.ts`, - output: { - file: `public/dist/firebase-auth.js`, - format: 'umd', - sourcemap: true, - extend: true, - name: 'firebase', - globals: { - '@firebase/app-compat': 'firebase', - '@firebase/app-exp': 'firebase.INTERNAL.modularAPIs' - }, - /** - * use iife to avoid below error in the old Safari browser - * SyntaxError: Functions cannot be declared in a nested block in strict mode - * https://github.com/firebase/firebase-js-sdk/issues/1228 - * - */ - intro: ` - try { - (function() {`, - outro: ` - }).apply(this, arguments); - } catch(err) { - console.error(err); - throw new Error( - 'Cannot instantiate firebase-auth.js - ' + - 'be sure to load firebase-app.js first.' - ); - }` - }, - plugins: [...plugins], - external: ['@firebase/app-compat', '@firebase/app-exp'] - } -]; - -export default [...umdBuilds]; diff --git a/packages-exp/auth-compat-exp/demo/yarn.lock b/packages-exp/auth-compat-exp/demo/yarn.lock deleted file mode 100644 index 660a62aa5cb..00000000000 --- a/packages-exp/auth-compat-exp/demo/yarn.lock +++ /dev/null @@ -1,5427 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@evocateur/libnpmaccess@^3.1.2": - version "3.1.2" - resolved "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz#ecf7f6ce6b004e9f942b098d92200be4a4b1c845" - integrity sha512-KSCAHwNWro0CF2ukxufCitT9K5LjL/KuMmNzSu8wuwN2rjyKHD8+cmOsiybK+W5hdnwc5M1SmRlVCaMHQo+3rg== - dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - aproba "^2.0.0" - figgy-pudding "^3.5.1" - get-stream "^4.0.0" - npm-package-arg "^6.1.0" - -"@evocateur/libnpmpublish@^1.2.2": - version "1.2.2" - resolved "https://registry.npmjs.org/@evocateur/libnpmpublish/-/libnpmpublish-1.2.2.tgz#55df09d2dca136afba9c88c759ca272198db9f1a" - integrity sha512-MJrrk9ct1FeY9zRlyeoyMieBjGDG9ihyyD9/Ft6MMrTxql9NyoEx2hw9casTIP4CdqEVu+3nQ2nXxoJ8RCXyFg== - dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - aproba "^2.0.0" - figgy-pudding "^3.5.1" - get-stream "^4.0.0" - lodash.clonedeep "^4.5.0" - normalize-package-data "^2.4.0" - npm-package-arg "^6.1.0" - semver "^5.5.1" - ssri "^6.0.1" - -"@evocateur/npm-registry-fetch@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@evocateur/npm-registry-fetch/-/npm-registry-fetch-4.0.0.tgz#8c4c38766d8d32d3200fcb0a83f064b57365ed66" - integrity sha512-k1WGfKRQyhJpIr+P17O5vLIo2ko1PFLKwoetatdduUSt/aQ4J2sJrJwwatdI5Z3SiYk/mRH9S3JpdmMFd/IK4g== - dependencies: - JSONStream "^1.3.4" - bluebird "^3.5.1" - figgy-pudding "^3.4.1" - lru-cache "^5.1.1" - make-fetch-happen "^5.0.0" - npm-package-arg "^6.1.0" - safe-buffer "^5.1.2" - -"@evocateur/pacote@^9.6.3": - version "9.6.5" - resolved "https://registry.npmjs.org/@evocateur/pacote/-/pacote-9.6.5.tgz#33de32ba210b6f17c20ebab4d497efc6755f4ae5" - integrity sha512-EI552lf0aG2nOV8NnZpTxNo2PcXKPmDbF9K8eCBFQdIZwHNGN/mi815fxtmUMa2wTa1yndotICIDt/V0vpEx2w== - dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - bluebird "^3.5.3" - cacache "^12.0.3" - chownr "^1.1.2" - figgy-pudding "^3.5.1" - get-stream "^4.1.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^5.1.1" - make-fetch-happen "^5.0.0" - minimatch "^3.0.4" - minipass "^2.3.5" - mississippi "^3.0.0" - mkdirp "^0.5.1" - normalize-package-data "^2.5.0" - npm-package-arg "^6.1.0" - npm-packlist "^1.4.4" - npm-pick-manifest "^3.0.0" - osenv "^0.1.5" - promise-inflight "^1.0.1" - promise-retry "^1.1.1" - protoduck "^5.0.1" - rimraf "^2.6.3" - safe-buffer "^5.2.0" - semver "^5.7.0" - ssri "^6.0.1" - tar "^4.4.10" - unique-filename "^1.1.1" - which "^1.3.1" - -"@firebase/logger@^0.2.2": - version "0.2.6" - resolved "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz#3aa2ca4fe10327cabf7808bd3994e88db26d7989" - integrity sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw== - -"@firebase/util@^0.3.0": - version "0.3.2" - resolved "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz#87de27f9cffc2324651cabf6ec133d0a9eb21b52" - integrity sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g== - dependencies: - tslib "^1.11.1" - -"@lerna/add@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b" - integrity sha512-vhUXXF6SpufBE1EkNEXwz1VLW03f177G9uMOFMQkp6OJ30/PWg4Ekifuz9/3YfgB2/GH8Tu4Lk3O51P2Hskg/A== - dependencies: - "@evocateur/pacote" "^9.6.3" - "@lerna/bootstrap" "3.21.0" - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/npm-conf" "3.16.0" - "@lerna/validation-error" "3.13.0" - dedent "^0.7.0" - npm-package-arg "^6.1.0" - p-map "^2.1.0" - semver "^6.2.0" - -"@lerna/bootstrap@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.21.0.tgz#bcd1b651be5b0970b20d8fae04c864548123aed6" - integrity sha512-mtNHlXpmvJn6JTu0KcuTTPl2jLsDNud0QacV/h++qsaKbhAaJr/FElNZ5s7MwZFUM3XaDmvWzHKaszeBMHIbBw== - dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/has-npm-version" "3.16.5" - "@lerna/npm-install" "3.16.5" - "@lerna/package-graph" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/rimraf-dir" "3.16.5" - "@lerna/run-lifecycle" "3.16.2" - "@lerna/run-topologically" "3.18.5" - "@lerna/symlink-binary" "3.17.0" - "@lerna/symlink-dependencies" "3.17.0" - "@lerna/validation-error" "3.13.0" - dedent "^0.7.0" - get-port "^4.2.0" - multimatch "^3.0.0" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - p-finally "^1.0.0" - p-map "^2.1.0" - p-map-series "^1.0.0" - p-waterfall "^1.0.0" - read-package-tree "^5.1.6" - semver "^6.2.0" - -"@lerna/changed@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/changed/-/changed-3.21.0.tgz#108e15f679bfe077af500f58248c634f1044ea0b" - integrity sha512-hzqoyf8MSHVjZp0gfJ7G8jaz+++mgXYiNs9iViQGA8JlN/dnWLI5sWDptEH3/B30Izo+fdVz0S0s7ydVE3pWIw== - dependencies: - "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.21.0" - "@lerna/listable" "3.18.5" - "@lerna/output" "3.13.0" - -"@lerna/check-working-tree@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.16.5.tgz#b4f8ae61bb4523561dfb9f8f8d874dd46bb44baa" - integrity sha512-xWjVBcuhvB8+UmCSb5tKVLB5OuzSpw96WEhS2uz6hkWVa/Euh1A0/HJwn2cemyK47wUrCQXtczBUiqnq9yX5VQ== - dependencies: - "@lerna/collect-uncommitted" "3.16.5" - "@lerna/describe-ref" "3.16.5" - "@lerna/validation-error" "3.13.0" - -"@lerna/child-process@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.16.5.tgz#38fa3c18064aa4ac0754ad80114776a7b36a69b2" - integrity sha512-vdcI7mzei9ERRV4oO8Y1LHBZ3A5+ampRKg1wq5nutLsUA4mEBN6H7JqjWOMY9xZemv6+kATm2ofjJ3lW5TszQg== - dependencies: - chalk "^2.3.1" - execa "^1.0.0" - strong-log-transformer "^2.0.0" - -"@lerna/clean@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/clean/-/clean-3.21.0.tgz#c0b46b5300cc3dae2cda3bec14b803082da3856d" - integrity sha512-b/L9l+MDgE/7oGbrav6rG8RTQvRiZLO1zTcG17zgJAAuhlsPxJExMlh2DFwJEVi2les70vMhHfST3Ue1IMMjpg== - dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/prompt" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/rimraf-dir" "3.16.5" - p-map "^2.1.0" - p-map-series "^1.0.0" - p-waterfall "^1.0.0" - -"@lerna/cli@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/cli/-/cli-3.18.5.tgz#c90c461542fcd35b6d5b015a290fb0dbfb41d242" - integrity sha512-erkbxkj9jfc89vVs/jBLY/fM0I80oLmJkFUV3Q3wk9J3miYhP14zgVEBsPZY68IZlEjT6T3Xlq2xO1AVaatHsA== - dependencies: - "@lerna/global-options" "3.13.0" - dedent "^0.7.0" - npmlog "^4.1.2" - yargs "^14.2.2" - -"@lerna/collect-uncommitted@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-3.16.5.tgz#a494d61aac31cdc7aec4bbe52c96550274132e63" - integrity sha512-ZgqnGwpDZiWyzIQVZtQaj9tRizsL4dUOhuOStWgTAw1EMe47cvAY2kL709DzxFhjr6JpJSjXV5rZEAeU3VE0Hg== - dependencies: - "@lerna/child-process" "3.16.5" - chalk "^2.3.1" - figgy-pudding "^3.5.1" - npmlog "^4.1.2" - -"@lerna/collect-updates@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.20.0.tgz#62f9d76ba21a25b7d9fbf31c02de88744a564bd1" - integrity sha512-qBTVT5g4fupVhBFuY4nI/3FSJtQVcDh7/gEPOpRxoXB/yCSnT38MFHXWl+y4einLciCjt/+0x6/4AG80fjay2Q== - dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/describe-ref" "3.16.5" - minimatch "^3.0.4" - npmlog "^4.1.2" - slash "^2.0.0" - -"@lerna/command@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/command/-/command-3.21.0.tgz#9a2383759dc7b700dacfa8a22b2f3a6e190121f7" - integrity sha512-T2bu6R8R3KkH5YoCKdutKv123iUgUbW8efVjdGCDnCMthAQzoentOJfDeodBwn0P2OqCl3ohsiNVtSn9h78fyQ== - dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/package-graph" "3.18.5" - "@lerna/project" "3.21.0" - "@lerna/validation-error" "3.13.0" - "@lerna/write-log-file" "3.13.0" - clone-deep "^4.0.1" - dedent "^0.7.0" - execa "^1.0.0" - is-ci "^2.0.0" - npmlog "^4.1.2" - -"@lerna/conventional-commits@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.22.0.tgz#2798f4881ee2ef457bdae027ab7d0bf0af6f1e09" - integrity sha512-z4ZZk1e8Mhz7+IS8NxHr64wyklHctCJyWpJKEZZPJiLFJ8yKto/x38O80R10pIzC0rr8Sy/OsjSH4bl0TbbgqA== - dependencies: - "@lerna/validation-error" "3.13.0" - conventional-changelog-angular "^5.0.3" - conventional-changelog-core "^3.1.6" - conventional-recommended-bump "^5.0.0" - fs-extra "^8.1.0" - get-stream "^4.0.0" - lodash.template "^4.5.0" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - pify "^4.0.1" - semver "^6.2.0" - -"@lerna/create-symlink@3.16.2": - version "3.16.2" - resolved "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-3.16.2.tgz#412cb8e59a72f5a7d9463e4e4721ad2070149967" - integrity sha512-pzXIJp6av15P325sgiIRpsPXLFmkisLhMBCy4764d+7yjf2bzrJ4gkWVMhsv4AdF0NN3OyZ5jjzzTtLNqfR+Jw== - dependencies: - "@zkochan/cmd-shim" "^3.1.0" - fs-extra "^8.1.0" - npmlog "^4.1.2" - -"@lerna/create@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/create/-/create-3.22.0.tgz#d6bbd037c3dc5b425fe5f6d1b817057c278f7619" - integrity sha512-MdiQQzCcB4E9fBF1TyMOaAEz9lUjIHp1Ju9H7f3lXze5JK6Fl5NYkouAvsLgY6YSIhXMY8AHW2zzXeBDY4yWkw== - dependencies: - "@evocateur/pacote" "^9.6.3" - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/npm-conf" "3.16.0" - "@lerna/validation-error" "3.13.0" - camelcase "^5.0.0" - dedent "^0.7.0" - fs-extra "^8.1.0" - globby "^9.2.0" - init-package-json "^1.10.3" - npm-package-arg "^6.1.0" - p-reduce "^1.0.0" - pify "^4.0.1" - semver "^6.2.0" - slash "^2.0.0" - validate-npm-package-license "^3.0.3" - validate-npm-package-name "^3.0.0" - whatwg-url "^7.0.0" - -"@lerna/describe-ref@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.16.5.tgz#a338c25aaed837d3dc70b8a72c447c5c66346ac0" - integrity sha512-c01+4gUF0saOOtDBzbLMFOTJDHTKbDFNErEY6q6i9QaXuzy9LNN62z+Hw4acAAZuJQhrVWncVathcmkkjvSVGw== - dependencies: - "@lerna/child-process" "3.16.5" - npmlog "^4.1.2" - -"@lerna/diff@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/diff/-/diff-3.21.0.tgz#e6df0d8b9916167ff5a49fcb02ac06424280a68d" - integrity sha512-5viTR33QV3S7O+bjruo1SaR40m7F2aUHJaDAC7fL9Ca6xji+aw1KFkpCtVlISS0G8vikUREGMJh+c/VMSc8Usw== - dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/validation-error" "3.13.0" - npmlog "^4.1.2" - -"@lerna/exec@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/exec/-/exec-3.21.0.tgz#17f07533893cb918a17b41bcc566dc437016db26" - integrity sha512-iLvDBrIE6rpdd4GIKTY9mkXyhwsJ2RvQdB9ZU+/NhR3okXfqKc6py/24tV111jqpXTtZUW6HNydT4dMao2hi1Q== - dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/profiler" "3.20.0" - "@lerna/run-topologically" "3.18.5" - "@lerna/validation-error" "3.13.0" - p-map "^2.1.0" - -"@lerna/filter-options@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.20.0.tgz#0f0f5d5a4783856eece4204708cc902cbc8af59b" - integrity sha512-bmcHtvxn7SIl/R9gpiNMVG7yjx7WyT0HSGw34YVZ9B+3xF/83N3r5Rgtjh4hheLZ+Q91Or0Jyu5O3Nr+AwZe2g== - dependencies: - "@lerna/collect-updates" "3.20.0" - "@lerna/filter-packages" "3.18.0" - dedent "^0.7.0" - figgy-pudding "^3.5.1" - npmlog "^4.1.2" - -"@lerna/filter-packages@3.18.0": - version "3.18.0" - resolved "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.18.0.tgz#6a7a376d285208db03a82958cfb8172e179b4e70" - integrity sha512-6/0pMM04bCHNATIOkouuYmPg6KH3VkPCIgTfQmdkPJTullERyEQfNUKikrefjxo1vHOoCACDpy65JYyKiAbdwQ== - dependencies: - "@lerna/validation-error" "3.13.0" - multimatch "^3.0.0" - npmlog "^4.1.2" - -"@lerna/get-npm-exec-opts@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.13.0.tgz#d1b552cb0088199fc3e7e126f914e39a08df9ea5" - integrity sha512-Y0xWL0rg3boVyJk6An/vurKzubyJKtrxYv2sj4bB8Mc5zZ3tqtv0ccbOkmkXKqbzvNNF7VeUt1OJ3DRgtC/QZw== - dependencies: - npmlog "^4.1.2" - -"@lerna/get-packed@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-3.16.0.tgz#1b316b706dcee86c7baa55e50b087959447852ff" - integrity sha512-AjsFiaJzo1GCPnJUJZiTW6J1EihrPkc2y3nMu6m3uWFxoleklsSCyImumzVZJssxMi3CPpztj8LmADLedl9kXw== - dependencies: - fs-extra "^8.1.0" - ssri "^6.0.1" - tar "^4.4.8" - -"@lerna/github-client@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.22.0.tgz#5d816aa4f76747ed736ae64ff962b8f15c354d95" - integrity sha512-O/GwPW+Gzr3Eb5bk+nTzTJ3uv+jh5jGho9BOqKlajXaOkMYGBELEAqV5+uARNGWZFvYAiF4PgqHb6aCUu7XdXg== - dependencies: - "@lerna/child-process" "3.16.5" - "@octokit/plugin-enterprise-rest" "^6.0.1" - "@octokit/rest" "^16.28.4" - git-url-parse "^11.1.2" - npmlog "^4.1.2" - -"@lerna/gitlab-client@3.15.0": - version "3.15.0" - resolved "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-3.15.0.tgz#91f4ec8c697b5ac57f7f25bd50fe659d24aa96a6" - integrity sha512-OsBvRSejHXUBMgwWQqNoioB8sgzL/Pf1pOUhHKtkiMl6aAWjklaaq5HPMvTIsZPfS6DJ9L5OK2GGZuooP/5c8Q== - dependencies: - node-fetch "^2.5.0" - npmlog "^4.1.2" - whatwg-url "^7.0.0" - -"@lerna/global-options@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.13.0.tgz#217662290db06ad9cf2c49d8e3100ee28eaebae1" - integrity sha512-SlZvh1gVRRzYLVluz9fryY1nJpZ0FHDGB66U9tFfvnnxmueckRQxLopn3tXj3NU1kc3QANT2I5BsQkOqZ4TEFQ== - -"@lerna/has-npm-version@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.16.5.tgz#ab83956f211d8923ea6afe9b979b38cc73b15326" - integrity sha512-WL7LycR9bkftyqbYop5rEGJ9sRFIV55tSGmbN1HLrF9idwOCD7CLrT64t235t3t4O5gehDnwKI5h2U3oxTrF8Q== - dependencies: - "@lerna/child-process" "3.16.5" - semver "^6.2.0" - -"@lerna/import@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/import/-/import-3.22.0.tgz#1a5f0394f38e23c4f642a123e5e1517e70d068d2" - integrity sha512-uWOlexasM5XR6tXi4YehODtH9Y3OZrFht3mGUFFT3OIl2s+V85xIGFfqFGMTipMPAGb2oF1UBLL48kR43hRsOg== - dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/prompt" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/validation-error" "3.13.0" - dedent "^0.7.0" - fs-extra "^8.1.0" - p-map-series "^1.0.0" - -"@lerna/info@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/info/-/info-3.21.0.tgz#76696b676fdb0f35d48c83c63c1e32bb5e37814f" - integrity sha512-0XDqGYVBgWxUquFaIptW2bYSIu6jOs1BtkvRTWDDhw4zyEdp6q4eaMvqdSap1CG+7wM5jeLCi6z94wS0AuiuwA== - dependencies: - "@lerna/command" "3.21.0" - "@lerna/output" "3.13.0" - envinfo "^7.3.1" - -"@lerna/init@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/init/-/init-3.21.0.tgz#1e810934dc8bf4e5386c031041881d3b4096aa5c" - integrity sha512-6CM0z+EFUkFfurwdJCR+LQQF6MqHbYDCBPyhu/d086LRf58GtYZYj49J8mKG9ktayp/TOIxL/pKKjgLD8QBPOg== - dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - fs-extra "^8.1.0" - p-map "^2.1.0" - write-json-file "^3.2.0" - -"@lerna/link@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/link/-/link-3.21.0.tgz#8be68ff0ccee104b174b5bbd606302c2f06e9d9b" - integrity sha512-tGu9GxrX7Ivs+Wl3w1+jrLi1nQ36kNI32dcOssij6bg0oZ2M2MDEFI9UF2gmoypTaN9uO5TSsjCFS7aR79HbdQ== - dependencies: - "@lerna/command" "3.21.0" - "@lerna/package-graph" "3.18.5" - "@lerna/symlink-dependencies" "3.17.0" - p-map "^2.1.0" - slash "^2.0.0" - -"@lerna/list@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/list/-/list-3.21.0.tgz#42f76fafa56dea13b691ec8cab13832691d61da2" - integrity sha512-KehRjE83B1VaAbRRkRy6jLX1Cin8ltsrQ7FHf2bhwhRHK0S54YuA6LOoBnY/NtA8bHDX/Z+G5sMY78X30NS9tg== - dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/listable" "3.18.5" - "@lerna/output" "3.13.0" - -"@lerna/listable@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/listable/-/listable-3.18.5.tgz#e82798405b5ed8fc51843c8ef1e7a0e497388a1a" - integrity sha512-Sdr3pVyaEv5A7ZkGGYR7zN+tTl2iDcinryBPvtuv20VJrXBE8wYcOks1edBTcOWsPjCE/rMP4bo1pseyk3UTsg== - dependencies: - "@lerna/query-graph" "3.18.5" - chalk "^2.3.1" - columnify "^1.5.4" - -"@lerna/log-packed@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-3.16.0.tgz#f83991041ee77b2495634e14470b42259fd2bc16" - integrity sha512-Fp+McSNBV/P2mnLUYTaSlG8GSmpXM7krKWcllqElGxvAqv6chk2K3c2k80MeVB4WvJ9tRjUUf+i7HUTiQ9/ckQ== - dependencies: - byte-size "^5.0.1" - columnify "^1.5.4" - has-unicode "^2.0.1" - npmlog "^4.1.2" - -"@lerna/npm-conf@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-3.16.0.tgz#1c10a89ae2f6c2ee96962557738685300d376827" - integrity sha512-HbO3DUrTkCAn2iQ9+FF/eisDpWY5POQAOF1m7q//CZjdC2HSW3UYbKEGsSisFxSfaF9Z4jtrV+F/wX6qWs3CuA== - dependencies: - config-chain "^1.1.11" - pify "^4.0.1" - -"@lerna/npm-dist-tag@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.18.5.tgz#9ef9abb7c104077b31f6fab22cc73b314d54ac55" - integrity sha512-xw0HDoIG6HreVsJND9/dGls1c+lf6vhu7yJoo56Sz5bvncTloYGLUppIfDHQr4ZvmPCK8rsh0euCVh2giPxzKQ== - dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - "@lerna/otplease" "3.18.5" - figgy-pudding "^3.5.1" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - -"@lerna/npm-install@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.16.5.tgz#d6bfdc16f81285da66515ae47924d6e278d637d3" - integrity sha512-hfiKk8Eku6rB9uApqsalHHTHY+mOrrHeWEs+gtg7+meQZMTS3kzv4oVp5cBZigndQr3knTLjwthT/FX4KvseFg== - dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/get-npm-exec-opts" "3.13.0" - fs-extra "^8.1.0" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - signal-exit "^3.0.2" - write-pkg "^3.1.0" - -"@lerna/npm-publish@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.18.5.tgz#240e4039959fd9816b49c5b07421e11b5cb000af" - integrity sha512-3etLT9+2L8JAx5F8uf7qp6iAtOLSMj+ZYWY6oUgozPi/uLqU0/gsMsEXh3F0+YVW33q0M61RpduBoAlOOZnaTg== - dependencies: - "@evocateur/libnpmpublish" "^1.2.2" - "@lerna/otplease" "3.18.5" - "@lerna/run-lifecycle" "3.16.2" - figgy-pudding "^3.5.1" - fs-extra "^8.1.0" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - pify "^4.0.1" - read-package-json "^2.0.13" - -"@lerna/npm-run-script@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.16.5.tgz#9c2ec82453a26c0b46edc0bb7c15816c821f5c15" - integrity sha512-1asRi+LjmVn3pMjEdpqKJZFT/3ZNpb+VVeJMwrJaV/3DivdNg7XlPK9LTrORuKU4PSvhdEZvJmSlxCKyDpiXsQ== - dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/get-npm-exec-opts" "3.13.0" - npmlog "^4.1.2" - -"@lerna/otplease@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/otplease/-/otplease-3.18.5.tgz#b77b8e760b40abad9f7658d988f3ea77d4fd0231" - integrity sha512-S+SldXAbcXTEDhzdxYLU0ZBKuYyURP/ND2/dK6IpKgLxQYh/z4ScljPDMyKymmEvgiEJmBsPZAAPfmNPEzxjog== - dependencies: - "@lerna/prompt" "3.18.5" - figgy-pudding "^3.5.1" - -"@lerna/output@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/output/-/output-3.13.0.tgz#3ded7cc908b27a9872228a630d950aedae7a4989" - integrity sha512-7ZnQ9nvUDu/WD+bNsypmPG5MwZBwu86iRoiW6C1WBuXXDxM5cnIAC1m2WxHeFnjyMrYlRXM9PzOQ9VDD+C15Rg== - dependencies: - npmlog "^4.1.2" - -"@lerna/pack-directory@3.16.4": - version "3.16.4" - resolved "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-3.16.4.tgz#3eae5f91bdf5acfe0384510ed53faddc4c074693" - integrity sha512-uxSF0HZeGyKaaVHz5FroDY9A5NDDiCibrbYR6+khmrhZtY0Bgn6hWq8Gswl9iIlymA+VzCbshWIMX4o2O8C8ng== - dependencies: - "@lerna/get-packed" "3.16.0" - "@lerna/package" "3.16.0" - "@lerna/run-lifecycle" "3.16.2" - figgy-pudding "^3.5.1" - npm-packlist "^1.4.4" - npmlog "^4.1.2" - tar "^4.4.10" - temp-write "^3.4.0" - -"@lerna/package-graph@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.18.5.tgz#c740e2ea3578d059e551633e950690831b941f6b" - integrity sha512-8QDrR9T+dBegjeLr+n9WZTVxUYUhIUjUgZ0gvNxUBN8S1WB9r6H5Yk56/MVaB64tA3oGAN9IIxX6w0WvTfFudA== - dependencies: - "@lerna/prerelease-id-from-version" "3.16.0" - "@lerna/validation-error" "3.13.0" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - semver "^6.2.0" - -"@lerna/package@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/package/-/package-3.16.0.tgz#7e0a46e4697ed8b8a9c14d59c7f890e0d38ba13c" - integrity sha512-2lHBWpaxcBoiNVbtyLtPUuTYEaB/Z+eEqRS9duxpZs6D+mTTZMNy6/5vpEVSCBmzvdYpyqhqaYjjSLvjjr5Riw== - dependencies: - load-json-file "^5.3.0" - npm-package-arg "^6.1.0" - write-pkg "^3.1.0" - -"@lerna/prerelease-id-from-version@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-3.16.0.tgz#b24bfa789f5e1baab914d7b08baae9b7bd7d83a1" - integrity sha512-qZyeUyrE59uOK8rKdGn7jQz+9uOpAaF/3hbslJVFL1NqF9ELDTqjCPXivuejMX/lN4OgD6BugTO4cR7UTq/sZA== - dependencies: - semver "^6.2.0" - -"@lerna/profiler@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/profiler/-/profiler-3.20.0.tgz#0f6dc236f4ea8f9ea5f358c6703305a4f32ad051" - integrity sha512-bh8hKxAlm6yu8WEOvbLENm42i2v9SsR4WbrCWSbsmOElx3foRnMlYk7NkGECa+U5c3K4C6GeBbwgqs54PP7Ljg== - dependencies: - figgy-pudding "^3.5.1" - fs-extra "^8.1.0" - npmlog "^4.1.2" - upath "^1.2.0" - -"@lerna/project@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/project/-/project-3.21.0.tgz#5d784d2d10c561a00f20320bcdb040997c10502d" - integrity sha512-xT1mrpET2BF11CY32uypV2GPtPVm6Hgtha7D81GQP9iAitk9EccrdNjYGt5UBYASl4CIDXBRxwmTTVGfrCx82A== - dependencies: - "@lerna/package" "3.16.0" - "@lerna/validation-error" "3.13.0" - cosmiconfig "^5.1.0" - dedent "^0.7.0" - dot-prop "^4.2.0" - glob-parent "^5.0.0" - globby "^9.2.0" - load-json-file "^5.3.0" - npmlog "^4.1.2" - p-map "^2.1.0" - resolve-from "^4.0.0" - write-json-file "^3.2.0" - -"@lerna/prompt@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.18.5.tgz#628cd545f225887d060491ab95df899cfc5218a1" - integrity sha512-rkKj4nm1twSbBEb69+Em/2jAERK8htUuV8/xSjN0NPC+6UjzAwY52/x9n5cfmpa9lyKf/uItp7chCI7eDmNTKQ== - dependencies: - inquirer "^6.2.0" - npmlog "^4.1.2" - -"@lerna/publish@3.22.1": - version "3.22.1" - resolved "https://registry.npmjs.org/@lerna/publish/-/publish-3.22.1.tgz#b4f7ce3fba1e9afb28be4a1f3d88222269ba9519" - integrity sha512-PG9CM9HUYDreb1FbJwFg90TCBQooGjj+n/pb3gw/eH5mEDq0p8wKdLFe0qkiqUkm/Ub5C8DbVFertIo0Vd0zcw== - dependencies: - "@evocateur/libnpmaccess" "^3.1.2" - "@evocateur/npm-registry-fetch" "^4.0.0" - "@evocateur/pacote" "^9.6.3" - "@lerna/check-working-tree" "3.16.5" - "@lerna/child-process" "3.16.5" - "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.21.0" - "@lerna/describe-ref" "3.16.5" - "@lerna/log-packed" "3.16.0" - "@lerna/npm-conf" "3.16.0" - "@lerna/npm-dist-tag" "3.18.5" - "@lerna/npm-publish" "3.18.5" - "@lerna/otplease" "3.18.5" - "@lerna/output" "3.13.0" - "@lerna/pack-directory" "3.16.4" - "@lerna/prerelease-id-from-version" "3.16.0" - "@lerna/prompt" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/run-lifecycle" "3.16.2" - "@lerna/run-topologically" "3.18.5" - "@lerna/validation-error" "3.13.0" - "@lerna/version" "3.22.1" - figgy-pudding "^3.5.1" - fs-extra "^8.1.0" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - p-finally "^1.0.0" - p-map "^2.1.0" - p-pipe "^1.2.0" - semver "^6.2.0" - -"@lerna/pulse-till-done@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-3.13.0.tgz#c8e9ce5bafaf10d930a67d7ed0ccb5d958fe0110" - integrity sha512-1SOHpy7ZNTPulzIbargrgaJX387csN7cF1cLOGZiJQA6VqnS5eWs2CIrG8i8wmaUavj2QlQ5oEbRMVVXSsGrzA== - dependencies: - npmlog "^4.1.2" - -"@lerna/query-graph@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-3.18.5.tgz#df4830bb5155273003bf35e8dda1c32d0927bd86" - integrity sha512-50Lf4uuMpMWvJ306be3oQDHrWV42nai9gbIVByPBYJuVW8dT8O8pA3EzitNYBUdLL9/qEVbrR0ry1HD7EXwtRA== - dependencies: - "@lerna/package-graph" "3.18.5" - figgy-pudding "^3.5.1" - -"@lerna/resolve-symlink@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-3.16.0.tgz#37fc7095fabdbcf317c26eb74e0d0bde8efd2386" - integrity sha512-Ibj5e7njVHNJ/NOqT4HlEgPFPtPLWsO7iu59AM5bJDcAJcR96mLZ7KGVIsS2tvaO7akMEJvt2P+ErwCdloG3jQ== - dependencies: - fs-extra "^8.1.0" - npmlog "^4.1.2" - read-cmd-shim "^1.0.1" - -"@lerna/rimraf-dir@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.16.5.tgz#04316ab5ffd2909657aaf388ea502cb8c2f20a09" - integrity sha512-bQlKmO0pXUsXoF8lOLknhyQjOZsCc0bosQDoX4lujBXSWxHVTg1VxURtWf2lUjz/ACsJVDfvHZbDm8kyBk5okA== - dependencies: - "@lerna/child-process" "3.16.5" - npmlog "^4.1.2" - path-exists "^3.0.0" - rimraf "^2.6.2" - -"@lerna/run-lifecycle@3.16.2": - version "3.16.2" - resolved "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-3.16.2.tgz#67b288f8ea964db9ea4fb1fbc7715d5bbb0bce00" - integrity sha512-RqFoznE8rDpyyF0rOJy3+KjZCeTkO8y/OB9orPauR7G2xQ7PTdCpgo7EO6ZNdz3Al+k1BydClZz/j78gNCmL2A== - dependencies: - "@lerna/npm-conf" "3.16.0" - figgy-pudding "^3.5.1" - npm-lifecycle "^3.1.2" - npmlog "^4.1.2" - -"@lerna/run-topologically@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-3.18.5.tgz#3cd639da20e967d7672cb88db0f756b92f2fdfc3" - integrity sha512-6N1I+6wf4hLOnPW+XDZqwufyIQ6gqoPfHZFkfWlvTQ+Ue7CuF8qIVQ1Eddw5HKQMkxqN10thKOFfq/9NQZ4NUg== - dependencies: - "@lerna/query-graph" "3.18.5" - figgy-pudding "^3.5.1" - p-queue "^4.0.0" - -"@lerna/run@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/run/-/run-3.21.0.tgz#2a35ec84979e4d6e42474fe148d32e5de1cac891" - integrity sha512-fJF68rT3veh+hkToFsBmUJ9MHc9yGXA7LSDvhziAojzOb0AI/jBDp6cEcDQyJ7dbnplba2Lj02IH61QUf9oW0Q== - dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/npm-run-script" "3.16.5" - "@lerna/output" "3.13.0" - "@lerna/profiler" "3.20.0" - "@lerna/run-topologically" "3.18.5" - "@lerna/timer" "3.13.0" - "@lerna/validation-error" "3.13.0" - p-map "^2.1.0" - -"@lerna/symlink-binary@3.17.0": - version "3.17.0" - resolved "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.17.0.tgz#8f8031b309863814883d3f009877f82e38aef45a" - integrity sha512-RLpy9UY6+3nT5J+5jkM5MZyMmjNHxZIZvXLV+Q3MXrf7Eaa1hNqyynyj4RO95fxbS+EZc4XVSk25DGFQbcRNSQ== - dependencies: - "@lerna/create-symlink" "3.16.2" - "@lerna/package" "3.16.0" - fs-extra "^8.1.0" - p-map "^2.1.0" - -"@lerna/symlink-dependencies@3.17.0": - version "3.17.0" - resolved "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.17.0.tgz#48d6360e985865a0e56cd8b51b308a526308784a" - integrity sha512-KmjU5YT1bpt6coOmdFueTJ7DFJL4H1w5eF8yAQ2zsGNTtZ+i5SGFBWpb9AQaw168dydc3s4eu0W0Sirda+F59Q== - dependencies: - "@lerna/create-symlink" "3.16.2" - "@lerna/resolve-symlink" "3.16.0" - "@lerna/symlink-binary" "3.17.0" - fs-extra "^8.1.0" - p-finally "^1.0.0" - p-map "^2.1.0" - p-map-series "^1.0.0" - -"@lerna/timer@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/timer/-/timer-3.13.0.tgz#bcd0904551db16e08364d6c18e5e2160fc870781" - integrity sha512-RHWrDl8U4XNPqY5MQHkToWS9jHPnkLZEt5VD+uunCKTfzlxGnRCr3/zVr8VGy/uENMYpVP3wJa4RKGY6M0vkRw== - -"@lerna/validation-error@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.13.0.tgz#c86b8f07c5ab9539f775bd8a54976e926f3759c3" - integrity sha512-SiJP75nwB8GhgwLKQfdkSnDufAaCbkZWJqEDlKOUPUvVOplRGnfL+BPQZH5nvq2BYSRXsksXWZ4UHVnQZI/HYA== - dependencies: - npmlog "^4.1.2" - -"@lerna/version@3.22.1": - version "3.22.1" - resolved "https://registry.npmjs.org/@lerna/version/-/version-3.22.1.tgz#9805a9247a47ee62d6b81bd9fa5fb728b24b59e2" - integrity sha512-PSGt/K1hVqreAFoi3zjD0VEDupQ2WZVlVIwesrE5GbrL2BjXowjCsTDPqblahDUPy0hp6h7E2kG855yLTp62+g== - dependencies: - "@lerna/check-working-tree" "3.16.5" - "@lerna/child-process" "3.16.5" - "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.21.0" - "@lerna/conventional-commits" "3.22.0" - "@lerna/github-client" "3.22.0" - "@lerna/gitlab-client" "3.15.0" - "@lerna/output" "3.13.0" - "@lerna/prerelease-id-from-version" "3.16.0" - "@lerna/prompt" "3.18.5" - "@lerna/run-lifecycle" "3.16.2" - "@lerna/run-topologically" "3.18.5" - "@lerna/validation-error" "3.13.0" - chalk "^2.3.1" - dedent "^0.7.0" - load-json-file "^5.3.0" - minimatch "^3.0.4" - npmlog "^4.1.2" - p-map "^2.1.0" - p-pipe "^1.2.0" - p-reduce "^1.0.0" - p-waterfall "^1.0.0" - semver "^6.2.0" - slash "^2.0.0" - temp-write "^3.4.0" - write-json-file "^3.2.0" - -"@lerna/write-log-file@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-3.13.0.tgz#b78d9e4cfc1349a8be64d91324c4c8199e822a26" - integrity sha512-RibeMnDPvlL8bFYW5C8cs4mbI3AHfQef73tnJCQ/SgrXZHehmHnsyWUiE7qDQCAo+B1RfTapvSyFF69iPj326A== - dependencies: - npmlog "^4.1.2" - write-file-atomic "^2.3.0" - -"@mrmlnc/readdir-enhanced@^2.2.1": - version "2.2.1" - resolved "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" - integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== - dependencies: - call-me-maybe "^1.0.1" - glob-to-regexp "^0.3.0" - -"@nodelib/fs.stat@^1.1.2": - version "1.1.3" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" - integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== - -"@octokit/auth-token@^2.4.0": - version "2.4.2" - resolved "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.2.tgz#10d0ae979b100fa6b72fa0e8e63e27e6d0dbff8a" - integrity sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ== - dependencies: - "@octokit/types" "^5.0.0" - -"@octokit/endpoint@^6.0.1": - version "6.0.6" - resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.6.tgz#4f09f2b468976b444742a1d5069f6fa45826d999" - integrity sha512-7Cc8olaCoL/mtquB7j/HTbPM+sY6Ebr4k2X2y4JoXpVKQ7r5xB4iGQE0IoO58wIPsUk4AzoT65AMEpymSbWTgQ== - dependencies: - "@octokit/types" "^5.0.0" - is-plain-object "^5.0.0" - universal-user-agent "^6.0.0" - -"@octokit/plugin-enterprise-rest@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" - integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== - -"@octokit/plugin-paginate-rest@^1.1.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc" - integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q== - dependencies: - "@octokit/types" "^2.0.1" - -"@octokit/plugin-request-log@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e" - integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw== - -"@octokit/plugin-rest-endpoint-methods@2.4.0": - version "2.4.0" - resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e" - integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ== - dependencies: - "@octokit/types" "^2.0.1" - deprecation "^2.3.1" - -"@octokit/request-error@^1.0.2": - version "1.2.1" - resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" - integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA== - dependencies: - "@octokit/types" "^2.0.0" - deprecation "^2.0.0" - once "^1.4.0" - -"@octokit/request-error@^2.0.0": - version "2.0.2" - resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.2.tgz#0e76b83f5d8fdda1db99027ea5f617c2e6ba9ed0" - integrity sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw== - dependencies: - "@octokit/types" "^5.0.1" - deprecation "^2.0.0" - once "^1.4.0" - -"@octokit/request@^5.2.0": - version "5.4.9" - resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.9.tgz#0a46f11b82351b3416d3157261ad9b1558c43365" - integrity sha512-CzwVvRyimIM1h2n9pLVYfTDmX9m+KHSgCpqPsY8F1NdEK8IaWqXhSBXsdjOBFZSpEcxNEeg4p0UO9cQ8EnOCLA== - dependencies: - "@octokit/endpoint" "^6.0.1" - "@octokit/request-error" "^2.0.0" - "@octokit/types" "^5.0.0" - deprecation "^2.0.0" - is-plain-object "^5.0.0" - node-fetch "^2.6.1" - once "^1.4.0" - universal-user-agent "^6.0.0" - -"@octokit/rest@^16.28.4": - version "16.43.2" - resolved "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.2.tgz#c53426f1e1d1044dee967023e3279c50993dd91b" - integrity sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ== - dependencies: - "@octokit/auth-token" "^2.4.0" - "@octokit/plugin-paginate-rest" "^1.1.1" - "@octokit/plugin-request-log" "^1.0.0" - "@octokit/plugin-rest-endpoint-methods" "2.4.0" - "@octokit/request" "^5.2.0" - "@octokit/request-error" "^1.0.2" - atob-lite "^2.0.0" - before-after-hook "^2.0.0" - btoa-lite "^1.0.0" - deprecation "^2.0.0" - lodash.get "^4.4.2" - lodash.set "^4.3.2" - lodash.uniq "^4.5.0" - octokit-pagination-methods "^1.1.0" - once "^1.4.0" - universal-user-agent "^4.0.0" - -"@octokit/types@^2.0.0", "@octokit/types@^2.0.1": - version "2.16.2" - resolved "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz#4c5f8da3c6fecf3da1811aef678fda03edac35d2" - integrity sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q== - dependencies: - "@types/node" ">= 8" - -"@octokit/types@^5.0.0", "@octokit/types@^5.0.1": - version "5.5.0" - resolved "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b" - integrity sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ== - dependencies: - "@types/node" ">= 8" - -"@rollup/plugin-commonjs@15.1.0": - version "15.1.0" - resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz#1e7d076c4f1b2abf7e65248570e555defc37c238" - integrity sha512-xCQqz4z/o0h2syQ7d9LskIMvBSH4PX5PjYdpSSvgS+pQik3WahkQVNWg3D8XJeYjZoVWnIUQYDghuEMRGrmQYQ== - dependencies: - "@rollup/pluginutils" "^3.1.0" - commondir "^1.0.1" - estree-walker "^2.0.1" - glob "^7.1.6" - is-reference "^1.2.1" - magic-string "^0.25.7" - resolve "^1.17.0" - -"@rollup/plugin-json@4.1.0": - version "4.1.0" - resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" - integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== - dependencies: - "@rollup/pluginutils" "^3.0.8" - -"@rollup/plugin-node-resolve@9.0.0": - version "9.0.0" - resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-9.0.0.tgz#39bd0034ce9126b39c1699695f440b4b7d2b62e6" - integrity sha512-gPz+utFHLRrd41WMP13Jq5mqqzHL3OXrfj3/MkSyB6UBIcuNt9j60GCbarzMzdf1VHFpOxfQh/ez7wyadLMqkg== - dependencies: - "@rollup/pluginutils" "^3.1.0" - "@types/resolve" "1.17.1" - builtin-modules "^3.1.0" - deepmerge "^4.2.2" - is-module "^1.0.0" - resolve "^1.17.0" - -"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" - integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== - dependencies: - "@types/estree" "0.0.39" - estree-walker "^1.0.1" - picomatch "^2.2.2" - -"@types/estree@*": - version "0.0.45" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884" - integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g== - -"@types/estree@0.0.39": - version "0.0.39" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" - integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== - -"@types/glob@^7.1.1": - version "7.1.3" - resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - -"@types/minimatch@*": - version "3.0.3" - resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== - -"@types/minimist@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" - integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= - -"@types/node@*", "@types/node@>= 8": - version "14.11.2" - resolved "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" - integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== - -"@types/normalize-package-data@^2.4.0": - version "2.4.0" - resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" - integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== - -"@types/resolve@1.17.1": - version "1.17.1" - resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" - integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== - dependencies: - "@types/node" "*" - -"@zkochan/cmd-shim@^3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz#2ab8ed81f5bb5452a85f25758eb9b8681982fd2e" - integrity sha512-o8l0+x7C7sMZU3v9GuJIAU10qQLtwR1dtRQIOmlNMtyaqhmpXOzx1HWiYoWfmmf9HHZoAkXpc9TM9PQYF9d4Jg== - dependencies: - is-windows "^1.0.0" - mkdirp-promise "^5.0.1" - mz "^2.5.0" - -JSONStream@^1.0.4, JSONStream@^1.3.4: - version "1.3.5" - resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - -abbrev@1: - version "1.1.1" - resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -acorn@^7.1.0: - version "7.4.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== - -agent-base@4, agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== - dependencies: - es6-promisify "^5.0.0" - -agent-base@~4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" - integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== - dependencies: - es6-promisify "^5.0.0" - -agentkeepalive@^3.4.1: - version "3.5.2" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" - integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ== - dependencies: - humanize-ms "^1.2.1" - -ajv@^6.12.3: - version "6.12.5" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" - integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-escapes@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -any-promise@^1.0.0: - version "1.3.0" - resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= - -aproba@^1.0.3, aproba@^1.1.1: - version "1.2.0" - resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -aproba@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-differ@^2.0.3: - version "2.1.0" - resolved "https://registry.npmjs.org/array-differ/-/array-differ-2.1.0.tgz#4b9c1c3f14b906757082925769e8ab904f4801b1" - integrity sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w== - -array-find-index@^1.0.1, array-find-index@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - -array-ify@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" - integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= - -array-union@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= - dependencies: - array-uniq "^1.0.1" - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -arrify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= - -asap@^2.0.0: - version "2.0.6" - resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -atob-lite@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" - integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY= - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.10.1" - resolved "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" - integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.npmjs.org/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - 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" - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -before-after-hook@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" - integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== - -bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: - version "3.7.2" - resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1: - version "2.3.2" - resolved "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - 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" - -btoa-lite@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" - integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -builtin-modules@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" - integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw== - -builtins@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" - integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= - -byline@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" - integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= - -byte-size@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/byte-size/-/byte-size-5.0.1.tgz#4b651039a5ecd96767e71a3d7ed380e48bed4191" - integrity sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw== - -cacache@^12.0.0, cacache@^12.0.3: - version "12.0.4" - resolved "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" - integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== - dependencies: - bluebird "^3.5.5" - chownr "^1.1.1" - figgy-pudding "^3.5.1" - glob "^7.1.4" - graceful-fs "^4.1.15" - infer-owner "^1.0.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.3" - ssri "^6.0.1" - unique-filename "^1.1.1" - y18n "^4.0.0" - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - 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-me-maybe@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" - integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= - -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase-keys@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" - integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c= - dependencies: - camelcase "^4.1.0" - map-obj "^2.0.0" - quick-lru "^1.0.0" - -camelcase-keys@^6.2.2: - version "6.2.2" - resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" - integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== - dependencies: - camelcase "^5.3.1" - map-obj "^4.0.0" - quick-lru "^4.0.1" - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - -camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -chalk@^2.0.0, chalk@^2.3.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -chownr@^1.1.1, chownr@^1.1.2: - version "1.1.4" - resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= - dependencies: - restore-cursor "^2.0.0" - -cli-width@^2.0.0: - version "2.2.1" - resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" - integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -columnify@^1.5.4: - version "1.5.4" - resolved "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" - integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs= - dependencies: - strip-ansi "^3.0.0" - wcwidth "^1.0.0" - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commenting@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/commenting/-/commenting-1.1.0.tgz#fae14345c6437b8554f30bc6aa6c1e1633033590" - integrity sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -compare-func@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" - integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== - dependencies: - array-ify "^1.0.0" - dot-prop "^5.1.0" - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -concat-stream@^1.5.0: - version "1.6.2" - resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -concat-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" - integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.0.2" - typedarray "^0.0.6" - -config-chain@^1.1.11: - version "1.1.12" - resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" - integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -conventional-changelog-angular@^5.0.3: - version "5.0.11" - resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.11.tgz#99a3ca16e4a5305e0c2c2fae3ef74fd7631fc3fb" - integrity sha512-nSLypht/1yEflhuTogC03i7DX7sOrXGsRn14g131Potqi6cbGbGEE9PSDEHKldabB6N76HiSyw9Ph+kLmC04Qw== - dependencies: - compare-func "^2.0.0" - q "^1.5.1" - -conventional-changelog-core@^3.1.6: - version "3.2.3" - resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-3.2.3.tgz#b31410856f431c847086a7dcb4d2ca184a7d88fb" - integrity sha512-LMMX1JlxPIq/Ez5aYAYS5CpuwbOk6QFp8O4HLAcZxe3vxoCtABkhfjetk8IYdRB9CDQGwJFLR3Dr55Za6XKgUQ== - dependencies: - conventional-changelog-writer "^4.0.6" - conventional-commits-parser "^3.0.3" - dateformat "^3.0.0" - get-pkg-repo "^1.0.0" - git-raw-commits "2.0.0" - git-remote-origin-url "^2.0.0" - git-semver-tags "^2.0.3" - lodash "^4.2.1" - normalize-package-data "^2.3.5" - q "^1.5.1" - read-pkg "^3.0.0" - read-pkg-up "^3.0.0" - through2 "^3.0.0" - -conventional-changelog-preset-loader@^2.1.1: - version "2.3.4" - resolved "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" - integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== - -conventional-changelog-writer@^4.0.6: - version "4.0.17" - resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.17.tgz#4753aaa138bf5aa59c0b274cb5937efcd2722e21" - integrity sha512-IKQuK3bib/n032KWaSb8YlBFds+aLmzENtnKtxJy3+HqDq5kohu3g/UdNbIHeJWygfnEbZjnCKFxAW0y7ArZAw== - dependencies: - compare-func "^2.0.0" - conventional-commits-filter "^2.0.6" - dateformat "^3.0.0" - handlebars "^4.7.6" - json-stringify-safe "^5.0.1" - lodash "^4.17.15" - meow "^7.0.0" - semver "^6.0.0" - split "^1.0.0" - through2 "^3.0.0" - -conventional-commits-filter@^2.0.2, conventional-commits-filter@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.6.tgz#0935e1240c5ca7698329affee1b6a46d33324c4c" - integrity sha512-4g+sw8+KA50/Qwzfr0hL5k5NWxqtrOVw4DDk3/h6L85a9Gz0/Eqp3oP+CWCNfesBvZZZEFHF7OTEbRe+yYSyKw== - dependencies: - lodash.ismatch "^4.4.0" - modify-values "^1.0.0" - -conventional-commits-parser@^3.0.3: - version "3.1.0" - resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.1.0.tgz#10140673d5e7ef5572633791456c5d03b69e8be4" - integrity sha512-RSo5S0WIwXZiRxUGTPuYFbqvrR4vpJ1BDdTlthFgvHt5kEdnd1+pdvwWphWn57/oIl4V72NMmOocFqqJ8mFFhA== - dependencies: - JSONStream "^1.0.4" - is-text-path "^1.0.1" - lodash "^4.17.15" - meow "^7.0.0" - split2 "^2.0.0" - through2 "^3.0.0" - trim-off-newlines "^1.0.0" - -conventional-recommended-bump@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-5.0.1.tgz#5af63903947b6e089e77767601cb592cabb106ba" - integrity sha512-RVdt0elRcCxL90IrNP0fYCpq1uGt2MALko0eyeQ+zQuDVWtMGAy9ng6yYn3kax42lCj9+XBxQ8ZN6S9bdKxDhQ== - dependencies: - concat-stream "^2.0.0" - conventional-changelog-preset-loader "^2.1.1" - conventional-commits-filter "^2.0.2" - conventional-commits-parser "^3.0.3" - git-raw-commits "2.0.0" - git-semver-tags "^2.0.3" - meow "^4.0.0" - q "^1.5.1" - -copy-concurrently@^1.0.0: - version "1.0.5" - resolved "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" - integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== - dependencies: - aproba "^1.1.1" - fs-write-stream-atomic "^1.0.8" - iferr "^0.1.5" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.0" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cosmiconfig@^5.1.0: - version "5.2.1" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - -cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - -cyclist@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" - integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= - -dargs@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17" - integrity sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc= - dependencies: - number-is-nan "^1.0.0" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -dateformat@^3.0.0: - version "3.0.3" - resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" - integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== - -debug@3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.2.6" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -debuglog@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" - integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= - -decamelize-keys@^1.0.0, decamelize-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" - integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= - dependencies: - decamelize "^1.1.0" - map-obj "^1.0.0" - -decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -dedent@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" - integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= - -deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== - -defaults@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" - integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= - dependencies: - clone "^1.0.2" - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -deprecation@^2.0.0, deprecation@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" - integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== - -detect-indent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" - integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= - -dezalgo@^1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" - integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= - dependencies: - asap "^2.0.0" - wrappy "1" - -dir-glob@^2.2.2: - version "2.2.2" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" - integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== - dependencies: - path-type "^3.0.0" - -dot-prop@^4.2.0: - version "4.2.1" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4" - integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ== - dependencies: - is-obj "^1.0.0" - -dot-prop@^5.1.0: - version "5.3.0" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.0.0" - -duplexer@^0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -duplexify@^3.4.2, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -encoding@^0.1.11: - version "0.1.13" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -env-paths@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" - integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== - -envinfo@^7.3.1: - version "7.7.3" - resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.7.3.tgz#4b2d8622e3e7366afb8091b23ed95569ea0208cc" - integrity sha512-46+j5QxbPWza0PB1i15nZx0xQ4I/EfQxg9J8Had3b408SV63nEtor2e+oiY63amTo9KTuh2a3XLObNwduxYwwA== - -err-code@^1.0.0: - version "1.1.2" - resolved "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" - integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= - -error-ex@^1.2.0, error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: - version "1.17.7" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" - integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-abstract@^1.18.0-next.0: - version "1.18.0-next.1" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -estree-walker@^0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" - integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== - -estree-walker@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" - integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== - -estree-walker@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.1.tgz#f8e030fb21cefa183b44b7ad516b747434e7a3e0" - integrity sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg== - -eventemitter3@^3.1.0: - version "3.1.2" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" - integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.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" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - 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" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - 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" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^2.2.6: - version "2.2.7" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" - integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== - dependencies: - "@mrmlnc/readdir-enhanced" "^2.2.1" - "@nodelib/fs.stat" "^1.1.2" - glob-parent "^3.1.0" - is-glob "^4.0.0" - merge2 "^1.2.3" - micromatch "^3.1.10" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: - version "3.5.2" - resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" - integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -find-cache-dir@^3.3.1: - version "3.3.1" - resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" - integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flush-write-stream@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -from2@^2.1.0: - version "2.3.0" - resolved "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - -fs-extra@8.1.0, fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-minipass@^1.2.5: - version "1.2.7" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" - integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== - dependencies: - minipass "^2.6.0" - -fs-write-stream-atomic@^1.0.8: - version "1.0.10" - resolved "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" - integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= - dependencies: - graceful-fs "^4.1.2" - iferr "^0.1.5" - imurmurhash "^0.1.4" - readable-stream "1 || 2" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - 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" - -genfun@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" - integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-pkg-repo@^1.0.0: - version "1.4.0" - resolved "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" - integrity sha1-xztInAbYDMVTbCyFP54FIyBWly0= - dependencies: - 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-port@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" - integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== - -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - -get-stream@^4.0.0, get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -git-raw-commits@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" - integrity sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg== - dependencies: - dargs "^4.0.1" - lodash.template "^4.0.2" - meow "^4.0.0" - split2 "^2.0.0" - through2 "^2.0.0" - -git-remote-origin-url@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" - integrity sha1-UoJlna4hBxRaERJhEq0yFuxfpl8= - dependencies: - gitconfiglocal "^1.0.0" - pify "^2.3.0" - -git-semver-tags@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-2.0.3.tgz#48988a718acf593800f99622a952a77c405bfa34" - integrity sha512-tj4FD4ww2RX2ae//jSrXZzrocla9db5h0V7ikPl1P/WwoZar9epdUhwR7XHXSgc+ZkNq72BEEerqQuicoEQfzA== - dependencies: - meow "^4.0.0" - semver "^6.0.0" - -git-up@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/git-up/-/git-up-4.0.2.tgz#10c3d731051b366dc19d3df454bfca3f77913a7c" - integrity sha512-kbuvus1dWQB2sSW4cbfTeGpCMd8ge9jx9RKnhXhuJ7tnvT+NIrTVfYZxjtflZddQYcmdOTlkAcjmx7bor+15AQ== - dependencies: - is-ssh "^1.3.0" - parse-url "^5.0.0" - -git-url-parse@^11.1.2: - version "11.3.0" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.3.0.tgz#1515b4574c4eb2efda7d25cc50b29ce8beaefaae" - integrity sha512-i3XNa8IKmqnUqWBcdWBjOcnyZYfN3C1WRvnKI6ouFWwsXCZEnlgbwbm55ZpJ3OJMhfEP/ryFhqW8bBhej3C5Ug== - dependencies: - git-up "^4.0.0" - -gitconfiglocal@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" - integrity sha1-QdBF84UaXqiPA/JMocYXgRRGS5s= - dependencies: - ini "^1.3.2" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - -glob-to-regexp@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" - integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= - -glob@7.1.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: - version "7.1.6" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - 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" - -globby@^9.2.0: - version "9.2.0" - resolved "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" - integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg== - dependencies: - "@types/glob" "^7.1.1" - array-union "^1.0.2" - dir-glob "^2.2.2" - fast-glob "^2.2.6" - glob "^7.1.3" - ignore "^4.0.3" - pify "^4.0.1" - slash "^2.0.0" - -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: - version "4.2.4" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -handlebars@^4.7.6: - version "4.7.6" - resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" - integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== - dependencies: - minimist "^1.2.5" - neo-async "^2.6.0" - source-map "^0.6.1" - wordwrap "^1.0.0" - optionalDependencies: - uglify-js "^3.1.4" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - -hard-rejection@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" - integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -has-unicode@^2.0.0, has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: - version "2.8.8" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== - -http-cache-semantics@^3.8.1: - version "3.8.1" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" - integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== - -http-proxy-agent@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" - integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== - dependencies: - agent-base "4" - debug "3.1.0" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -https-proxy-agent@^2.2.3: - version "2.2.4" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= - dependencies: - ms "^2.0.0" - -iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@^0.6.2: - version "0.6.2" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" - integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -iferr@^0.1.5: - version "0.1.5" - resolved "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" - integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= - -ignore-walk@^3.0.1: - version "3.0.3" - resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" - integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== - dependencies: - minimatch "^3.0.4" - -ignore@^4.0.3: - version "4.0.6" - resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - -import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - -indent-string@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" - integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -infer-owner@^1.0.3, infer-owner@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@^1.3.2, ini@^1.3.4: - version "1.3.5" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== - -init-package-json@^1.10.3: - version "1.10.3" - resolved "https://registry.npmjs.org/init-package-json/-/init-package-json-1.10.3.tgz#45ffe2f610a8ca134f2bd1db5637b235070f6cbe" - integrity sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw== - dependencies: - glob "^7.1.1" - npm-package-arg "^4.0.0 || ^5.0.0 || ^6.0.0" - promzard "^0.3.0" - read "~1.0.1" - read-package-json "1 || 2" - semver "2.x || 3.x || 4 || 5" - validate-npm-package-license "^3.0.1" - validate-npm-package-name "^3.0.0" - -inquirer@^6.2.0: - version "6.5.2" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - -ip@1.1.5: - version "1.1.5" - resolved "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.4, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== - -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - -is-core-module@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.0.0.tgz#58531b70aed1db7c0e8d4eb1a0a2d1ddd64bd12d" - integrity sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw== - dependencies: - has "^1.0.3" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-module@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" - integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= - -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - -is-reference@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" - integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== - dependencies: - "@types/estree" "*" - -is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -is-ssh@^1.3.0: - version "1.3.2" - resolved "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.2.tgz#a4b82ab63d73976fd8263cceee27f99a88bdae2b" - integrity sha512-elEw0/0c2UscLrNG+OAorbP539E3rhliKPg+hDMWN9VwrDXfYK+4PBEykDPfxlYYtQvl84TascnQyobfQLHEhQ== - dependencies: - protocols "^1.1.0" - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - -is-text-path@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" - integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= - dependencies: - text-extensions "^1.0.0" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-windows@^1.0.0, is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -isarray@1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -jest-worker@^24.0.0: - version "24.9.0" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" - integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== - dependencies: - merge-stream "^2.0.0" - supports-color "^6.1.0" - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= - optionalDependencies: - graceful-fs "^4.1.6" - -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -lerna@^3.22.1: - version "3.22.1" - resolved "https://registry.npmjs.org/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" - integrity sha512-vk1lfVRFm+UuEFA7wkLKeSF7Iz13W+N/vFd48aW2yuS7Kv0RbNm2/qcDPV863056LMfkRlsEe+QYOw3palj5Lg== - dependencies: - "@lerna/add" "3.21.0" - "@lerna/bootstrap" "3.21.0" - "@lerna/changed" "3.21.0" - "@lerna/clean" "3.21.0" - "@lerna/cli" "3.18.5" - "@lerna/create" "3.22.0" - "@lerna/diff" "3.21.0" - "@lerna/exec" "3.21.0" - "@lerna/import" "3.22.0" - "@lerna/info" "3.21.0" - "@lerna/init" "3.21.0" - "@lerna/link" "3.21.0" - "@lerna/list" "3.21.0" - "@lerna/publish" "3.22.1" - "@lerna/run" "3.21.0" - "@lerna/version" "3.22.1" - import-local "^2.0.0" - npmlog "^4.1.2" - -lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - -load-json-file@^5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz#4d3c1e01fa1c03ea78a60ac7af932c9ce53403f3" - integrity sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw== - dependencies: - graceful-fs "^4.1.15" - parse-json "^4.0.0" - pify "^4.0.1" - strip-bom "^3.0.0" - type-fest "^0.3.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= - -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - -lodash.ismatch@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" - integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= - -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= - -lodash.template@^4.0.2, lodash.template@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" - integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" - integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== - dependencies: - lodash._reinterpolate "^3.0.0" - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= - -lodash@4.17.15: - version "4.17.15" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -lodash@^4.17.12, lodash@^4.17.15, lodash@^4.2.1: - version "4.17.20" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -macos-release@^2.2.0: - version "2.4.1" - resolved "https://registry.npmjs.org/macos-release/-/macos-release-2.4.1.tgz#64033d0ec6a5e6375155a74b1a1eba8e509820ac" - integrity sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg== - -magic-string@0.25.7, magic-string@^0.25.2, magic-string@^0.25.7: - version "0.25.7" - resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" - integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== - dependencies: - sourcemap-codec "^1.4.4" - -make-dir@^1.0.0: - version "1.3.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" - integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== - dependencies: - pify "^3.0.0" - -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -make-dir@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-fetch-happen@^5.0.0: - version "5.0.2" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz#aa8387104f2687edca01c8687ee45013d02d19bd" - integrity sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag== - dependencies: - agentkeepalive "^3.4.1" - cacache "^12.0.0" - http-cache-semantics "^3.8.1" - http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - node-fetch-npm "^2.0.2" - promise-retry "^1.1.1" - socks-proxy-agent "^4.0.0" - ssri "^6.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -map-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" - integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= - -map-obj@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" - integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g== - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -meow@^3.3.0: - version "3.7.0" - resolved "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - 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" - -meow@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz#d48598f6f4b1472f35bf6317a95945ace347f975" - integrity sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A== - dependencies: - 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" - -meow@^7.0.0: - version "7.1.1" - resolved "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306" - integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA== - dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "4.1.0" - normalize-package-data "^2.5.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.13.1" - yargs-parser "^18.1.3" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.2.3: - version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^3.1.10: - version "3.1.10" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - 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" - -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.27" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - -min-indent@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" - integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist-options@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" - integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - kind-of "^6.0.3" - -minimist-options@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" - integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minipass@^2.3.5, minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: - version "2.9.0" - resolved "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" - integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.2.1: - version "1.3.3" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" - integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== - dependencies: - minipass "^2.9.0" - -mississippi@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" - integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^3.0.0" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp-promise@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz#e9b8f68e552c68a9c1713b84883f7a1dd039b8a1" - integrity sha1-6bj2jlUsaKnBcTuEiD96HdA5uKE= - dependencies: - mkdirp "*" - -mkdirp@*: - version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -mkdirp@^0.5.0, mkdirp@^0.5.1: - version "0.5.5" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -modify-values@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" - integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== - -moment@2.24.0: - version "2.24.0" - resolved "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== - -move-concurrently@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" - integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= - dependencies: - aproba "^1.1.1" - copy-concurrently "^1.0.0" - fs-write-stream-atomic "^1.0.8" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.3" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.0.0, ms@^2.1.1: - version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -multimatch@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/multimatch/-/multimatch-3.0.0.tgz#0e2534cc6bc238d9ab67e1b9cd5fcd85a6dbf70b" - integrity sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA== - dependencies: - array-differ "^2.0.3" - array-union "^1.0.2" - arrify "^1.0.1" - minimatch "^3.0.4" - -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= - -mute-stream@~0.0.4: - version "0.0.8" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -mz@^2.5.0: - version "2.7.0" - resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" - integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - 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" - -neo-async@^2.6.0: - version "2.6.2" - resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-fetch-npm@^2.0.2: - version "2.0.4" - resolved "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4" - integrity sha512-iOuIQDWDyjhv9qSDrj9aq/klt6F9z1p2otB3AV7v3zBDcL/x+OfGsvGQZZCcMZbUf4Ujw1xGNQkjvGnVT22cKg== - dependencies: - encoding "^0.1.11" - json-parse-better-errors "^1.0.0" - safe-buffer "^5.1.1" - -node-fetch@^2.5.0, node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-gyp@^5.0.2: - version "5.1.1" - resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.1.tgz#eb915f7b631c937d282e33aed44cb7a025f62a3e" - integrity sha512-WH0WKGi+a4i4DUt2mHnvocex/xPLp9pYt5R6M2JdFB7pJ7Z34hveZ4nDTGTiLXCkitA9T8HFZjhinBCiVHYcWw== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.2" - mkdirp "^0.5.1" - nopt "^4.0.1" - npmlog "^4.1.2" - request "^2.88.0" - rimraf "^2.6.3" - semver "^5.7.1" - tar "^4.4.12" - which "^1.3.1" - -nopt@^4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== - dependencies: - abbrev "1" - osenv "^0.1.4" - -normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-url@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" - integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== - -npm-bundled@^1.0.1: - version "1.1.1" - resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" - integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== - dependencies: - npm-normalize-package-bin "^1.0.1" - -npm-lifecycle@^3.1.2: - version "3.1.5" - resolved "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz#9882d3642b8c82c815782a12e6a1bfeed0026309" - integrity sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g== - dependencies: - byline "^5.0.0" - graceful-fs "^4.1.15" - node-gyp "^5.0.2" - resolve-from "^4.0.0" - slide "^1.1.6" - uid-number "0.0.6" - umask "^1.1.0" - which "^1.3.1" - -npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" - integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== - -"npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: - version "6.1.1" - resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7" - integrity sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg== - dependencies: - hosted-git-info "^2.7.1" - osenv "^0.1.5" - semver "^5.6.0" - validate-npm-package-name "^3.0.0" - -npm-packlist@^1.4.4: - version "1.4.8" - resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" - integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - npm-normalize-package-bin "^1.0.1" - -npm-pick-manifest@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz#f4d9e5fd4be2153e5f4e5f9b7be8dc419a99abb7" - integrity sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw== - dependencies: - figgy-pudding "^3.5.1" - npm-package-arg "^6.0.0" - semver "^5.4.1" - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -npmlog@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - 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@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4.0.1, object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -object.getownpropertydescriptors@^2.0.3: - version "2.1.0" - resolved "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" - integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -octokit-pagination-methods@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" - integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= - dependencies: - mimic-fn "^1.0.0" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-name@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" - integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg== - dependencies: - macos-release "^2.2.0" - windows-release "^3.1.0" - -os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@^0.1.4, osenv@^0.1.5: - version "0.1.5" - resolved "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.0.0, p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-map-series@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz#bf98fe575705658a9e1351befb85ae4c1f07bdca" - integrity sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco= - dependencies: - p-reduce "^1.0.0" - -p-map@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - -p-pipe@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" - integrity sha1-SxoROZoRUgpneQ7loMHViB1r7+k= - -p-queue@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-queue/-/p-queue-4.0.0.tgz#ed0eee8798927ed6f2c2f5f5b77fdb2061a5d346" - integrity sha512-3cRXXn3/O0o3+eVmUroJPSj/esxoEFIm0ZOno/T+NzG/VZgPOqQ8WKmlNqubSEpZmCIngEy34unkHGg83ZIBmg== - dependencies: - eventemitter3 "^3.1.0" - -p-reduce@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" - integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -p-waterfall@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-waterfall/-/p-waterfall-1.0.0.tgz#7ed94b3ceb3332782353af6aae11aa9fc235bb00" - integrity sha1-ftlLPOszMngjU69qrhGqn8I1uwA= - dependencies: - p-reduce "^1.0.0" - -parallel-transform@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" - integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== - dependencies: - cyclist "^1.0.1" - inherits "^2.0.3" - readable-stream "^2.1.5" - -parse-github-repo-url@^1.3.0: - version "1.4.1" - resolved "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" - integrity sha1-nn2LslKmy2ukJZUGC3v23z28H1A= - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-json@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646" - integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -parse-path@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/parse-path/-/parse-path-4.0.2.tgz#ef14f0d3d77bae8dd4bc66563a4c151aac9e65aa" - integrity sha512-HSqVz6iuXSiL8C1ku5Gl1Z5cwDd9Wo0q8CoffdAghP6bz8pJa1tcMC+m4N+z6VAS8QdksnIGq1TB6EgR4vPR6w== - dependencies: - is-ssh "^1.3.0" - protocols "^1.4.0" - -parse-url@^5.0.0: - version "5.0.2" - resolved "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz#856a3be1fcdf78dc93fc8b3791f169072d898b59" - integrity sha512-Czj+GIit4cdWtxo3ISZCvLiUjErSo0iI3wJ+q9Oi3QuMYTI6OZu+7cewMWZ+C1YAnKhYTk6/TLuhIgCypLthPA== - dependencies: - is-ssh "^1.3.0" - normalize-url "^3.3.0" - parse-path "^4.0.0" - protocols "^1.4.0" - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -picomatch@^2.2.2: - version "2.2.2" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -pify@^2.0.0, pify@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -pkg-dir@^4.1.0: - version "4.2.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= - -promise-retry@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d" - integrity sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0= - dependencies: - err-code "^1.0.0" - retry "^0.10.0" - -promzard@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" - integrity sha1-JqXW7ox97kyxIggwWs+5O6OCqe4= - dependencies: - read "1" - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" - integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= - -protocols@^1.1.0, protocols@^1.4.0: - version "1.4.8" - resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" - integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== - -protoduck@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz#03c3659ca18007b69a50fd82a7ebcc516261151f" - integrity sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg== - dependencies: - genfun "^5.0.0" - -psl@^1.1.28: - version "1.8.0" - resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.3: - version "1.5.1" - resolved "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -q@^1.5.1: - version "1.5.1" - resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -quick-lru@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" - integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= - -quick-lru@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" - integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== - -read-cmd-shim@^1.0.1: - version "1.0.5" - resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" - integrity sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA== - dependencies: - graceful-fs "^4.1.2" - -"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.13: - version "2.1.2" - resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz#6992b2b66c7177259feb8eaac73c3acd28b9222a" - integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA== - dependencies: - glob "^7.1.1" - json-parse-even-better-errors "^2.3.0" - normalize-package-data "^2.0.0" - npm-normalize-package-bin "^1.0.0" - -read-package-tree@^5.1.6: - version "5.3.1" - resolved "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" - integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== - dependencies: - read-package-json "^2.0.0" - readdir-scoped-modules "^1.0.0" - util-promisify "^2.1.0" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg-up@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" - integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= - dependencies: - find-up "^2.0.0" - read-pkg "^3.0.0" - -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== - dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== - dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" - -read@1, read@~1.0.1: - version "1.0.7" - resolved "https://registry.npmjs.org/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" - integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= - dependencies: - mute-stream "~0.0.4" - -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - 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" - -"readable-stream@2 || 3", readable-stream@^3.0.2: - version "3.6.0" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdir-scoped-modules@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" - integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== - dependencies: - debuglog "^1.0.1" - dezalgo "^1.0.0" - graceful-fs "^4.1.2" - once "^1.3.0" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -redent@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" - integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo= - dependencies: - indent-string "^3.0.0" - strip-indent "^2.0.0" - -redent@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" - integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== - dependencies: - indent-string "^4.0.0" - strip-indent "^3.0.0" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -request@^2.88.0: - version "2.88.2" - resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@1.17.0, resolve@^1.10.0: - version "1.17.0" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - -resolve@^1.17.0: - version "1.18.1" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130" - integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA== - dependencies: - is-core-module "^2.0.0" - path-parse "^1.0.6" - -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= - dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -retry@^0.10.0: - version "0.10.1" - resolved "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" - integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= - -rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rollup-plugin-license@0.14.0: - version "0.14.0" - resolved "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-0.14.0.tgz#623e5b6306b1521d7b89927d0a28d0f99ac52915" - integrity sha512-+MCcBBQF9j3PHPus+3Qgk8NfiRT569HBfBAk0UP3hEo3T3buY3lMZUXuTkE+VpTq1HSritCboJ6RxBhm6CEsdA== - dependencies: - commenting "1.1.0" - glob "7.1.6" - lodash "4.17.15" - magic-string "0.25.7" - mkdirp "0.5.1" - moment "2.24.0" - spdx-expression-validate "2.0.0" - spdx-satisfies "5.0.0" - -rollup-plugin-replace@2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz#f41ae5372e11e7a217cde349c8b5d5fd115e70e3" - integrity sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA== - dependencies: - magic-string "^0.25.2" - rollup-pluginutils "^2.6.0" - -rollup-plugin-sourcemaps@0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz#bf93913ffe056e414419607f1d02780d7ece84ed" - integrity sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw== - dependencies: - "@rollup/pluginutils" "^3.0.9" - source-map-resolve "^0.6.0" - -rollup-plugin-typescript2@0.29.0: - version "0.29.0" - resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.29.0.tgz#b7ad83f5241dbc5bdf1e98d9c3fca005ffe39e1a" - integrity sha512-YytahBSZCIjn/elFugEGQR5qTsVhxhUwGZIsA9TmrSsC88qroGo65O5HZP/TTArH2dm0vUmYWhKchhwi2wL9bw== - dependencies: - "@rollup/pluginutils" "^3.1.0" - find-cache-dir "^3.3.1" - fs-extra "8.1.0" - resolve "1.17.0" - tslib "2.0.1" - -rollup-plugin-uglify@6.0.4: - version "6.0.4" - resolved "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-6.0.4.tgz#65a0959d91586627f1e46a7db966fd504ec6c4e6" - integrity sha512-ddgqkH02klveu34TF0JqygPwZnsbhHVI6t8+hGTcYHngPkQb5MIHI0XiztXIN/d6V9j+efwHAqEL7LspSxQXGw== - dependencies: - "@babel/code-frame" "^7.0.0" - jest-worker "^24.0.0" - serialize-javascript "^2.1.2" - uglify-js "^3.4.9" - -rollup-pluginutils@^2.6.0: - version "2.8.2" - resolved "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" - integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== - dependencies: - estree-walker "^0.6.1" - -rollup@1.32.1: - version "1.32.1" - resolved "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz#4480e52d9d9e2ae4b46ba0d9ddeaf3163940f9c4" - integrity sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A== - dependencies: - "@types/estree" "*" - "@types/node" "*" - acorn "^7.1.0" - -run-async@^2.2.0: - version "2.4.1" - resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -run-queue@^1.0.0, run-queue@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" - integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= - dependencies: - aproba "^1.1.1" - -rxjs@^6.4.0: - version "6.6.3" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== - dependencies: - tslib "^1.9.0" - -safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.2.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -serialize-javascript@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" - integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -slash@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" - integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== - -slide@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" - integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= - -smart-buffer@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" - integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - 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" - -socks-proxy-agent@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" - integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg== - dependencies: - agent-base "~4.2.1" - socks "~2.3.2" - -socks@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3" - integrity sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA== - dependencies: - ip "1.1.5" - smart-buffer "^4.1.0" - -sort-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" - integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= - dependencies: - is-plain-obj "^1.0.0" - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-resolve@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" - integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -sourcemap-codec@^1.4.4: - version "1.4.8" - resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - -spdx-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz#2c55f117362078d7409e6d7b08ce70a857cd3ed7" - integrity sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A== - dependencies: - array-find-index "^1.0.2" - spdx-expression-parse "^3.0.0" - spdx-ranges "^2.0.0" - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-expression-validate@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/spdx-expression-validate/-/spdx-expression-validate-2.0.0.tgz#25c9408e1c63fad94fff5517bb7101ffcd23350b" - integrity sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg== - dependencies: - spdx-expression-parse "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.6" - resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" - integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== - -spdx-ranges@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz#87573927ba51e92b3f4550ab60bfc83dd07bac20" - integrity sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA== - -spdx-satisfies@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-5.0.0.tgz#d740b8f14caeada36fb307629dee87146970a256" - integrity sha512-/hGhwh20BeGmkA+P/lm06RvXD94JduwNxtx/oX3B5ClPt1/u/m5MCaDNo1tV3Y9laLkQr/NRde63b9lLMhlNfw== - dependencies: - spdx-compare "^1.0.0" - spdx-expression-parse "^3.0.0" - spdx-ranges "^2.0.0" - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -split2@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" - integrity sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw== - dependencies: - through2 "^2.0.2" - -split@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - 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" - -ssri@^6.0.0, ssri@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" - integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== - dependencies: - figgy-pudding "^3.5.1" - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -stream-each@^1.1.0: - version "1.2.3" - resolved "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" - integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== - dependencies: - end-of-stream "^1.1.0" - stream-shift "^1.0.0" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2", string-width@^2.1.0: - version "2.1.1" - resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= - -strip-indent@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== - dependencies: - min-indent "^1.0.0" - -strong-log-transformer@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" - integrity sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA== - dependencies: - duplexer "^0.1.1" - minimist "^1.2.0" - through "^2.3.4" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: - version "4.4.13" - resolved "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" - integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== - dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.8.6" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" - -temp-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" - integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= - -temp-write@^3.4.0: - version "3.4.0" - resolved "https://registry.npmjs.org/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492" - integrity sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI= - dependencies: - graceful-fs "^4.1.2" - is-stream "^1.1.0" - make-dir "^1.0.0" - pify "^3.0.0" - temp-dir "^1.0.0" - uuid "^3.0.1" - -text-extensions@^1.0.0: - version "1.9.0" - resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" - integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== - -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.1" - resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" - integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== - dependencies: - any-promise "^1.0.0" - -through2@^2.0.0, through2@^2.0.2: - version "2.0.5" - resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -through2@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" - integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ== - dependencies: - inherits "^2.0.4" - readable-stream "2 || 3" - -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: - version "2.3.8" - resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= - dependencies: - punycode "^2.1.0" - -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - -trim-newlines@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" - integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= - -trim-newlines@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" - integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== - -trim-off-newlines@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" - integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM= - -tslib@1.14.1: - version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" - integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== - -tslib@^1.11.1, tslib@^1.9.0: - version "1.13.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -type-fest@^0.13.1: - version "0.13.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" - integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== - -type-fest@^0.3.0: - version "0.3.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" - integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== - -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== - -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -typescript@4.0.5: - version "4.0.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" - integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== - -uglify-js@^3.1.4, uglify-js@^3.4.9: - version "3.11.0" - resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.0.tgz#67317658d76c21e0e54d3224aee2df4ee6c3e1dc" - integrity sha512-e1KQFRCpOxnrJsJVqDUCjURq+wXvIn7cK2sRAx9XL3HYLL9aezOP4Pb1+Y3/o693EPk111Yj2Q+IUXxcpHlygQ== - -uid-number@0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" - integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= - -umask@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" - integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - -universal-user-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" - integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg== - dependencies: - os-name "^3.1.0" - -universal-user-agent@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" - integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -uri-js@^4.2.2: - version "4.4.0" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" - integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util-promisify@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz#3c2236476c4d32c5ff3c47002add7c13b9a82a53" - integrity sha1-PCI2R2xNMsX/PEcAKt18E7moKlM= - dependencies: - object.getownpropertydescriptors "^2.0.3" - -uuid@^3.0.1, uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: - version "3.0.4" - resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -validate-npm-package-name@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" - integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= - dependencies: - builtins "^1.0.3" - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -wcwidth@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" - integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= - dependencies: - defaults "^1.0.3" - -webidl-conversions@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== - -whatwg-url@^7.0.0: - version "7.1.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" - integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.9, which@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -windows-release@^3.1.0: - version "3.3.3" - resolved "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz#1c10027c7225743eec6b89df160d64c2e0293999" - integrity sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg== - dependencies: - execa "^1.0.0" - -wordwrap@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^2.0.0, write-file-atomic@^2.3.0, write-file-atomic@^2.4.2: - version "2.4.3" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" - integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - signal-exit "^3.0.2" - -write-json-file@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-2.3.0.tgz#2b64c8a33004d54b8698c76d585a77ceb61da32f" - integrity sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8= - dependencies: - detect-indent "^5.0.0" - graceful-fs "^4.1.2" - make-dir "^1.0.0" - pify "^3.0.0" - sort-keys "^2.0.0" - write-file-atomic "^2.0.0" - -write-json-file@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz#65bbdc9ecd8a1458e15952770ccbadfcff5fe62a" - integrity sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ== - dependencies: - detect-indent "^5.0.0" - graceful-fs "^4.1.15" - make-dir "^2.1.0" - pify "^4.0.1" - sort-keys "^2.0.0" - write-file-atomic "^2.4.2" - -write-pkg@^3.1.0: - version "3.2.0" - resolved "https://registry.npmjs.org/write-pkg/-/write-pkg-3.2.0.tgz#0e178fe97820d389a8928bc79535dbe68c2cff21" - integrity sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw== - dependencies: - sort-keys "^2.0.0" - write-json-file "^2.2.0" - -xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== - -yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: - version "3.1.1" - resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yargs-parser@^15.0.1: - version "15.0.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3" - integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^18.1.3: - version "18.1.3" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^14.2.2: - version "14.2.3" - resolved "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" - integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== - dependencies: - cliui "^5.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^15.0.1" diff --git a/packages-exp/auth-compat-exp/index.node.ts b/packages-exp/auth-compat-exp/index.node.ts deleted file mode 100644 index da6929c604f..00000000000 --- a/packages-exp/auth-compat-exp/index.node.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * This is the file that people using Node.js will actually import. You should - * only include this file if you have something specific about your - * implementation that mandates having a separate entrypoint. Otherwise you can - * just use index.ts - */ - -export * from './index'; diff --git a/packages-exp/auth-compat-exp/index.ts b/packages-exp/auth-compat-exp/index.ts deleted file mode 100644 index 58cd8c9f544..00000000000 --- a/packages-exp/auth-compat-exp/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import firebase from '@firebase/app-compat'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; -import * as impl from '@firebase/auth-exp/internal'; -import * as externs from '@firebase/auth-types-exp'; -import { - Component, - ComponentType, - InstantiationMode -} from '@firebase/component'; - -import { version } from './package.json'; -import { Auth } from './src/auth'; -import { Persistence } from './src/persistence'; -import { PhoneAuthProvider } from './src/phone_auth_provider'; -import { _getClientPlatform } from './src/platform'; -import { RecaptchaVerifier } from './src/recaptcha_verifier'; - -const AUTH_TYPE = 'auth'; - -// Create auth components to register with firebase. -// Provides Auth public APIs. -function registerAuthCompat(instance: _FirebaseNamespace): void { - instance.INTERNAL.registerComponent( - new Component( - AUTH_TYPE, - container => { - // getImmediate for FirebaseApp will always succeed - const app = container.getProvider('app').getImmediate(); - const auth = container.getProvider('auth-exp').getImmediate(); - return new Auth(app, auth as impl.AuthImpl); - }, - ComponentType.PUBLIC - ) - .setServiceProps({ - ActionCodeInfo: { - Operation: { - EMAIL_SIGNIN: externs.ActionCodeOperation.EMAIL_SIGNIN, - PASSWORD_RESET: externs.ActionCodeOperation.PASSWORD_RESET, - RECOVER_EMAIL: externs.ActionCodeOperation.RECOVER_EMAIL, - REVERT_SECOND_FACTOR_ADDITION: - externs.ActionCodeOperation.REVERT_SECOND_FACTOR_ADDITION, - VERIFY_AND_CHANGE_EMAIL: - externs.ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - VERIFY_EMAIL: externs.ActionCodeOperation.VERIFY_EMAIL - } - }, - EmailAuthProvider: impl.EmailAuthProvider, - FacebookAuthProvider: impl.FacebookAuthProvider, - GithubAuthProvider: impl.GithubAuthProvider, - GoogleAuthProvider: impl.GoogleAuthProvider, - OAuthProvider: impl.OAuthProvider, - // SAMLAuthProvider, - PhoneAuthProvider, - PhoneMultiFactorGenerator: impl.PhoneMultiFactorGenerator, - RecaptchaVerifier, - TwitterAuthProvider: impl.TwitterAuthProvider, - Auth: { - Persistence - }, - AuthCredential: impl.AuthCredential - // 'Error': fireauth.AuthError - }) - .setInstantiationMode(InstantiationMode.LAZY) - .setMultipleInstances(false) - ); - - instance.registerVersion('auth', version); -} - -impl.registerAuth(_getClientPlatform()); -registerAuthCompat(firebase as _FirebaseNamespace); diff --git a/packages-exp/auth-compat-exp/karma.conf.js b/packages-exp/auth-compat-exp/karma.conf.js deleted file mode 100644 index c0917db53d1..00000000000 --- a/packages-exp/auth-compat-exp/karma.conf.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const karmaBase = require('../../config/karma.base'); - -const files = ['src/**/*.test.ts']; - -module.exports = function (config) { - const karmaConfig = Object.assign({}, karmaBase, { - // files to load into karma - files: files, - preprocessors: { '**/*.ts': ['webpack', 'sourcemap'] }, - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'] - }); - - config.set(karmaConfig); -}; - -module.exports.files = files; diff --git a/packages-exp/auth-compat-exp/package.json b/packages-exp/auth-compat-exp/package.json deleted file mode 100644 index a7630380d59..00000000000 --- a/packages-exp/auth-compat-exp/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@firebase/auth-compat", - "version": "0.0.900", - "private": true, - "description": "FirebaseAuth compatibility package that uses API style compatible with Firebase@7 and prior versions", - "author": "Firebase (https://firebase.google.com/)", - "main": "dist/index.node.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "esm2017": "dist/index.esm2017.js", - "files": ["dist"], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c", - "build:deps": "lerna run --scope @firebase/auth-compat --include-dependencies build", - "build:release": "rollup -c rollup.config.release.js", - "dev": "rollup -c -w", - "test": "run-p lint test:all", - "test:all": "run-p test:browser test:node", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", - "test:browser": "karma start --single-run", - "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha src/**/*.test.* --config ../../config/mocharc.node.js" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x", - "@firebase/app-types": "0.x" - }, - "dependencies": { - "@firebase/auth-types": "0.10.1", - "@firebase/auth-exp": "0.0.900", - "@firebase/auth-types-exp": "0.0.900", - "@firebase/component": "0.1.21", - "@firebase/util": "0.3.4", - "tslib": "^1.11.1" - }, - "license": "Apache-2.0", - "devDependencies": { - "@firebase/app-compat": "0.x", - "rollup": "2.35.1", - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-replace": "2.2.0", - "rollup-plugin-typescript2": "0.29.0", - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages-exp/auth-compat-exp", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "dist/index.d.ts", - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/auth-compat-exp/rollup.config.js b/packages-exp/auth-compat-exp/rollup.config.js deleted file mode 100644 index 06596518856..00000000000 --- a/packages-exp/auth-compat-exp/rollup.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getAllBuilds } from './rollup.config.shared'; - -// eslint-disable-next-line import/no-default-export -export default getAllBuilds({}); diff --git a/packages-exp/auth-compat-exp/rollup.config.release.js b/packages-exp/auth-compat-exp/rollup.config.release.js deleted file mode 100644 index 03fbdc6434d..00000000000 --- a/packages-exp/auth-compat-exp/rollup.config.release.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import { getAllBuilds } from './rollup.config.shared'; - -// eslint-disable-next-line import/no-default-export -export default getAllBuilds({ - clean: true, - abortOnError: false, - transformers: [importPathTransformer] -}); diff --git a/packages-exp/auth-compat-exp/rollup.config.shared.js b/packages-exp/auth-compat-exp/rollup.config.shared.js deleted file mode 100644 index abdf4bc53f1..00000000000 --- a/packages-exp/auth-compat-exp/rollup.config.shared.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; -import resolve from '@rollup/plugin-node-resolve'; -import { uglify } from 'rollup-plugin-uglify'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import pkg from './package.json'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * Common plugins for all builds - */ -const commonPlugins = [json(), resolve()]; - -/** - * ES5 Builds - */ -export function getEs5Builds(additionalTypescriptPlugins = {}) { - const es5BuildPlugins = [ - ...commonPlugins, - typescriptPlugin({ - typescript, - ...additionalTypescriptPlugins - }) - ]; - - return [ - /** - * Browser Builds - */ - { - input: 'index.ts', - output: [{ file: pkg.module, format: 'esm', sourcemap: true }], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - /** - * Node.js Build - */ - { - input: 'index.node.ts', - output: [{ file: pkg.main, format: 'cjs', sourcemap: true }], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - /** - * UMD build - */ - { - input: `./index.ts`, - output: { - compact: true, - file: `dist/firebase-auth.js`, - format: 'umd', - sourcemap: true, - extend: true, - name: 'firebase', - globals: { - '@firebase/app-compat': 'firebase' - }, - /** - * use iife to avoid below error in the old Safari browser - * SyntaxError: Functions cannot be declared in a nested block in strict mode - * https://github.com/firebase/firebase-js-sdk/issues/1228 - * - */ - intro: ` - try { - (function() {`, - outro: ` - }).apply(this, arguments); - } catch(err) { - console.error(err); - throw new Error( - 'Cannot instantiate firebase-auth.js - ' + - 'be sure to load firebase-app.js first.' - ); - }` - }, - plugins: [...es5BuildPlugins, uglify()], - external: ['@firebase/app-compat'] - } - ]; -} - -/** - * ES2017 Builds - */ -export function getEs2017Builds(additionalTypescriptPlugins = {}) { - const es2017BuildPlugins = [ - ...commonPlugins, - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - ...additionalTypescriptPlugins - }) - ]; - return [ - /** - * Browser Builds - */ - { - input: 'index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - plugins: es2017BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } - ]; -} - -export function getAllBuilds(additionalTypescriptPlugins = {}) { - return [ - ...getEs5Builds(additionalTypescriptPlugins), - ...getEs2017Builds(additionalTypescriptPlugins) - ]; -} diff --git a/packages-exp/auth-compat-exp/src/auth.test.ts b/packages-exp/auth-compat-exp/src/auth.test.ts deleted file mode 100644 index d7f50cd86a0..00000000000 --- a/packages-exp/auth-compat-exp/src/auth.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types'; -import * as impl from '@firebase/auth-exp/internal'; -import { Config } from '@firebase/auth-types-exp'; -import { expect, use } from 'chai'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; -import { Auth } from './auth'; - -use(sinonChai); - -// For the most part, the auth methods just call straight through. Some parts -// of the auth compat layer are more complicated: these tests cover those -describe('auth compat', () => { - context('redirect persistence key storage', () => { - let underlyingAuth: impl.AuthImpl; - let app: FirebaseApp; - beforeEach(() => { - app = { options: { apiKey: 'api-key' } } as FirebaseApp; - underlyingAuth = new impl.AuthImpl(app, { - apiKey: 'api-key' - } as Config); - sinon.stub(underlyingAuth, '_initializeWithPersistence'); - }); - - afterEach(() => { - sinon.restore; - }); - - it('saves the persistence into session storage if available', () => { - const authCompat = new Auth(app, underlyingAuth); - if (typeof self !== 'undefined') { - sinon.stub(underlyingAuth, '_getPersistence').returns('TEST'); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - authCompat.signInWithRedirect(new impl.GoogleAuthProvider()); - expect( - sessionStorage.getItem('firebase:persistence:api-key:undefined') - ).to.eq('TEST'); - } - }); - - it('pulls the persistence and sets as the main persitsence if set', () => { - if (typeof self !== 'undefined') { - sessionStorage.setItem( - 'firebase:persistence:api-key:undefined', - 'NONE' - ); - new Auth(app, underlyingAuth); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expect( - underlyingAuth._initializeWithPersistence - ).to.have.been.calledWith( - [ - impl._getInstance(impl.inMemoryPersistence), - impl._getInstance(impl.indexedDBLocalPersistence) - ], - impl.browserPopupRedirectResolver - ); - } - }); - }); -}); diff --git a/packages-exp/auth-compat-exp/src/auth.ts b/packages-exp/auth-compat-exp/src/auth.ts deleted file mode 100644 index e6fd52a02f8..00000000000 --- a/packages-exp/auth-compat-exp/src/auth.ts +++ /dev/null @@ -1,376 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types'; -import { _FirebaseService } from '@firebase/app-types-exp'; -import * as impl from '@firebase/auth-exp/internal'; -import * as compat from '@firebase/auth-types'; -import * as externs from '@firebase/auth-types-exp'; -import { - ErrorFn, - isIndexedDBAvailable, - Observer, - Unsubscribe -} from '@firebase/util'; - -import { _validatePersistenceArgument, Persistence } from './persistence'; -import { _isPopupRedirectSupported } from './platform'; -import { User } from './user'; -import { - convertConfirmationResult, - convertCredential -} from './user_credential'; -import { unwrap, Wrapper } from './wrap'; - -const PERSISTENCE_KEY = 'persistence'; -const _assert: typeof impl._assert = impl._assert; - -export class Auth - implements compat.FirebaseAuth, Wrapper, _FirebaseService { - // private readonly auth: impl.AuthImpl; - - constructor(readonly app: FirebaseApp, private readonly auth: impl.AuthImpl) { - const { apiKey } = app.options; - if (this.auth._deleted) { - return; - } - - // Note this is slightly different behavior: in this case, the stored - // persistence is checked *first* rather than last. This is because we want - // the fallback (if no user is found) to be the stored persistence type - const storedPersistence = this.getPersistenceFromRedirect(); - const persistences = storedPersistence ? [storedPersistence] : []; - persistences.push(impl.indexedDBLocalPersistence); - - // TODO(avolkovi): Implement proper persistence fallback - const hierarchy = persistences.map(impl._getInstance); - - // TODO: platform needs to be determined using heuristics - _assert(apiKey, impl.AuthErrorCode.INVALID_API_KEY, { - appName: app.name - }); - - this.auth._updateErrorMap(impl.debugErrorMap); - - // This promise is intended to float; auth initialization happens in the - // background, meanwhile the auth object may be used by the app. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.auth._initializeWithPersistence( - hierarchy, - impl.browserPopupRedirectResolver - ); - } - - get currentUser(): compat.User | null { - if (!this.auth.currentUser) { - return null; - } - - return User.getOrCreate(this.auth.currentUser); - } - get languageCode(): string | null { - return this.auth.languageCode; - } - get settings(): compat.AuthSettings { - return this.auth.settings; - } - get tenantId(): string | null { - return this.auth.tenantId; - } - useDeviceLanguage(): void { - this.auth.useDeviceLanguage(); - } - signOut(): Promise { - return this.auth.signOut(); - } - useEmulator(url: string, options?: { disableWarnings: boolean }): void { - this.auth.useEmulator(url, options); - } - applyActionCode(code: string): Promise { - return impl.applyActionCode(this.auth, code); - } - - checkActionCode(code: string): Promise { - return impl.checkActionCode(this.auth, code); - } - - confirmPasswordReset(code: string, newPassword: string): Promise { - return impl.confirmPasswordReset(this.auth, code, newPassword); - } - - async createUserWithEmailAndPassword( - email: string, - password: string - ): Promise { - return convertCredential( - this.auth, - impl.createUserWithEmailAndPassword(this.auth, email, password) - ); - } - fetchProvidersForEmail(email: string): Promise { - return this.fetchSignInMethodsForEmail(email); - } - fetchSignInMethodsForEmail(email: string): Promise { - return impl.fetchSignInMethodsForEmail(this.auth, email); - } - isSignInWithEmailLink(emailLink: string): boolean { - return impl.isSignInWithEmailLink(this.auth, emailLink); - } - async getRedirectResult(): Promise { - _assert( - _isPopupRedirectSupported(), - this.auth, - impl.AuthErrorCode.OPERATION_NOT_SUPPORTED - ); - const credential = await impl.getRedirectResult( - this.auth, - impl.browserPopupRedirectResolver - ); - if (!credential) { - return { - credential: null, - user: null - }; - } - return convertCredential(this.auth, Promise.resolve(credential)); - } - onAuthStateChanged( - nextOrObserver: Observer | ((a: compat.User | null) => unknown), - errorFn?: (error: compat.Error) => unknown, - completed?: Unsubscribe - ): Unsubscribe { - const { next, error, complete } = wrapObservers( - nextOrObserver, - errorFn, - completed - ); - return this.auth.onAuthStateChanged(next!, error, complete); - } - onIdTokenChanged( - nextOrObserver: Observer | ((a: compat.User | null) => unknown), - errorFn?: (error: compat.Error) => unknown, - completed?: Unsubscribe - ): Unsubscribe { - const { next, error, complete } = wrapObservers( - nextOrObserver, - errorFn, - completed - ); - return this.auth.onIdTokenChanged(next!, error, complete); - } - sendSignInLinkToEmail( - email: string, - actionCodeSettings: compat.ActionCodeSettings - ): Promise { - return impl.sendSignInLinkToEmail(this.auth, email, actionCodeSettings); - } - sendPasswordResetEmail( - email: string, - actionCodeSettings?: compat.ActionCodeSettings | null - ): Promise { - return impl.sendPasswordResetEmail( - this.auth, - email, - actionCodeSettings || undefined - ); - } - async setPersistence(persistence: string): Promise { - function convertPersistence( - auth: externs.Auth, - persistenceCompat: string - ): externs.Persistence { - _validatePersistenceArgument(auth, persistence); - switch (persistenceCompat) { - case Persistence.SESSION: - return impl.browserSessionPersistence; - case Persistence.LOCAL: - return isIndexedDBAvailable() - ? impl.indexedDBLocalPersistence - : impl.browserLocalPersistence; - case Persistence.NONE: - return impl.inMemoryPersistence; - default: - return impl._fail(impl.AuthErrorCode.ARGUMENT_ERROR, { - appName: auth.name - }); - } - } - - return this.auth.setPersistence(convertPersistence(this.auth, persistence)); - } - - signInAndRetrieveDataWithCredential( - credential: compat.AuthCredential - ): Promise { - return this.signInWithCredential(credential); - } - signInAnonymously(): Promise { - return convertCredential(this.auth, impl.signInAnonymously(this.auth)); - } - signInWithCredential( - credential: compat.AuthCredential - ): Promise { - return convertCredential( - this.auth, - impl.signInWithCredential(this.auth, credential as externs.AuthCredential) - ); - } - signInWithCustomToken(token: string): Promise { - return convertCredential( - this.auth, - impl.signInWithCustomToken(this.auth, token) - ); - } - signInWithEmailAndPassword( - email: string, - password: string - ): Promise { - return convertCredential( - this.auth, - impl.signInWithEmailAndPassword(this.auth, email, password) - ); - } - signInWithEmailLink( - email: string, - emailLink?: string - ): Promise { - return convertCredential( - this.auth, - impl.signInWithEmailLink(this.auth, email, emailLink) - ); - } - signInWithPhoneNumber( - phoneNumber: string, - applicationVerifier: compat.ApplicationVerifier - ): Promise { - return convertConfirmationResult( - this.auth, - impl.signInWithPhoneNumber( - this.auth, - phoneNumber, - unwrap(applicationVerifier) - ) - ); - } - async signInWithPopup( - provider: compat.AuthProvider - ): Promise { - _assert( - _isPopupRedirectSupported(), - this.auth, - impl.AuthErrorCode.OPERATION_NOT_SUPPORTED - ); - return convertCredential( - this.auth, - impl.signInWithPopup( - this.auth, - provider as externs.AuthProvider, - impl.browserPopupRedirectResolver - ) - ); - } - async signInWithRedirect(provider: compat.AuthProvider): Promise { - _assert( - _isPopupRedirectSupported(), - this.auth, - impl.AuthErrorCode.OPERATION_NOT_SUPPORTED - ); - this.savePersistenceForRedirect(); - return impl.signInWithRedirect( - this.auth, - provider as externs.AuthProvider, - impl.browserPopupRedirectResolver - ); - } - updateCurrentUser(user: compat.User | null): Promise { - return this.auth.updateCurrentUser(unwrap(user)); - } - verifyPasswordResetCode(code: string): Promise { - return impl.verifyPasswordResetCode(this.auth, code); - } - unwrap(): externs.Auth { - return this.auth; - } - _delete(): Promise { - return this.auth._delete(); - } - - private savePersistenceForRedirect(): void { - const win = getSelfWindow(); - const key = impl._persistenceKeyName( - PERSISTENCE_KEY, - this.auth.config.apiKey, - this.auth.name - ); - if (win?.sessionStorage) { - win.sessionStorage.setItem(key, this.auth._getPersistence()); - } - } - - private getPersistenceFromRedirect(): externs.Persistence | null { - const win = getSelfWindow(); - if (!win?.sessionStorage) { - return null; - } - - const key = impl._persistenceKeyName( - PERSISTENCE_KEY, - this.auth.config.apiKey, - this.auth.name - ); - const persistence = win.sessionStorage.getItem(key); - - switch (persistence) { - case impl.inMemoryPersistence.type: - return impl.inMemoryPersistence; - case impl.indexedDBLocalPersistence.type: - return impl.indexedDBLocalPersistence; - case impl.browserSessionPersistence.type: - return impl.browserSessionPersistence; - case impl.browserLocalPersistence.type: - return impl.browserLocalPersistence; - default: - return null; - } - } -} - -function getSelfWindow(): Window | null { - return typeof window !== 'undefined' ? window : null; -} - -function wrapObservers( - nextOrObserver: Observer | ((a: compat.User | null) => unknown), - error?: (error: compat.Error) => unknown, - complete?: Unsubscribe -): Partial> { - let next = nextOrObserver; - if (typeof nextOrObserver !== 'function') { - ({ next, error, complete } = nextOrObserver); - } - - // We know 'next' is now a function - const oldNext = next as (a: compat.User | null) => unknown; - - const newNext = (user: externs.User | null): unknown => - oldNext(user && User.getOrCreate(user as externs.User)); - return { - next: newNext, - error: error as ErrorFn, - complete - }; -} diff --git a/packages-exp/auth-compat-exp/src/persistence.ts b/packages-exp/auth-compat-exp/src/persistence.ts deleted file mode 100644 index 920a87814ca..00000000000 --- a/packages-exp/auth-compat-exp/src/persistence.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _assert, AuthErrorCode } from '@firebase/auth-exp/internal'; -import * as externs from '@firebase/auth-types-exp'; -import { isIndexedDBAvailable, isNode, isReactNative } from '@firebase/util'; -import { _isWebStorageSupported, _isWorker } from './platform'; - -export const Persistence = { - LOCAL: 'LOCAL', - NONE: 'NONE', - SESSION: 'SESSION' -}; - -/** - * Validates that an argument is a valid persistence value. If an invalid type - * is specified, an error is thrown synchronously. - */ -export function _validatePersistenceArgument( - auth: externs.Auth, - persistence: string -): void { - _assert( - Object.values(Persistence).includes(persistence), - auth, - AuthErrorCode.INVALID_PERSISTENCE - ); - // Validate if the specified type is supported in the current environment. - if (isReactNative()) { - // This is only supported in a browser. - _assert( - persistence !== Persistence.SESSION, - auth, - AuthErrorCode.UNSUPPORTED_PERSISTENCE - ); - return; - } - if (isNode()) { - // Only none is supported in Node.js. - _assert( - persistence === Persistence.NONE, - auth, - AuthErrorCode.UNSUPPORTED_PERSISTENCE - ); - return; - } - if (_isWorker()) { - // In a worker environment, either LOCAL or NONE are supported. - // If indexedDB not supported and LOCAL provided, throw an error - _assert( - persistence === Persistence.NONE || - (persistence === Persistence.LOCAL && isIndexedDBAvailable()), - auth, - AuthErrorCode.UNSUPPORTED_PERSISTENCE - ); - return; - } - // This is restricted by what the browser supports. - _assert( - persistence === Persistence.NONE || _isWebStorageSupported(), - auth, - AuthErrorCode.UNSUPPORTED_PERSISTENCE - ); -} diff --git a/packages-exp/auth-compat-exp/src/phone_auth_provider.ts b/packages-exp/auth-compat-exp/src/phone_auth_provider.ts deleted file mode 100644 index 4a9441f944a..00000000000 --- a/packages-exp/auth-compat-exp/src/phone_auth_provider.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as impl from '@firebase/auth-exp/internal'; -import * as compat from '@firebase/auth-types'; -import * as externs from '@firebase/auth-types-exp'; -import firebase from '@firebase/app-compat'; -import { unwrap, Wrapper } from './wrap'; - -export class PhoneAuthProvider - implements compat.PhoneAuthProvider, Wrapper { - providerId = 'phone'; - private readonly phoneProvider: impl.PhoneAuthProvider; - - static PHONE_SIGN_IN_METHOD = impl.PhoneAuthProvider.PHONE_SIGN_IN_METHOD; - static PROVIDER_ID = impl.PhoneAuthProvider.PROVIDER_ID; - - static credential( - verificationId: string, - verificationCode: string - ): compat.AuthCredential { - return impl.PhoneAuthProvider.credential(verificationId, verificationCode); - } - - constructor() { - this.phoneProvider = new impl.PhoneAuthProvider(unwrap(firebase.auth!())); - } - - verifyPhoneNumber( - phoneInfoOptions: - | string - | compat.PhoneSingleFactorInfoOptions - | compat.PhoneMultiFactorEnrollInfoOptions - | compat.PhoneMultiFactorSignInInfoOptions, - applicationVerifier: compat.ApplicationVerifier - ): Promise { - return this.phoneProvider.verifyPhoneNumber( - // The implementation matches but the types are subtly incompatible - // eslint-disable-next-line @typescript-eslint/no-explicit-any - phoneInfoOptions as any, - unwrap(applicationVerifier) - ); - } - - unwrap(): externs.PhoneAuthProvider { - return this.phoneProvider; - } -} diff --git a/packages-exp/auth-compat-exp/src/recaptcha_verifier.ts b/packages-exp/auth-compat-exp/src/recaptcha_verifier.ts deleted file mode 100644 index baf38592db8..00000000000 --- a/packages-exp/auth-compat-exp/src/recaptcha_verifier.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import firebase from '@firebase/app-compat'; -import { FirebaseApp } from '@firebase/app-types'; -import * as impl from '@firebase/auth-exp/internal'; -import * as compat from '@firebase/auth-types'; -import * as externs from '@firebase/auth-types-exp'; -import { unwrap, Wrapper } from './wrap'; - -const _assert: typeof impl._assert = impl._assert; - -export class RecaptchaVerifier - implements compat.RecaptchaVerifier, Wrapper { - readonly verifier: externs.RecaptchaVerifier; - type: string; - constructor( - container: HTMLElement | string, - parameters?: object | null, - app: FirebaseApp = firebase.app() - ) { - // API key is required for web client RPC calls. - _assert(app.options?.apiKey, impl.AuthErrorCode.INVALID_API_KEY, { - appName: app.name - }); - this.verifier = new impl.RecaptchaVerifier( - container, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parameters as any, - - unwrap(app.auth!()) - ); - this.type = this.verifier.type; - } - clear(): void { - this.verifier.clear(); - } - render(): Promise { - return this.verifier.render(); - } - verify(): Promise { - return this.verifier.verify(); - } - unwrap(): externs.ApplicationVerifier { - return this.verifier; - } -} diff --git a/packages-exp/auth-compat-exp/src/user.ts b/packages-exp/auth-compat-exp/src/user.ts deleted file mode 100644 index 2bd42039e26..00000000000 --- a/packages-exp/auth-compat-exp/src/user.ts +++ /dev/null @@ -1,233 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as impl from '@firebase/auth-exp/internal'; -import * as compat from '@firebase/auth-types'; -import * as externs from '@firebase/auth-types-exp'; -import { - convertConfirmationResult, - convertCredential -} from './user_credential'; -import { unwrap, Wrapper } from './wrap'; - -export class User implements compat.User, Wrapper { - // Maintain a map so that there's always a 1:1 mapping between new User and - // legacy compat users - private static readonly USER_MAP = new WeakMap(); - - readonly multiFactor: compat.MultiFactorUser; - - private constructor(private readonly user: externs.User) { - this.multiFactor = impl.multiFactor(user); - } - - static getOrCreate(user: externs.User): User { - if (!User.USER_MAP.has(user)) { - User.USER_MAP.set(user, new User(user)); - } - - return User.USER_MAP.get(user)!; - } - - delete(): Promise { - return this.user.delete(); - } - reload(): Promise { - return this.user.reload(); - } - toJSON(): object { - return this.user.toJSON(); - } - getIdTokenResult(forceRefresh?: boolean): Promise { - return this.user.getIdTokenResult(forceRefresh); - } - getIdToken(forceRefresh?: boolean): Promise { - return this.user.getIdToken(forceRefresh); - } - linkAndRetrieveDataWithCredential( - credential: compat.AuthCredential - ): Promise { - return this.linkWithCredential(credential); - } - async linkWithCredential( - credential: compat.AuthCredential - ): Promise { - return convertCredential( - this.auth, - impl.linkWithCredential(this.user, credential as externs.AuthCredential) - ); - } - async linkWithPhoneNumber( - phoneNumber: string, - applicationVerifier: compat.ApplicationVerifier - ): Promise { - return convertConfirmationResult( - this.auth, - impl.linkWithPhoneNumber( - this.user, - phoneNumber, - unwrap(applicationVerifier) - ) - ); - } - async linkWithPopup( - provider: compat.AuthProvider - ): Promise { - return convertCredential( - this.auth, - impl.linkWithPopup( - this.user, - provider as externs.AuthProvider, - impl.browserPopupRedirectResolver - ) - ); - } - linkWithRedirect(provider: compat.AuthProvider): Promise { - return impl.linkWithRedirect( - this.user, - provider as externs.AuthProvider, - impl.browserPopupRedirectResolver - ); - } - reauthenticateAndRetrieveDataWithCredential( - credential: compat.AuthCredential - ): Promise { - return this.reauthenticateWithCredential(credential); - } - async reauthenticateWithCredential( - credential: compat.AuthCredential - ): Promise { - return convertCredential( - (this.auth as unknown) as externs.Auth, - impl.reauthenticateWithCredential( - this.user, - credential as externs.AuthCredential - ) - ); - } - reauthenticateWithPhoneNumber( - phoneNumber: string, - applicationVerifier: compat.ApplicationVerifier - ): Promise { - return convertConfirmationResult( - this.auth, - impl.reauthenticateWithPhoneNumber( - this.user, - phoneNumber, - unwrap(applicationVerifier) - ) - ); - } - reauthenticateWithPopup( - provider: compat.AuthProvider - ): Promise { - return convertCredential( - this.auth, - impl.reauthenticateWithPopup( - this.user, - provider as externs.AuthProvider, - impl.browserPopupRedirectResolver - ) - ); - } - reauthenticateWithRedirect(provider: compat.AuthProvider): Promise { - return impl.reauthenticateWithRedirect( - this.user, - provider as externs.AuthProvider, - impl.browserPopupRedirectResolver - ); - } - sendEmailVerification( - actionCodeSettings?: compat.ActionCodeSettings | null - ): Promise { - return impl.sendEmailVerification(this.user, actionCodeSettings); - } - async unlink(providerId: string): Promise { - await impl.unlink(this.user, providerId as externs.ProviderId); - return this; - } - updateEmail(newEmail: string): Promise { - return impl.updateEmail(this.user, newEmail); - } - updatePassword(newPassword: string): Promise { - return impl.updatePassword(this.user, newPassword); - } - updatePhoneNumber(phoneCredential: compat.AuthCredential): Promise { - return impl.updatePhoneNumber( - this.user, - phoneCredential as externs.AuthCredential - ); - } - updateProfile(profile: { - displayName?: string | null; - photoURL?: string | null; - }): Promise { - return impl.updateProfile(this.user, profile); - } - verifyBeforeUpdateEmail( - newEmail: string, - actionCodeSettings?: compat.ActionCodeSettings | null - ): Promise { - return impl.verifyBeforeUpdateEmail( - this.user, - newEmail, - actionCodeSettings - ); - } - unwrap(): externs.User { - return this.user; - } - get emailVerified(): boolean { - return this.user.emailVerified; - } - get isAnonymous(): boolean { - return this.user.isAnonymous; - } - get metadata(): compat.UserMetadata { - return this.user.metadata; - } - get phoneNumber(): string | null { - return this.user.phoneNumber; - } - get providerData(): Array { - return this.user.providerData; - } - get refreshToken(): string { - return this.user.refreshToken; - } - get tenantId(): string | null { - return this.user.tenantId; - } - get displayName(): string | null { - return this.user.displayName; - } - get email(): string | null { - return this.user.email; - } - get photoURL(): string | null { - return this.user.photoURL; - } - get providerId(): string { - return this.user.providerId; - } - get uid(): string { - return this.user.uid; - } - private get auth(): externs.Auth { - return ((this.user as impl.UserImpl).auth as unknown) as externs.Auth; - } -} diff --git a/packages-exp/auth-compat-exp/src/user_credential.ts b/packages-exp/auth-compat-exp/src/user_credential.ts deleted file mode 100644 index 628340ebb19..00000000000 --- a/packages-exp/auth-compat-exp/src/user_credential.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as impl from '@firebase/auth-exp/internal'; -import * as compat from '@firebase/auth-types'; -import * as externs from '@firebase/auth-types-exp'; -import { User } from './user'; - -function credentialFromResponse( - userCredential: impl.UserCredential -): externs.AuthCredential | null { - const { providerId, _tokenResponse } = userCredential; - if (!_tokenResponse) { - return null; - } - // Handle phone Auth credential responses, as they have a different format - // from other backend responses (i.e. no providerId). - if ('temporaryProof' in _tokenResponse && 'phoneNumber' in _tokenResponse) { - return impl.PhoneAuthProvider.credentialFromResult(userCredential); - } - // Email and password is not supported as there is no situation where the - // server would return the password to the client. - if (!providerId || providerId === externs.ProviderId.PASSWORD) { - return null; - } - - switch (providerId) { - case externs.ProviderId.GOOGLE: - return impl.GoogleAuthProvider.credentialFromResult(userCredential); - case externs.ProviderId.FACEBOOK: - return impl.FacebookAuthProvider.credentialFromResult(userCredential!); - case externs.ProviderId.GITHUB: - return impl.GithubAuthProvider.credentialFromResult(userCredential!); - case externs.ProviderId.TWITTER: - return impl.TwitterAuthProvider.credentialFromResult(userCredential); - default: - const { - oauthIdToken, - oauthAccessToken, - oauthTokenSecret, - pendingToken, - nonce - } = _tokenResponse as impl.SignInWithIdpResponse; - if ( - !oauthAccessToken && - !oauthTokenSecret && - !oauthIdToken && - !pendingToken - ) { - return null; - } - // TODO(avolkovi): uncomment this and get it working with SAML & OIDC - // if (pendingToken) { - // if (providerId.indexOf(compat.constants.SAML_PREFIX) == 0) { - // return new impl.SAMLAuthCredential(providerId, pendingToken); - // } else { - // // OIDC and non-default providers excluding Twitter. - // return new impl.OAuthCredential( - // providerId, - // { - // pendingToken, - // idToken: oauthIdToken, - // accessToken: oauthAccessToken - // }, - // providerId); - // } - // } - return new impl.OAuthProvider(providerId).credential({ - idToken: oauthIdToken, - accessToken: oauthAccessToken, - rawNonce: nonce - }); - } -} - -export async function convertCredential( - auth: externs.Auth, - credentialPromise: Promise -): Promise { - let credential: externs.UserCredential; - try { - credential = await credentialPromise; - } catch (e) { - if (e.code === 'auth/multi-factor-auth-required') { - e.resolver = impl.getMultiFactorResolver(auth, e); - } - throw e; - } - const { operationType, user } = await credential; - - return { - operationType, - credential: credentialFromResponse(credential as impl.UserCredential), - additionalUserInfo: impl.getAdditionalUserInfo( - credential as impl.UserCredential - ), - user: User.getOrCreate(user) - }; -} - -export async function convertConfirmationResult( - auth: externs.Auth, - confirmationResultPromise: Promise -): Promise { - const confirmationResultExp = await confirmationResultPromise; - return { - verificationId: confirmationResultExp.verificationId, - confirm: (verificationCode: string) => - convertCredential(auth, confirmationResultExp.confirm(verificationCode)) - }; -} diff --git a/packages-exp/auth-compat-exp/src/wrap.ts b/packages-exp/auth-compat-exp/src/wrap.ts deleted file mode 100644 index 43c9ba5c417..00000000000 --- a/packages-exp/auth-compat-exp/src/wrap.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface Wrapper { - unwrap(): T; -} - -export function unwrap(object: unknown): T { - return (object as Wrapper).unwrap(); -} diff --git a/packages-exp/auth-compat-exp/tsconfig.json b/packages-exp/auth-compat-exp/tsconfig.json deleted file mode 100644 index a06ed9a374c..00000000000 --- a/packages-exp/auth-compat-exp/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist" - }, - "exclude": [ - "dist/**/*" - ] -} \ No newline at end of file diff --git a/packages-exp/auth-exp/.eslintrc.js b/packages-exp/auth-exp/.eslintrc.js deleted file mode 100644 index 674937d0c30..00000000000 --- a/packages-exp/auth-exp/.eslintrc.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module.exports = { - extends: '../../config/.eslintrc.js', - ignorePatterns: ['demo/'], - parserOptions: { - project: 'tsconfig.json', - // to make vscode-eslint work with monorepo - // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 - tsconfigRootDir: __dirname - } -}; diff --git a/packages-exp/auth-exp/README.md b/packages-exp/auth-exp/README.md deleted file mode 100644 index 6b08dc72029..00000000000 --- a/packages-exp/auth-exp/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @firebase/auth-exp - -This is the Firebase Authentication component of the Firebase JS SDK. - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** \ No newline at end of file diff --git a/packages-exp/auth-exp/api-extractor.json b/packages-exp/auth-exp/api-extractor.json deleted file mode 100644 index 620d10a071c..00000000000 --- a/packages-exp/auth-exp/api-extractor.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/dist/index.d.ts" -} \ No newline at end of file diff --git a/packages-exp/auth-exp/demo/.gitignore b/packages-exp/auth-exp/demo/.gitignore deleted file mode 100644 index 798f66cee47..00000000000 --- a/packages-exp/auth-exp/demo/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -src/config.js -.firebaserc -.firebase -public/service-worker.* -public/web-worker.* -public/index.js* \ No newline at end of file diff --git a/packages-exp/auth-exp/demo/README.md b/packages-exp/auth-exp/demo/README.md deleted file mode 100644 index 6e62f12fa01..00000000000 --- a/packages-exp/auth-exp/demo/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Firebase-Auth for web - Auth Demo (Auth Next) - -## Prerequisite - -You need to have created a Firebase Project in the -[Firebase Console](https://firebase.google.com/console/) as well as configured a web app. - -## Installation -Make sure you run `yarn` to install all dependencies in the root directory. - -Enable the Auth providers you would like to offer your users in the console, under -Auth > Sign-in methods. - -Run: - -```bash -git clone https://github.com/firebase/firebase-js-sdk.git -cd firebase-js-sdk/packages-exp/auth-exp/demo -``` - -This will clone the repository in the current directory. - -If you want to be able to deploy the demo app to one of your own Firebase Hosting instance, -configure it using the following command: - -```bash -firebase use --add -``` - -Select the project you have created in the prerequisite, and type in `default` or -any other name as the alias to use for this project. - -Copy `src/sample-config.js` to `src/config.js`: - -```bash -cp src/sample-config.js src/config.js -``` - -Then copy and paste the Web snippet config found in the console (either by clicking "Add Firebase to -your web app" button in your Project overview, or clicking the "Web setup" button in the Auth page) -in the `config.js` file. - -## Deploy - -Before deploying, you may need to build the auth-exp package: -```bash -yarn build:deps -``` - -This can take some time, and you only need to do it if you've modified the auth-exp package. - -To run the app locally, simply issue the following command in the `auth-exp/demo` directory: - -```bash -yarn run demo -``` - -This will compile all the files needed to run Firebase Auth, and start a Firebase server locally at -[http://localhost:5000](http://localhost:5000). - diff --git a/packages-exp/auth-exp/demo/database.rules.json b/packages-exp/auth-exp/demo/database.rules.json deleted file mode 100644 index 03292687cf2..00000000000 --- a/packages-exp/auth-exp/demo/database.rules.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "rules": { - ".read": "auth != null", - ".write": "auth != null", - "users": { - "$user_id": { - ".read": "$user_id === auth.uid", - ".write": "$user_id === auth.uid" - } - } - } -} - diff --git a/packages-exp/auth-exp/demo/firebase.json b/packages-exp/auth-exp/demo/firebase.json deleted file mode 100644 index 300ada0abc9..00000000000 --- a/packages-exp/auth-exp/demo/firebase.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "database": { - "rules": "database.rules.json" - }, - "hosting": { - "public": "public", - "rewrites": [ - { - "source": "/checkIfAuthenticated", - "function": "checkIfAuthenticated" - } - ] - } -} diff --git a/packages-exp/auth-exp/demo/functions/index.js b/packages-exp/auth-exp/demo/functions/index.js deleted file mode 100644 index f3a9091bf20..00000000000 --- a/packages-exp/auth-exp/demo/functions/index.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Defines the HTTP endpoints hosted via firebase functions which - * are used to test service worker functionality for Firebase Auth via demo - * app. - */ - -const functions = require('firebase-functions'); -const admin = require('firebase-admin'); - -admin.initializeApp(functions.config().firebase); - -exports.checkIfAuthenticated = functions.https.onRequest((req, res) => { - const idToken = req.get('x-id-token'); - res.setHeader('Content-Type', 'application/json'); - if (idToken) { - admin - .auth() - .verifyIdToken(idToken) - .then(decodedIdToken => { - res.status(200).send(JSON.stringify({ uid: decodedIdToken.sub })); - }) - .catch(error => { - res.status(400).send(JSON.stringify({ error: error.code })); - }); - } else { - res.status(403).send(JSON.stringify({ error: 'Unauthorized access' })); - } -}); diff --git a/packages-exp/auth-exp/demo/functions/package.json b/packages-exp/auth-exp/demo/functions/package.json deleted file mode 100644 index 461ef8bfc4b..00000000000 --- a/packages-exp/auth-exp/demo/functions/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "functions", - "description": "Cloud Functions for Firebase", - "scripts": { - "serve": "firebase serve --only functions", - "shell": "firebase experimental:functions:shell", - "start": "yarn shell", - "deploy": "firebase deploy --only functions", - "logs": "firebase functions:log" - }, - "dependencies": { - "firebase-admin": "8.13.0", - "firebase-functions": "3.13.0" - }, - "private": true, - "engines": { - "node": "10" - } -} diff --git a/packages-exp/auth-exp/demo/functions/yarn.lock b/packages-exp/auth-exp/demo/functions/yarn.lock deleted file mode 100644 index 0193d674cbb..00000000000 --- a/packages-exp/auth-exp/demo/functions/yarn.lock +++ /dev/null @@ -1,1730 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@firebase/app-types@0.6.1": - version "0.6.1" - resolved "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz#dcbd23030a71c0c74fc95d4a3f75ba81653850e9" - integrity sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg== - -"@firebase/auth-interop-types@0.1.5": - version "0.1.5" - resolved "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz#9fc9bd7c879f16b8d1bb08373a0f48c3a8b74557" - integrity sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw== - -"@firebase/component@0.1.19": - version "0.1.19" - resolved "https://registry.npmjs.org/@firebase/component/-/component-0.1.19.tgz#bd2ac601652c22576b574c08c40da245933dbac7" - integrity sha512-L0S3g8eqaerg8y0zox3oOHSTwn/FE8RbcRHiurnbESvDViZtP5S5WnhuAPd7FnFxa8ElWK0z1Tr3ikzWDv1xdQ== - dependencies: - "@firebase/util" "0.3.2" - tslib "^1.11.1" - -"@firebase/database-types@0.5.2": - version "0.5.2" - resolved "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.2.tgz#23bec8477f84f519727f165c687761e29958b63c" - integrity sha512-ap2WQOS3LKmGuVFKUghFft7RxXTyZTDr0Xd8y2aqmWsbJVjgozi0huL/EUMgTjGFrATAjcf2A7aNs8AKKZ2a8g== - dependencies: - "@firebase/app-types" "0.6.1" - -"@firebase/database@^0.6.0": - version "0.6.13" - resolved "https://registry.npmjs.org/@firebase/database/-/database-0.6.13.tgz#b96fe0c53757dd6404ee085fdcb45c0f9f525c17" - integrity sha512-NommVkAPzU7CKd1gyehmi3lz0K78q0KOfiex7Nfy7MBMwknLm7oNqKovXSgQV1PCLvKXvvAplDSFhDhzIf9obA== - dependencies: - "@firebase/auth-interop-types" "0.1.5" - "@firebase/component" "0.1.19" - "@firebase/database-types" "0.5.2" - "@firebase/logger" "0.2.6" - "@firebase/util" "0.3.2" - faye-websocket "0.11.3" - tslib "^1.11.1" - -"@firebase/logger@0.2.6": - version "0.2.6" - resolved "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz#3aa2ca4fe10327cabf7808bd3994e88db26d7989" - integrity sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw== - -"@firebase/util@0.3.2": - version "0.3.2" - resolved "https://registry.npmjs.org/@firebase/util/-/util-0.3.2.tgz#87de27f9cffc2324651cabf6ec133d0a9eb21b52" - integrity sha512-Dqs00++c8rwKky6KCKLLY2T1qYO4Q+X5t+lF7DInXDNF4ae1Oau35bkD+OpJ9u7l1pEv7KHowP6CUKuySCOc8g== - dependencies: - tslib "^1.11.1" - -"@google-cloud/common@^2.1.1": - version "2.4.0" - resolved "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz#2783b7de8435024a31453510f2dab5a6a91a4c82" - integrity sha512-zWFjBS35eI9leAHhjfeOYlK5Plcuj/77EzstnrJIZbKgF/nkqjcQuGiMCpzCwOfPyUbz8ZaEOYgbHa759AKbjg== - dependencies: - "@google-cloud/projectify" "^1.0.0" - "@google-cloud/promisify" "^1.0.0" - arrify "^2.0.0" - duplexify "^3.6.0" - ent "^2.2.0" - extend "^3.0.2" - google-auth-library "^5.5.0" - retry-request "^4.0.0" - teeny-request "^6.0.0" - -"@google-cloud/firestore@^3.0.0": - version "3.8.6" - resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-3.8.6.tgz#9e6dea57323a5824563430a759244825fb01d834" - integrity sha512-ox80NbrM1MLJgvAAUd1quFLx/ie/nSjrk1PtscSicpoYDlKb9e6j7pHrVpbopBMyliyfNl3tLJWaDh+x+uCXqw== - dependencies: - deep-equal "^2.0.0" - functional-red-black-tree "^1.0.1" - google-gax "^1.15.3" - readable-stream "^3.4.0" - through2 "^3.0.0" - -"@google-cloud/paginator@^2.0.0": - version "2.0.3" - resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz#c7987ad05d1c3ebcef554381be80e9e8da4e4882" - integrity sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg== - dependencies: - arrify "^2.0.0" - extend "^3.0.2" - -"@google-cloud/projectify@^1.0.0": - version "1.0.4" - resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz#28daabebba6579ed998edcadf1a8f3be17f3b5f0" - integrity sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg== - -"@google-cloud/promisify@^1.0.0": - version "1.0.4" - resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz#ce86ffa94f9cfafa2e68f7b3e4a7fad194189723" - integrity sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ== - -"@google-cloud/storage@^4.1.2": - version "4.7.0" - resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.7.0.tgz#a7466086a83911c7979cc238d00a127ffb645615" - integrity sha512-f0guAlbeg7Z0m3gKjCfBCu7FG9qS3M3oL5OQQxlvGoPtK7/qg3+W+KQV73O2/sbuS54n0Kh2mvT5K2FWzF5vVQ== - dependencies: - "@google-cloud/common" "^2.1.1" - "@google-cloud/paginator" "^2.0.0" - "@google-cloud/promisify" "^1.0.0" - arrify "^2.0.0" - compressible "^2.0.12" - concat-stream "^2.0.0" - date-and-time "^0.13.0" - duplexify "^3.5.0" - extend "^3.0.2" - gaxios "^3.0.0" - gcs-resumable-upload "^2.2.4" - hash-stream-validation "^0.2.2" - mime "^2.2.0" - mime-types "^2.0.8" - onetime "^5.1.0" - p-limit "^2.2.0" - pumpify "^2.0.0" - readable-stream "^3.4.0" - snakeize "^0.1.0" - stream-events "^1.0.1" - through2 "^3.0.0" - xdg-basedir "^4.0.0" - -"@grpc/grpc-js@~1.0.3": - version "1.0.5" - resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.0.5.tgz#09948c0810e62828fdd61455b2eb13d7879888b0" - integrity sha512-Hm+xOiqAhcpT9RYM8lc15dbQD7aQurM7ZU8ulmulepiPlN7iwBXXwP3vSBUimoFoApRqz7pSIisXU8pZaCB4og== - dependencies: - semver "^6.2.0" - -"@grpc/proto-loader@^0.5.1": - version "0.5.5" - resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz#6725e7a1827bdf8e92e29fbf4e9ef0203c0906a9" - integrity sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ== - dependencies: - lodash.camelcase "^4.3.0" - protobufjs "^6.8.6" - -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= - -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - -"@types/body-parser@*": - version "1.19.0" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" - integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.33" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" - integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== - dependencies: - "@types/node" "*" - -"@types/express-serve-static-core@*": - version "4.17.13" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" - integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - -"@types/express@4.17.3": - version "4.17.3" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz#38e4458ce2067873b09a73908df488870c303bd9" - integrity sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "*" - "@types/serve-static" "*" - -"@types/fs-extra@^8.0.1": - version "8.1.1" - resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" - integrity sha512-TcUlBem321DFQzBNuz8p0CLLKp0VvF/XH9E4KHNmgwyp4E3AfgI5cjiIVZWlbfThBop2qxFIh4+LeY6hVWWZ2w== - dependencies: - "@types/node" "*" - -"@types/long@^4.0.0", "@types/long@^4.0.1": - version "4.0.1" - resolved "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" - integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== - -"@types/mime@*": - version "2.0.3" - resolved "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" - integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== - -"@types/node@*": - version "14.11.2" - resolved "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" - integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== - -"@types/node@^13.7.0": - version "13.13.21" - resolved "https://registry.npmjs.org/@types/node/-/node-13.13.21.tgz#e48d3c2e266253405cf404c8654d1bcf0d333e5c" - integrity sha512-tlFWakSzBITITJSxHV4hg4KvrhR/7h3xbJdSFbYJBVzKubrASbnnIFuSgolUh7qKGo/ZeJPKUfbZ0WS6Jp14DQ== - -"@types/node@^8.10.59": - version "8.10.64" - resolved "https://registry.npmjs.org/@types/node/-/node-8.10.64.tgz#0dddc4c53ca4819a32b7478232d8b446ca90e1c6" - integrity sha512-/EwBIb+imu8Qi/A3NF9sJ9iuKo7yV+pryqjmeRqaU0C4wBAOhas5mdvoYeJ5PCKrh6thRSJHdoasFqh3BQGILA== - -"@types/qs@*": - version "6.9.5" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" - integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== - -"@types/range-parser@*": - version "1.2.3" - resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" - integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== - -"@types/serve-static@*": - version "1.13.5" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" - integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== - dependencies: - "@types/express-serve-static-core" "*" - "@types/mime" "*" - -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -agent-base@6: - version "6.0.1" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" - integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== - dependencies: - debug "4" - -array-filter@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" - integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -arrify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" - integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== - -available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" - integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== - dependencies: - array-filter "^1.0.0" - -base64-js@^1.3.0: - version "1.3.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== - -bignumber.js@^9.0.0: - version "9.0.1" - resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" - integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== - -body-parser@1.19.0: - version "1.19.0" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -compressible@^2.0.12: - version "2.0.18" - resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -concat-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" - integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.0.2" - typedarray "^0.0.6" - -configstore@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cors@^2.8.5: - version "2.8.5" - resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - -date-and-time@^0.13.0: - version "0.13.1" - resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-0.13.1.tgz#d12ba07ac840d5b112dc4c83f8a03e8a51f78dd6" - integrity sha512-/Uge9DJAT+s+oAcDxtBhyR8+sKjUnZbYmyhbmWjTHNtX7B7oWD8YyYdeXcBRbwSj6hVvj+IQegJam7m7czhbFw== - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.1.1: - version "4.2.0" - resolved "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== - dependencies: - ms "2.1.2" - -deep-equal@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0" - integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA== - dependencies: - es-abstract "^1.17.5" - es-get-iterator "^1.1.0" - is-arguments "^1.0.4" - is-date-object "^1.0.2" - is-regex "^1.0.5" - isarray "^2.0.5" - object-is "^1.1.2" - object-keys "^1.1.1" - object.assign "^4.1.0" - regexp.prototype.flags "^1.3.0" - side-channel "^1.0.2" - which-boxed-primitive "^1.0.1" - which-collection "^1.0.1" - which-typed-array "^1.1.2" - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -dicer@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" - integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== - dependencies: - streamsearch "0.1.2" - -dot-prop@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.0.0" - -duplexify@^3.5.0, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -duplexify@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz#7027dc374f157b122a8ae08c2d3ea4d2d953aa61" - integrity sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA== - dependencies: - end-of-stream "^1.4.1" - inherits "^2.0.3" - readable-stream "^3.1.1" - stream-shift "^1.0.0" - -ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: - version "1.0.11" - resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -ent@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" - integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= - -es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.7" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" - integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: - version "1.18.0-next.1" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-get-iterator@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" - integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== - dependencies: - es-abstract "^1.17.4" - has-symbols "^1.0.1" - is-arguments "^1.0.4" - is-map "^2.0.1" - is-set "^2.0.1" - is-string "^1.0.5" - isarray "^2.0.5" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -express@^4.17.1: - version "4.17.1" - resolved "https://registry.npmjs.org/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" - content-type "~1.0.4" - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" - range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -fast-text-encoding@^1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" - integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== - -faye-websocket@0.11.3: - version "0.11.3" - resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" - integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== - dependencies: - websocket-driver ">=0.5.1" - -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -firebase-admin@8.13.0: - version "8.13.0" - resolved "https://registry.npmjs.org/firebase-admin/-/firebase-admin-8.13.0.tgz#997d34ae8357d7dc162ba622148bbebcf7f2e923" - integrity sha512-krXj5ncWMJBhCpXSn9UFY6zmDWjFjqgx+1e9ATXKFYndEjmKtNBuJzqdrAdDh7aTUR7X6+0TPx4Hbc08kd0lwQ== - dependencies: - "@firebase/database" "^0.6.0" - "@types/node" "^8.10.59" - dicer "^0.3.0" - jsonwebtoken "^8.5.1" - node-forge "^0.7.6" - optionalDependencies: - "@google-cloud/firestore" "^3.0.0" - "@google-cloud/storage" "^4.1.2" - -firebase-functions@3.13.0: - version "3.13.0" - resolved "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.13.0.tgz#66278dbeb45f343a179814f2b1d95b383beec5e7" - integrity sha512-tnltJL5KlGtbeBD9scsVjoKTSTMeo6EAy1gsdOfRlrwAu6idgLRKYVdmw0YymS8N7SwJ3CXo+3fuvSSihKhXbA== - dependencies: - "@types/express" "4.17.3" - cors "^2.8.5" - express "^4.17.1" - lodash "^4.17.14" - -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -gaxios@^2.0.0, gaxios@^2.1.0: - version "2.3.4" - resolved "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz#eea99353f341c270c5f3c29fc46b8ead56f0a173" - integrity sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA== - dependencies: - abort-controller "^3.0.0" - extend "^3.0.2" - https-proxy-agent "^5.0.0" - is-stream "^2.0.0" - node-fetch "^2.3.0" - -gaxios@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz#11b6f0e8fb08d94a10d4d58b044ad3bec6dd486a" - integrity sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q== - dependencies: - abort-controller "^3.0.0" - extend "^3.0.2" - https-proxy-agent "^5.0.0" - is-stream "^2.0.0" - node-fetch "^2.3.0" - -gcp-metadata@^3.4.0: - version "3.5.0" - resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz#6d28343f65a6bbf8449886a0c0e4a71c77577055" - integrity sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA== - dependencies: - gaxios "^2.1.0" - json-bigint "^0.3.0" - -gcs-resumable-upload@^2.2.4: - version "2.3.3" - resolved "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.3.tgz#02c616ed17eff6676e789910aeab3907d412c5f8" - integrity sha512-sf896I5CC/1AxeaGfSFg3vKMjUq/r+A3bscmVzZm10CElyRanN0XwPu/MxeIO4LSP+9uF6yKzXvNsaTsMXUG6Q== - dependencies: - abort-controller "^3.0.0" - configstore "^5.0.0" - gaxios "^2.0.0" - google-auth-library "^5.0.0" - pumpify "^2.0.0" - stream-events "^1.0.4" - -google-auth-library@^5.0.0, google-auth-library@^5.5.0: - version "5.10.1" - resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz#504ec75487ad140e68dd577c21affa363c87ddff" - integrity sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg== - dependencies: - arrify "^2.0.0" - base64-js "^1.3.0" - ecdsa-sig-formatter "^1.0.11" - fast-text-encoding "^1.0.0" - gaxios "^2.1.0" - gcp-metadata "^3.4.0" - gtoken "^4.1.0" - jws "^4.0.0" - lru-cache "^5.0.0" - -google-gax@^1.15.3: - version "1.15.3" - resolved "https://registry.npmjs.org/google-gax/-/google-gax-1.15.3.tgz#e88cdcbbd19c7d88cc5fd7d7b932c4d1979a5aca" - integrity sha512-3JKJCRumNm3x2EksUTw4P1Rad43FTpqrtW9jzpf3xSMYXx+ogaqTM1vGo7VixHB4xkAyATXVIa3OcNSh8H9zsQ== - dependencies: - "@grpc/grpc-js" "~1.0.3" - "@grpc/proto-loader" "^0.5.1" - "@types/fs-extra" "^8.0.1" - "@types/long" "^4.0.0" - abort-controller "^3.0.0" - duplexify "^3.6.0" - google-auth-library "^5.0.0" - is-stream-ended "^0.1.4" - lodash.at "^4.6.0" - lodash.has "^4.5.2" - node-fetch "^2.6.0" - protobufjs "^6.8.9" - retry-request "^4.0.0" - semver "^6.0.0" - walkdir "^0.4.0" - -google-p12-pem@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz#036462394e266472632a78b685f0cc3df4ef337b" - integrity sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg== - dependencies: - node-forge "^0.9.0" - -graceful-fs@^4.1.2: - version "4.2.4" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -gtoken@^4.1.0: - version "4.1.4" - resolved "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz#925ff1e7df3aaada06611d30ea2d2abf60fcd6a7" - integrity sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA== - dependencies: - gaxios "^2.1.0" - google-p12-pem "^2.0.0" - jws "^4.0.0" - mime "^2.2.0" - -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-stream-validation@^0.2.2: - version "0.2.4" - resolved "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" - integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-parser-js@>=0.5.1: - version "0.5.2" - resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" - integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== - -http-proxy-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== - -is-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" - integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== - -is-boolean-object@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" - integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== - -is-callable@^1.1.4, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== - -is-date-object@^1.0.1, is-date-object@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -is-map@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" - integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== - -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= - -is-number-object@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-regex@^1.0.5, is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -is-set@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" - integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== - -is-stream-ended@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" - integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== - -is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - -is-string@^1.0.4, is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== - -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - -is-typed-array@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" - integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== - dependencies: - available-typed-arrays "^1.0.0" - es-abstract "^1.17.4" - foreach "^2.0.5" - has-symbols "^1.0.1" - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== - -is-weakset@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" - integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -json-bigint@^0.3.0: - version "0.3.1" - resolved "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz#0c1729d679f580d550899d6a2226c228564afe60" - integrity sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ== - dependencies: - bignumber.js "^9.0.0" - -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jwa@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" - integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - -jws@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" - integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== - dependencies: - jwa "^2.0.0" - safe-buffer "^5.0.1" - -lodash.at@^4.6.0: - version "4.6.0" - resolved "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz#93cdce664f0a1994ea33dd7cd40e23afd11b0ff8" - integrity sha1-k83OZk8KGZTqM9181A4jr9EbD/g= - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - -lodash.has@^4.5.2: - version "4.5.2" - resolved "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" - integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI= - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - -lodash@^4.17.14: - version "4.17.20" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -long@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== - -lru-cache@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -"mime-db@>= 1.43.0 < 2": - version "1.45.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== - -mime-types@^2.0.8, mime-types@~2.1.24: - version "2.1.27" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.2.0: - version "2.4.6" - resolved "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" - integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@2.1.2, ms@^2.1.1: - version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.6.0: - version "2.6.1" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-forge@^0.7.6: - version "0.7.6" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" - integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== - -node-forge@^0.9.0: - version "0.9.2" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.2.tgz#b35a44c28889b2ea55cabf8c79e3563f9676190a" - integrity sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw== - -object-assign@^4: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== - -object-is@^1.1.2: - version "1.1.3" - resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81" - integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0, object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^5.1.0: - version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -protobufjs@^6.8.6, protobufjs@^6.8.9: - version "6.10.1" - resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz#e6a484dd8f04b29629e9053344e3970cccf13cd2" - integrity sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.1" - "@types/node" "^13.7.0" - long "^4.0.0" - -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" - integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw== - dependencies: - duplexify "^4.1.1" - inherits "^2.0.3" - pump "^3.0.0" - -qs@6.7.0: - version "6.7.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - -"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^2.0.0: - version "2.3.7" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - 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" - -regexp.prototype.flags@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -retry-request@^4.0.0: - version "4.1.3" - resolved "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" - integrity sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ== - dependencies: - debug "^4.1.1" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -semver@^5.6.0: - version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.2.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -send@0.17.1: - version "0.17.1" - resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.7.2" - mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -side-channel@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3" - integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g== - dependencies: - es-abstract "^1.18.0-next.0" - object-inspect "^1.8.0" - -signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -snakeize@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d" - integrity sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0= - -"statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -stream-events@^1.0.1, stream-events@^1.0.4, stream-events@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" - integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== - dependencies: - stubs "^3.0.0" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -streamsearch@0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" - integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= - -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -stubs@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" - integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls= - -teeny-request@^6.0.0: - version "6.0.3" - resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.3.tgz#b617f9d5b7ba95c76a3f257f6ba2342b70228b1f" - integrity sha512-TZG/dfd2r6yeji19es1cUIwAlVD8y+/svB1kAC2Y0bjEyysrfbO8EZvJBRwIE6WkwmUoB7uvWLwTIhJbMXZ1Dw== - dependencies: - http-proxy-agent "^4.0.0" - https-proxy-agent "^5.0.0" - node-fetch "^2.2.0" - stream-events "^1.0.5" - uuid "^7.0.0" - -through2@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" - integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ== - dependencies: - inherits "^2.0.4" - readable-stream "2 || 3" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -tslib@^1.11.1: - version "1.13.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== - -type-is@~1.6.17, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^7.0.0: - version "7.0.3" - resolved "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" - integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== - -vary@^1, vary@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -walkdir@^0.4.0: - version "0.4.1" - resolved "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" - integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ== - -websocket-driver@>=0.5.1: - version "0.7.4" - resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -which-boxed-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" - integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== - dependencies: - is-bigint "^1.0.0" - is-boolean-object "^1.0.0" - is-number-object "^1.0.3" - is-string "^1.0.4" - is-symbol "^1.0.2" - -which-collection@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== - dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" - -which-typed-array@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" - integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== - dependencies: - available-typed-arrays "^1.0.2" - es-abstract "^1.17.5" - foreach "^2.0.5" - function-bind "^1.1.1" - has-symbols "^1.0.1" - is-typed-array "^1.1.3" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/packages-exp/auth-exp/demo/package.json b/packages-exp/auth-exp/demo/package.json deleted file mode 100644 index c9b8d1af4ba..00000000000 --- a/packages-exp/auth-exp/demo/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@firebase/auth-exp-demo", - "version": "0.1.0", - "private": true, - "description": "Demo for Auth TS SDK", - "author": "Firebase (https://firebase.google.com/)", - "browser": "public/index.js", - "webworker": "public/web-worker.js", - "serviceworker": "public/service-worker.js", - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../../.gitignore'", - "demo": "rollup -c && firebase serve", - "build": "rollup -c", - "build:deps": "lerna run --scope @firebase/'{app-exp,auth-exp}' --include-dependencies build", - "dev": "rollup -c -w" - }, - "dependencies": { - "@firebase/app-exp": "0.0.900", - "@firebase/app-types-exp": "0.0.900", - "@firebase/auth-exp": "0.0.900", - "@firebase/auth-types-exp": "0.0.900", - "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.4", - "tslib": "^1.11.1" - }, - "license": "Apache-2.0", - "devDependencies": { - "@rollup/plugin-strip": "2.0.0", - "rollup": "2.35.1", - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-replace": "2.2.0", - "rollup-plugin-terser": "6.1.0", - "rollup-plugin-typescript2": "0.29.0", - "rollup-plugin-uglify": "6.0.4", - "@rollup/plugin-node-resolve": "9.0.0", - "lerna": "3.22.1" - }, - "repository": { - "directory": "packages-exp/auth-exp/demo", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "dist/index.d.ts", - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/auth-exp/demo/public/common.js b/packages-exp/auth-exp/demo/public/common.js deleted file mode 100644 index 9224926af95..00000000000 --- a/packages-exp/auth-exp/demo/public/common.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Utilities for Auth test app features. - */ - -/** - * Initializes the widget for toggling reCAPTCHA size. - * @param {function(string):void} callback The callback to call when the - * size toggler is changed, which takes in the new reCAPTCHA size. - */ -function initRecaptchaToggle(callback) { - // Listen to recaptcha config togglers. - var $recaptchaConfigTogglers = $('.toggleRecaptcha'); - $recaptchaConfigTogglers.click(function (e) { - // Remove currently active option. - $recaptchaConfigTogglers.removeClass('active'); - // Set currently selected option. - $(this).addClass('active'); - // Get the current reCAPTCHA setting label. - var size = $(e.target).text().toLowerCase(); - callback(size); - }); -} - -// Install servicerWorker if supported. -if ('serviceWorker' in navigator) { - navigator.serviceWorker - .register('/service-worker.js', { scope: '/' }) - .then(function (reg) { - // Registration worked. - console.log('Registration succeeded. Scope is ' + reg.scope); - }) - .catch(function (error) { - // Registration failed. - console.log('Registration failed with ' + error.message); - }); -} - -var webWorker = null; -if (window.Worker) { - webWorker = new Worker('/web-worker.js'); - /** - * Handles the incoming message from the web worker. - * @param {!Object} e The message event received. - */ - webWorker.onmessage = function (e) { - console.log('User data passed through web worker: ', e.data); - switch (e.data.type) { - case 'GET_USER_INFO': - alertSuccess( - 'User data passed through web worker: ' + JSON.stringify(e.data) - ); - break; - case 'RUN_TESTS': - if (e.data.status == 'success') { - alertSuccess('Web worker tests ran successfully!'); - } else { - alertError('Error: ' + JSON.stringify(e.data.error)); - } - break; - default: - return; - } - }; -} - -/** - * Asks the web worker, if supported in current browser, to return the user info - * corresponding to the currentUser as seen within the worker. - */ -function onGetCurrentUserDataFromWebWorker() { - if (webWorker) { - webWorker.postMessage({ type: 'GET_USER_INFO' }); - } else { - alertError('Error: Web workers are not supported in the current browser!'); - } -} - -/** - * Runs various Firebase Auth tests in a web worker environment and confirms the - * expected behavior. This is useful for manual testing in different browsers. - * @param {string} googleIdToken The Google ID token to sign in with. - */ -function runWebWorkerTests(googleIdToken) { - if (webWorker) { - webWorker.postMessage({ - type: 'RUN_TESTS', - googleIdToken: googleIdToken - }); - } else { - alertError('Error: Web workers are not supported in the current browser!'); - } -} diff --git a/packages-exp/auth-exp/demo/public/index.html b/packages-exp/auth-exp/demo/public/index.html deleted file mode 100644 index aa32e144ed5..00000000000 --- a/packages-exp/auth-exp/demo/public/index.html +++ /dev/null @@ -1,763 +0,0 @@ - - - - - Headless App - - - - - - - - - - - -
- -
- - - - - -
-
- - -
-
- - -
-
- - - [anonymous] / - uid: - -
-
- - / - - - - - - - -
- - - -
-
-
-
- - -
- -
-
-
- - - -
-
- -
Development mode APIs
-
-
- - -
- -
- - -
Web Worker Testing
-
- -
- -
Service Worker Testing
-
- -
- - -
Auth State Persistence
-
- - -
- - -
Language code
-
- - - -
- - -
Sign Up
-
- - - -
- - - -
Sign In
-
- - - -
-
- - -
- -
- - - - - -
- -
- - - - - -
- - -
Sign In with Email Link
-
- - - -
-
- - -
- - -
Password Reset
-
- - -
-
- - - - -
- -
Fetch Sign In Methods
-
- - -
- - -
Update Current User
-
- -
-
- -
-
-
- -
Update Profile
-
- - -
-
- - -
-
- - - -
- - - -
Linking/Unlinking
- -
- - - -
-
- - - - - -
- -
- - - - - - - -
- -
- - - - - -
- -
- - -
- - -
Enroll Second Factor
- -
-
-
- - - - - - -
-
-
- - -
Other Actions
- -
- - -
- - - - - - - -
Delete account
- -
- -
-
Web
-
- -
-
Android
-
-
- -
- - -
- -
-
-
iOS
-
-
- -
-
-
-
- - -
- -
-
-
-

-              
-            
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages-exp/auth-exp/demo/public/manifest.json b/packages-exp/auth-exp/demo/public/manifest.json deleted file mode 100644 index 646c61a9320..00000000000 --- a/packages-exp/auth-exp/demo/public/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "Firebse Auth Test App", - "short_name": "FirebaseAuthTest", - "start_url": "/", - "display": "standalone", - "background_color": "#fff", - "lang": "en-US", - "description": "Test app to test all functionality for Firebase Auth.", - "prefer_related_applications": false, - "theme_color": "#fff", - "scope": "/", - "orientation": "portrait-primary" -} diff --git a/packages-exp/auth-exp/demo/public/style.css b/packages-exp/auth-exp/demo/public/style.css deleted file mode 100644 index cdd999f8e8b..00000000000 --- a/packages-exp/auth-exp/demo/public/style.css +++ /dev/null @@ -1,207 +0,0 @@ -/** - * Copyright 2017 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -body { - padding-top: 70px; -} -body.user-info-displayed { - padding-top: 120px; -} - -@media (min-width: 768px) { - body { - padding-bottom: 100px; - } -} -@media (max-width: 767px) { - body { - padding-bottom: 15px; - } -} - -#tab-menu { - margin-bottom: 15px; -} - -#user-info { - display: none; - padding: 10px 15px; - position: fixed; - top: 50px; - width: 100%; - z-index: 1000; -} - -#toggle-user-placeholder { - margin-bottom: 15px; -} - -#user-info.current-user, -#toggle-user-placeholder .current-user, -#toggle-user label.active.current-user { - background-color: #d6e9c6; - border: 1px solid #dff0d8; - color: #3c763d; -} - -#user-info.last-user, -#toggle-user label.active.last-user { - background-color: #d9edf7; - border: 1px solid #bce8f1; - color: #31708f; -} - -#recaptcha-container { - border: 5px solid red; - bottom: 0; - position: fixed; - right: 0; - z-index: 1500; -} - -#recaptcha-container:empty { - border: none; -} - -.logs { - color: #555; - font-family: 'Courier New', Courier; - font-size: 0.9em; - word-wrap: break-word; -} - -.logs > .error { - color: #d9534f; -} - -/* Margin top for small screens when the logs are below the buttons */ -@media (max-width: 767px) { - .logs { - margin-top: 20px; - } -} - -.overlaying-alert { - bottom: 15px; - pointer-events: none; - position: fixed; - width: 100%; - word-wrap: break-word; - z-index: 1010; -} - -.actions { - margin-bottom: 15px; -} - -.group { - border-top: 1px solid #555; - color: #555; - font-size: 0.9em; - font-weight: bold; - letter-spacing: 0.2em; - margin-bottom: 15px; - padding: 2px 0 0 10px; -} - -button + .group, -div + .group, -.btn-block + .group, -.form + .group { - margin-top: 30px; -} - -.form { - text-align: center; -} - -.form-bordered { - border: 1px solid #CCC; - border-radius: 9px; - padding: 5px; -} - -button + .form, -input + .form, -.form + .form { - margin: 15px 0; -} - -.form + button, -.form-control + .btn-block, -.form-control + .form-control { - margin-top: 5px; -} - -/* Bootstrap .hidden adds the !important which invalides jQuery .show() */ -.hidden, -.hide, -.profile, -.overlaying-alert > .alert, -#toggle-user { - display: none; -} - -.profile { - line-height: 30px; -} - -@media (max-width: 767px) { - .profile { - /* Use a smaller line height for small screens so user information doesn't - take up half the screen. */ - line-height: normal; - } -} - -.profile-uid { - font-family: 'Courier New', Courier; -} - -.profile-image { - float: left; - height: 30px; - margin-right: 10px; -} - -.profile-email-not-verified { - color: #d9534f; -} - -.profile-providers { - color: #333; -} - -.profile-providers > i { - margin: 0 5px; -} - -.radio-block { - margin-bottom: 15px; - width: 100%; -} - -.radio-block > label { - width: 50%; -} - -/** Overrides default drop down menu styles for enrolled factors display. */ -.enrolled-second-factors { - left: initial; - min-width: 400px; - right: 0px; - width: 100%; -} diff --git a/packages-exp/auth-exp/demo/rollup.config.js b/packages-exp/auth-exp/demo/rollup.config.js deleted file mode 100644 index 49aff45b419..00000000000 --- a/packages-exp/auth-exp/demo/rollup.config.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import resolve from '@rollup/plugin-node-resolve'; -import strip from '@rollup/plugin-strip'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; - -import pkg from './package.json'; - -/** - * Common plugins for all builds - */ -const commonPlugins = [ - strip({ - functions: ['debugAssert.*'] - }) -]; - -const workerPlugins = [ - ...commonPlugins, - resolve({ - mainFields: ['webworker', 'module', 'main'] - }), - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - declaration: false, - target: 'es2017', - lib: [ - // TODO: remove this - 'dom', - 'es2015', - 'webworker' - ] - } - } - }) -]; - -const es5Builds = [ - /** - * Browser Builds - */ - { - input: 'src/index.js', - output: [{ file: pkg.browser, format: 'esm', sourcemap: true }], - plugins: [ - ...commonPlugins, - resolve({ - mainFields: ['module', 'main'] - }) - ] - }, - { - input: 'src/worker/web-worker.ts', - output: [{ file: pkg.webworker, format: 'esm', sourcemap: true }], - plugins: workerPlugins - }, - { - input: 'src/worker/service-worker.ts', - output: [{ file: pkg.serviceworker, format: 'esm', sourcemap: true }], - plugins: workerPlugins - } -]; - -export default [...es5Builds]; diff --git a/packages-exp/auth-exp/demo/src/index.js b/packages-exp/auth-exp/demo/src/index.js deleted file mode 100644 index 9ae38f876a9..00000000000 --- a/packages-exp/auth-exp/demo/src/index.js +++ /dev/null @@ -1,1931 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview This code is for the most part copied over from the packages/auth/demo - * package. - */ - -import { initializeApp } from '@firebase/app-exp'; -import { - applyActionCode, - browserLocalPersistence, - browserSessionPersistence, - confirmPasswordReset, - createUserWithEmailAndPassword, - EmailAuthProvider, - fetchSignInMethodsForEmail, - indexedDBLocalPersistence, - initializeAuth, - getAuth, - inMemoryPersistence, - isSignInWithEmailLink, - linkWithCredential, - multiFactor, - PhoneAuthProvider, - PhoneMultiFactorGenerator, - reauthenticateWithCredential, - RecaptchaVerifier, - sendEmailVerification, - sendPasswordResetEmail, - sendSignInLinkToEmail, - signInAnonymously, - signInWithCredential, - signInWithCustomToken, - signInWithEmailAndPassword, - unlink, - updateEmail, - updatePassword, - updateProfile, - verifyPasswordResetCode, - getMultiFactorResolver, - OAuthProvider, - GoogleAuthProvider, - FacebookAuthProvider, - TwitterAuthProvider, - GithubAuthProvider, - signInWithPopup, - linkWithPopup, - reauthenticateWithPopup, - signInWithRedirect, - linkWithRedirect, - reauthenticateWithRedirect, - getRedirectResult, - browserPopupRedirectResolver -} from '@firebase/auth-exp'; - -import { config } from './config'; -import { - alertError, - alertNotImplemented, - alertSuccess, - clearLogs, - log, - logAtLevel_ -} from './logging'; - -let app = null; -let auth = null; -let currentTab = null; -let lastUser = null; -let applicationVerifier = null; -let multiFactorErrorResolver = null; -let selectedMultiFactorHint = null; -let recaptchaSize = 'normal'; -let webWorker = null; - -// The corresponding Font Awesome icons for each provider. -const providersIcons = { - 'google.com': 'fa-google', - 'facebook.com': 'fa-facebook-official', - 'twitter.com': 'fa-twitter-square', - 'github.com': 'fa-github', - 'yahoo.com': 'fa-yahoo', - 'phone': 'fa-phone' -}; - -/** - * Returns the active user (i.e. currentUser or lastUser). - * @return {!firebase.User} - */ -function activeUser() { - const type = $('input[name=toggle-user-selection]:checked').val(); - if (type === 'lastUser') { - return lastUser; - } else { - return auth.currentUser; - } -} - -/** - * Refreshes the current user data in the UI, displaying a user info box if - * a user is signed in, or removing it. - */ -function refreshUserData() { - if (activeUser()) { - const user = activeUser(); - $('.profile').show(); - $('body').addClass('user-info-displayed'); - $('div.profile-email,span.profile-email').text(user.email || 'No Email'); - $('div.profile-phone,span.profile-phone').text( - user.phoneNumber || 'No Phone' - ); - $('div.profile-uid,span.profile-uid').text(user.uid); - $('div.profile-name,span.profile-name').text(user.displayName || 'No Name'); - $('input.profile-name').val(user.displayName); - $('input.photo-url').val(user.photoURL); - if (user.photoURL != null) { - let photoURL = user.photoURL; - // Append size to the photo URL for Google hosted images to avoid requesting - // the image with its original resolution (using more bandwidth than needed) - // when it is going to be presented in smaller size. - if ( - photoURL.indexOf('googleusercontent.com') !== -1 || - photoURL.indexOf('ggpht.com') !== -1 - ) { - photoURL = photoURL + '?sz=' + $('img.profile-image').height(); - } - $('img.profile-image').attr('src', photoURL).show(); - } else { - $('img.profile-image').hide(); - } - $('.profile-email-verified').toggle(user.emailVerified); - $('.profile-email-not-verified').toggle(!user.emailVerified); - $('.profile-anonymous').toggle(user.isAnonymous); - // Display/Hide providers icons. - $('.profile-providers').empty(); - if (user['providerData'] && user['providerData'].length) { - const providersCount = user['providerData'].length; - for (let i = 0; i < providersCount; i++) { - addProviderIcon(user['providerData'][i]['providerId']); - } - } - // Show enrolled second factors if available for the active user. - showMultiFactorStatus(user); - // Change color. - if (user === auth.currentUser) { - $('#user-info').removeClass('last-user'); - $('#user-info').addClass('current-user'); - } else { - $('#user-info').removeClass('current-user'); - $('#user-info').addClass('last-user'); - } - } else { - $('.profile').slideUp(); - $('body').removeClass('user-info-displayed'); - $('input.profile-data').val(''); - } -} - -/** - * Sets last signed in user and updates UI. - * @param {?firebase.User} user The last signed in user. - */ -function setLastUser(user) { - lastUser = user; - if (user) { - // Displays the toggle. - $('#toggle-user').show(); - $('#toggle-user-placeholder').hide(); - } else { - $('#toggle-user').hide(); - $('#toggle-user-placeholder').show(); - } -} - -/** - * Add a provider icon to the profile info. - * @param {string} providerId The providerId of the provider. - */ -function addProviderIcon(providerId) { - const pElt = $('') - .addClass('fa ' + providersIcons[providerId]) - .attr('title', providerId) - .data({ - 'toggle': 'tooltip', - 'placement': 'bottom' - }); - $('.profile-providers').append(pElt); - pElt.tooltip(); -} - -/** - * Updates the active user's multi-factor enrollment status. - * @param {!firebase.User} activeUser The corresponding user. - */ -function showMultiFactorStatus(activeUser) { - mfaUser = multiFactor(activeUser); - const enrolledFactors = (mfaUser && mfaUser.enrolledFactors) || []; - const $listGroup = $('#user-info .dropdown-menu.enrolled-second-factors'); - // Hide the drop down menu initially. - $listGroup.empty().parent().hide(); - if (enrolledFactors.length) { - // If enrolled factors are available, show the drop down menu. - $listGroup.parent().show(); - // Populate the enrolled factors. - showMultiFactors( - $listGroup, - enrolledFactors, - // On row click, do nothing. This is needed to prevent the drop down - // menu from closing. - e => { - e.preventDefault(); - e.stopPropagation(); - }, - // On delete click unenroll the selected factor. - function (e) { - e.preventDefault(); - // Get the corresponding second factor index. - const index = parseInt($(this).attr('data-index'), 10); - // Get the second factor info. - const info = enrolledFactors[index]; - // Get the display name. If not available, use uid. - const label = info && (info.displayName || info.uid); - if (label) { - $('#enrolled-factors-drop-down').removeClass('open'); - mfaUser.unenroll(info).then(() => { - refreshUserData(); - alertSuccess('Multi-factor successfully unenrolled.'); - }, onAuthError); - } - } - ); - } -} - -/** - * Updates the UI when the user is successfully authenticated. - * @param {!firebase.User} user User authenticated. - */ -function onAuthSuccess(user) { - console.log(user); - alertSuccess('User authenticated, id: ' + user.uid); - refreshUserData(); -} - -/** - * Displays an error message when the authentication failed. - * @param {!Error} error Error message to display. - */ -function onAuthError(error) { - logAtLevel_(error, 'error'); - if (error.code === 'auth/multi-factor-auth-required') { - // Handle second factor sign-in. - handleMultiFactorSignIn(getMultiFactorResolver(auth, error)); - } else { - alertError('Error: ' + error.code); - } -} - -/** - * Changes the UI when the user has been signed out. - */ -function signOut() { - log('User successfully signed out.'); - alertSuccess('User successfully signed out.'); - refreshUserData(); -} - -/** - * Saves the new language code provided in the language code input field. - */ -function onSetLanguageCode() { - const languageCode = $('#language-code').val() || null; - try { - auth.languageCode = languageCode; - alertSuccess('Language code changed to "' + languageCode + '".'); - } catch (error) { - alertError('Error: ' + error.code); - } -} - -/** - * Switches Auth instance language to device language. - */ -function onUseDeviceLanguage() { - auth.useDeviceLanguage(); - $('#language-code').val(auth.languageCode); - alertSuccess('Using device language "' + auth.languageCode + '".'); -} - -/** - * Changes the Auth state persistence to the specified one. - */ -function onSetPersistence() { - const type = $('#persistence-type').val(); - let persistence; - switch (type) { - case 'local': - persistence = browserLocalPersistence; - break; - case 'session': - persistence = browserSessionPersistence; - break; - case 'indexedDB': - persistence = indexedDBLocalPersistence; - break; - case 'none': - persistence = inMemoryPersistence; - break; - default: - alertError('Unexpected persistence type: ' + type); - } - try { - auth.setPersistence(persistence).then( - () => { - log('Persistence state change to "' + type + '".'); - alertSuccess('Persistence state change to "' + type + '".'); - }, - error => { - alertError('Error: ' + error.code); - } - ); - } catch (error) { - alertError('Error: ' + error.code); - } -} - -/** - * Signs up a new user with an email and a password. - */ -function onSignUp() { - const email = $('#signup-email').val(); - const password = $('#signup-password').val(); - createUserWithEmailAndPassword(auth, email, password).then( - onAuthUserCredentialSuccess, - onAuthError - ); -} - -/** - * Signs in a user with an email and a password. - */ -function onSignInWithEmailAndPassword() { - const email = $('#signin-email').val(); - const password = $('#signin-password').val(); - signInWithEmailAndPassword(auth, email, password).then( - onAuthUserCredentialSuccess, - onAuthError - ); -} - -/** - * Signs in a user with an email link. - */ -function onSignInWithEmailLink() { - const email = $('#sign-in-with-email-link-email').val(); - const link = $('#sign-in-with-email-link-link').val() || undefined; - if (isSignInWithEmailLink(auth, link)) { - signInWithEmailLink(auth, email, link).then(onAuthSuccess, onAuthError); - } else { - alertError('Sign in link is invalid'); - } -} - -/** - * Links a user with an email link. - */ -function onLinkWithEmailLink() { - const email = $('#link-with-email-link-email').val(); - const link = $('#link-with-email-link-link').val() || undefined; - const credential = EmailAuthProvider.credentialWithLink(email, link); - linkWithCredential(activeUser(), credential).then( - onAuthUserCredentialSuccess, - onAuthError - ); -} - -/** - * Re-authenticate a user with email link credential. - */ -function onReauthenticateWithEmailLink() { - const email = $('#link-with-email-link-email').val(); - const link = $('#link-with-email-link-link').val() || undefined; - const credential = EmailAuthProvider.credentialWithLink(email, link); - reauthenticateWithCredential(activeUser(), credential).then(result => { - logAdditionalUserInfo(result); - refreshUserData(); - alertSuccess('User reauthenticated!'); - }, onAuthError); -} - -/** - * Signs in with a custom token. - * @param {DOMEvent} _event HTML DOM event returned by the listener. - */ -function onSignInWithCustomToken(_event) { - // The token can be directly specified on the html element. - const token = $('#user-custom-token').val(); - - signInWithCustomToken(auth, token).then( - onAuthUserCredentialSuccess, - onAuthError - ); -} - -/** - * Signs in anonymously. - */ -function onSignInAnonymously() { - signInAnonymously(auth).then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Signs in with a generic IdP credential. - */ -function onSignInWithGenericIdPCredential() { - alertNotImplemented(); - // var providerId = $('#signin-generic-idp-provider-id').val(); - // var idToken = $('#signin-generic-idp-id-token').val() || undefined; - // var rawNonce = $('#signin-generic-idp-raw-nonce').val() || undefined; - // var accessToken = $('#signin-generic-idp-access-token').val() || undefined; - // var provider = new OAuthProvider(providerId); - // signInWithCredential( - // auth, - // provider.credential({ - // idToken: idToken, - // accessToken: accessToken, - // rawNonce: rawNonce, - // })).then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Initializes the ApplicationVerifier. - * @param {string} submitButtonId The ID of the DOM element of the button to - * which we attach the invisible reCAPTCHA. This is required even in visible - * mode. - */ -function makeApplicationVerifier(submitButtonId) { - const container = - recaptchaSize === 'invisible' ? submitButtonId : 'recaptcha-container'; - applicationVerifier = new RecaptchaVerifier( - container, - { 'size': recaptchaSize }, - auth - ); -} - -/** - * Clears the ApplicationVerifier. - */ -function clearApplicationVerifier() { - if (applicationVerifier) { - applicationVerifier.clear(); - applicationVerifier = null; - } -} - -/** - * Sends a phone number verification code for sign-in. - */ -function onSignInVerifyPhoneNumber() { - const phoneNumber = $('#signin-phone-number').val(); - const provider = new PhoneAuthProvider(auth); - // Clear existing reCAPTCHA as an existing reCAPTCHA could be targeted for a - // link/re-auth operation. - clearApplicationVerifier(); - // Initialize a reCAPTCHA application verifier. - makeApplicationVerifier('signin-verify-phone-number'); - provider.verifyPhoneNumber(phoneNumber, applicationVerifier).then( - verificationId => { - clearApplicationVerifier(); - $('#signin-phone-verification-id').val(verificationId); - alertSuccess('Phone verification sent!'); - }, - error => { - clearApplicationVerifier(); - onAuthError(error); - } - ); -} - -/** - * Confirms a phone number verification for sign-in. - */ -function onSignInConfirmPhoneVerification() { - const verificationId = $('#signin-phone-verification-id').val(); - const verificationCode = $('#signin-phone-verification-code').val(); - const credential = PhoneAuthProvider.credential( - verificationId, - verificationCode - ); - signInOrLinkCredential(credential); -} - -/** - * Sends a phone number verification code for linking or reauth. - */ -function onLinkReauthVerifyPhoneNumber() { - const phoneNumber = $('#link-reauth-phone-number').val(); - const provider = new PhoneAuthProvider(auth); - // Clear existing reCAPTCHA as an existing reCAPTCHA could be targeted for a - // sign-in operation. - clearApplicationVerifier(); - // Initialize a reCAPTCHA application verifier. - makeApplicationVerifier('link-reauth-verify-phone-number'); - provider.verifyPhoneNumber(phoneNumber, applicationVerifier).then( - verificationId => { - clearApplicationVerifier(); - $('#link-reauth-phone-verification-id').val(verificationId); - alertSuccess('Phone verification sent!'); - }, - error => { - clearApplicationVerifier(); - onAuthError(error); - } - ); -} - -/** - * Updates the user's phone number. - */ -function onUpdateConfirmPhoneVerification() { - if (!activeUser()) { - alertError('You need to sign in before linking an account.'); - return; - } - const verificationId = $('#link-reauth-phone-verification-id').val(); - const verificationCode = $('#link-reauth-phone-verification-code').val(); - const credential = PhoneAuthProvider.credential( - verificationId, - verificationCode - ); - activeUser() - .updatePhoneNumber(credential) - .then(() => { - refreshUserData(); - alertSuccess('Phone number updated!'); - }, onAuthError); -} - -/** - * Confirms a phone number verification for linking. - */ -function onLinkConfirmPhoneVerification() { - const verificationId = $('#link-reauth-phone-verification-id').val(); - const verificationCode = $('#link-reauth-phone-verification-code').val(); - const credential = PhoneAuthProvider.credential( - verificationId, - verificationCode - ); - signInOrLinkCredential(credential); -} - -/** - * Confirms a phone number verification for reauthentication. - */ -function onReauthConfirmPhoneVerification() { - const verificationId = $('#link-reauth-phone-verification-id').val(); - const verificationCode = $('#link-reauth-phone-verification-code').val(); - const credential = PhoneAuthProvider.credential( - verificationId, - verificationCode - ); - reauthenticateWithCredential(activeUser(), credential).then(result => { - logAdditionalUserInfo(result); - refreshUserData(); - alertSuccess('User reauthenticated!'); - }, onAuthError); -} - -/** - * Sends a phone number verification code for enrolling second factor. - */ -function onStartEnrollWithPhoneMultiFactor() { - const phoneNumber = $('#enroll-mfa-phone-number').val(); - if (!phoneNumber || !activeUser()) { - return; - } - const provider = new PhoneAuthProvider(auth); - // Clear existing reCAPTCHA as an existing reCAPTCHA could be targeted for a - // sign-in operation. - clearApplicationVerifier(); - // Initialize a reCAPTCHA application verifier. - makeApplicationVerifier('enroll-mfa-verify-phone-number'); - multiFactor(activeUser()) - .getSession() - .then(multiFactorSession => { - const phoneInfoOptions = { - phoneNumber, - 'session': multiFactorSession - }; - return provider.verifyPhoneNumber(phoneInfoOptions, applicationVerifier); - }) - .then( - verificationId => { - clearApplicationVerifier(); - $('#enroll-mfa-phone-verification-id').val(verificationId); - alertSuccess('Phone verification sent!'); - }, - error => { - clearApplicationVerifier(); - onAuthError(error); - } - ); -} - -/** - * Confirms a phone number verification for MFA enrollment. - */ -function onFinalizeEnrollWithPhoneMultiFactor() { - const verificationId = $('#enroll-mfa-phone-verification-id').val(); - const verificationCode = $('#enroll-mfa-phone-verification-code').val(); - if (!verificationId || !verificationCode || !activeUser()) { - return; - } - const credential = PhoneAuthProvider.credential( - verificationId, - verificationCode - ); - const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(credential); - const displayName = $('#enroll-mfa-phone-display-name').val() || undefined; - - multiFactor(activeUser()) - .enroll(multiFactorAssertion, displayName) - .then(() => { - refreshUserData(); - alertSuccess('Phone number enrolled!'); - }, onAuthError); -} - -/** - * Signs in or links a provider's credential, based on current tab opened. - * @param {!AuthCredential} credential The provider's credential. - */ -function signInOrLinkCredential(credential) { - if (currentTab === '#user-section') { - if (!activeUser()) { - alertError('You need to sign in before linking an account.'); - return; - } - - linkWithCredential(activeUser(), credential).then(result => { - logAdditionalUserInfo(result); - refreshUserData(); - alertSuccess('Provider linked!'); - }, onAuthError); - } else { - signInWithCredential(auth, credential).then( - onAuthUserCredentialSuccess, - onAuthError - ); - } -} - -/** @return {!Object} The Action Code Settings object. */ -function getActionCodeSettings() { - const actionCodeSettings = {}; - const url = $('#continueUrl').val(); - const apn = $('#apn').val(); - const amv = $('#amv').val(); - const ibi = $('#ibi').val(); - const installApp = $('input[name=install-app]:checked').val() === 'Yes'; - const handleCodeInApp = - $('input[name=handle-in-app]:checked').val() === 'Yes'; - if (url || apn || ibi) { - actionCodeSettings['url'] = url; - if (apn) { - actionCodeSettings['android'] = { - 'packageName': apn, - 'installApp': !!installApp, - 'minimumVersion': amv || undefined - }; - } - if (ibi) { - actionCodeSettings['iOS'] = { - 'bundleId': ibi - }; - } - actionCodeSettings['handleCodeInApp'] = handleCodeInApp; - } - return actionCodeSettings; -} - -/** Reset action code settings form. */ -function onActionCodeSettingsReset() { - $('#continueUrl').val(''); - $('#apn').val(''); - $('#amv').val(''); - $('#ibi').val(''); -} - -/** - * Changes the user's email. - */ -function onChangeEmail() { - const email = $('#changed-email').val(); - updateEmail(activeUser(), email).then(() => { - refreshUserData(); - alertSuccess('Email changed!'); - }, onAuthError); -} - -/** - * Changes the user's password. - */ -function onChangePassword() { - const password = $('#changed-password').val(); - updatePassword(activeUser(), password).then(() => { - refreshUserData(); - alertSuccess('Password changed!'); - }, onAuthError); -} - -/** - * Changes the user's password. - */ -function onUpdateProfile() { - const displayName = $('#display-name').val(); - const photoURL = $('#photo-url').val(); - updateProfile(activeUser(), { - displayName, - photoURL - }).then(() => { - refreshUserData(); - alertSuccess('Profile updated!'); - }, onAuthError); -} - -/** - * Sends sign in with email link to the user. - */ -function onSendSignInLinkToEmail() { - const email = $('#sign-in-with-email-link-email').val(); - sendSignInLinkToEmail(auth, email, getActionCodeSettings()).then(() => { - alertSuccess('Email sent!'); - }, onAuthError); -} - -/** - * Sends sign in with email link to the user and pass in current url. - */ -function onSendSignInLinkToEmailCurrentUrl() { - const email = $('#sign-in-with-email-link-email').val(); - const actionCodeSettings = { - 'url': window.location.href, - 'handleCodeInApp': true - }; - - sendSignInLinkToEmail(auth, email, actionCodeSettings).then(() => { - if ('localStorage' in window && window['localStorage'] !== null) { - window.localStorage.setItem( - 'emailForSignIn', - // Save the email and the timestamp. - JSON.stringify({ - email, - timestamp: new Date().getTime() - }) - ); - } - alertSuccess('Email sent!'); - }, onAuthError); -} - -/** - * Sends email link to link the user. - */ -function onSendLinkEmailLink() { - const email = $('#link-with-email-link-email').val(); - sendSignInLinkToEmail(auth, email, getActionCodeSettings()).then(() => { - alertSuccess('Email sent!'); - }, onAuthError); -} - -/** - * Sends password reset email to the user. - */ -function onSendPasswordResetEmail() { - const email = $('#password-reset-email').val(); - sendPasswordResetEmail(auth, email, getActionCodeSettings()).then(() => { - alertSuccess('Email sent!'); - }, onAuthError); -} - -/** - * Verifies the password reset code entered by the user. - */ -function onVerifyPasswordResetCode() { - const code = $('#password-reset-code').val(); - verifyPasswordResetCode(auth, code).then(() => { - alertSuccess('Password reset code is valid!'); - }, onAuthError); -} - -/** - * Confirms the password reset with the code and password supplied by the user. - */ -function onConfirmPasswordReset() { - const code = $('#password-reset-code').val(); - const password = $('#password-reset-password').val(); - confirmPasswordReset(auth, code, password).then(() => { - alertSuccess('Password has been changed!'); - }, onAuthError); -} - -/** - * Gets the list of possible sign in methods for the given email address. - */ -function onFetchSignInMethodsForEmail() { - const email = $('#fetch-sign-in-methods-email').val(); - fetchSignInMethodsForEmail(auth, email).then(signInMethods => { - log('Sign in methods for ' + email + ' :'); - log(signInMethods); - if (signInMethods.length === 0) { - alertSuccess('Sign In Methods for ' + email + ': N/A'); - } else { - alertSuccess( - 'Sign In Methods for ' + email + ': ' + signInMethods.join(', ') - ); - } - }, onAuthError); -} - -/** - * Fetches and logs the user's providers data. - */ -function onGetProviderData() { - log('Providers data:'); - log(activeUser()['providerData']); -} - -/** - * Links a signed in user with an email and password account. - */ -function onLinkWithEmailAndPassword() { - const email = $('#link-email').val(); - const password = $('#link-password').val(); - linkWithCredential( - activeUser(), - EmailAuthProvider.credential(email, password) - ).then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Links with a generic IdP credential. - */ -function onLinkWithGenericIdPCredential() { - alertNotImplemented(); - // var providerId = $('#link-generic-idp-provider-id').val(); - // var idToken = $('#link-generic-idp-id-token').val() || undefined; - // var rawNonce = $('#link-generic-idp-raw-nonce').val() || undefined; - // var accessToken = $('#link-generic-idp-access-token').val() || undefined; - // var provider = new OAuthProvider(providerId); - // activeUser().linkWithCredential( - // provider.credential({ - // idToken: idToken, - // accessToken: accessToken, - // rawNonce: rawNonce, - // })).then(onAuthUserCredentialSuccess, onAuthError); -} - -/** - * Unlinks the specified provider. - */ -function onUnlinkProvider() { - const providerId = $('#unlinked-provider-id').val(); - unlink(activeUser(), providerId).then(_user => { - alertSuccess('Provider unlinked from user.'); - refreshUserData(); - }, onAuthError); -} - -/** - * Sends email verification to the user. - */ -function onSendEmailVerification() { - sendEmailVerification(activeUser(), getActionCodeSettings()).then(() => { - alertSuccess('Email verification sent!'); - }, onAuthError); -} - -/** - * Confirms the email verification code given. - */ -function onApplyActionCode() { - var code = $('#email-verification-code').val(); - applyActionCode(auth, code).then(function () { - alertSuccess('Email successfully verified!'); - refreshUserData(); - }, onAuthError); -} - -/** - * Gets or refreshes the ID token. - * @param {boolean} forceRefresh Whether to force the refresh of the token - * or not. - */ -function getIdToken(forceRefresh) { - if (activeUser() == null) { - alertError('No user logged in.'); - return; - } - if (activeUser().getIdToken) { - activeUser() - .getIdToken(forceRefresh) - .then(alertSuccess, () => { - log('No token'); - }); - } else { - activeUser() - .getToken(forceRefresh) - .then(alertSuccess, () => { - log('No token'); - }); - } -} - -/** - * Gets or refreshes the ID token result. - * @param {boolean} forceRefresh Whether to force the refresh of the token - * or not - */ -function getIdTokenResult(forceRefresh) { - if (activeUser() == null) { - alertError('No user logged in.'); - return; - } - activeUser() - .getIdTokenResult(forceRefresh) - .then(idTokenResult => { - alertSuccess(JSON.stringify(idTokenResult)); - }, onAuthError); -} - -/** - * Triggers the retrieval of the ID token result. - */ -function onGetIdTokenResult() { - getIdTokenResult(false); -} - -/** - * Triggers the refresh of the ID token result. - */ -function onRefreshTokenResult() { - getIdTokenResult(true); -} - -/** - * Triggers the retrieval of the ID token. - */ -function onGetIdToken() { - getIdToken(false); -} - -/** - * Triggers the refresh of the ID token. - */ -function onRefreshToken() { - getIdToken(true); -} - -/** - * Signs out the user. - */ -function onSignOut() { - setLastUser(auth.currentUser); - auth.signOut().then(signOut, onAuthError); -} - -/** - * Handles multi-factor sign-in completion. - * @param {!MultiFactorResolver} resolver The multi-factor error - * resolver. - */ -function handleMultiFactorSignIn(resolver) { - // Save multi-factor error resolver. - multiFactorErrorResolver = resolver; - // Populate 2nd factor options from resolver. - const $listGroup = $('#multiFactorModal div.enrolled-second-factors'); - // Populate the list of 2nd factors in the list group specified. - showMultiFactors( - $listGroup, - multiFactorErrorResolver.hints, - // On row click, select the corresponding second factor to complete - // sign-in with. - function (e) { - e.preventDefault(); - // Remove all other active entries. - $listGroup.find('a').removeClass('active'); - // Mark current entry as active. - $(this).addClass('active'); - // Select current factor. - onSelectMultiFactorHint(parseInt($(this).attr('data-index'), 10)); - }, - // Do not show delete option - null - ); - // Hide phone form (other second factor types could be supported). - $('#multi-factor-phone').addClass('hidden'); - // Show second factor recovery dialog. - $('#multiFactorModal').modal(); -} - -/** - * Displays the list of multi-factors in the provided list group. - * @param {!jQuery} $listGroup The list group where the enrolled - * factors will be displayed. - * @param {!Array} multiFactorInfo The list of - * multi-factors to display. - * @param {?function(!jQuery.Event)} onClick The click handler when a second - * factor is clicked. - * @param {?function(!jQuery.Event)} onDelete The click handler when a second - * factor is delete. If not provided, no delete button is shown. - */ -function showMultiFactors($listGroup, multiFactorInfo, onClick, onDelete) { - // Append entry to list. - $listGroup.empty(); - $.each(multiFactorInfo, i => { - // Append entry to list. - const info = multiFactorInfo[i]; - const displayName = info.displayName || 'N/A'; - const $a = $('') - .addClass('list-group-item') - .addClass('list-group-item-action') - // Set index on entry. - .attr('data-index', i) - .appendTo($listGroup); - $a.append($('

').text(info.uid)); - $a.append($('').text(info.factorId)); - $a.append($('

').text(displayName)); - if (info.phoneNumber) { - $a.append($('').text(info.phoneNumber)); - } - // Check if a delete button is to be displayed. - if (onDelete) { - const $deleteBtn = $( - '' + - '' + - '' - ); - // Append delete button to row. - $a.append($deleteBtn); - // Add delete button click handler. - $a.find('button.delete-factor').click(onDelete); - } - // On entry click. - if (onClick) { - $a.click(onClick); - } - }); -} - -/** - * Handles the user selection of second factor to complete sign-in with. - * @param {number} index The selected multi-factor hint index. - */ -function onSelectMultiFactorHint(index) { - // Hide all forms for handling each type of second factors. - // Currently only phone is supported. - $('#multi-factor-phone').addClass('hidden'); - if ( - !multiFactorErrorResolver || - typeof multiFactorErrorResolver.hints[index] === 'undefined' - ) { - return; - } - - if (multiFactorErrorResolver.hints[index].factorId === 'phone') { - // Save selected second factor. - selectedMultiFactorHint = multiFactorErrorResolver.hints[index]; - // Show options for phone 2nd factor. - // Get reCAPTCHA ready. - clearApplicationVerifier(); - makeApplicationVerifier('send-2fa-phone-code'); - // Show sign-in with phone second factor menu. - $('#multi-factor-phone').removeClass('hidden'); - // Clear all input. - $('#multi-factor-sign-in-verification-id').val(''); - $('#multi-factor-sign-in-verification-code').val(''); - } else { - // 2nd factor not found or not supported by app. - alertError('Selected 2nd factor is not supported!'); - } -} - -/** - * Start sign-in with the 2nd factor phone number. - * @param {!jQuery.Event} event The jQuery event object. - */ -function onStartSignInWithPhoneMultiFactor(event) { - event.preventDefault(); - // Make sure a second factor is selected. - if (!selectedMultiFactorHint || !multiFactorErrorResolver) { - return; - } - // Initialize a reCAPTCHA application verifier. - const provider = new PhoneAuthProvider(auth); - const signInRequest = { - multiFactorHint: selectedMultiFactorHint, - session: multiFactorErrorResolver.session - }; - provider.verifyPhoneNumber(signInRequest, applicationVerifier).then( - verificationId => { - clearApplicationVerifier(); - $('#multi-factor-sign-in-verification-id').val(verificationId); - alertSuccess('Phone verification sent!'); - }, - error => { - clearApplicationVerifier(); - onAuthError(error); - } - ); -} - -/** - * Completes sign-in with the 2nd factor phone assertion. - * @param {!jQuery.Event} event The jQuery event object. - */ -function onFinalizeSignInWithPhoneMultiFactor(event) { - event.preventDefault(); - const verificationId = $('#multi-factor-sign-in-verification-id').val(); - const code = $('#multi-factor-sign-in-verification-code').val(); - if (!code || !verificationId || !multiFactorErrorResolver) { - return; - } - const cred = PhoneAuthProvider.credential(verificationId, code); - const assertion = PhoneMultiFactorGenerator.assertion(cred); - multiFactorErrorResolver.resolveSignIn(assertion).then(userCredential => { - onAuthUserCredentialSuccess(userCredential); - $('#multiFactorModal').modal('hide'); - }, onAuthError); -} - -/** - * Adds a new row to insert an OAuth custom parameter key/value pair. - * @param {!jQuery.Event} _event The jQuery event object. - */ -function onPopupRedirectAddCustomParam(_event) { - // Form container. - let html = '

'; - // OAuth parameter key input. - html += - ''; - // OAuth parameter value input. - html += - ''; - // Button to remove current key/value pair. - html += ''; - html += ''; - // Create jQuery node. - const $node = $(html); - // Add button click event listener to remove item. - $node.find('button').on('click', function (e) { - // Remove button click event listener. - $(this).off('click'); - // Get row container and remove it. - $(this).closest('form.customParamItem').remove(); - e.preventDefault(); - }); - // Append constructed row to parameter list container. - $('#popup-redirect-custom-parameters').append($node); -} - -/** - * Performs the corresponding popup/redirect action for a generic provider. - */ -function onPopupRedirectGenericProviderClick() { - var providerId = $('#popup-redirect-generic-providerid').val(); - var provider = new OAuthProvider(providerId); - signInWithPopupRedirect(provider); -} - -/** - * Performs the corresponding popup/redirect action for a SAML provider. - */ -function onPopupRedirectSamlProviderClick() { - alertNotImplemented(); - // var providerId = $('#popup-redirect-saml-providerid').val(); - // var provider = new SAMLAuthProvider(providerId); - // signInWithPopupRedirect(provider); -} - -/** - * Performs the corresponding popup/redirect action based on user's selection. - * @param {!jQuery.Event} _event The jQuery event object. - */ -function onPopupRedirectProviderClick(_event) { - const providerId = $(event.currentTarget).data('provider'); - let provider = null; - switch (providerId) { - case 'google.com': - provider = new GoogleAuthProvider(); - break; - case 'facebook.com': - provider = new FacebookAuthProvider(); - break; - case 'github.com': - provider = new GithubAuthProvider(); - break; - case 'twitter.com': - provider = new TwitterAuthProvider(); - break; - default: - return; - } - signInWithPopupRedirect(provider); -} - -/** - * Performs a popup/redirect action based on a given provider and the user's - * selections. - * @param {!AuthProvider} provider The provider with which to - * sign in. - */ -function signInWithPopupRedirect(provider) { - let action = $('input[name=popup-redirect-action]:checked').val(); - let type = $('input[name=popup-redirect-type]:checked').val(); - let method = null; - let inst = null; - - switch (action) { - case 'link': - if (!activeUser()) { - alertError('No user logged in.'); - return; - } - inst = activeUser(); - method = type === 'popup' ? linkWithPopup : linkWithRedirect; - break; - case 'reauthenticate': - if (!activeUser()) { - alertError('No user logged in.'); - return; - } - inst = activeUser(); - method = - type === 'popup' ? reauthenticateWithPopup : reauthenticateWithRedirect; - break; - default: - inst = auth; - method = type === 'popup' ? signInWithPopup : signInWithRedirect; - } - - // Get custom OAuth parameters. - const customParameters = {}; - // For each entry. - $('form.customParamItem').each(function (_index) { - // Get parameter key. - const key = $(this).find('input.customParamKey').val(); - // Get parameter value. - const value = $(this).find('input.customParamValue').val(); - // Save to list if valid. - if (key && value) { - customParameters[key] = value; - } - }); - console.log('customParameters: ', customParameters); - // For older jscore versions that do not support this. - if (provider.setCustomParameters) { - // Set custom parameters on current provider. - provider.setCustomParameters(customParameters); - } - - // Add scopes for providers who do have scopes available (i.e. not Twitter). - if (provider.addScope) { - // String.prototype.trim not available in IE8. - const scopes = $.trim($('#scopes').val()).split(/\s*,\s*/); - for (let i = 0; i < scopes.length; i++) { - provider.addScope(scopes[i]); - } - } - console.log('Provider:'); - console.log(provider); - if (type == 'popup') { - method(inst, provider, browserPopupRedirectResolver).then(response => { - console.log('Popup response:'); - console.log(response); - alertSuccess(action + ' with ' + provider['providerId'] + ' successful!'); - logAdditionalUserInfo(response); - onAuthSuccess(activeUser()); - }, onAuthError); - } else { - try { - method(inst, provider, browserPopupRedirectResolver).catch(onAuthError); - } catch (error) { - console.log('Error while calling ' + method); - console.error(error); - } - } -} - -/** - * Displays user credential result. - * @param {!UserCredential} result The UserCredential result - * object. - */ -function onAuthUserCredentialSuccess(result) { - onAuthSuccess(result.user); - logAdditionalUserInfo(result); -} - -/** - * Displays redirect result. - */ -function onGetRedirectResult() { - getRedirectResult(auth, browserPopupRedirectResolver).then(function ( - response - ) { - log('Redirect results:'); - if (response.credential) { - log('Credential:'); - log(response.credential); - } else { - log('No credential'); - } - if (response.user) { - log("User's id:"); - log(response.user.uid); - } else { - log('No user'); - } - logAdditionalUserInfo(response); - console.log(response); - }, - onAuthError); -} - -/** - * Logs additional user info returned by a sign-in event, if available. - * @param {!Object} response - */ -function logAdditionalUserInfo(response) { - if (response?.additionalUserInfo) { - if (response.additionalUserInfo.username) { - log( - response.additionalUserInfo['providerId'] + - ' username: ' + - response.additionalUserInfo.username - ); - } - if (response.additionalUserInfo.profile) { - log(response.additionalUserInfo['providerId'] + ' profile information:'); - log(JSON.stringify(response.additionalUserInfo.profile, null, 2)); - } - if (typeof response.additionalUserInfo.isNewUser !== 'undefined') { - log( - response.additionalUserInfo['providerId'] + - ' isNewUser: ' + - response.additionalUserInfo.isNewUser - ); - } - if (response.credential) { - log('credential: ' + JSON.stringify(response.credential.toJSON())); - } - } -} - -/** - * Deletes the user account. - */ -function onDelete() { - activeUser() - ['delete']() - .then(() => { - log('User successfully deleted.'); - alertSuccess('User successfully deleted.'); - refreshUserData(); - }, onAuthError); -} - -/** - * Gets a specific query parameter from the current URL. - * @param {string} name Name of the parameter. - * @return {string} The query parameter requested. - */ -function getParameterByName(name) { - const url = window.location.href; - name = name.replace(/[\[\]]/g, '\\$&'); - const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); - const results = regex.exec(url); - if (!results) { - return null; - } - if (!results[2]) { - return ''; - } - return decodeURIComponent(results[2].replace(/\+/g, ' ')); -} - -/** - * Detects if an action code is passed in the URL, and populates accordingly - * the input field for the confirm email verification process. - */ -function populateActionCodes() { - let emailForSignIn = null; - let signInTime = 0; - if ('localStorage' in window && window['localStorage'] !== null) { - try { - // Try to parse as JSON first using new storage format. - const emailForSignInData = JSON.parse( - window.localStorage.getItem('emailForSignIn') - ); - emailForSignIn = emailForSignInData['email'] || null; - signInTime = emailForSignInData['timestamp'] || 0; - } catch (e) { - // JSON parsing failed. This means the email is stored in the old string - // format. - emailForSignIn = window.localStorage.getItem('emailForSignIn'); - } - if (emailForSignIn) { - // Clear old codes. Old format codes should be cleared immediately. - if (new Date().getTime() - signInTime >= 1 * 24 * 3600 * 1000) { - // Remove email from storage. - window.localStorage.removeItem('emailForSignIn'); - } - } - } - const actionCode = getParameterByName('oobCode'); - if (actionCode != null) { - const mode = getParameterByName('mode'); - if (mode === 'verifyEmail') { - $('#email-verification-code').val(actionCode); - } else if (mode === 'resetPassword') { - $('#password-reset-code').val(actionCode); - } else if (mode === 'signIn') { - if (emailForSignIn) { - $('#sign-in-with-email-link-email').val(emailForSignIn); - $('#sign-in-with-email-link-link').val(window.location.href); - onSignInWithEmailLink(); - // Remove email from storage as the code is only usable once. - window.localStorage.removeItem('emailForSignIn'); - } - } else { - $('#email-verification-code').val(actionCode); - $('#password-reset-code').val(actionCode); - } - } -} - -/** - * Provides basic Database checks for authenticated and unauthenticated access. - * The Database node being tested has the following rule: - * "users": { - * "$user_id": { - * ".read": "$user_id === auth.uid", - * ".write": "$user_id === auth.uid" - * } - * } - * This applies when Real-time database service is available. - */ -function checkDatabaseAuthAccess() { - const randomString = Math.floor(Math.random() * 10000000).toString(); - let dbRef; - let dbPath; - let errMessage; - // Run this check only when Database module is available. - if ( - typeof firebase !== 'undefined' && - typeof firebase.database !== 'undefined' - ) { - if (lastUser && !auth.currentUser) { - dbPath = 'users/' + lastUser.uid; - // After sign out, confirm read/write access to users/$user_id blocked. - dbRef = firebase.database().ref(dbPath); - dbRef - .set({ - 'test': randomString - }) - .then(() => { - alertError( - 'Error: Unauthenticated write to Database node ' + - dbPath + - ' unexpectedly succeeded!' - ); - }) - .catch(error => { - errMessage = error.message.toLowerCase(); - // Permission denied error should be thrown. - if (errMessage.indexOf('permission_denied') === -1) { - alertError('Error: ' + error.code); - return; - } - dbRef - .once('value') - .then(() => { - alertError( - 'Error: Unauthenticated read to Database node ' + - dbPath + - ' unexpectedly succeeded!' - ); - }) - .catch(error => { - errMessage = error.message.toLowerCase(); - // Permission denied error should be thrown. - if (errMessage.indexOf('permission_denied') === -1) { - alertError('Error: ' + error.code); - return; - } - log( - 'Unauthenticated read/write to Database node ' + - dbPath + - ' failed as expected!' - ); - }); - }); - } else if (auth.currentUser) { - dbPath = 'users/' + auth.currentUser.uid; - // Confirm read/write access to users/$user_id allowed. - dbRef = firebase.database().ref(dbPath); - dbRef - .set({ - 'test': randomString - }) - .then(() => { - return dbRef.once('value'); - }) - .then(snapshot => { - if (snapshot.val().test === randomString) { - // read/write successful. - log( - 'Authenticated read/write to Database node ' + - dbPath + - ' succeeded!' - ); - } else { - throw new Error( - 'Authenticated read/write to Database node ' + dbPath + ' failed!' - ); - } - // Clean up: clear that node's content. - return dbRef.remove(); - }) - .catch(error => { - alertError('Error: ' + error.code); - }); - } - } -} - -/** - * Runs various Firebase Auth tests in a web worker environment and confirms the - * expected behavior. This is useful for manual testing in different browsers. - * @param {string} googleIdToken The Google ID token to sign in with. - */ -function onRunWebWorkTests() { - if (!webWorker) { - alertError('Error: Web workers are not supported in the current browser!'); - return; - } - signInWithPopup( - auth, - new GoogleAuthProvider(), - browserPopupRedirectResolver - ).then( - result => { - const credential = GoogleAuthProvider.credentialFromResult(result); - webWorker.postMessage({ - type: 'RUN_TESTS', - googleIdToken: credential.idToken - }); - }, - error => { - alertError('Error code: ' + error.code + ' message: ' + error.message); - } - ); -} - -/** Runs service worker tests if supported. */ -function onRunServiceWorkTests() { - $.ajax('/checkIfAuthenticated').then( - (data, _textStatus, _jqXHR) => { - alertSuccess('User authenticated: ' + data.uid); - }, - (jqXHR, _textStatus, _errorThrown) => { - alertError(jqXHR.status + ': ' + JSON.stringify(jqXHR.responseJSON)); - } - ); -} - -/** Copy current user of auth to tempAuth. */ -function onCopyActiveUser() { - tempAuth.updateCurrentUser(activeUser()).then( - () => { - alertSuccess('Copied active user to temp Auth'); - }, - error => { - alertError('Error: ' + error.code); - } - ); -} - -/** Copy last user to auth. */ -function onCopyLastUser() { - // If last user is null, NULL_USER error will be thrown. - auth.updateCurrentUser(lastUser).then( - () => { - alertSuccess('Copied last user to Auth'); - }, - error => { - alertError('Error: ' + error.code); - } - ); -} - -/** Applies selected auth settings change. */ -function onApplyAuthSettingsChange() { - try { - auth.settings.appVerificationDisabledForTesting = - $('input[name=enable-app-verification]:checked').val() === 'No'; - alertSuccess('Auth settings changed'); - } catch (error) { - alertError('Error: ' + error.code); - } -} - -/** - * Initiates the application by setting event listeners on the various buttons. - */ -function initApp() { - log('Initializing app...'); - app = initializeApp(config); - auth = getAuth(app); - - tempApp = initializeApp( - { - apiKey: config.apiKey, - authDomain: config.authDomain - }, - `${auth.name}-temp` - ); - tempAuth = initializeAuth(tempApp, { - persistence: inMemoryPersistence, - popupRedirectResolver: browserPopupRedirectResolver - }); - - // Listen to reCAPTCHA config togglers. - initRecaptchaToggle(size => { - clearApplicationVerifier(); - recaptchaSize = size; - }); - - // The action code for email verification or password reset - // can be passed in the url address as a parameter, and for convenience - // this preloads the input field. - populateActionCodes(); - - // Allows to login the user if previously logged in. - if (auth.onIdTokenChanged) { - auth.onIdTokenChanged(user => { - refreshUserData(); - if (user) { - user.getIdTokenResult(false).then( - idTokenResult => { - log(JSON.stringify(idTokenResult)); - }, - () => { - log('No token.'); - } - ); - } else { - log('No user logged in.'); - } - }); - } - - if (auth.onAuthStateChanged) { - auth.onAuthStateChanged(user => { - if (user) { - log('user state change detected: ' + user.uid); - } else { - log('user state change detected: no user'); - } - // Check Database Auth access. - checkDatabaseAuthAccess(); - }); - } - - if (tempAuth.onAuthStateChanged) { - tempAuth.onAuthStateChanged(user => { - if (user) { - log('user state change on temp Auth detect: ' + JSON.stringify(user)); - alertSuccess('user state change on temp Auth detect: ' + user.uid); - } - }); - } - - /** - * @fileoverview Utilities for Auth test app features. - */ - - /** - * Initializes the widget for toggling reCAPTCHA size. - * @param {function(string):void} callback The callback to call when the - * size toggler is changed, which takes in the new reCAPTCHA size. - */ - function initRecaptchaToggle(callback) { - // Listen to recaptcha config togglers. - const $recaptchaConfigTogglers = $('.toggleRecaptcha'); - $recaptchaConfigTogglers.click(function (e) { - // Remove currently active option. - $recaptchaConfigTogglers.removeClass('active'); - // Set currently selected option. - $(this).addClass('active'); - // Get the current reCAPTCHA setting label. - const size = $(e.target).text().toLowerCase(); - callback(size); - }); - } - - // Install servicerWorker if supported. - if ('serviceWorker' in navigator) { - navigator.serviceWorker - .register('/service-worker.js') - .then(reg => { - // Registration worked. - console.log('Registration succeeded. Scope is ' + reg.scope); - }) - .catch(error => { - // Registration failed. - console.log('Registration failed with ' + error.message); - }); - } - - if (window.Worker) { - webWorker = new Worker('/web-worker.js'); - /** - * Handles the incoming message from the web worker. - * @param {!Object} e The message event received. - */ - webWorker.onmessage = function (e) { - console.log('User data passed through web worker: ', e.data); - switch (e.data.type) { - case 'GET_USER_INFO': - alertSuccess( - 'User data passed through web worker: ' + JSON.stringify(e.data) - ); - break; - case 'RUN_TESTS': - if (e.data.status === 'success') { - alertSuccess('Web worker tests ran successfully!'); - } else { - alertError('Error: ' + JSON.stringify(e.data.error)); - } - break; - default: - return; - } - }; - } - - /** - * Asks the web worker, if supported in current browser, to return the user info - * corresponding to the currentUser as seen within the worker. - */ - function onGetCurrentUserDataFromWebWorker() { - if (webWorker) { - webWorker.postMessage({ type: 'GET_USER_INFO' }); - } else { - alertError( - 'Error: Web workers are not supported in the current browser!' - ); - } - } - - // We check for redirect result to refresh user's data. - getRedirectResult(auth, browserPopupRedirectResolver).then(function ( - response - ) { - refreshUserData(); - logAdditionalUserInfo(response); - }, - onAuthError); - - // Bootstrap tooltips. - $('[data-toggle="tooltip"]').tooltip(); - - // Auto submit the choose library type form. - $('#library-form').on('change', 'input.library-option', () => { - $('#library-form').submit(); - }); - - // To clear the logs in the page. - $('.clear-logs').click(clearLogs); - - // Disables JS forms. - $('form.no-submit').on('submit', () => { - return false; - }); - - // Keeps track of the current tab opened. - $('#tab-menu a').click(event => { - currentTab = $(event.currentTarget).attr('href'); - }); - - // Toggles user. - $('input[name=toggle-user-selection]').change(refreshUserData); - - // Actions listeners. - $('#sign-up-with-email-and-password').click(onSignUp); - $('#sign-in-with-email-and-password').click(onSignInWithEmailAndPassword); - $('.sign-in-with-custom-token').click(onSignInWithCustomToken); - $('#sign-in-anonymously').click(onSignInAnonymously); - $('#sign-in-with-generic-idp-credential').click( - onSignInWithGenericIdPCredential - ); - $('#signin-verify-phone-number').click(onSignInVerifyPhoneNumber); - $('#signin-confirm-phone-verification').click( - onSignInConfirmPhoneVerification - ); - // On enter click in verification code, complete phone sign-in. This prevents - // reCAPTCHA from being re-rendered (default behavior on enter). - $('#signin-phone-verification-code').keypress(e => { - if (e.which === 13) { - onSignInConfirmPhoneVerification(); - e.preventDefault(); - } - }); - $('#sign-in-with-email-link').click(onSignInWithEmailLink); - $('#link-with-email-link').click(onLinkWithEmailLink); - $('#reauth-with-email-link').click(onReauthenticateWithEmailLink); - - $('#change-email').click(onChangeEmail); - $('#change-password').click(onChangePassword); - $('#update-profile').click(onUpdateProfile); - - $('#send-sign-in-link-to-email').click(onSendSignInLinkToEmail); - $('#send-sign-in-link-to-email-current-url').click( - onSendSignInLinkToEmailCurrentUrl - ); - $('#send-link-email-link').click(onSendLinkEmailLink); - - $('#send-password-reset-email').click(onSendPasswordResetEmail); - $('#verify-password-reset-code').click(onVerifyPasswordResetCode); - $('#confirm-password-reset').click(onConfirmPasswordReset); - - $('#get-provider-data').click(onGetProviderData); - $('#link-with-email-and-password').click(onLinkWithEmailAndPassword); - $('#link-with-generic-idp-credential').click(onLinkWithGenericIdPCredential); - $('#unlink-provider').click(onUnlinkProvider); - $('#link-reauth-verify-phone-number').click(onLinkReauthVerifyPhoneNumber); - $('#update-confirm-phone-verification').click( - onUpdateConfirmPhoneVerification - ); - $('#link-confirm-phone-verification').click(onLinkConfirmPhoneVerification); - $('#reauth-confirm-phone-verification').click( - onReauthConfirmPhoneVerification - ); - // On enter click in verification code, complete phone sign-in. This prevents - // reCAPTCHA from being re-rendered (default behavior on enter). - $('#link-reauth-phone-verification-code').keypress(e => { - if (e.which === 13) { - // User first option option as default. - onUpdateConfirmPhoneVerification(); - e.preventDefault(); - } - }); - - $('#send-email-verification').click(onSendEmailVerification); - $('#confirm-email-verification').click(onApplyActionCode); - $('#get-token-result').click(onGetIdTokenResult); - $('#refresh-token-result').click(onRefreshTokenResult); - $('#get-token').click(onGetIdToken); - $('#refresh-token').click(onRefreshToken); - $('#get-token-worker').click(onGetCurrentUserDataFromWebWorker); - $('#sign-out').click(onSignOut); - - $('.popup-redirect-provider').click(onPopupRedirectProviderClick); - $('#popup-redirect-generic').click(onPopupRedirectGenericProviderClick); - $('#popup-redirect-get-redirect-result').click(onGetRedirectResult); - $('#popup-redirect-add-custom-parameter').click( - onPopupRedirectAddCustomParam - ); - $('#popup-redirect-saml').click(onPopupRedirectSamlProviderClick); - - $('#action-code-settings-reset').click(onActionCodeSettingsReset); - - $('#delete').click(onDelete); - - $('#set-persistence').click(onSetPersistence); - - $('#set-language-code').click(onSetLanguageCode); - $('#use-device-language').click(onUseDeviceLanguage); - - $('#fetch-sign-in-methods-for-email').click(onFetchSignInMethodsForEmail); - - $('#run-web-worker-tests').click(onRunWebWorkTests); - $('#run-service-worker-tests').click(onRunServiceWorkTests); - $('#copy-active-user').click(onCopyActiveUser); - $('#copy-last-user').click(onCopyLastUser); - - $('#apply-auth-settings-change').click(onApplyAuthSettingsChange); - - // Multi-factor operations. - // Starts multi-factor sign-in with selected phone number. - $('#send-2fa-phone-code').click(onStartSignInWithPhoneMultiFactor); - // Completes multi-factor sign-in with supplied SMS code. - $('#sign-in-with-phone-multi-factor').click( - onFinalizeSignInWithPhoneMultiFactor - ); - // Starts multi-factor enrollment with phone number. - $('#enroll-mfa-verify-phone-number').click(onStartEnrollWithPhoneMultiFactor); - // Completes multi-factor enrollment with supplied SMS code. - $('#enroll-mfa-confirm-phone-verification').click( - onFinalizeEnrollWithPhoneMultiFactor - ); -} - -$(initApp); diff --git a/packages-exp/auth-exp/demo/src/worker/service-worker.ts b/packages-exp/auth-exp/demo/src/worker/service-worker.ts deleted file mode 100644 index 564e46517d4..00000000000 --- a/packages-exp/auth-exp/demo/src/worker/service-worker.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Service worker for Firebase Auth test app application. The - * service worker caches all content and only serves cached content in offline - * mode. - */ -import { initializeApp } from '@firebase/app-exp'; -import { getAuth } from '@firebase/auth-exp'; -import { User } from '@firebase/auth-types-exp'; - -import { config } from '../config'; - -declare let self: ServiceWorkerGlobalScope; - -const CACHE_NAME = 'cache-v1'; -const urlsToCache = [ - '/', - '/manifest.json', - '/config.js', - '/common.js', - '/style.css' -]; - -// Initialize the Firebase app in the service worker. -const app = initializeApp(config); -const auth = getAuth(app); - -/** - * Returns a promise that resolves with an ID token if available. - * @return {!Promise} The promise that resolves with an ID token if - * available. Otherwise, the promise resolves with null. - */ -function getIdToken(): Promise { - return new Promise(resolve => { - auth.onAuthStateChanged((user: User | null) => { - if (user) { - user - .getIdToken() - .then(resolve) - .catch(() => { - resolve(null); - }); - } else { - resolve(null); - } - }); - }); -} - -/** - * @param {string} url The URL whose origin is to be returned. - * @return {string} The origin corresponding to given URL. - */ -function getOriginFromUrl(url: string): string { - // https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript - const pathArray = url.split('/'); - const protocol = pathArray[0]; - const host = pathArray[2]; - return protocol + '//' + host; -} - -self.addEventListener('install', (event: ExtendableEvent) => { - // Perform install steps. - event.waitUntil( - async (): Promise => { - const cache = await caches.open(CACHE_NAME); - // Add all URLs of resources we want to cache. - try { - await cache.addAll(urlsToCache); - } catch { - // Suppress error as some of the files may not be available for the - // current page. - } - } - ); -}); - -// As this is a test app, let's only return cached data when offline. -self.addEventListener('fetch', (event: FetchEvent) => { - event.respondWith( - (async function () { - const idToken = await getIdToken(); - // Try to fetch the resource first after checking for the ID token. - let req = event.request; - // For same origin https requests, append idToken to header. - if ( - self.location.origin === getOriginFromUrl(event.request.url) && - (self.location.protocol === 'https:' || - self.location.hostname === 'localhost') && - idToken - ) { - // Clone headers as request headers are immutable. - const headers = new Headers(); - req.headers.forEach((value, key) => { - headers.append(key, value); - }); - // Add ID token to header. We can't add to Authentication header as it - // will break HTTP basic authentication. - headers.append('x-id-token', idToken); - try { - req = new Request(req.url, { - method: req.method, - headers, - mode: 'same-origin', - credentials: req.credentials, - cache: req.cache, - redirect: req.redirect, - referrer: req.referrer, - body: req.body - }); - } catch (e) { - // This will fail for CORS requests. We just continue with the - // fetch caching logic below and do not pass the ID token. - } - } - return fetch(req) - .then(response => { - // Check if we received a valid response. - // If not, just funnel the error response. - if (response.status !== 200 || response.type !== 'basic') { - return response; - } - // If response is valid, clone it and save it to the cache. - var responseToCache = response.clone(); - // Save response to cache. - caches.open(CACHE_NAME).then(function (cache) { - cache.put(event.request, responseToCache); - }); - // After caching, return response. - return response; - }) - .catch(() => { - // For fetch errors, attempt to retrieve the resource from cache. - return caches.match(event.request.clone()) as Promise; - }) - .catch(error => { - // If error getting resource from cache, do nothing. - console.log(error); - throw error; - }); - })() - ); -}); - -self.addEventListener('activate', (event: ExtendableEvent) => { - // Update this list with all caches that need to remain cached. - const cacheWhitelist = ['cache-v1']; - event.waitUntil( - async (): Promise => { - const cacheNames = await caches.keys(); - await Promise.all( - cacheNames.map(cacheName => { - // Check if cache is not whitelisted above. - if (cacheWhitelist.indexOf(cacheName) === -1) { - // If not whitelisted, delete it. - return caches.delete(cacheName); - } - }) - ); - } - ); -}); diff --git a/packages-exp/auth-exp/demo/src/worker/tsconfig.json b/packages-exp/auth-exp/demo/src/worker/tsconfig.json deleted file mode 100644 index e160c5c6b47..00000000000 --- a/packages-exp/auth-exp/demo/src/worker/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "target": "es2015", - "lib": [ - "es2015", - "webworker" - ] - }, - "exclude": [ - "dist/**/*" - ] -} \ No newline at end of file diff --git a/packages-exp/auth-exp/index.node.ts b/packages-exp/auth-exp/index.node.ts deleted file mode 100644 index aea3585e4f1..00000000000 --- a/packages-exp/auth-exp/index.node.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * This is the file that people using Node.js will actually import. You should - * only include this file if you have something specific about your - * implementation that mandates having a separate entrypoint. Otherwise you can - * just use index.browser.ts - */ - -import * as fetchImpl from 'node-fetch'; - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { Auth } from '@firebase/auth-types-exp'; - -import { initializeAuth } from './src'; -import { registerAuth } from './src/core/auth/register'; -import { FetchProvider } from './src/core/util/fetch_provider'; -import { ClientPlatform } from './src/core/util/version'; - -// Initialize the fetch polyfill, the types are slightly off so just cast and hope for the best -FetchProvider.initialize( - (fetchImpl.default as unknown) as typeof fetch, - (fetchImpl.Headers as unknown) as typeof Headers, - (fetchImpl.Response as unknown) as typeof Response -); - -// Core functionality shared by all clients -export * from './src'; - -export function getAuth(app: FirebaseApp): Auth { - return initializeAuth(app); -} - -registerAuth(ClientPlatform.NODE); diff --git a/packages-exp/auth-exp/index.rn.ts b/packages-exp/auth-exp/index.rn.ts deleted file mode 100644 index 169de2536af..00000000000 --- a/packages-exp/auth-exp/index.rn.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * This is the file that people using React Native will actually import. You - * should only include this file if you have something specific about your - * implementation that mandates having a separate entrypoint. Otherwise you can - * just use index.ts - */ - -import { AsyncStorage } from 'react-native'; - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { Auth } from '@firebase/auth-types-exp'; - -import { initializeAuth } from './src'; -import { registerAuth } from './src/core/auth/register'; -import { ClientPlatform } from './src/core/util/version'; -import { getReactNativePersistence } from './src/platform_react_native/persistence/react_native'; - -// Core functionality shared by all clients -export * from './src'; - -export const reactNativeLocalPersistence = getReactNativePersistence( - AsyncStorage -); - -export function getAuth(app: FirebaseApp): Auth { - return initializeAuth(app, { - persistence: reactNativeLocalPersistence - }); -} - -registerAuth(ClientPlatform.REACT_NATIVE); diff --git a/packages-exp/auth-exp/index.ts b/packages-exp/auth-exp/index.ts deleted file mode 100644 index ac15088cdb6..00000000000 --- a/packages-exp/auth-exp/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { Auth } from '@firebase/auth-types-exp'; - -import { initializeAuth } from './src'; -import { registerAuth } from './src/core/auth/register'; -import { ClientPlatform } from './src/core/util/version'; -import { browserLocalPersistence } from './src/platform_browser/persistence/local_storage'; -import { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db'; -import { browserPopupRedirectResolver } from './src/platform_browser/popup_redirect'; - -// Core functionality shared by all clients -export * from './src'; - -// Additional DOM dependend functionality - -// persistence -export { browserLocalPersistence } from './src/platform_browser/persistence/local_storage'; -export { browserSessionPersistence } from './src/platform_browser/persistence/session_storage'; -export { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db'; - -// providers -export { PhoneAuthProvider } from './src/platform_browser/providers/phone'; - -// strategies -export { - signInWithPhoneNumber, - linkWithPhoneNumber, - reauthenticateWithPhoneNumber, - updatePhoneNumber -} from './src/platform_browser/strategies/phone'; -export { - signInWithPopup, - linkWithPopup, - reauthenticateWithPopup -} from './src/platform_browser/strategies/popup'; -export { - signInWithRedirect, - linkWithRedirect, - reauthenticateWithRedirect, - getRedirectResult -} from './src/platform_browser/strategies/redirect'; - -export { RecaptchaVerifier } from './src/platform_browser/recaptcha/recaptcha_verifier'; -export { browserPopupRedirectResolver } from './src/platform_browser/popup_redirect'; - -// MFA -export { PhoneMultiFactorGenerator } from './src/platform_browser/mfa/assertions/phone'; - -/** - * Initializes an Auth instance with platform specific default dependencies. - * - * @param app - The Firebase App. - * - * @public - */ -export function getAuth(app: FirebaseApp): Auth { - return initializeAuth(app, { - popupRedirectResolver: browserPopupRedirectResolver, - persistence: [indexedDBLocalPersistence, browserLocalPersistence] - }); -} - -registerAuth(ClientPlatform.BROWSER); diff --git a/packages-exp/auth-exp/internal/index.ts b/packages-exp/auth-exp/internal/index.ts deleted file mode 100644 index bcec1430866..00000000000 --- a/packages-exp/auth-exp/internal/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * This interface is intended only for use by @firebase/auth-compat-exp, do not use directly - */ -export * from '../index'; - -export { SignInWithIdpResponse } from '../src/api/authentication/idp'; -export { AuthErrorCode } from '../src/core/errors'; -export { Persistence } from '../src/core/persistence'; -export { _persistenceKeyName } from '../src/core/persistence/persistence_user_manager'; -export { UserImpl } from '../src/core/user/user_impl'; -export { _getInstance } from '../src/core/util/instantiator'; -export { UserCredential, UserParameters } from '../src/model/user'; -export { registerAuth } from '../src/core/auth/register'; -export { DefaultConfig, AuthImpl } from '../src/core/auth/auth_impl'; - -export { ClientPlatform, _getClientVersion } from '../src/core/util/version'; - -export { _generateEventId } from '../src/core/util/event_id'; - -export { _fail, _assert } from '../src/core/util/assert'; diff --git a/packages-exp/auth-exp/internal/package.json b/packages-exp/auth-exp/internal/package.json deleted file mode 100644 index eaaf217e240..00000000000 --- a/packages-exp/auth-exp/internal/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@firebase/auth-exp/internal", - "description": "An internal version of the Auth SDK for use in the compatibility layer", - "main": "../dist/node/internal.js", - "module": "../dist/esm5/internal.js", - "browser": "../dist/esm5/internal.js", - "esm2017": "../dist/esm2017/internal.js", - "typings": "../dist/esm5/internal/index.d.ts", - "private": true -} diff --git a/packages-exp/auth-exp/karma.conf.js b/packages-exp/auth-exp/karma.conf.js deleted file mode 100644 index ac7f41abfe4..00000000000 --- a/packages-exp/auth-exp/karma.conf.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const karmaBase = require('../../config/karma.base'); -const { argv } = require('yargs'); - -module.exports = function (config) { - const karmaConfig = Object.assign({}, karmaBase, { - // files to load into karma - files: getTestFiles(argv), - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'] - }); - - config.set(karmaConfig); -}; - -function getTestFiles(argv) { - if (argv.unit) { - return ['src/**/*.test.ts', 'test/helpers/**/*.test.ts']; - } else if (argv.integration) { - return ['test/integration/**/*.test.ts']; - } else { - // For the catch-all yarn:test, ignore the phone integration test - return [ - 'src/**/*.test.ts', - 'test/helpers/**/*.test.ts', - 'test/integration/flows/anonymous.test.ts', - 'test/integration/flows/email.test.ts' - ]; - } -} - -module.exports.files = getTestFiles(argv); diff --git a/packages-exp/auth-exp/package.json b/packages-exp/auth-exp/package.json deleted file mode 100644 index fc3679d7b53..00000000000 --- a/packages-exp/auth-exp/package.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "name": "@firebase/auth-exp", - "version": "0.0.900", - "private": true, - "description": "The Firebase Authenticaton component of the Firebase JS SDK.", - "author": "Firebase (https://firebase.google.com/)", - "main": "dist/node/index.js", - "react-native": "dist/rn/index.js", - "browser": "dist/esm5/index.js", - "module": "dist/esm5/index.js", - "esm2017": "dist/esm2017/index.js", - "webworker": "dist/index.webworker.esm5.js", - "files": ["dist"], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c && yarn api-report", - "build:deps": "lerna run --scope @firebase/auth-exp --include-dependencies build", - "build:release": "rollup -c rollup.config.release.js", - "dev": "rollup -c -w", - "test": "run-p lint test:all", - "test:all": "run-p test:browser test:node", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", - "test:browser": "karma start --single-run", - "test:browser:unit": "karma start --single-run --unit", - "test:browser:integration": "karma start --single-run --integration", - "test:browser:debug": "karma start --auto-watch", - "test:browser:unit:debug": "karma start --auto-watch --unit", - "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'src/!(platform_browser|platform_react_native)/**/*.test.ts' --file index.node.ts --config ../../config/mocharc.node.js", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn build && yarn doc" - }, - "peerDependencies": { - "@firebase/app-exp": "0.x", - "@firebase/app-types-exp": "0.x" - }, - "dependencies": { - "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.4", - "@firebase/component": "0.1.21", - "@firebase/auth-types-exp": "0.0.900", - "node-fetch": "2.6.1", - "tslib": "^1.11.1" - }, - "license": "Apache-2.0", - "devDependencies": { - "@firebase/app-exp": "0.0.900", - "rollup": "2.35.1", - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-sourcemaps": "0.6.3", - "rollup-plugin-typescript2": "0.29.0", - "@rollup/plugin-strip": "2.0.0", - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages-exp/auth-exp", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "dist/esm5/index.d.ts", - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/auth-exp/rollup.config.js b/packages-exp/auth-exp/rollup.config.js deleted file mode 100644 index 09b91aecf0c..00000000000 --- a/packages-exp/auth-exp/rollup.config.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getConfig } from './rollup.config.shared'; - -export default getConfig({ isReleaseBuild: false }); diff --git a/packages-exp/auth-exp/rollup.config.release.js b/packages-exp/auth-exp/rollup.config.release.js deleted file mode 100644 index 23c611a5b21..00000000000 --- a/packages-exp/auth-exp/rollup.config.release.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getConfig } from './rollup.config.shared'; - -export default getConfig({ isReleaseBuild: true }); diff --git a/packages-exp/auth-exp/rollup.config.shared.js b/packages-exp/auth-exp/rollup.config.shared.js deleted file mode 100644 index eaeb293dad7..00000000000 --- a/packages-exp/auth-exp/rollup.config.shared.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import strip from '@rollup/plugin-strip'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import json from '@rollup/plugin-json'; -import typescript from 'typescript'; -import pkg from './package.json'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * Common plugins for all builds - */ -const commonPlugins = [ - json(), - strip({ - functions: ['debugAssert.*'] - }) -]; - -export function getConfig({ isReleaseBuild }) { - /** - * ES5 Builds - */ - const es5BuildPlugins = [ - ...commonPlugins, - getTypesScriptPlugin({ isReleaseBuild }) - ]; - - const es5Builds = [ - /** - * Browser Builds - */ - { - input: { - index: 'index.ts', - internal: 'internal/index.ts' - }, - output: [{ dir: 'dist/esm5', format: 'esm', sourcemap: true }], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - /** - * Web Worker Build (compiled without DOM) - */ - { - input: 'index.webworker.ts', - output: [{ file: pkg.webworker, format: 'es', sourcemap: true }], - plugins: [ - ...commonPlugins, - getTypesScriptPlugin({ - isReleaseBuild, - compilerOptions: { - lib: [ - // Remove dom after we figure out why navigator stuff doesn't exist - 'dom', - 'es2015', - 'webworker' - ] - } - }) - ], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - /** - * Node.js Build - */ - { - input: { - index: 'index.node.ts', - internal: 'internal/index.ts' - }, - output: [{ dir: 'dist/node', format: 'cjs', sourcemap: true }], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - /** - * React Native Builds - */ - { - input: { - index: 'index.rn.ts', - internal: 'internal/index.ts' - }, - output: [{ dir: 'dist/rn', format: 'cjs', sourcemap: true }], - plugins: es5BuildPlugins, - external: id => - [...deps, 'react-native'].some( - dep => id === dep || id.startsWith(`${dep}/`) - ) - } - ]; - - /** - * ES2017 Builds - */ - const es2017BuildPlugins = [ - ...commonPlugins, - getTypesScriptPlugin({ - isReleaseBuild, - compilerOptions: { target: 'es2017' } - }) - ]; - - const es2017Builds = [ - /** - * Browser Builds - */ - { - input: { - index: 'index.ts', - internal: 'internal/index.ts' - }, - output: { - dir: 'dist/esm2017', - format: 'es', - sourcemap: true - }, - plugins: es2017BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } - ]; - - const allBuilds = [...es5Builds, ...es2017Builds]; - - if (isReleaseBuild) { - for (const build of allBuilds) { - build.treeshake = { - moduleSideEffects: false - }; - } - } - - return allBuilds; -} - -function getTypesScriptPlugin({ compilerOptions, isReleaseBuild }) { - let options = { - typescript, - tsconfigOverride: { - compilerOptions - } - }; - - if (isReleaseBuild) { - options = { - ...options, - clean: true, - abortOnError: false, - transformers: [importPathTransformer] - }; - } - - return typescriptPlugin(options); -} diff --git a/packages-exp/auth-exp/src/api/account_management/email_and_password.test.ts b/packages-exp/auth-exp/src/api/account_management/email_and_password.test.ts deleted file mode 100644 index 9a0c4065b92..00000000000 --- a/packages-exp/auth-exp/src/api/account_management/email_and_password.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { FirebaseError } from '@firebase/util'; - -import { Endpoint, HttpHeader } from '../'; -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { ServerError } from '../errors'; -import { - applyActionCode, - resetPassword, - updateEmailPassword -} from './email_and_password'; - -use(chaiAsPromised); - -describe('api/account_management/resetPassword', () => { - const request = { - oobCode: 'oob-code', - newPassword: 'new-password' - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.RESET_PASSWORD, { - email: 'test@foo.com' - }); - - const response = await resetPassword(auth, request); - expect(response.email).to.eq('test@foo.com'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.RESET_PASSWORD, - { - error: { - code: 400, - message: ServerError.RESET_PASSWORD_EXCEED_LIMIT, - errors: [ - { - message: ServerError.RESET_PASSWORD_EXCEED_LIMIT - } - ] - } - }, - 400 - ); - - await expect(resetPassword(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: We have blocked all requests from this device due to unusual activity. Try again later. (auth/too-many-requests).' - ); - expect(mock.calls[0].request).to.eql(request); - }); -}); - -describe('api/account_management/updateEmailPassword', () => { - const request = { - idToken: 'id-token', - returnSecureToken: true, - email: 'test@foo.com', - password: 'new-password' - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, { - idToken: 'id-token' - }); - - const response = await updateEmailPassword(auth, request); - expect(response.idToken).to.eq('id-token'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SET_ACCOUNT_INFO, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL, - errors: [ - { - message: ServerError.INVALID_EMAIL - } - ] - } - }, - 400 - ); - - await expect(updateEmailPassword(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls[0].request).to.eql(request); - }); -}); - -describe('api/account_management/applyActionCode', () => { - const request = { - oobCode: 'oob-code' - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {}); - - const response = await applyActionCode(auth, request); - expect(response).to.be.empty; - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SET_ACCOUNT_INFO, - { - error: { - code: 400, - message: ServerError.INVALID_OOB_CODE, - errors: [ - { - message: ServerError.INVALID_OOB_CODE - } - ] - } - }, - 400 - ); - - await expect(applyActionCode(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).' - ); - expect(mock.calls[0].request).to.eql(request); - }); -}); diff --git a/packages-exp/auth-exp/src/api/account_management/email_and_password.ts b/packages-exp/auth-exp/src/api/account_management/email_and_password.ts deleted file mode 100644 index d45d98cff1f..00000000000 --- a/packages-exp/auth-exp/src/api/account_management/email_and_password.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ActionCodeOperation, Auth } from '@firebase/auth-types-exp'; - -import { Endpoint, HttpMethod, _performApiRequest } from '../index'; -import { IdTokenResponse } from '../../model/id_token'; -import { MfaEnrollment } from './mfa'; - -export interface ResetPasswordRequest { - oobCode: string; - newPassword?: string; -} - -export interface ResetPasswordResponse { - email: string; - newEmail?: string; - requestType?: ActionCodeOperation; - mfaInfo?: MfaEnrollment; -} - -export async function resetPassword( - auth: Auth, - request: ResetPasswordRequest -): Promise { - return _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.RESET_PASSWORD, - request - ); -} -export interface UpdateEmailPasswordRequest { - idToken: string; - returnSecureToken?: boolean; - email?: string; - password?: string; -} - -export interface UpdateEmailPasswordResponse extends IdTokenResponse {} - -export async function updateEmailPassword( - auth: Auth, - request: UpdateEmailPasswordRequest -): Promise { - return _performApiRequest< - UpdateEmailPasswordRequest, - UpdateEmailPasswordResponse - >(auth, HttpMethod.POST, Endpoint.SET_ACCOUNT_INFO, request); -} - -export interface ApplyActionCodeRequest { - oobCode: string; -} - -export interface ApplyActionCodeResponse {} - -export async function applyActionCode( - auth: Auth, - request: ApplyActionCodeRequest -): Promise { - return _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SET_ACCOUNT_INFO, - request - ); -} diff --git a/packages-exp/auth-exp/src/api/account_management/mfa.test.ts b/packages-exp/auth-exp/src/api/account_management/mfa.test.ts deleted file mode 100644 index f05c393413e..00000000000 --- a/packages-exp/auth-exp/src/api/account_management/mfa.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { FirebaseError } from '@firebase/util'; - -import { Endpoint, HttpHeader } from '../'; -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { ServerError } from '../errors'; -import { - finalizeEnrollPhoneMfa, - startEnrollPhoneMfa, - withdrawMfa -} from './mfa'; - -use(chaiAsPromised); - -describe('api/account_management/startEnrollPhoneMfa', () => { - const request = { - idToken: 'id-token', - phoneEnrollmentInfo: { - phoneNumber: 'phone-number', - recaptchaToken: 'captcha-token' - } - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.START_PHONE_MFA_ENROLLMENT, { - phoneSessionInfo: { - sessionInfo: 'session-info' - } - }); - - const response = await startEnrollPhoneMfa(auth, request); - expect(response.phoneSessionInfo.sessionInfo).to.eq('session-info'); - expect(mock.calls[0].request).to.eql({ - tenantId: null, - ...request - }); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.START_PHONE_MFA_ENROLLMENT, - { - error: { - code: 400, - message: ServerError.INVALID_ID_TOKEN, - errors: [ - { - message: ServerError.INVALID_ID_TOKEN - } - ] - } - }, - 400 - ); - - await expect(startEnrollPhoneMfa(auth, request)).to.be.rejectedWith( - FirebaseError, - "Firebase: This user's credential isn't valid for this project. This can happen if the user's token has been tampered with, or if the user isn't for the project associated with this API key. (auth/invalid-user-token)." - ); - expect(mock.calls[0].request).to.eql({ - tenantId: null, - ...request - }); - }); -}); - -describe('api/account_management/finalizeEnrollPhoneMfa', () => { - const request = { - idToken: 'id-token', - phoneVerificationInfo: { - temporaryProof: 'temporary-proof', - phoneNumber: 'phone-number', - sessionInfo: 'session-info', - code: 'code' - } - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.FINALIZE_PHONE_MFA_ENROLLMENT, { - idToken: 'id-token', - refreshToken: 'refresh-token' - }); - - const response = await finalizeEnrollPhoneMfa(auth, request); - expect(response.idToken).to.eq('id-token'); - expect(response.refreshToken).to.eq('refresh-token'); - expect(mock.calls[0].request).to.eql({ - tenantId: null, - ...request - }); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.FINALIZE_PHONE_MFA_ENROLLMENT, - { - error: { - code: 400, - message: ServerError.INVALID_SESSION_INFO, - errors: [ - { - message: ServerError.INVALID_SESSION_INFO - } - ] - } - }, - 400 - ); - - await expect(finalizeEnrollPhoneMfa(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The verification ID used to create the phone auth credential is invalid. (auth/invalid-verification-id).' - ); - expect(mock.calls[0].request).to.eql({ - tenantId: null, - ...request - }); - }); -}); - -describe('api/account_management/withdrawMfa', () => { - const request = { - idToken: 'id-token', - mfaEnrollmentId: 'mfa-enrollment-id' - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.WITHDRAW_MFA, { - idToken: 'id-token', - refreshToken: 'refresh-token' - }); - - const response = await withdrawMfa(auth, request); - expect(response.idToken).to.eq('id-token'); - expect(response.refreshToken).to.eq('refresh-token'); - expect(mock.calls[0].request).to.eql({ - tenantId: null, - ...request - }); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.WITHDRAW_MFA, - { - error: { - code: 400, - message: ServerError.INVALID_ID_TOKEN, - errors: [ - { - message: ServerError.INVALID_ID_TOKEN - } - ] - } - }, - 400 - ); - - await expect(withdrawMfa(auth, request)).to.be.rejectedWith( - FirebaseError, - "Firebase: This user's credential isn't valid for this project. This can happen if the user's token has been tampered with, or if the user isn't for the project associated with this API key. (auth/invalid-user-token)." - ); - expect(mock.calls[0].request).to.eql({ - tenantId: null, - ...request - }); - }); -}); diff --git a/packages-exp/auth-exp/src/api/account_management/mfa.ts b/packages-exp/auth-exp/src/api/account_management/mfa.ts deleted file mode 100644 index 7b9223f52eb..00000000000 --- a/packages-exp/auth-exp/src/api/account_management/mfa.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Endpoint, HttpMethod, _performApiRequest } from '../index'; -import { SignInWithPhoneNumberRequest } from '../authentication/sms'; -import { FinalizeMfaResponse } from '../authentication/mfa'; -import { Auth } from '../../model/auth'; - -/** - * MFA Info as returned by the API - */ -interface BaseMfaEnrollment { - mfaEnrollmentId: string; - enrolledAt: number; - displayName?: string; -} - -/** - * An MFA provided by SMS verification - */ -export interface PhoneMfaEnrollment extends BaseMfaEnrollment { - phoneInfo: string; -} - -/** - * MfaEnrollment can be any subtype of BaseMfaEnrollment, currently only PhoneMfaEnrollment is supported - */ -export type MfaEnrollment = PhoneMfaEnrollment; - -export interface StartPhoneMfaEnrollmentRequest { - idToken: string; - phoneEnrollmentInfo: { - phoneNumber: string; - recaptchaToken: string; - }; - tenantId: string | null; -} - -export interface StartPhoneMfaEnrollmentResponse { - phoneSessionInfo: { - sessionInfo: string; - }; -} - -export function startEnrollPhoneMfa( - auth: Auth, - request: Omit -): Promise { - return _performApiRequest< - StartPhoneMfaEnrollmentRequest, - StartPhoneMfaEnrollmentResponse - >(auth, HttpMethod.POST, Endpoint.START_PHONE_MFA_ENROLLMENT, { - tenantId: auth.tenantId, - ...request - }); -} - -export interface FinalizePhoneMfaEnrollmentRequest { - idToken: string; - phoneVerificationInfo: SignInWithPhoneNumberRequest; - displayName?: string | null; - tenantId: string | null; -} - -export interface FinalizePhoneMfaEnrollmentResponse - extends FinalizeMfaResponse {} - -export function finalizeEnrollPhoneMfa( - auth: Auth, - request: Omit -): Promise { - return _performApiRequest< - FinalizePhoneMfaEnrollmentRequest, - FinalizePhoneMfaEnrollmentResponse - >(auth, HttpMethod.POST, Endpoint.FINALIZE_PHONE_MFA_ENROLLMENT, { - tenantId: auth.tenantId, - ...request - }); -} - -export interface WithdrawMfaRequest { - idToken: string; - mfaEnrollmentId: string; - tenantId: string | null; -} - -export interface WithdrawMfaResponse extends FinalizeMfaResponse {} - -export function withdrawMfa( - auth: Auth, - request: Omit -): Promise { - return _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.WITHDRAW_MFA, - { - tenantId: auth.tenantId, - ...request - } - ); -} diff --git a/packages-exp/auth-exp/src/api/authentication/custom_token.test.ts b/packages-exp/auth-exp/src/api/authentication/custom_token.test.ts deleted file mode 100644 index bc39e27c804..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/custom_token.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { ProviderId } from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { Endpoint, HttpHeader } from '../'; -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { ServerError } from '../errors'; -import { signInWithCustomToken } from './custom_token'; - -use(chaiAsPromised); - -describe('api/authentication/signInWithCustomToken', () => { - const request = { - token: 'my-token', - returnSecureToken: true - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_CUSTOM_TOKEN, { - providerId: ProviderId.CUSTOM, - idToken: 'id-token', - expiresIn: '1000', - localId: '1234' - }); - - const response = await signInWithCustomToken(auth, request); - expect(response.providerId).to.eq(ProviderId.CUSTOM); - expect(response.idToken).to.eq('id-token'); - expect(response.expiresIn).to.eq('1000'); - expect(response.localId).to.eq('1234'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_IN_WITH_CUSTOM_TOKEN, - { - error: { - code: 400, - message: ServerError.INVALID_CUSTOM_TOKEN, - errors: [ - { - message: ServerError.INVALID_CUSTOM_TOKEN - } - ] - } - }, - 400 - ); - - await expect(signInWithCustomToken(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The custom token format is incorrect. Please check the documentation. (auth/invalid-custom-token).' - ); - expect(mock.calls[0].request).to.eql(request); - }); -}); diff --git a/packages-exp/auth-exp/src/api/authentication/custom_token.ts b/packages-exp/auth-exp/src/api/authentication/custom_token.ts deleted file mode 100644 index 40ea6192298..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/custom_token.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Endpoint, HttpMethod, _performSignInRequest } from '../index'; -import { IdTokenResponse } from '../../model/id_token'; -import { Auth } from '@firebase/auth-types-exp'; - -export interface SignInWithCustomTokenRequest { - token: string; - returnSecureToken: boolean; -} - -export interface SignInWithCustomTokenResponse extends IdTokenResponse {} - -export async function signInWithCustomToken( - auth: Auth, - request: SignInWithCustomTokenRequest -): Promise { - return _performSignInRequest< - SignInWithCustomTokenRequest, - SignInWithCustomTokenResponse - >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_CUSTOM_TOKEN, request); -} diff --git a/packages-exp/auth-exp/src/api/authentication/email_and_password.test.ts b/packages-exp/auth-exp/src/api/authentication/email_and_password.test.ts deleted file mode 100644 index 609ac8f3295..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/email_and_password.test.ts +++ /dev/null @@ -1,330 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { ActionCodeOperation } from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { Endpoint, HttpHeader } from '../'; -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { ServerError } from '../errors'; -import { - EmailSignInRequest, - PasswordResetRequest, - sendEmailVerification, - sendPasswordResetEmail, - sendSignInLinkToEmail, - signInWithPassword, - verifyAndChangeEmail, - VerifyAndChangeEmailRequest, - VerifyEmailRequest -} from './email_and_password'; - -use(chaiAsPromised); - -describe('api/authentication/signInWithPassword', () => { - const request = { - returnSecureToken: true, - email: 'test@foo.com', - password: 'my-password' - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_PASSWORD, { - displayName: 'my-name', - email: 'test@foo.com' - }); - - const response = await signInWithPassword(auth, request); - expect(response.displayName).to.eq('my-name'); - expect(response.email).to.eq('test@foo.com'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_IN_WITH_PASSWORD, - { - error: { - code: 400, - message: ServerError.INVALID_PASSWORD, - errors: [ - { - message: ServerError.INVALID_PASSWORD - } - ] - } - }, - 400 - ); - - await expect(signInWithPassword(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The password is invalid or the user does not have a password. (auth/wrong-password).' - ); - expect(mock.calls[0].request).to.eql(request); - }); -}); - -describe('api/authentication/sendEmailVerification', () => { - const request: VerifyEmailRequest = { - requestType: ActionCodeOperation.VERIFY_EMAIL, - idToken: 'my-token' - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - email: 'test@foo.com' - }); - - const response = await sendEmailVerification(auth, request); - expect(response.email).to.eq('test@foo.com'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SEND_OOB_CODE, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL, - errors: [ - { - message: ServerError.INVALID_EMAIL - } - ] - } - }, - 400 - ); - - await expect(sendEmailVerification(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls[0].request).to.eql(request); - }); -}); - -describe('api/authentication/sendPasswordResetEmail', () => { - const request: PasswordResetRequest = { - requestType: ActionCodeOperation.PASSWORD_RESET, - email: 'test@foo.com' - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - email: 'test@foo.com' - }); - - const response = await sendPasswordResetEmail(auth, request); - expect(response.email).to.eq('test@foo.com'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SEND_OOB_CODE, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL, - errors: [ - { - message: ServerError.INVALID_EMAIL - } - ] - } - }, - 400 - ); - - await expect(sendPasswordResetEmail(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls[0].request).to.eql(request); - }); -}); - -describe('api/authentication/sendSignInLinkToEmail', () => { - const request: EmailSignInRequest = { - requestType: ActionCodeOperation.EMAIL_SIGNIN, - email: 'test@foo.com' - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - email: 'test@foo.com' - }); - - const response = await sendSignInLinkToEmail(auth, request); - expect(response.email).to.eq('test@foo.com'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SEND_OOB_CODE, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL, - errors: [ - { - message: ServerError.INVALID_EMAIL - } - ] - } - }, - 400 - ); - - await expect(sendSignInLinkToEmail(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls[0].request).to.eql(request); - }); -}); - -describe('api/authentication/verifyAndChangeEmail', () => { - const request: VerifyAndChangeEmailRequest = { - requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - idToken: 'id-token', - newEmail: 'test@foo.com' - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - email: 'test@foo.com' - }); - - const response = await verifyAndChangeEmail(auth, request); - expect(response.email).to.eq('test@foo.com'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SEND_OOB_CODE, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL, - errors: [ - { - message: ServerError.INVALID_EMAIL - } - ] - } - }, - 400 - ); - - await expect(verifyAndChangeEmail(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls[0].request).to.eql(request); - }); -}); diff --git a/packages-exp/auth-exp/src/api/authentication/email_and_password.ts b/packages-exp/auth-exp/src/api/authentication/email_and_password.ts deleted file mode 100644 index a808e041dfb..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/email_and_password.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ActionCodeOperation, Auth } from '@firebase/auth-types-exp'; - -import { - Endpoint, - HttpMethod, - _performApiRequest, - _performSignInRequest -} from '../index'; -import { IdToken, IdTokenResponse } from '../../model/id_token'; - -export interface SignInWithPasswordRequest { - returnSecureToken?: boolean; - email: string; - password: string; -} - -export interface SignInWithPasswordResponse extends IdTokenResponse { - email: string; - displayName: string; -} - -export async function signInWithPassword( - auth: Auth, - request: SignInWithPasswordRequest -): Promise { - return _performSignInRequest< - SignInWithPasswordRequest, - SignInWithPasswordResponse - >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_PASSWORD, request); -} - -export interface GetOobCodeRequest { - email?: string; // Everything except VERIFY_AND_CHANGE_EMAIL - continueUrl?: string; - iosBundleId?: string; - iosAppStoreId?: string; - androidPackageName?: string; - androidInstallApp?: boolean; - androidMinimumVersionCode?: string; - canHandleCodeInApp?: boolean; - dynamicLinkDomain?: string; - tenantId?: string; - targetProjectid?: string; -} - -export interface VerifyEmailRequest extends GetOobCodeRequest { - requestType: ActionCodeOperation.VERIFY_EMAIL; - idToken: IdToken; -} - -export interface PasswordResetRequest extends GetOobCodeRequest { - requestType: ActionCodeOperation.PASSWORD_RESET; - email: string; - captchaResp?: string; -} - -export interface EmailSignInRequest extends GetOobCodeRequest { - requestType: ActionCodeOperation.EMAIL_SIGNIN; - email: string; -} - -export interface VerifyAndChangeEmailRequest extends GetOobCodeRequest { - requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL; - idToken: IdToken; - newEmail: string; -} - -interface GetOobCodeResponse { - email: string; -} - -export interface VerifyEmailResponse extends GetOobCodeResponse {} -export interface PasswordResetResponse extends GetOobCodeResponse {} -export interface EmailSignInResponse extends GetOobCodeResponse {} -export interface VerifyAndChangeEmailResponse extends GetOobCodeRequest {} - -async function sendOobCode( - auth: Auth, - request: GetOobCodeRequest -): Promise { - return _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SEND_OOB_CODE, - request - ); -} - -export async function sendEmailVerification( - auth: Auth, - request: VerifyEmailRequest -): Promise { - return sendOobCode(auth, request); -} - -export async function sendPasswordResetEmail( - auth: Auth, - request: PasswordResetRequest -): Promise { - return sendOobCode(auth, request); -} - -export async function sendSignInLinkToEmail( - auth: Auth, - request: EmailSignInRequest -): Promise { - return sendOobCode(auth, request); -} - -export async function verifyAndChangeEmail( - auth: Auth, - request: VerifyAndChangeEmailRequest -): Promise { - return sendOobCode(auth, request); -} diff --git a/packages-exp/auth-exp/src/api/authentication/email_link.test.ts b/packages-exp/auth-exp/src/api/authentication/email_link.test.ts deleted file mode 100644 index ec45a02413c..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/email_link.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { FirebaseError } from '@firebase/util'; - -import { Endpoint, HttpHeader } from '../'; -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { ServerError } from '../errors'; -import { - signInWithEmailLink, - signInWithEmailLinkForLinking -} from './email_link'; - -use(chaiAsPromised); - -describe('api/authentication/email_link', () => { - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - context('signInWithEmailLink', () => { - const request = { - email: 'foo@bar.com', - oobCode: 'my-code' - }; - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { - displayName: 'my-name', - email: 'test@foo.com' - }); - - const response = await signInWithEmailLink(auth, request); - expect(response.displayName).to.eq('my-name'); - expect(response.email).to.eq('test@foo.com'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_IN_WITH_EMAIL_LINK, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL, - errors: [ - { - message: ServerError.INVALID_EMAIL - } - ] - } - }, - 400 - ); - - await expect(signInWithEmailLink(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls[0].request).to.eql(request); - }); - }); - - context('signInWithEmailLinkForLinking', () => { - const request = { - email: 'foo@bar.com', - oobCode: 'my-code', - idToken: 'id-token-2' - }; - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { - displayName: 'my-name', - email: 'test@foo.com' - }); - - const response = await signInWithEmailLinkForLinking(auth, request); - expect(response.displayName).to.eq('my-name'); - expect(response.email).to.eq('test@foo.com'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_IN_WITH_EMAIL_LINK, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL, - errors: [ - { - message: ServerError.INVALID_EMAIL - } - ] - } - }, - 400 - ); - - await expect( - signInWithEmailLinkForLinking(auth, request) - ).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls[0].request).to.eql(request); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/api/authentication/email_link.ts b/packages-exp/auth-exp/src/api/authentication/email_link.ts deleted file mode 100644 index 4b70642cacd..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/email_link.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _performSignInRequest, Endpoint, HttpMethod } from '../index'; -import { IdTokenResponse } from '../../model/id_token'; -import { Auth } from '@firebase/auth-types-exp'; - -export interface SignInWithEmailLinkRequest { - email: string; - oobCode: string; -} - -export interface SignInWithEmailLinkResponse extends IdTokenResponse { - email: string; - isNewUser: boolean; -} - -export async function signInWithEmailLink( - auth: Auth, - request: SignInWithEmailLinkRequest -): Promise { - return _performSignInRequest< - SignInWithEmailLinkRequest, - SignInWithEmailLinkResponse - >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_EMAIL_LINK, request); -} - -export interface SignInWithEmailLinkForLinkingRequest - extends SignInWithEmailLinkRequest { - idToken: string; -} - -export async function signInWithEmailLinkForLinking( - auth: Auth, - request: SignInWithEmailLinkForLinkingRequest -): Promise { - return _performSignInRequest< - SignInWithEmailLinkForLinkingRequest, - SignInWithEmailLinkResponse - >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_EMAIL_LINK, request); -} diff --git a/packages-exp/auth-exp/src/api/authentication/idp.test.ts b/packages-exp/auth-exp/src/api/authentication/idp.test.ts deleted file mode 100644 index 48ae6da45e7..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/idp.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { FirebaseError } from '@firebase/util'; - -import { Endpoint, HttpHeader } from '../'; -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { ServerError } from '../errors'; -import { signInWithIdp } from './idp'; - -use(chaiAsPromised); - -describe('api/authentication/signInWithIdp', () => { - const request = { - returnSecureToken: true, - requestUri: 'request-uri', - postBody: null - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_IDP, { - displayName: 'my-name', - idToken: 'id-token' - }); - - const response = await signInWithIdp(auth, request); - expect(response.displayName).to.eq('my-name'); - expect(response.idToken).to.eq('id-token'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_IN_WITH_IDP, - { - error: { - code: 400, - message: ServerError.INVALID_IDP_RESPONSE, - errors: [ - { - message: ServerError.INVALID_IDP_RESPONSE - } - ] - } - }, - 400 - ); - - await expect(signInWithIdp(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The supplied auth credential is malformed or has expired. (auth/invalid-credential).' - ); - expect(mock.calls[0].request).to.eql(request); - }); -}); diff --git a/packages-exp/auth-exp/src/api/authentication/idp.ts b/packages-exp/auth-exp/src/api/authentication/idp.ts deleted file mode 100644 index 59533d2b069..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/idp.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Endpoint, HttpMethod, _performSignInRequest } from '../index'; -import { IdToken, IdTokenResponse } from '../../model/id_token'; -import { Auth } from '@firebase/auth-types-exp'; - -export interface SignInWithIdpRequest { - requestUri: string; - postBody: string | null; - sessionId?: string; - tenantId?: string; - returnSecureToken: boolean; - idToken?: IdToken; - autoCreate?: boolean; - pendingToken?: string; -} - -export interface SignInWithIdpResponse extends IdTokenResponse { - oauthAccessToken?: string; - oauthTokenSecret?: string; - nonce?: string; - oauthIdToken?: string; - pendingToken?: string; -} - -export async function signInWithIdp( - auth: Auth, - request: SignInWithIdpRequest -): Promise { - return _performSignInRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_IN_WITH_IDP, - request - ); -} diff --git a/packages-exp/auth-exp/src/api/authentication/mfa.test.ts b/packages-exp/auth-exp/src/api/authentication/mfa.test.ts deleted file mode 100644 index 767edd22943..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/mfa.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { FirebaseError } from '@firebase/util'; - -import { Endpoint, HttpHeader } from '../'; -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { ServerError } from '../errors'; -import { finalizeSignInPhoneMfa, startSignInPhoneMfa } from './mfa'; - -use(chaiAsPromised); - -describe('api/authentication/startSignInPhoneMfa', () => { - const request = { - mfaPendingCredential: 'my-creds', - mfaEnrollmentId: 'my-enrollment-id', - phoneSignInInfo: { - recaptchaToken: 'catpcha-token' - } - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.START_PHONE_MFA_SIGN_IN, { - phoneResponseInfo: { - sessionInfo: 'session-info' - } - }); - - const response = await startSignInPhoneMfa(auth, request); - expect(response.phoneResponseInfo.sessionInfo).to.eq('session-info'); - expect(mock.calls[0].request).to.eql({ - tenantId: null, - ...request - }); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.START_PHONE_MFA_SIGN_IN, - { - error: { - code: 400, - message: ServerError.INVALID_PENDING_TOKEN, - errors: [ - { - message: ServerError.INVALID_PENDING_TOKEN - } - ] - } - }, - 400 - ); - - await expect(startSignInPhoneMfa(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The supplied auth credential is malformed or has expired. (auth/invalid-credential).' - ); - expect(mock.calls[0].request).to.eql({ - tenantId: null, - ...request - }); - }); -}); - -describe('api/authentication/finalizeSignInPhoneMfa', () => { - const request = { - mfaPendingCredential: 'pending-cred', - phoneVerificationInfo: { - temporaryProof: 'proof', - phoneNumber: '123456789', - sessionInfo: 'session-info', - code: 'my-code' - } - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.FINALIZE_PHONE_MFA_SIGN_IN, { - idToken: 'id-token', - refreshToken: 'refresh-token' - }); - - const response = await finalizeSignInPhoneMfa(auth, request); - expect(response.idToken).to.eq('id-token'); - expect(response.refreshToken).to.eq('refresh-token'); - expect(mock.calls[0].request).to.eql({ - tenantId: null, - ...request - }); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.FINALIZE_PHONE_MFA_SIGN_IN, - { - error: { - code: 400, - message: ServerError.INVALID_CODE, - errors: [ - { - message: ServerError.INVALID_CODE - } - ] - } - }, - 400 - ); - - await expect(finalizeSignInPhoneMfa(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The SMS verification code used to create the phone auth credential is invalid. Please resend the verification code sms and be sure use the verification code provided by the user. (auth/invalid-verification-code).' - ); - expect(mock.calls[0].request).to.eql({ - tenantId: null, - ...request - }); - }); -}); diff --git a/packages-exp/auth-exp/src/api/authentication/mfa.ts b/packages-exp/auth-exp/src/api/authentication/mfa.ts deleted file mode 100644 index a928af4b59a..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/mfa.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _performApiRequest, Endpoint, HttpMethod } from '../index'; -import { Auth } from '@firebase/auth-types-exp'; -import { IdTokenResponse } from '../../model/id_token'; -import { MfaEnrollment } from '../account_management/mfa'; -import { SignInWithIdpResponse } from './idp'; -import { - SignInWithPhoneNumberRequest, - SignInWithPhoneNumberResponse -} from './sms'; - -export interface FinalizeMfaResponse { - idToken: string; - refreshToken: string; -} - -export interface IdTokenMfaResponse extends IdTokenResponse { - mfaPendingCredential?: string; - mfaInfo?: MfaEnrollment[]; -} - -export interface StartPhoneMfaSignInRequest { - mfaPendingCredential: string; - mfaEnrollmentId: string; - phoneSignInInfo: { - recaptchaToken: string; - }; - tenantId: string | null; -} - -export interface StartPhoneMfaSignInResponse { - phoneResponseInfo: { - sessionInfo: string; - }; -} - -export function startSignInPhoneMfa( - auth: Auth, - request: Omit -): Promise { - return _performApiRequest< - StartPhoneMfaSignInRequest, - StartPhoneMfaSignInResponse - >(auth, HttpMethod.POST, Endpoint.START_PHONE_MFA_SIGN_IN, { - tenantId: auth.tenantId, - ...request - }); -} - -export interface FinalizePhoneMfaSignInRequest { - mfaPendingCredential: string; - phoneVerificationInfo: SignInWithPhoneNumberRequest; - tenantId: string | null; -} - -export interface FinalizePhoneMfaSignInResponse extends FinalizeMfaResponse {} - -export function finalizeSignInPhoneMfa( - auth: Auth, - request: Omit -): Promise { - return _performApiRequest< - FinalizePhoneMfaSignInRequest, - FinalizePhoneMfaSignInResponse - >(auth, HttpMethod.POST, Endpoint.FINALIZE_PHONE_MFA_SIGN_IN, { - tenantId: auth.tenantId, - ...request - }); -} - -export type PhoneOrOauthTokenResponse = - | SignInWithPhoneNumberResponse - | SignInWithIdpResponse - | IdTokenResponse; diff --git a/packages-exp/auth-exp/src/api/authentication/recaptcha.test.ts b/packages-exp/auth-exp/src/api/authentication/recaptcha.test.ts deleted file mode 100644 index 5e400701ccd..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/recaptcha.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { FirebaseError } from '@firebase/util'; - -import { Endpoint, HttpHeader } from '../'; -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { ServerError } from '../errors'; -import { getRecaptchaParams } from './recaptcha'; - -use(chaiAsPromised); - -describe('api/authentication/getRecaptchaParams', () => { - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should GET to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.GET_RECAPTCHA_PARAM, { - recaptchaSiteKey: 'site-key' - }); - - const response = await getRecaptchaParams(auth); - expect(response).to.eq('site-key'); - expect(mock.calls[0].request).to.be.undefined; - expect(mock.calls[0].method).to.eq('GET'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.GET_RECAPTCHA_PARAM, - { - error: { - code: 400, - message: ServerError.TOO_MANY_ATTEMPTS_TRY_LATER, - errors: [ - { - message: ServerError.TOO_MANY_ATTEMPTS_TRY_LATER - } - ] - } - }, - 400 - ); - - await expect(getRecaptchaParams(auth)).to.be.rejectedWith( - FirebaseError, - 'Firebase: We have blocked all requests from this device due to unusual activity. Try again later. (auth/too-many-requests).' - ); - expect(mock.calls[0].request).to.be.undefined; - }); -}); diff --git a/packages-exp/auth-exp/src/api/authentication/recaptcha.ts b/packages-exp/auth-exp/src/api/authentication/recaptcha.ts deleted file mode 100644 index 945c84cbfa1..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/recaptcha.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Endpoint, HttpMethod, _performApiRequest } from '../index'; -import { Auth } from '@firebase/auth-types-exp'; - -interface GetRecaptchaParamResponse { - recaptchaSiteKey?: string; -} - -export async function getRecaptchaParams(auth: Auth): Promise { - return ( - ( - await _performApiRequest( - auth, - HttpMethod.GET, - Endpoint.GET_RECAPTCHA_PARAM - ) - ).recaptchaSiteKey || '' - ); -} diff --git a/packages-exp/auth-exp/src/api/authentication/sign_up.ts b/packages-exp/auth-exp/src/api/authentication/sign_up.ts deleted file mode 100644 index db5a60d1d37..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/sign_up.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Endpoint, HttpMethod, _performSignInRequest } from '../index'; -import { IdTokenResponse } from '../../model/id_token'; -import { Auth } from '@firebase/auth-types-exp'; - -export interface SignUpRequest { - returnSecureToken?: boolean; - email?: string; - password?: string; -} - -export interface SignUpResponse extends IdTokenResponse { - displayName?: string; - email?: string; -} - -export async function signUp( - auth: Auth, - request: SignUpRequest -): Promise { - return _performSignInRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request - ); -} diff --git a/packages-exp/auth-exp/src/api/authentication/sms.ts b/packages-exp/auth-exp/src/api/authentication/sms.ts deleted file mode 100644 index bd892176427..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/sms.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Endpoint, - HttpMethod, - _performApiRequest, - _performSignInRequest -} from '../index'; -import { AuthErrorCode } from '../../core/errors'; -import { IdTokenResponse } from '../../model/id_token'; -import { ServerError, ServerErrorMap } from '../errors'; -import { Auth } from '@firebase/auth-types-exp'; - -export interface SendPhoneVerificationCodeRequest { - phoneNumber: string; - recaptchaToken: string; -} - -export interface SendPhoneVerificationCodeResponse { - sessionInfo: string; -} - -export async function sendPhoneVerificationCode( - auth: Auth, - request: SendPhoneVerificationCodeRequest -): Promise { - return _performApiRequest< - SendPhoneVerificationCodeRequest, - SendPhoneVerificationCodeResponse - >(auth, HttpMethod.POST, Endpoint.SEND_VERIFICATION_CODE, request); -} - -export interface SignInWithPhoneNumberRequest { - temporaryProof?: string; - phoneNumber?: string; - sessionInfo?: string; - code?: string; -} - -export interface LinkWithPhoneNumberRequest - extends SignInWithPhoneNumberRequest { - idToken: string; -} - -export interface SignInWithPhoneNumberResponse extends IdTokenResponse { - temporaryProof?: string; - phoneNumber?: string; -} - -export async function signInWithPhoneNumber( - auth: Auth, - request: SignInWithPhoneNumberRequest -): Promise { - return _performSignInRequest< - SignInWithPhoneNumberRequest, - SignInWithPhoneNumberResponse - >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_PHONE_NUMBER, request); -} - -export async function linkWithPhoneNumber( - auth: Auth, - request: LinkWithPhoneNumberRequest -): Promise { - return _performSignInRequest< - LinkWithPhoneNumberRequest, - SignInWithPhoneNumberResponse - >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_PHONE_NUMBER, request); -} - -interface VerifyPhoneNumberForExistingRequest - extends SignInWithPhoneNumberRequest { - operation: 'REAUTH'; -} - -const VERIFY_PHONE_NUMBER_FOR_EXISTING_ERROR_MAP_: Partial> = { - [ServerError.USER_NOT_FOUND]: AuthErrorCode.USER_DELETED -}; - -export async function verifyPhoneNumberForExisting( - auth: Auth, - request: SignInWithPhoneNumberRequest -): Promise { - const apiRequest: VerifyPhoneNumberForExistingRequest = { - ...request, - operation: 'REAUTH' - }; - return _performSignInRequest< - VerifyPhoneNumberForExistingRequest, - SignInWithPhoneNumberResponse - >( - auth, - HttpMethod.POST, - Endpoint.SIGN_IN_WITH_PHONE_NUMBER, - apiRequest, - VERIFY_PHONE_NUMBER_FOR_EXISTING_ERROR_MAP_ - ); -} diff --git a/packages-exp/auth-exp/src/api/authentication/token.test.ts b/packages-exp/auth-exp/src/api/authentication/token.test.ts deleted file mode 100644 index fa0373868e9..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/token.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { FirebaseError, querystringDecode } from '@firebase/util'; - -import { HttpHeader } from '../'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as fetch from '../../../test/helpers/mock_fetch'; -import { ServerError } from '../errors'; -import { Endpoint, requestStsToken } from './token'; - -use(chaiAsPromised); - -describe('requestStsToken', () => { - let auth: TestAuth; - let endpoint: string; - - beforeEach(async () => { - auth = await testAuth(); - const { apiKey, tokenApiHost, apiScheme } = auth.config; - endpoint = `${apiScheme}://${tokenApiHost}${Endpoint.TOKEN}?key=${apiKey}`; - fetch.setUp(); - }); - - afterEach(fetch.tearDown); - - it('should POST to the correct endpoint', async () => { - const mock = fetch.mock(endpoint, { - 'access_token': 'new-access-token', - 'expires_in': '3600', - 'refresh_token': 'new-refresh-token' - }); - - const response = await requestStsToken(auth, 'old-refresh-token'); - expect(response.accessToken).to.eq('new-access-token'); - expect(response.expiresIn).to.eq('3600'); - expect(response.refreshToken).to.eq('new-refresh-token'); - const request = querystringDecode(`?${mock.calls[0].request}`); - expect(request).to.eql({ - 'grant_type': 'refresh_token', - 'refresh_token': 'old-refresh-token' - }); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/x-www-form-urlencoded' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should handle errors', async () => { - const mock = fetch.mock( - endpoint, - { - error: { - code: 400, - message: ServerError.TOKEN_EXPIRED, - errors: [ - { - message: ServerError.TOKEN_EXPIRED - } - ] - } - }, - 400 - ); - - await expect(requestStsToken(auth, 'old-token')).to.be.rejectedWith( - FirebaseError, - "Firebase: The user's credential is no longer valid. The user must sign in again. (auth/user-token-expired)" - ); - const request = querystringDecode(`?${mock.calls[0].request}`); - expect(request).to.eql({ - 'grant_type': 'refresh_token', - 'refresh_token': 'old-token' - }); - }); -}); diff --git a/packages-exp/auth-exp/src/api/authentication/token.ts b/packages-exp/auth-exp/src/api/authentication/token.ts deleted file mode 100644 index 8801c5fd137..00000000000 --- a/packages-exp/auth-exp/src/api/authentication/token.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* eslint-disable camelcase */ - -import { querystring } from '@firebase/util'; - -import { - _getFinalTarget, - _performFetchWithErrorHandling, - HttpMethod -} from '../index'; -import { FetchProvider } from '../../core/util/fetch_provider'; -import { Auth } from '@firebase/auth-types-exp'; - -export const enum Endpoint { - TOKEN = '/v1/token' -} - -/** The server responses with snake_case; we convert to camelCase */ -interface RequestStsTokenServerResponse { - access_token: string; - expires_in: string; - refresh_token: string; -} - -export interface RequestStsTokenResponse { - accessToken: string; - expiresIn: string; - refreshToken: string; -} - -export async function requestStsToken( - auth: Auth, - refreshToken: string -): Promise { - const response = await _performFetchWithErrorHandling< - RequestStsTokenServerResponse - >(auth, {}, () => { - const body = querystring({ - 'grant_type': 'refresh_token', - 'refresh_token': refreshToken - }).slice(1); - const { tokenApiHost, apiKey, sdkClientVersion } = auth.config; - const url = _getFinalTarget( - auth, - tokenApiHost, - Endpoint.TOKEN, - `key=${apiKey}` - ); - - return FetchProvider.fetch()(url, { - method: HttpMethod.POST, - headers: { - 'X-Client-Version': sdkClientVersion, - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body - }); - }); - - // The response comes back in snake_case. Convert to camel: - return { - accessToken: response.access_token, - expiresIn: response.expires_in, - refreshToken: response.refresh_token - }; -} diff --git a/packages-exp/auth-exp/src/api/errors.ts b/packages-exp/auth-exp/src/api/errors.ts deleted file mode 100644 index e70f8523011..00000000000 --- a/packages-exp/auth-exp/src/api/errors.ts +++ /dev/null @@ -1,203 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AuthErrorCode } from '../core/errors'; - -/** - * Errors that can be returned by the backend - */ -export const enum ServerError { - ADMIN_ONLY_OPERATION = 'ADMIN_ONLY_OPERATION', - CAPTCHA_CHECK_FAILED = 'CAPTCHA_CHECK_FAILED', - CORS_UNSUPPORTED = 'CORS_UNSUPPORTED', - CREDENTIAL_MISMATCH = 'CREDENTIAL_MISMATCH', - CREDENTIAL_TOO_OLD_LOGIN_AGAIN = 'CREDENTIAL_TOO_OLD_LOGIN_AGAIN', - DYNAMIC_LINK_NOT_ACTIVATED = 'DYNAMIC_LINK_NOT_ACTIVATED', - EMAIL_CHANGE_NEEDS_VERIFICATION = 'EMAIL_CHANGE_NEEDS_VERIFICATION', - EMAIL_EXISTS = 'EMAIL_EXISTS', - EMAIL_NOT_FOUND = 'EMAIL_NOT_FOUND', - EXPIRED_OOB_CODE = 'EXPIRED_OOB_CODE', - FEDERATED_USER_ID_ALREADY_LINKED = 'FEDERATED_USER_ID_ALREADY_LINKED', - INVALID_APP_CREDENTIAL = 'INVALID_APP_CREDENTIAL', - INVALID_APP_ID = 'INVALID_APP_ID', - INVALID_CERT_HASH = 'INVALID_CERT_HASH', - INVALID_CODE = 'INVALID_CODE', - INVALID_CONTINUE_URI = 'INVALID_CONTINUE_URI', - INVALID_CUSTOM_TOKEN = 'INVALID_CUSTOM_TOKEN', - INVALID_DYNAMIC_LINK_DOMAIN = 'INVALID_DYNAMIC_LINK_DOMAIN', - INVALID_EMAIL = 'INVALID_EMAIL', - INVALID_ID_TOKEN = 'INVALID_ID_TOKEN', - INVALID_IDP_RESPONSE = 'INVALID_IDP_RESPONSE', - INVALID_IDENTIFIER = 'INVALID_IDENTIFIER', - INVALID_MESSAGE_PAYLOAD = 'INVALID_MESSAGE_PAYLOAD', - INVALID_MFA_PENDING_CREDENTIAL = 'INVALID_MFA_PENDING_CREDENTIAL', - INVALID_OAUTH_CLIENT_ID = 'INVALID_OAUTH_CLIENT_ID', - INVALID_OOB_CODE = 'INVALID_OOB_CODE', - INVALID_PASSWORD = 'INVALID_PASSWORD', - INVALID_PENDING_TOKEN = 'INVALID_PENDING_TOKEN', - INVALID_PHONE_NUMBER = 'INVALID_PHONE_NUMBER', - INVALID_PROVIDER_ID = 'INVALID_PROVIDER_ID', - INVALID_RECIPIENT_EMAIL = 'INVALID_RECIPIENT_EMAIL', - INVALID_SENDER = 'INVALID_SENDER', - INVALID_SESSION_INFO = 'INVALID_SESSION_INFO', - INVALID_TEMPORARY_PROOF = 'INVALID_TEMPORARY_PROOF', - INVALID_TENANT_ID = 'INVALID_TENANT_ID', - MFA_ENROLLMENT_NOT_FOUND = 'MFA_ENROLLMENT_NOT_FOUND', - MISSING_ANDROID_PACKAGE_NAME = 'MISSING_ANDROID_PACKAGE_NAME', - MISSING_APP_CREDENTIAL = 'MISSING_APP_CREDENTIAL', - MISSING_CODE = 'MISSING_CODE', - MISSING_CONTINUE_URI = 'MISSING_CONTINUE_URI', - MISSING_CUSTOM_TOKEN = 'MISSING_CUSTOM_TOKEN', - MISSING_IOS_BUNDLE_ID = 'MISSING_IOS_BUNDLE_ID', - MISSING_MFA_ENROLLMENT_ID = 'MISSING_MFA_ENROLLMENT_ID', - MISSING_MFA_PENDING_CREDENTIAL = 'MISSING_MFA_PENDING_CREDENTIAL', - MISSING_OOB_CODE = 'MISSING_OOB_CODE', - MISSING_OR_INVALID_NONCE = 'MISSING_OR_INVALID_NONCE', - MISSING_PASSWORD = 'MISSING_PASSWORD', - MISSING_REQ_TYPE = 'MISSING_REQ_TYPE', - MISSING_PHONE_NUMBER = 'MISSING_PHONE_NUMBER', - MISSING_SESSION_INFO = 'MISSING_SESSION_INFO', - OPERATION_NOT_ALLOWED = 'OPERATION_NOT_ALLOWED', - PASSWORD_LOGIN_DISABLED = 'PASSWORD_LOGIN_DISABLED', - QUOTA_EXCEEDED = 'QUOTA_EXCEEDED', - RESET_PASSWORD_EXCEED_LIMIT = 'RESET_PASSWORD_EXCEED_LIMIT', - REJECTED_CREDENTIAL = 'REJECTED_CREDENTIAL', - SECOND_FACTOR_EXISTS = 'SECOND_FACTOR_EXISTS', - SECOND_FACTOR_LIMIT_EXCEEDED = 'SECOND_FACTOR_LIMIT_EXCEEDED', - SESSION_EXPIRED = 'SESSION_EXPIRED', - TENANT_ID_MISMATCH = 'TENANT_ID_MISMATCH', - TOKEN_EXPIRED = 'TOKEN_EXPIRED', - TOO_MANY_ATTEMPTS_TRY_LATER = 'TOO_MANY_ATTEMPTS_TRY_LATER', - UNSUPPORTED_FIRST_FACTOR = 'UNSUPPORTED_FIRST_FACTOR', - UNSUPPORTED_TENANT_OPERATION = 'UNSUPPORTED_TENANT_OPERATION', - UNAUTHORIZED_DOMAIN = 'UNAUTHORIZED_DOMAIN', - UNVERIFIED_EMAIL = 'UNVERIFIED_EMAIL', - USER_CANCELLED = 'USER_CANCELLED', - USER_DISABLED = 'USER_DISABLED', - USER_NOT_FOUND = 'USER_NOT_FOUND', - WEAK_PASSWORD = 'WEAK_PASSWORD' -} - -/** - * API Response in the event of an error - */ -export interface JsonError { - error: { - code: number; - message: string; - errors?: [ - { - message: ServerError; - domain: string; - reason: string; - } - ]; - }; -} - -/** - * Type definition for a map from server errors to developer visible errors - */ -export declare type ServerErrorMap = { - readonly [K in ApiError]: AuthErrorCode; -}; - -/** - * Map from errors returned by the server to errors to developer visible errors - */ -export const SERVER_ERROR_MAP: Partial> = { - // Custom token errors. - [ServerError.CREDENTIAL_MISMATCH]: AuthErrorCode.CREDENTIAL_MISMATCH, - // This can only happen if the SDK sends a bad request. - [ServerError.MISSING_CUSTOM_TOKEN]: AuthErrorCode.INTERNAL_ERROR, - - // Create Auth URI errors. - [ServerError.INVALID_IDENTIFIER]: AuthErrorCode.INVALID_EMAIL, - // This can only happen if the SDK sends a bad request. - [ServerError.MISSING_CONTINUE_URI]: AuthErrorCode.INTERNAL_ERROR, - - // Sign in with email and password errors (some apply to sign up too). - [ServerError.INVALID_PASSWORD]: AuthErrorCode.INVALID_PASSWORD, - // This can only happen if the SDK sends a bad request. - [ServerError.MISSING_PASSWORD]: AuthErrorCode.INTERNAL_ERROR, - - // Sign up with email and password errors. - [ServerError.EMAIL_EXISTS]: AuthErrorCode.EMAIL_EXISTS, - [ServerError.PASSWORD_LOGIN_DISABLED]: AuthErrorCode.OPERATION_NOT_ALLOWED, - - // Verify assertion for sign in with credential errors: - [ServerError.INVALID_IDP_RESPONSE]: AuthErrorCode.INVALID_IDP_RESPONSE, - [ServerError.INVALID_PENDING_TOKEN]: AuthErrorCode.INVALID_IDP_RESPONSE, - [ServerError.FEDERATED_USER_ID_ALREADY_LINKED]: - AuthErrorCode.CREDENTIAL_ALREADY_IN_USE, - - // This can only happen if the SDK sends a bad request. - [ServerError.MISSING_REQ_TYPE]: AuthErrorCode.INTERNAL_ERROR, - - // Send Password reset email errors: - [ServerError.EMAIL_NOT_FOUND]: AuthErrorCode.USER_DELETED, - [ServerError.RESET_PASSWORD_EXCEED_LIMIT]: - AuthErrorCode.TOO_MANY_ATTEMPTS_TRY_LATER, - - [ServerError.EXPIRED_OOB_CODE]: AuthErrorCode.EXPIRED_OOB_CODE, - [ServerError.INVALID_OOB_CODE]: AuthErrorCode.INVALID_OOB_CODE, - // This can only happen if the SDK sends a bad request. - [ServerError.MISSING_OOB_CODE]: AuthErrorCode.INTERNAL_ERROR, - - // Operations that require ID token in request: - [ServerError.CREDENTIAL_TOO_OLD_LOGIN_AGAIN]: - AuthErrorCode.CREDENTIAL_TOO_OLD_LOGIN_AGAIN, - [ServerError.INVALID_ID_TOKEN]: AuthErrorCode.INVALID_AUTH, - [ServerError.TOKEN_EXPIRED]: AuthErrorCode.TOKEN_EXPIRED, - [ServerError.USER_NOT_FOUND]: AuthErrorCode.TOKEN_EXPIRED, - - // Other errors. - [ServerError.TOO_MANY_ATTEMPTS_TRY_LATER]: - AuthErrorCode.TOO_MANY_ATTEMPTS_TRY_LATER, - - // Phone Auth related errors. - [ServerError.INVALID_CODE]: AuthErrorCode.INVALID_CODE, - [ServerError.INVALID_SESSION_INFO]: AuthErrorCode.INVALID_SESSION_INFO, - [ServerError.INVALID_TEMPORARY_PROOF]: AuthErrorCode.INVALID_IDP_RESPONSE, - [ServerError.MISSING_SESSION_INFO]: AuthErrorCode.MISSING_SESSION_INFO, - [ServerError.SESSION_EXPIRED]: AuthErrorCode.CODE_EXPIRED, - - // Other action code errors when additional settings passed. - // MISSING_CONTINUE_URI is getting mapped to INTERNAL_ERROR above. - // This is OK as this error will be caught by client side validation. - [ServerError.MISSING_ANDROID_PACKAGE_NAME]: - AuthErrorCode.MISSING_ANDROID_PACKAGE_NAME, - [ServerError.UNAUTHORIZED_DOMAIN]: AuthErrorCode.UNAUTHORIZED_DOMAIN, - - // getProjectConfig errors when clientId is passed. - [ServerError.INVALID_OAUTH_CLIENT_ID]: AuthErrorCode.INVALID_OAUTH_CLIENT_ID, - - // User actions (sign-up or deletion) disabled errors. - [ServerError.ADMIN_ONLY_OPERATION]: AuthErrorCode.ADMIN_ONLY_OPERATION, - - // Multi factor related errors. - [ServerError.INVALID_MFA_PENDING_CREDENTIAL]: - AuthErrorCode.INVALID_MFA_SESSION, - [ServerError.MFA_ENROLLMENT_NOT_FOUND]: AuthErrorCode.MFA_INFO_NOT_FOUND, - [ServerError.MISSING_MFA_ENROLLMENT_ID]: AuthErrorCode.MISSING_MFA_INFO, - [ServerError.MISSING_MFA_PENDING_CREDENTIAL]: - AuthErrorCode.MISSING_MFA_SESSION, - [ServerError.SECOND_FACTOR_EXISTS]: - AuthErrorCode.SECOND_FACTOR_ALREADY_ENROLLED, - [ServerError.SECOND_FACTOR_LIMIT_EXCEEDED]: - AuthErrorCode.SECOND_FACTOR_LIMIT_EXCEEDED -}; diff --git a/packages-exp/auth-exp/src/api/index.test.ts b/packages-exp/auth-exp/src/api/index.test.ts deleted file mode 100644 index ecf23b5eaaf..00000000000 --- a/packages-exp/auth-exp/src/api/index.test.ts +++ /dev/null @@ -1,372 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { assert, expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import { useFakeTimers } from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { FirebaseError } from '@firebase/util'; - -import { mockEndpoint } from '../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../test/helpers/mock_auth'; -import * as mockFetch from '../../test/helpers/mock_fetch'; -import { AuthErrorCode } from '../core/errors'; -import { ConfigInternal } from '../model/auth'; -import { - _getFinalTarget, - _performApiRequest, - DEFAULT_API_TIMEOUT_MS, - Endpoint, - HttpHeader, - HttpMethod -} from './'; -import { ServerError } from './errors'; - -use(sinonChai); -use(chaiAsPromised); - -describe('api/_performApiRequest', () => { - const request = { - requestKey: 'request-value' - }; - - const serverResponse = { - responseKey: 'response-value' - }; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - }); - - context('with regular requests', () => { - beforeEach(mockFetch.setUp); - afterEach(mockFetch.tearDown); - - it('should set the correct request, method and HTTP Headers', async () => { - const mock = mockEndpoint(Endpoint.SIGN_UP, serverResponse); - const response = await _performApiRequest< - typeof request, - typeof serverResponse - >(auth, HttpMethod.POST, Endpoint.SIGN_UP, request); - expect(response).to.eql(serverResponse); - expect(mock.calls.length).to.eq(1); - expect(mock.calls[0].method).to.eq(HttpMethod.POST); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( - 'application/json' - ); - expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq( - 'testSDK/0.0.0' - ); - }); - - it('should set the device language if available', async () => { - auth.languageCode = 'jp'; - const mock = mockEndpoint(Endpoint.SIGN_UP, serverResponse); - const response = await _performApiRequest< - typeof request, - typeof serverResponse - >(auth, HttpMethod.POST, Endpoint.SIGN_UP, request); - expect(response).to.eql(serverResponse); - expect(mock.calls[0].headers.get(HttpHeader.X_FIREBASE_LOCALE)).to.eq( - 'jp' - ); - }); - - it('should translate server errors to auth errors', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_UP, - { - error: { - code: 400, - message: ServerError.EMAIL_EXISTS, - errors: [ - { - message: ServerError.EMAIL_EXISTS - } - ] - } - }, - 400 - ); - const promise = _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request - ); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/email-already-in-use' - ); - expect(mock.calls[0].request).to.eql(request); - }); - - it('should translate complex server errors to auth errors', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_UP, - { - error: { - code: 400, - message: `${ServerError.INVALID_PHONE_NUMBER} : TOO_SHORT`, - errors: [ - { - message: ServerError.EMAIL_EXISTS - } - ] - } - }, - 400 - ); - const promise = _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request - ); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/invalid-phone-number' - ); - expect(mock.calls[0].request).to.eql(request); - }); - - it('should handle unknown server errors', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_UP, - { - error: { - code: 400, - message: 'Awesome error', - errors: [ - { - message: 'Awesome error' - } - ] - } - }, - 400 - ); - const promise = _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request - ); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/awesome-error' - ); - expect(mock.calls[0].request).to.eql(request); - }); - - it('should support custom error handling per endpoint', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_UP, - { - error: { - code: 400, - message: ServerError.EXPIRED_OOB_CODE, - errors: [ - { - message: ServerError.EXPIRED_OOB_CODE - } - ] - } - }, - 400 - ); - const promise = _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request, - { - [ServerError.EXPIRED_OOB_CODE]: AuthErrorCode.ARGUMENT_ERROR - } - ); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/argument-error' - ); - expect(mock.calls[0].request).to.eql(request); - }); - }); - - context('with network issues', () => { - afterEach(mockFetch.tearDown); - - it('should handle timeouts', async () => { - const clock = useFakeTimers(); - mockFetch.setUpWithOverride(() => { - return new Promise(() => null); - }); - const promise = _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request - ); - clock.tick(DEFAULT_API_TIMEOUT_MS.get() + 1); - await expect(promise).to.be.rejectedWith(FirebaseError, 'auth/timeout'); - clock.restore(); - }); - - it('should clear the network timeout on success', async () => { - const clock = useFakeTimers(); - sinon.spy(clock, 'clearTimeout'); - mockFetch.setUp(); - mockEndpoint(Endpoint.SIGN_UP, {}); - const promise = _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request - ); - await promise; - expect(clock.clearTimeout).to.have.been.called; - clock.restore(); - }); - - it('should handle network failure', async () => { - mockFetch.setUpWithOverride(() => { - return new Promise((_, reject) => - reject(new Error('network error')) - ); - }); - const promise = _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request - ); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/network-request-failed' - ); - }); - }); - - context('edgcase error mapping', () => { - beforeEach(mockFetch.setUp); - afterEach(mockFetch.tearDown); - - it('should generate a need_conirmation error with the response', async () => { - mockEndpoint(Endpoint.SIGN_UP, { - needConfirmation: true, - idToken: 'id-token' - }); - try { - await _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request - ); - assert.fail('Call should have failed'); - } catch (e) { - expect(e.code).to.eq(`auth/${AuthErrorCode.NEED_CONFIRMATION}`); - expect((e as FirebaseError).customData!._tokenResponse).to.eql({ - needConfirmation: true, - idToken: 'id-token' - }); - } - }); - - it('should generate a credential already in use error', async () => { - const response = { - error: { - code: 400, - message: ServerError.FEDERATED_USER_ID_ALREADY_LINKED, - errors: [ - { - message: ServerError.FEDERATED_USER_ID_ALREADY_LINKED - } - ] - } - }; - mockEndpoint(Endpoint.SIGN_UP, response, 400); - try { - await _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request - ); - assert.fail('Call should have failed'); - } catch (e) { - expect(e.code).to.eq(`auth/${AuthErrorCode.CREDENTIAL_ALREADY_IN_USE}`); - expect((e as FirebaseError).customData!._tokenResponse).to.eql( - response - ); - } - }); - - it('should pull out email and phone number', async () => { - const response = { - error: { - code: 400, - message: ServerError.EMAIL_EXISTS, - errors: [ - { - message: ServerError.EMAIL_EXISTS - } - ] - }, - email: 'email@test.com', - phoneNumber: '+1555-this-is-a-number' - }; - mockEndpoint(Endpoint.SIGN_UP, response, 400); - try { - await _performApiRequest( - auth, - HttpMethod.POST, - Endpoint.SIGN_UP, - request - ); - assert.fail('Call should have failed'); - } catch (e) { - expect(e.code).to.eq(`auth/${AuthErrorCode.EMAIL_EXISTS}`); - expect((e as FirebaseError).customData!.email).to.eq('email@test.com'); - expect((e as FirebaseError).customData!.phoneNumber).to.eq( - '+1555-this-is-a-number' - ); - } - }); - }); - - context('_getFinalTarget', () => { - it('works properly with a non-emulated environment', () => { - expect(_getFinalTarget(auth, 'host', '/path', 'query=test')).to.eq( - 'mock://host/path?query=test' - ); - }); - - it('works properly with an emulated environment', () => { - (auth.config as ConfigInternal).emulator = { - url: 'http://localhost:5000' - }; - expect(_getFinalTarget(auth, 'host', '/path', 'query=test')).to.eq( - 'http://localhost:5000/host/path?query=test' - ); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/api/index.ts b/packages-exp/auth-exp/src/api/index.ts deleted file mode 100644 index aeb5ec20d3d..00000000000 --- a/packages-exp/auth-exp/src/api/index.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseError, querystring } from '@firebase/util'; - -import { AuthErrorCode, NamedErrorParams } from '../core/errors'; -import { _createError, _fail } from '../core/util/assert'; -import { Delay } from '../core/util/delay'; -import { _emulatorUrl } from '../core/util/emulator'; -import { FetchProvider } from '../core/util/fetch_provider'; -import { Auth } from '@firebase/auth-types-exp'; -import { Auth as AuthInternal } from '../model/auth'; -import { IdTokenResponse, TaggedWithTokenResponse } from '../model/id_token'; -import { IdTokenMfaResponse } from './authentication/mfa'; -import { SERVER_ERROR_MAP, ServerError, ServerErrorMap } from './errors'; - -export const enum HttpMethod { - POST = 'POST', - GET = 'GET' -} - -export const enum HttpHeader { - CONTENT_TYPE = 'Content-Type', - X_FIREBASE_LOCALE = 'X-Firebase-Locale', - X_CLIENT_VERSION = 'X-Client-Version' -} - -export const enum Endpoint { - CREATE_AUTH_URI = '/v1/accounts:createAuthUri', - DELETE_ACCOUNT = '/v1/accounts:delete', - RESET_PASSWORD = '/v1/accounts:resetPassword', - SIGN_UP = '/v1/accounts:signUp', - SIGN_IN_WITH_CUSTOM_TOKEN = '/v1/accounts:signInWithCustomToken', - SIGN_IN_WITH_EMAIL_LINK = '/v1/accounts:signInWithEmailLink', - SIGN_IN_WITH_IDP = '/v1/accounts:signInWithIdp', - SIGN_IN_WITH_PASSWORD = '/v1/accounts:signInWithPassword', - SIGN_IN_WITH_PHONE_NUMBER = '/v1/accounts:signInWithPhoneNumber', - SEND_VERIFICATION_CODE = '/v1/accounts:sendVerificationCode', - SEND_OOB_CODE = '/v1/accounts:sendOobCode', - SET_ACCOUNT_INFO = '/v1/accounts:update', - GET_ACCOUNT_INFO = '/v1/accounts:lookup', - GET_RECAPTCHA_PARAM = '/v1/recaptchaParams', - START_PHONE_MFA_ENROLLMENT = '/v2/accounts/mfaEnrollment:start', - FINALIZE_PHONE_MFA_ENROLLMENT = '/v2/accounts/mfaEnrollment:finalize', - START_PHONE_MFA_SIGN_IN = '/v2/accounts/mfaSignIn:start', - FINALIZE_PHONE_MFA_SIGN_IN = '/v2/accounts/mfaSignIn:finalize', - WITHDRAW_MFA = '/v2/accounts/mfaEnrollment:withdraw', - GET_PROJECT_CONFIG = '/v1/projects' -} - -export const DEFAULT_API_TIMEOUT_MS = new Delay(30_000, 60_000); - -export async function _performApiRequest( - auth: Auth, - method: HttpMethod, - path: Endpoint, - request?: T, - customErrorMap: Partial> = {} -): Promise { - return _performFetchWithErrorHandling(auth, customErrorMap, () => { - let body = {}; - let params = {}; - if (request) { - if (method === HttpMethod.GET) { - params = request; - } else { - body = { - body: JSON.stringify(request) - }; - } - } - - const query = querystring({ - key: auth.config.apiKey, - ...params - }).slice(1); - - const headers = new (FetchProvider.headers())(); - headers.set(HttpHeader.CONTENT_TYPE, 'application/json'); - headers.set(HttpHeader.X_CLIENT_VERSION, auth.config.sdkClientVersion); - - if (auth.languageCode) { - headers.set(HttpHeader.X_FIREBASE_LOCALE, auth.languageCode); - } - - return FetchProvider.fetch()( - _getFinalTarget(auth, auth.config.apiHost, path, query), - { - method, - headers, - referrerPolicy: 'no-referrer', - ...body - } - ); - }); -} - -export async function _performFetchWithErrorHandling( - auth: Auth, - customErrorMap: Partial>, - fetchFn: () => Promise -): Promise { - (auth as AuthInternal)._canInitEmulator = false; - const errorMap = { ...SERVER_ERROR_MAP, ...customErrorMap }; - try { - const networkTimeout = new NetworkTimeout(auth); - const response: Response = await Promise.race>([ - fetchFn(), - networkTimeout.promise - ]); - - // If we've reached this point, the fetch succeeded and the networkTimeout - // didn't throw; clear the network timeout delay so that Node won't hang - networkTimeout.clearNetworkTimeout(); - - const json = await response.json(); - if ('needConfirmation' in json) { - throw makeTaggedError(auth, AuthErrorCode.NEED_CONFIRMATION, json); - } - - if (response.ok) { - return json; - } else { - const serverErrorCode = json.error.message.split(' : ')[0] as ServerError; - if (serverErrorCode === ServerError.FEDERATED_USER_ID_ALREADY_LINKED) { - throw makeTaggedError( - auth, - AuthErrorCode.CREDENTIAL_ALREADY_IN_USE, - json - ); - } else if (serverErrorCode === ServerError.EMAIL_EXISTS) { - throw makeTaggedError(auth, AuthErrorCode.EMAIL_EXISTS, json); - } - const authError = - errorMap[serverErrorCode] || - ((serverErrorCode - .toLowerCase() - .replace(/[_\s]+/g, '-') as unknown) as AuthErrorCode); - _fail(auth, authError); - } - } catch (e) { - if (e instanceof FirebaseError) { - throw e; - } - _fail(auth, AuthErrorCode.NETWORK_REQUEST_FAILED); - } -} - -export async function _performSignInRequest( - auth: Auth, - method: HttpMethod, - path: Endpoint, - request?: T, - customErrorMap: Partial> = {} -): Promise { - const serverResponse = (await _performApiRequest( - auth, - method, - path, - request, - customErrorMap - )) as V; - if ('mfaPendingCredential' in serverResponse) { - _fail(auth, AuthErrorCode.MFA_REQUIRED, { - serverResponse - }); - } - - return serverResponse; -} - -export function _getFinalTarget( - auth: Auth, - host: string, - path: string, - query: string -): string { - const base = `${host}${path}?${query}`; - - if (!(auth as AuthInternal).config.emulator) { - return `${auth.config.apiScheme}://${base}`; - } - - return _emulatorUrl(auth.config, base); -} - -class NetworkTimeout { - // Node timers and browser timers are fundamentally incompatible, but we - // don't care about the value here - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private timer: any | null = null; - readonly promise = new Promise((_, reject) => { - this.timer = setTimeout(() => { - return reject(_createError(this.auth, AuthErrorCode.TIMEOUT)); - }, DEFAULT_API_TIMEOUT_MS.get()); - }); - - clearNetworkTimeout(): void { - clearTimeout(this.timer); - } - - constructor(private readonly auth: Auth) {} -} - -interface PotentialResponse extends IdTokenResponse { - email?: string; - phoneNumber?: string; -} - -function makeTaggedError( - auth: Auth, - code: AuthErrorCode, - response: PotentialResponse -): FirebaseError { - const errorParams: NamedErrorParams = { - appName: auth.name - }; - - if (response.email) { - errorParams.email = response.email; - } - if (response.phoneNumber) { - errorParams.phoneNumber = response.phoneNumber; - } - - const error = _createError(auth, code, errorParams); - - // We know customData is defined on error because errorParams is defined - (error.customData! as TaggedWithTokenResponse)._tokenResponse = response; - return error; -} diff --git a/packages-exp/auth-exp/src/core/action_code_url.ts b/packages-exp/auth-exp/src/core/action_code_url.ts deleted file mode 100644 index 35de12fcc16..00000000000 --- a/packages-exp/auth-exp/src/core/action_code_url.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { AuthErrorCode } from './errors'; -import { _assert } from './util/assert'; - -/** - * Enums for fields in URL query string. - * - * @enum {string} - * @internal - */ -const enum QueryField { - API_KEY = 'apiKey', - CODE = 'oobCode', - CONTINUE_URL = 'continueUrl', - LANGUAGE_CODE = 'languageCode', - MODE = 'mode', - TENANT_ID = 'tenantId' -} - -/** - * Maps the mode string in action code URL to Action Code Info operation. - * - * @param mode - * @internal - */ -function parseMode(mode: string | null): externs.ActionCodeOperation | null { - switch (mode) { - case 'recoverEmail': - return externs.ActionCodeOperation.RECOVER_EMAIL; - case 'resetPassword': - return externs.ActionCodeOperation.PASSWORD_RESET; - case 'signIn': - return externs.ActionCodeOperation.EMAIL_SIGNIN; - case 'verifyEmail': - return externs.ActionCodeOperation.VERIFY_EMAIL; - case 'verifyAndChangeEmail': - return externs.ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL; - case 'revertSecondFactorAddition': - return externs.ActionCodeOperation.REVERT_SECOND_FACTOR_ADDITION; - default: - return null; - } -} - -/** - * Helper to parse FDL links - * - * @param url - * @internal - */ -function parseDeepLink(url: string): string { - const uri = new URL(url); - const link = uri.searchParams.get('link'); - // Double link case (automatic redirect). - const doubleDeepLink = link ? new URL(link).searchParams.get('link') : null; - // iOS custom scheme links. - const iOSDeepLink = uri.searchParams.get('deep_link_id'); - const iOSDoubleDeepLink = iOSDeepLink - ? new URL(iOSDeepLink).searchParams.get('link') - : null; - return iOSDoubleDeepLink || iOSDeepLink || doubleDeepLink || link || url; -} - -/** - * {@inheritDoc @firebase/auth-types#ActionCodeURL} - * - * @public - */ -export class ActionCodeURL implements externs.ActionCodeURL { - /** {@inheritDoc @firebase/auth-types#ActionCodeURL.apiKey} */ - readonly apiKey: string; - /** {@inheritDoc @firebase/auth-types#ActionCodeURL.code} */ - readonly code: string; - /** {@inheritDoc @firebase/auth-types#ActionCodeURL.continueUrl} */ - readonly continueUrl: string | null; - /** {@inheritDoc @firebase/auth-types#ActionCodeURL.languageCode} */ - readonly languageCode: string | null; - /** {@inheritDoc @firebase/auth-types#ActionCodeURL.operation} */ - readonly operation: externs.ActionCodeOperation; - /** {@inheritDoc @firebase/auth-types#ActionCodeURL.tenantId} */ - readonly tenantId: string | null; - - /** - * @param actionLink - The link from which to extract the URL. - * @returns The ActionCodeURL object, or null if the link is invalid. - * - * @internal - */ - constructor(actionLink: string) { - const uri = new URL(actionLink); - const apiKey = uri.searchParams.get(QueryField.API_KEY); - const code = uri.searchParams.get(QueryField.CODE); - const operation = parseMode(uri.searchParams.get(QueryField.MODE)); - // Validate API key, code and mode. - _assert(apiKey && code && operation, AuthErrorCode.ARGUMENT_ERROR); - this.apiKey = apiKey; - this.operation = operation; - this.code = code; - this.continueUrl = uri.searchParams.get(QueryField.CONTINUE_URL); - this.languageCode = uri.searchParams.get(QueryField.LANGUAGE_CODE); - this.tenantId = uri.searchParams.get(QueryField.TENANT_ID); - } - - /** {@inheritDoc @firebase/auth-types#ActionCodeURL.parseLink} */ - static parseLink(link: string): externs.ActionCodeURL | null { - const actionLink = parseDeepLink(link); - try { - return new ActionCodeURL(actionLink); - } catch { - return null; - } - } -} - -/** - * {@inheritDoc @firebase/auth-types#ActionCodeURL.parseLink} - * - * @public - */ -export function parseActionCodeURL(link: string): externs.ActionCodeURL | null { - return ActionCodeURL.parseLink(link); -} diff --git a/packages-exp/auth-exp/src/core/auth/auth_impl.test.ts b/packages-exp/auth-exp/src/core/auth/auth_impl.test.ts deleted file mode 100644 index d5d447aaa2b..00000000000 --- a/packages-exp/auth-exp/src/core/auth/auth_impl.test.ts +++ /dev/null @@ -1,586 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { endpointUrl, mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth, testUser } from '../../../test/helpers/mock_auth'; -import * as fetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { Auth } from '../../model/auth'; -import { User } from '../../model/user'; -import { Persistence } from '../persistence'; -import { inMemoryPersistence } from '../persistence/in_memory'; -import { _getInstance } from '../util/instantiator'; -import * as navigator from '../util/navigator'; -import * as reload from '../user/reload'; -import { _castAuth, AuthImpl, DefaultConfig } from './auth_impl'; -import { _initializeAuthInstance } from './initialize'; - -use(sinonChai); -use(chaiAsPromised); - -const FAKE_APP: FirebaseApp = { - name: 'test-app', - options: { - apiKey: 'api-key', - authDomain: 'auth-domain' - }, - automaticDataCollectionEnabled: false -}; - -describe('core/auth/auth_impl', () => { - let auth: Auth; - let persistenceStub: sinon.SinonStubbedInstance; - - beforeEach(async () => { - persistenceStub = sinon.stub(_getInstance(inMemoryPersistence)); - const authImpl = new AuthImpl(FAKE_APP, { - apiKey: FAKE_APP.options.apiKey!, - apiHost: DefaultConfig.API_HOST, - apiScheme: DefaultConfig.API_SCHEME, - tokenApiHost: DefaultConfig.TOKEN_API_HOST, - sdkClientVersion: 'v' - }); - - _initializeAuthInstance(authImpl, { persistence: inMemoryPersistence }); - auth = authImpl; - }); - - afterEach(sinon.restore); - - describe('#updateCurrentUser', () => { - it('sets the field on the auth object', async () => { - const user = testUser(auth, 'uid'); - await auth._updateCurrentUser(user); - expect(auth.currentUser).to.eq(user); - }); - - it('public version makes a copy', async () => { - const user = testUser(auth, 'uid'); - await auth.updateCurrentUser(user); - - // currentUser should deeply equal the user passed in, but should be a - // different block in memory. - expect(auth.currentUser).not.to.eq(user); - expect(auth.currentUser).to.eql(user); - }); - - it('public version throws if the auth is mismatched', async () => { - const auth2 = await testAuth(); - Object.assign(auth2, { name: 'not-the-right-auth' }); - const user = testUser(auth2, 'uid'); - await expect(auth.updateCurrentUser(user)).to.be.rejectedWith( - FirebaseError, - 'auth/argument-error' - ); - }); - - it('orders async operations correctly', async () => { - const users = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => { - return testUser(auth, `${n}`); - }); - - persistenceStub._set.callsFake(() => { - return new Promise(resolve => { - // Force into the async flow to make this test actually meaningful - setTimeout(() => resolve(), 1); - }); - }); - - await Promise.all(users.map(u => auth._updateCurrentUser(u))); - for (let i = 0; i < 10; i++) { - expect(persistenceStub._set.getCall(i)).to.have.been.calledWith( - sinon.match.any, - users[i].toJSON() - ); - } - }); - - it('setting to null triggers a remove call', async () => { - await auth._updateCurrentUser(null); - expect(persistenceStub._remove).to.have.been.called; - }); - - it('should throw an error if the user is from a different tenant', async () => { - const user = testUser(auth, 'uid'); - user.tenantId = 'other-tenant-id'; - await expect(auth._updateCurrentUser(user)).to.be.rejectedWith( - FirebaseError, - '(auth/tenant-id-mismatch)' - ); - }); - }); - - describe('#signOut', () => { - it('sets currentUser to null, calls remove', async () => { - await auth._updateCurrentUser(testUser(auth, 'test')); - await auth.signOut(); - expect(persistenceStub._remove).to.have.been.called; - expect(auth.currentUser).to.be.null; - }); - }); - - describe('#useDeviceLanguage', () => { - it('should update the language code', () => { - const mock = sinon.stub(navigator, '_getUserLanguage'); - mock.callsFake(() => 'jp'); - expect(auth.languageCode).to.be.null; - auth.useDeviceLanguage(); - expect(auth.languageCode).to.eq('jp'); - }); - }); - - describe('change listeners', () => { - // // Helpers to convert auth state change results to promise - // function onAuthStateChange(callback: NextFn) - - it('immediately calls authStateChange if initialization finished', done => { - const user = testUser(auth, 'uid'); - auth.currentUser = user; - auth._isInitialized = true; - auth.onAuthStateChanged(user => { - expect(user).to.eq(user); - done(); - }); - }); - - it('waits for initialization for authStateChange', done => { - const user = testUser(auth, 'uid'); - auth.currentUser = user; - auth._isInitialized = false; - auth.onAuthStateChanged(user => { - expect(user).to.eq(user); - done(); - }); - }); - - it('immediately calls idTokenChange if initialization finished', done => { - const user = testUser(auth, 'uid'); - auth.currentUser = user; - auth._isInitialized = true; - auth.onIdTokenChanged(user => { - expect(user).to.eq(user); - done(); - }); - }); - - it('waits for initialization for idTokenChanged', done => { - const user = testUser(auth, 'uid'); - auth.currentUser = user; - auth._isInitialized = false; - auth.onIdTokenChanged(user => { - expect(user).to.eq(user); - done(); - }); - }); - - it('immediate callback is done async', () => { - auth._isInitialized = true; - let callbackCalled = false; - auth.onIdTokenChanged(() => { - callbackCalled = true; - }); - - expect(callbackCalled).to.be.false; - }); - - describe('user logs in/out, tokens refresh', () => { - let user: User; - let authStateCallback: sinon.SinonSpy; - let idTokenCallback: sinon.SinonSpy; - - beforeEach(() => { - user = testUser(auth, 'uid'); - authStateCallback = sinon.spy(); - idTokenCallback = sinon.spy(); - }); - - context('initially currentUser is null', () => { - beforeEach(async () => { - auth.onAuthStateChanged(authStateCallback); - auth.onIdTokenChanged(idTokenCallback); - await auth._updateCurrentUser(null); - authStateCallback.resetHistory(); - idTokenCallback.resetHistory(); - }); - - it('onAuthStateChange triggers on log in', async () => { - await auth._updateCurrentUser(user); - expect(authStateCallback).to.have.been.calledWith(user); - }); - - it('onIdTokenChange triggers on log in', async () => { - await auth._updateCurrentUser(user); - expect(idTokenCallback).to.have.been.calledWith(user); - }); - }); - - context('initially currentUser is user', () => { - beforeEach(async () => { - auth.onAuthStateChanged(authStateCallback); - auth.onIdTokenChanged(idTokenCallback); - await auth._updateCurrentUser(user); - authStateCallback.resetHistory(); - idTokenCallback.resetHistory(); - }); - - it('onAuthStateChange triggers on log out', async () => { - await auth._updateCurrentUser(null); - expect(authStateCallback).to.have.been.calledWith(null); - }); - - it('onIdTokenChange triggers on log out', async () => { - await auth._updateCurrentUser(null); - expect(idTokenCallback).to.have.been.calledWith(null); - }); - - it('onAuthStateChange does not trigger for user props change', async () => { - user.photoURL = 'blah'; - await auth._updateCurrentUser(user); - expect(authStateCallback).not.to.have.been.called; - }); - - it('onIdTokenChange triggers for user props change', async () => { - user.photoURL = 'hey look I changed'; - await auth._updateCurrentUser(user); - expect(idTokenCallback).to.have.been.calledWith(user); - }); - - it('onAuthStateChange triggers if uid changes', async () => { - const newUser = testUser(auth, 'different-uid'); - await auth._updateCurrentUser(newUser); - expect(authStateCallback).to.have.been.calledWith(newUser); - }); - }); - - it('onAuthStateChange works for multiple listeners', async () => { - const cb1 = sinon.spy(); - const cb2 = sinon.spy(); - auth.onAuthStateChanged(cb1); - auth.onAuthStateChanged(cb2); - await auth._updateCurrentUser(null); - cb1.resetHistory(); - cb2.resetHistory(); - - await auth._updateCurrentUser(user); - expect(cb1).to.have.been.calledWith(user); - expect(cb2).to.have.been.calledWith(user); - }); - - it('onIdTokenChange works for multiple listeners', async () => { - const cb1 = sinon.spy(); - const cb2 = sinon.spy(); - auth.onIdTokenChanged(cb1); - auth.onIdTokenChanged(cb2); - await auth._updateCurrentUser(null); - cb1.resetHistory(); - cb2.resetHistory(); - - await auth._updateCurrentUser(user); - expect(cb1).to.have.been.calledWith(user); - expect(cb2).to.have.been.calledWith(user); - }); - }); - }); - - describe('#_onStorageEvent', () => { - let authStateCallback: sinon.SinonSpy; - let idTokenCallback: sinon.SinonSpy; - - beforeEach(async () => { - authStateCallback = sinon.spy(); - idTokenCallback = sinon.spy(); - auth.onAuthStateChanged(authStateCallback); - auth.onIdTokenChanged(idTokenCallback); - await auth._updateCurrentUser(null); // force event handlers to clear out - authStateCallback.resetHistory(); - idTokenCallback.resetHistory(); - }); - - context('previously logged out', () => { - context('still logged out', () => { - it('should do nothing', async () => { - await auth._onStorageEvent(); - - expect(authStateCallback).not.to.have.been.called; - expect(idTokenCallback).not.to.have.been.called; - }); - }); - - context('now logged in', () => { - let user: User; - - beforeEach(() => { - user = testUser(auth, 'uid'); - persistenceStub._get.returns(Promise.resolve(user.toJSON())); - }); - - it('should update the current user', async () => { - await auth._onStorageEvent(); - - expect(auth.currentUser?.toJSON()).to.eql(user.toJSON()); - expect(authStateCallback).to.have.been.called; - expect(idTokenCallback).to.have.been.called; - }); - }); - }); - - context('previously logged in', () => { - let user: User; - - beforeEach(async () => { - user = testUser(auth, 'uid', undefined, true); - await auth._updateCurrentUser(user); - authStateCallback.resetHistory(); - idTokenCallback.resetHistory(); - }); - - context('now logged out', () => { - beforeEach(() => { - persistenceStub._get.returns(Promise.resolve(null)); - }); - - it('should log out', async () => { - await auth._onStorageEvent(); - - expect(auth.currentUser).to.be.null; - expect(authStateCallback).to.have.been.called; - expect(idTokenCallback).to.have.been.called; - }); - }); - - context('still logged in as same user', () => { - it('should do nothing if nothing changed', async () => { - persistenceStub._get.returns(Promise.resolve(user.toJSON())); - - await auth._onStorageEvent(); - - expect(auth.currentUser?.toJSON()).to.eql(user.toJSON()); - expect(authStateCallback).not.to.have.been.called; - expect(idTokenCallback).not.to.have.been.called; - }); - - it('should update fields if they have changed', async () => { - const userObj = user.toJSON(); - userObj['displayName'] = 'other-name'; - persistenceStub._get.returns(Promise.resolve(userObj)); - - await auth._onStorageEvent(); - - expect(auth.currentUser?.uid).to.eq(user.uid); - expect(auth.currentUser?.displayName).to.eq('other-name'); - expect(authStateCallback).not.to.have.been.called; - expect(idTokenCallback).not.to.have.been.called; - }); - - it('should update tokens if they have changed', async () => { - const userObj = user.toJSON(); - (userObj['stsTokenManager'] as any)['accessToken'] = - 'new-access-token'; - persistenceStub._get.returns(Promise.resolve(userObj)); - - await auth._onStorageEvent(); - - expect(auth.currentUser?.uid).to.eq(user.uid); - expect((auth.currentUser as User)?.stsTokenManager.accessToken).to.eq( - 'new-access-token' - ); - expect(authStateCallback).not.to.have.been.called; - expect(idTokenCallback).to.have.been.called; - }); - }); - - context('now logged in as different user', () => { - it('should re-login as the new user', async () => { - const newUser = testUser(auth, 'other-uid', undefined, true); - persistenceStub._get.returns(Promise.resolve(newUser.toJSON())); - - await auth._onStorageEvent(); - - expect(auth.currentUser?.toJSON()).to.eql(newUser.toJSON()); - expect(authStateCallback).to.have.been.called; - expect(idTokenCallback).to.have.been.called; - }); - }); - }); - }); - - context('#_delete', () => { - beforeEach(async () => { - sinon.stub(reload, '_reloadWithoutSaving').returns(Promise.resolve()); - }); - - it('prevents initialization from completing', async () => { - const authImpl = new AuthImpl(FAKE_APP, { - apiKey: FAKE_APP.options.apiKey!, - apiHost: DefaultConfig.API_HOST, - apiScheme: DefaultConfig.API_SCHEME, - tokenApiHost: DefaultConfig.TOKEN_API_HOST, - sdkClientVersion: 'v' - }); - - persistenceStub._get.returns( - Promise.resolve(testUser(auth, 'uid').toJSON()) - ); - await authImpl._delete(); - await authImpl._initializeWithPersistence([ - persistenceStub as Persistence - ]); - expect(authImpl.currentUser).to.be.null; - }); - - it('no longer calls listeners', async () => { - const spy = sinon.spy(); - auth.onAuthStateChanged(spy); - await Promise.resolve(); - spy.resetHistory(); - await (auth as AuthImpl)._delete(); - await auth._updateCurrentUser(testUser(auth, 'blah')); - expect(spy).not.to.have.been.called; - }); - }); -}); - -// These tests are separate because they are using a different auth with -// separate setup and config -describe('core/auth/auth_impl useEmulator', () => { - let auth: TestAuth; - let user: User; - let normalEndpoint: fetch.Route; - let emulatorEndpoint: fetch.Route; - - beforeEach(async () => { - auth = await testAuth(); - user = testUser(_castAuth(auth), 'uid', 'email', true); - fetch.setUp(); - normalEndpoint = mockEndpoint(Endpoint.DELETE_ACCOUNT, {}); - emulatorEndpoint = fetch.mock( - `http://localhost:2020/${endpointUrl(Endpoint.DELETE_ACCOUNT).replace( - /^.*:\/\//, - '' - )}`, - {} - ); - }); - - afterEach(() => { - fetch.tearDown(); - sinon.restore(); - - // The DOM persists through tests; remove the banner if it is attached - const banner = - typeof document !== 'undefined' - ? document.querySelector('.firebase-emulator-warning') - : null; - if (banner) { - banner.parentElement?.removeChild(banner); - } - }); - - context('useEmulator', () => { - it('fails if a network request has already been made', async () => { - await user.delete(); - expect(() => auth.useEmulator('http://localhost:2020')).to.throw( - FirebaseError, - 'auth/emulator-config-failed' - ); - }); - - it('updates the endpoint appropriately', async () => { - auth.useEmulator('http://localhost:2020'); - await user.delete(); - expect(normalEndpoint.calls.length).to.eq(0); - expect(emulatorEndpoint.calls.length).to.eq(1); - }); - - it('checks the scheme properly', () => { - expect(() => auth.useEmulator('http://localhost:2020')).not.to.throw; - delete auth.config.emulator; - expect(() => auth.useEmulator('https://localhost:2020')).not.to.throw; - delete auth.config.emulator; - expect(() => auth.useEmulator('ssh://localhost:2020')).to.throw( - FirebaseError, - 'auth/invalid-emulator-scheme' - ); - delete auth.config.emulator; - expect(() => auth.useEmulator('localhost:2020')).to.throw( - FirebaseError, - 'auth/invalid-emulator-scheme' - ); - }); - - it('attaches a banner to the DOM', () => { - auth.useEmulator('http://localhost:2020'); - if (typeof document !== 'undefined') { - const el = document.querySelector('.firebase-emulator-warning')!; - expect(el).not.to.be.null; - expect(el.textContent).to.eq( - 'Running in emulator mode. ' + - 'Do not use with production credentials.' - ); - } - }); - - it('logs out a warning to the console', () => { - sinon.stub(console, 'info'); - auth.useEmulator('http://localhost:2020'); - expect(console.info).to.have.been.calledWith( - 'WARNING: You are using the Auth Emulator,' + - ' which is intended for local testing only. Do not use with' + - ' production credentials.' - ); - }); - - it('logs out the warning but has no banner if disableBanner true', () => { - sinon.stub(console, 'info'); - auth.useEmulator('http://localhost:2020', { disableWarnings: true }); - expect(console.info).to.have.been.calledWith( - 'WARNING: You are using the Auth Emulator,' + - ' which is intended for local testing only. Do not use with' + - ' production credentials.' - ); - if (typeof document !== 'undefined') { - expect(document.querySelector('.firebase-emulator-warning')).to.be.null; - } - }); - }); - - context('toJSON', () => { - it('works when theres no current user', () => { - expect(JSON.stringify(auth)).to.eq( - '{"apiKey":"test-api-key","authDomain":"localhost","appName":"test-app"}' - ); - }); - - it('also stringifies the current user', () => { - auth.currentUser = ({ - toJSON: (): object => ({ foo: 'bar' }) - } as unknown) as User; - expect(JSON.stringify(auth)).to.eq( - '{"apiKey":"test-api-key","authDomain":"localhost",' + - '"appName":"test-app","currentUser":{"foo":"bar"}}' - ); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/auth/auth_impl.ts b/packages-exp/auth-exp/src/core/auth/auth_impl.ts deleted file mode 100644 index fad70bd6033..00000000000 --- a/packages-exp/auth-exp/src/core/auth/auth_impl.ts +++ /dev/null @@ -1,612 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _FirebaseService, FirebaseApp } from '@firebase/app-types-exp'; -import * as externs from '@firebase/auth-types-exp'; -import { - CompleteFn, - createSubscribe, - ErrorFactory, - ErrorFn, - NextFn, - Observer, - Subscribe, - Unsubscribe -} from '@firebase/util'; - -import { Auth, ConfigInternal } from '../../model/auth'; -import { PopupRedirectResolver } from '../../model/popup_redirect'; -import { User } from '../../model/user'; -import { - AuthErrorCode, - AuthErrorParams, - ErrorMapRetriever, - _DEFAULT_AUTH_ERROR_FACTORY -} from '../errors'; -import { Persistence } from '../persistence'; -import { - KeyName, - PersistenceUserManager -} from '../persistence/persistence_user_manager'; -import { _reloadWithoutSaving } from '../user/reload'; -import { _assert } from '../util/assert'; -import { _getInstance } from '../util/instantiator'; -import { _getUserLanguage } from '../util/navigator'; - -interface AsyncAction { - (): Promise; -} - -export const enum DefaultConfig { - TOKEN_API_HOST = 'securetoken.googleapis.com', - API_HOST = 'identitytoolkit.googleapis.com', - API_SCHEME = 'https' -} - -export class AuthImpl implements Auth, _FirebaseService { - currentUser: externs.User | null = null; - private operations = Promise.resolve(); - private persistenceManager?: PersistenceUserManager; - private redirectPersistenceManager?: PersistenceUserManager; - private authStateSubscription = new Subscription(this); - private idTokenSubscription = new Subscription(this); - private redirectUser: User | null = null; - private isProactiveRefreshEnabled = false; - private redirectInitializerError: Error | null = null; - - // Any network calls will set this to true and prevent subsequent emulator - // initialization - _canInitEmulator = true; - _isInitialized = false; - _deleted = false; - _initializationPromise: Promise | null = null; - _popupRedirectResolver: PopupRedirectResolver | null = null; - _errorFactory: ErrorFactory< - AuthErrorCode, - AuthErrorParams - > = _DEFAULT_AUTH_ERROR_FACTORY; - readonly name: string; - - // Tracks the last notified UID for state change listeners to prevent - // repeated calls to the callbacks. Undefined means it's never been - // called, whereas null means it's been called with a signed out user - private lastNotifiedUid: string | null | undefined = undefined; - - languageCode: string | null = null; - tenantId: string | null = null; - settings: externs.AuthSettings = { appVerificationDisabledForTesting: false }; - - constructor( - public readonly app: FirebaseApp, - public readonly config: ConfigInternal - ) { - this.name = app.name; - } - - _initializeWithPersistence( - persistenceHierarchy: Persistence[], - popupRedirectResolver?: externs.PopupRedirectResolver - ): Promise { - // Have to check for app deletion throughout initialization (after each - // promise resolution) - this._initializationPromise = this.queue(async () => { - if (this._deleted) { - return; - } - - if (popupRedirectResolver) { - this._popupRedirectResolver = _getInstance(popupRedirectResolver); - } - - this.persistenceManager = await PersistenceUserManager.create( - this, - persistenceHierarchy - ); - - if (this._deleted) { - return; - } - - await this.initializeCurrentUser(popupRedirectResolver); - - if (this._deleted) { - return; - } - - this._isInitialized = true; - }); - - // After initialization completes, throw any error caused by redirect flow - return this._initializationPromise.then(() => { - if (this.redirectInitializerError) { - throw this.redirectInitializerError; - } - }); - } - - /** - * If the persistence is changed in another window, the user manager will let us know - */ - async _onStorageEvent(): Promise { - if (this._deleted) { - return; - } - - const user = await this.assertedPersistence.getCurrentUser(); - - if (!this.currentUser && !user) { - // No change, do nothing (was signed out and remained signed out). - return; - } - - // If the same user is to be synchronized. - if (this.currentUser && user && this.currentUser.uid === user.uid) { - // Data update, simply copy data changes. - this._currentUser._assign(user); - // If tokens changed from previous user tokens, this will trigger - // notifyAuthListeners_. - await this.currentUser.getIdToken(); - return; - } - - // Update current Auth state. Either a new login or logout. - await this._updateCurrentUser(user); - } - - private async initializeCurrentUser( - popupRedirectResolver?: externs.PopupRedirectResolver - ): Promise { - // First check to see if we have a pending redirect event. - let storedUser = (await this.assertedPersistence.getCurrentUser()) as User | null; - if (popupRedirectResolver && this.config.authDomain) { - await this.getOrInitRedirectPersistenceManager(); - const redirectUserEventId = this.redirectUser?._redirectEventId; - const storedUserEventId = storedUser?._redirectEventId; - const result = await this.tryRedirectSignIn(popupRedirectResolver); - - // If the stored user (i.e. the old "currentUser") has a redirectId that - // matches the redirect user, then we want to initially sign in with the - // new user object from result. - // TODO(samgho): More thoroughly test all of this - if ( - (!redirectUserEventId || redirectUserEventId === storedUserEventId) && - result?.user - ) { - storedUser = result.user as User; - } - } - - // If no user in persistence, there is no current user. Set to null. - if (!storedUser) { - return this.directlySetCurrentUser(null); - } - - if (!storedUser._redirectEventId) { - // This isn't a redirect user, we can reload and bail - // This will also catch the redirected user, if available, as that method - // strips the _redirectEventId - return this.reloadAndSetCurrentUserOrClear(storedUser); - } - - _assert(this._popupRedirectResolver, this, AuthErrorCode.ARGUMENT_ERROR); - await this.getOrInitRedirectPersistenceManager(); - - // If the redirect user's event ID matches the current user's event ID, - // DO NOT reload the current user, otherwise they'll be cleared from storage. - // This is important for the reauthenticateWithRedirect() flow. - if ( - this.redirectUser && - this.redirectUser._redirectEventId === storedUser._redirectEventId - ) { - return this.directlySetCurrentUser(storedUser); - } - - return this.reloadAndSetCurrentUserOrClear(storedUser); - } - - private async tryRedirectSignIn( - redirectResolver: externs.PopupRedirectResolver - ): Promise { - // The redirect user needs to be checked (and signed in if available) - // during auth initialization. All of the normal sign in and link/reauth - // flows call back into auth and push things onto the promise queue. We - // need to await the result of the redirect sign in *inside the promise - // queue*. This presents a problem: we run into deadlock. See: - // ┌> [Initialization] ─────┐ - // ┌> [] │ - // └─ [getRedirectResult] <─┘ - // where [] are tasks on the queue and arrows denote awaits - // Initialization will never complete because it's waiting on something - // that's waiting for initialization to complete! - // - // Instead, this method calls getRedirectResult() (stored in - // _completeRedirectFn) with an optional parameter that instructs all of - // the underlying auth operations to skip anything that mutates auth state. - - let result: externs.UserCredential | null = null; - try { - // We know this._popupRedirectResolver is set since redirectResolver - // is passed in. The _completeRedirectFn expects the unwrapped extern. - result = await this._popupRedirectResolver!._completeRedirectFn( - this, - redirectResolver, - true - ); - } catch (e) { - this.redirectInitializerError = e; - await this._setRedirectUser(null); - } - - return result; - } - - private async reloadAndSetCurrentUserOrClear(user: User): Promise { - try { - await _reloadWithoutSaving(user); - } catch (e) { - if (e.code !== `auth/${AuthErrorCode.NETWORK_REQUEST_FAILED}`) { - // Something's wrong with the user's token. Log them out and remove - // them from storage - return this.directlySetCurrentUser(null); - } - } - - return this.directlySetCurrentUser(user); - } - - useDeviceLanguage(): void { - this.languageCode = _getUserLanguage(); - } - - useEmulator(url: string, options?: { disableWarnings: boolean }): void { - _assert(this._canInitEmulator, this, AuthErrorCode.EMULATOR_CONFIG_FAILED); - - _assert( - /^https?:\/\//.test(url), - this, - AuthErrorCode.INVALID_EMULATOR_SCHEME - ); - - this.config.emulator = { url }; - this.settings.appVerificationDisabledForTesting = true; - emitEmulatorWarning(!!options?.disableWarnings); - } - - async _delete(): Promise { - this._deleted = true; - } - - async updateCurrentUser(userExtern: externs.User | null): Promise { - // The public updateCurrentUser method needs to make a copy of the user, - // and also needs to verify that the app matches - const user = userExtern as User | null; - _assert( - !user || user.auth.name === this.name, - this, - AuthErrorCode.ARGUMENT_ERROR - ); - - return this._updateCurrentUser(user && user._clone()); - } - - async _updateCurrentUser(user: externs.User | null): Promise { - if (this._deleted) { - return; - } - if (user) { - _assert( - this.tenantId === user.tenantId, - this, - AuthErrorCode.TENANT_ID_MISMATCH - ); - } - - return this.queue(async () => { - await this.directlySetCurrentUser(user as User | null); - this.notifyAuthListeners(); - }); - } - - async signOut(): Promise { - // Clear the redirect user when signOut is called - if (this.redirectPersistenceManager || this._popupRedirectResolver) { - await this._setRedirectUser(null); - } - - return this._updateCurrentUser(null); - } - - setPersistence(persistence: externs.Persistence): Promise { - return this.queue(async () => { - await this.assertedPersistence.setPersistence(_getInstance(persistence)); - }); - } - - _getPersistence(): string { - return this.assertedPersistence.persistence.type; - } - - _updateErrorMap(errorMap: externs.AuthErrorMap): void { - this._errorFactory = new ErrorFactory( - 'auth', - 'Firebase', - (errorMap as ErrorMapRetriever)() - ); - } - - onAuthStateChanged( - nextOrObserver: externs.NextOrObserver, - error?: ErrorFn, - completed?: CompleteFn - ): Unsubscribe { - return this.registerStateListener( - this.authStateSubscription, - nextOrObserver, - error, - completed - ); - } - - onIdTokenChanged( - nextOrObserver: externs.NextOrObserver, - error?: ErrorFn, - completed?: CompleteFn - ): Unsubscribe { - return this.registerStateListener( - this.idTokenSubscription, - nextOrObserver, - error, - completed - ); - } - - toJSON(): object { - return { - apiKey: this.config.apiKey, - authDomain: this.config.authDomain, - appName: this.name, - currentUser: this._currentUser?.toJSON() - }; - } - - async _setRedirectUser( - user: User | null, - popupRedirectResolver?: externs.PopupRedirectResolver - ): Promise { - const redirectManager = await this.getOrInitRedirectPersistenceManager( - popupRedirectResolver - ); - return user === null - ? redirectManager.removeCurrentUser() - : redirectManager.setCurrentUser(user); - } - - private async getOrInitRedirectPersistenceManager( - popupRedirectResolver?: externs.PopupRedirectResolver - ): Promise { - if (!this.redirectPersistenceManager) { - const resolver: PopupRedirectResolver | null = - (popupRedirectResolver && _getInstance(popupRedirectResolver)) || - this._popupRedirectResolver; - _assert(resolver, this, AuthErrorCode.ARGUMENT_ERROR); - this.redirectPersistenceManager = await PersistenceUserManager.create( - this, - [_getInstance(resolver._redirectPersistence)], - KeyName.REDIRECT_USER - ); - this.redirectUser = await this.redirectPersistenceManager.getCurrentUser(); - } - - return this.redirectPersistenceManager; - } - - async _redirectUserForId(id: string): Promise { - // Make sure we've cleared any pending persistence actions if we're not in - // the initializer - if (this._isInitialized) { - await this.queue(async () => {}); - } - - if (this._currentUser?._redirectEventId === id) { - return this._currentUser; - } - - if (this.redirectUser?._redirectEventId === id) { - return this.redirectUser; - } - - return null; - } - - async _persistUserIfCurrent(user: User): Promise { - if (user === this.currentUser) { - return this.queue(async () => this.directlySetCurrentUser(user)); - } - } - - /** Notifies listeners only if the user is current */ - _notifyListenersIfCurrent(user: User): void { - if (user === this.currentUser) { - this.notifyAuthListeners(); - } - } - - _key(): string { - return `${this.config.authDomain}:${this.config.apiKey}:${this.name}`; - } - - _startProactiveRefresh(): void { - this.isProactiveRefreshEnabled = true; - if (this.currentUser) { - this._currentUser._startProactiveRefresh(); - } - } - - _stopProactiveRefresh(): void { - this.isProactiveRefreshEnabled = false; - if (this.currentUser) { - this._currentUser._stopProactiveRefresh(); - } - } - - /** Returns the current user cast as the internal type */ - get _currentUser(): User { - return this.currentUser as User; - } - - private notifyAuthListeners(): void { - if (!this._isInitialized) { - return; - } - - this.idTokenSubscription.next(this.currentUser); - - const currentUid = this.currentUser?.uid ?? null; - if (this.lastNotifiedUid !== currentUid) { - this.lastNotifiedUid = currentUid; - this.authStateSubscription.next(this.currentUser); - } - } - - private registerStateListener( - subscription: Subscription, - nextOrObserver: externs.NextOrObserver, - error?: ErrorFn, - completed?: CompleteFn - ): Unsubscribe { - if (this._deleted) { - return () => {}; - } - - const cb = - typeof nextOrObserver === 'function' - ? nextOrObserver - : nextOrObserver.next; - - const promise = this._isInitialized - ? Promise.resolve() - : this._initializationPromise; - _assert(promise, this, AuthErrorCode.INTERNAL_ERROR); - // The callback needs to be called asynchronously per the spec. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - promise.then(() => cb(this.currentUser)); - - if (typeof nextOrObserver === 'function') { - return subscription.addObserver(nextOrObserver, error, completed); - } else { - return subscription.addObserver(nextOrObserver); - } - } - - /** - * Unprotected (from race conditions) method to set the current user. This - * should only be called from within a queued callback. This is necessary - * because the queue shouldn't rely on another queued callback. - */ - private async directlySetCurrentUser(user: User | null): Promise { - if (this.currentUser && this.currentUser !== user) { - this._currentUser._stopProactiveRefresh(); - if (user && this.isProactiveRefreshEnabled) { - user._startProactiveRefresh(); - } - } - - this.currentUser = user; - - if (user) { - await this.assertedPersistence.setCurrentUser(user); - } else { - await this.assertedPersistence.removeCurrentUser(); - } - } - - private queue(action: AsyncAction): Promise { - // In case something errors, the callback still should be called in order - // to keep the promise chain alive - this.operations = this.operations.then(action, action); - return this.operations; - } - - private get assertedPersistence(): PersistenceUserManager { - _assert(this.persistenceManager, this, AuthErrorCode.INTERNAL_ERROR); - return this.persistenceManager; - } -} - -/** - * Method to be used to cast down to our private implmentation of Auth - * - * @param auth Auth object passed in from developer - */ -export function _castAuth(auth: externs.Auth): Auth { - return (auth as unknown) as Auth; -} - -/** Helper class to wrap subscriber logic */ -class Subscription { - private observer: Observer | null = null; - readonly addObserver: Subscribe = createSubscribe( - observer => (this.observer = observer) - ); - - constructor(readonly auth: Auth) {} - - get next(): NextFn { - _assert(this.observer, this.auth, AuthErrorCode.INTERNAL_ERROR); - return this.observer.next.bind(this.observer); - } -} - -function emitEmulatorWarning(disableBanner: boolean): void { - function attachBanner(): void { - const el = document.createElement('p'); - const sty = el.style; - el.innerText = - 'Running in emulator mode. Do not use with production credentials.'; - sty.position = 'fixed'; - sty.width = '100%'; - sty.backgroundColor = '#ffffff'; - sty.border = '.1em solid #000000'; - sty.color = '#ff0000'; - sty.bottom = '0px'; - sty.left = '0px'; - sty.margin = '0px'; - sty.zIndex = '10000'; - sty.textAlign = 'center'; - el.classList.add('firebase-emulator-warning'); - document.body.appendChild(el); - } - - if (typeof console !== 'undefined' && typeof console.info === 'function') { - console.info( - 'WARNING: You are using the Auth Emulator,' + - ' which is intended for local testing only. Do not use with' + - ' production credentials.' - ); - } - if ( - typeof window !== 'undefined' && - typeof document !== 'undefined' && - !disableBanner - ) { - if (document.readyState === 'loading') { - window.addEventListener('DOMContentLoaded', attachBanner); - } else { - attachBanner(); - } - } -} diff --git a/packages-exp/auth-exp/src/core/auth/firebase_internal.ts b/packages-exp/auth-exp/src/core/auth/firebase_internal.ts deleted file mode 100644 index ee1e2473536..00000000000 --- a/packages-exp/auth-exp/src/core/auth/firebase_internal.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Unsubscribe } from '@firebase/util'; -import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; - -import { Auth } from '../../model/auth'; -import { User } from '../../model/user'; - -interface TokenListener { - (tok: string | null): unknown; -} - -export class AuthInternal implements FirebaseAuthInternal { - private readonly internalListeners: Map< - TokenListener, - Unsubscribe - > = new Map(); - - constructor(private readonly auth: Auth) {} - - getUid(): string | null { - return this.auth.currentUser?.uid || null; - } - - async getToken( - forceRefresh?: boolean - ): Promise<{ accessToken: string } | null> { - await this.auth._initializationPromise; - if (!this.auth.currentUser) { - return null; - } - - const accessToken = await this.auth.currentUser.getIdToken(forceRefresh); - return { accessToken }; - } - - addAuthTokenListener(listener: TokenListener): void { - if (this.internalListeners.has(listener)) { - return; - } - - const unsubscribe = this.auth.onIdTokenChanged(user => { - listener((user as User | null)?.stsTokenManager.accessToken || null); - }); - this.internalListeners.set(listener, unsubscribe); - this.updateProactiveRefresh(); - } - - removeAuthTokenListener(listener: TokenListener): void { - const unsubscribe = this.internalListeners.get(listener); - if (!unsubscribe) { - return; - } - - this.internalListeners.delete(listener); - unsubscribe(); - this.updateProactiveRefresh(); - } - - private updateProactiveRefresh(): void { - if (this.internalListeners.size > 0) { - this.auth._startProactiveRefresh(); - } else { - this.auth._stopProactiveRefresh(); - } - } -} diff --git a/packages-exp/auth-exp/src/core/auth/initialize.test.ts b/packages-exp/auth-exp/src/core/auth/initialize.test.ts deleted file mode 100644 index 65b2bcc4f9f..00000000000 --- a/packages-exp/auth-exp/src/core/auth/initialize.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { deleteApp, initializeApp } from '@firebase/app-exp'; -import { FirebaseApp, _FirebaseService } from '@firebase/app-types-exp'; -import * as externs from '@firebase/auth-types-exp'; -import { isNode } from '@firebase/util'; - -import { expect } from 'chai'; -import { inMemoryPersistence } from '../../../internal'; - -import { Auth } from '../../model/auth'; -import { - PopupRedirectResolver, - AuthEventType, - EventManager, - AuthEventConsumer -} from '../../model/popup_redirect'; -import { AuthPopup } from '../../platform_browser/util/popup'; -import { - Persistence, - PersistenceType, - PersistenceValue, - StorageEventListener -} from '../persistence'; -import { ClientPlatform, _getClientVersion } from '../util/version'; -import { initializeAuth } from './initialize'; -import { registerAuth } from './register'; - -describe('core/auth/initialize', () => { - let fakeApp: FirebaseApp; - - class FakeSessionPersistence implements Persistence { - static type: 'SESSION' = 'SESSION'; - readonly type = PersistenceType.SESSION; - storage: Record = {}; - - async _isAvailable(): Promise { - return true; - } - async _set(_key: string, _value: PersistenceValue): Promise { - return; - } - async _get(_key: string): Promise { - return null; - } - async _remove(_key: string): Promise { - return; - } - _addListener(_key: string, _listener: StorageEventListener): void { - return; - } - _removeListener(_key: string, _listener: StorageEventListener): void { - return; - } - } - - const fakeSessionPersistence: externs.Persistence = FakeSessionPersistence; - - class FakePopupRedirectResolver implements PopupRedirectResolver { - readonly _redirectPersistence = fakeSessionPersistence; - - async _initialize(_auth: Auth): Promise { - return new (class implements EventManager { - registerConsumer(_authEventConsumer: AuthEventConsumer): void { - return; - } - unregisterConsumer(_authEventConsumer: AuthEventConsumer): void { - return; - } - })(); - } - async _openPopup( - _auth: Auth, - _provider: externs.AuthProvider, - _authType: AuthEventType, - _eventId?: string - ): Promise { - return new AuthPopup(null); - } - _openRedirect( - _auth: Auth, - _provider: externs.AuthProvider, - _authType: AuthEventType, - _eventId?: string - ): Promise { - return new Promise((_, reject) => reject()); - } - _isIframeWebStorageSupported( - _auth: Auth, - cb: (support: boolean) => unknown - ): void { - cb(true); - } - async _completeRedirectFn( - _auth: externs.Auth, - _resolver: externs.PopupRedirectResolver, - _bypassAuthState: boolean - ): Promise { - return null; - } - } - - const fakePopupRedirectResolver: externs.PopupRedirectResolver = FakePopupRedirectResolver; - - before(() => { - registerAuth(ClientPlatform.BROWSER); - }); - - beforeEach(() => { - fakeApp = initializeApp({ - apiKey: 'fake-key', - appId: 'fake-app-id', - authDomain: 'fake-auth-domain' - }); - }); - - afterEach(async () => { - await deleteApp(fakeApp); - }); - - describe('initializeAuth', () => { - it('should work with no deps', async () => { - const auth = initializeAuth(fakeApp) as Auth; - await auth._initializationPromise; - - expect(auth.name).to.eq(fakeApp.name); - const expectedSdkClientVersion = _getClientVersion( - isNode() ? ClientPlatform.NODE : ClientPlatform.BROWSER - ); - - expect(auth.config).to.eql({ - apiHost: 'identitytoolkit.googleapis.com', - apiKey: 'fake-key', - apiScheme: 'https', - authDomain: 'fake-auth-domain', - sdkClientVersion: expectedSdkClientVersion, - tokenApiHost: 'securetoken.googleapis.com' - }); - expect(auth._getPersistence()).to.eq('NONE'); - }); - - it('should set persistence', async () => { - const auth = initializeAuth(fakeApp, { - persistence: fakeSessionPersistence - }) as Auth; - await auth._initializationPromise; - - expect(auth._getPersistence()).to.eq('SESSION'); - }); - - it('should set persistence with fallback', async () => { - const auth = initializeAuth(fakeApp, { - persistence: [fakeSessionPersistence, inMemoryPersistence] - }) as Auth; - await auth._initializationPromise; - - expect(auth._getPersistence()).to.eq('SESSION'); - }); - - it('should set resolver', async () => { - const auth = initializeAuth(fakeApp, { - popupRedirectResolver: fakePopupRedirectResolver - }) as Auth; - await auth._initializationPromise; - - expect(auth._popupRedirectResolver).to.be.instanceof( - FakePopupRedirectResolver - ); - }); - - it('should abort initialization if deleted synchronously', async () => { - const auth = initializeAuth(fakeApp, { - popupRedirectResolver: fakePopupRedirectResolver - }) as Auth; - await ((auth as unknown) as _FirebaseService)._delete(); - await auth._initializationPromise; - - expect(auth._isInitialized).to.be.false; - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/auth/initialize.ts b/packages-exp/auth-exp/src/core/auth/initialize.ts deleted file mode 100644 index c9f02ff682f..00000000000 --- a/packages-exp/auth-exp/src/core/auth/initialize.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _getProvider } from '@firebase/app-exp'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import * as externs from '@firebase/auth-types-exp'; - -import { Dependencies } from '../../model/auth'; -import { Persistence } from '../persistence'; -import { _getInstance } from '../util/instantiator'; -import { AuthImpl } from './auth_impl'; - -/** @public */ -export function initializeAuth( - app: FirebaseApp, - deps?: Dependencies -): externs.Auth { - const auth = _getProvider(app, 'auth-exp').getImmediate() as AuthImpl; - _initializeAuthInstance(auth, deps); - - return auth; -} - -export function _initializeAuthInstance( - auth: AuthImpl, - deps?: Dependencies -): void { - const persistence = deps?.persistence || []; - const hierarchy = (Array.isArray(persistence) - ? persistence - : [persistence] - ).map(_getInstance); - if (deps?.errorMap) { - auth._updateErrorMap(deps.errorMap); - } - - // This promise is intended to float; auth initialization happens in the - // background, meanwhile the auth object may be used by the app. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - auth._initializeWithPersistence(hierarchy, deps?.popupRedirectResolver); -} diff --git a/packages-exp/auth-exp/src/core/auth/register.ts b/packages-exp/auth-exp/src/core/auth/register.ts deleted file mode 100644 index 823a0327094..00000000000 --- a/packages-exp/auth-exp/src/core/auth/register.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _registerComponent, registerVersion } from '@firebase/app-exp'; -import * as externs from '@firebase/auth-types-exp'; -import { Component, ComponentType } from '@firebase/component'; - -import { version } from '../../../package.json'; -import { AuthErrorCode } from '../errors'; -import { _assert } from '../util/assert'; -import { _getClientVersion, ClientPlatform } from '../util/version'; -import { _castAuth, AuthImpl, DefaultConfig } from './auth_impl'; -import { AuthInternal } from './firebase_internal'; - -export const enum _ComponentName { - AUTH = 'auth-exp', - AUTH_INTERNAL = 'auth-internal' -} - -function getVersionForPlatform( - clientPlatform: ClientPlatform -): string | undefined { - switch (clientPlatform) { - case ClientPlatform.NODE: - return 'node'; - case ClientPlatform.REACT_NATIVE: - return 'rn'; - case ClientPlatform.WORKER: - return 'webworker'; - default: - return undefined; - } -} - -/** @internal */ -export function registerAuth(clientPlatform: ClientPlatform): void { - _registerComponent( - new Component( - _ComponentName.AUTH, - container => { - const app = container.getProvider('app-exp').getImmediate()!; - const { apiKey, authDomain } = app.options; - return (app => { - _assert(apiKey, AuthErrorCode.INVALID_API_KEY, { appName: app.name }); - const config: externs.Config = { - apiKey, - authDomain, - apiHost: DefaultConfig.API_HOST, - tokenApiHost: DefaultConfig.TOKEN_API_HOST, - apiScheme: DefaultConfig.API_SCHEME, - sdkClientVersion: _getClientVersion(clientPlatform) - }; - return new AuthImpl(app, config); - })(app); - }, - ComponentType.PUBLIC - ) - ); - - _registerComponent( - new Component( - _ComponentName.AUTH_INTERNAL, - container => { - const auth = _castAuth( - container.getProvider(_ComponentName.AUTH).getImmediate()! - ); - return (auth => new AuthInternal(auth))(auth); - }, - ComponentType.PRIVATE - ) - ); - - registerVersion( - _ComponentName.AUTH, - version, - getVersionForPlatform(clientPlatform) - ); -} diff --git a/packages-exp/auth-exp/src/core/credentials/auth_credential.ts b/packages-exp/auth-exp/src/core/credentials/auth_credential.ts deleted file mode 100644 index 0ca02c1dda5..00000000000 --- a/packages-exp/auth-exp/src/core/credentials/auth_credential.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { PhoneOrOauthTokenResponse } from '../../api/authentication/mfa'; -import { Auth } from '../../model/auth'; -import { IdTokenResponse } from '../../model/id_token'; -import { debugFail } from '../util/assert'; - -/** - * {@inheritdoc @firebase/auth-types#AuthCredential} - * - * @public - */ -export class AuthCredential { - /** @internal */ - protected constructor( - readonly providerId: string, - readonly signInMethod: string - ) {} - - /** {@inheritdoc @firebase/auth-types#AuthCredential.toJSON} */ - toJSON(): object { - return debugFail('not implemented'); - } - - /** @internal */ - _getIdTokenResponse(_auth: Auth): Promise { - return debugFail('not implemented'); - } - /** @internal */ - _linkToIdToken(_auth: Auth, _idToken: string): Promise { - return debugFail('not implemented'); - } - /** @internal */ - _getReauthenticationResolver(_auth: Auth): Promise { - return debugFail('not implemented'); - } -} diff --git a/packages-exp/auth-exp/src/core/credentials/email.test.ts b/packages-exp/auth-exp/src/core/credentials/email.test.ts deleted file mode 100644 index a39fde9c873..00000000000 --- a/packages-exp/auth-exp/src/core/credentials/email.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { ProviderId, SignInMethod } from '@firebase/auth-types-exp'; - -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { APIUserInfo } from '../../api/account_management/account'; -import { EmailAuthCredential } from './email'; - -use(chaiAsPromised); - -describe('core/credentials/email', () => { - let auth: TestAuth; - let apiMock: mockFetch.Route; - const serverUser: APIUserInfo = { - localId: 'local-id' - }; - - beforeEach(async () => { - auth = await testAuth(); - }); - - context('email & password', () => { - const credential = EmailAuthCredential._fromEmailAndPassword( - 'some-email', - 'some-password' - ); - - beforeEach(() => { - mockFetch.setUp(); - apiMock = mockEndpoint(Endpoint.SIGN_IN_WITH_PASSWORD, { - idToken: 'id-token', - refreshToken: 'refresh-token', - expiresIn: '1234', - localId: serverUser.localId! - }); - }); - afterEach(mockFetch.tearDown); - - it('should have an email provider', () => { - expect(credential.providerId).to.eq(ProviderId.PASSWORD); - }); - - it('should have an anonymous sign in method', () => { - expect(credential.signInMethod).to.eq(SignInMethod.EMAIL_PASSWORD); - }); - - describe('#toJSON', () => { - it('throws', () => { - expect(credential.toJSON).to.throw(Error); - }); - }); - - describe('#_getIdTokenResponse', () => { - it('calls sign in with password', async () => { - const idTokenResponse = await credential._getIdTokenResponse(auth); - expect(idTokenResponse.idToken).to.eq('id-token'); - expect(idTokenResponse.refreshToken).to.eq('refresh-token'); - expect(idTokenResponse.expiresIn).to.eq('1234'); - expect(idTokenResponse.localId).to.eq(serverUser.localId); - expect(apiMock.calls[0].request).to.eql({ - returnSecureToken: true, - email: 'some-email', - password: 'some-password' - }); - }); - }); - - describe('#_linkToIdToken', () => { - it('calls update email password', async () => { - apiMock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, { - idToken: 'id-token', - refreshToken: 'refresh-token', - expiresIn: '1234', - localId: serverUser.localId! - }); - - const idTokenResponse = await credential._linkToIdToken( - auth, - 'id-token-2' - ); - expect(idTokenResponse.idToken).to.eq('id-token'); - expect(idTokenResponse.refreshToken).to.eq('refresh-token'); - expect(idTokenResponse.expiresIn).to.eq('1234'); - expect(idTokenResponse.localId).to.eq(serverUser.localId); - expect(apiMock.calls[0].request).to.eql({ - idToken: 'id-token-2', - returnSecureToken: true, - email: 'some-email', - password: 'some-password' - }); - }); - }); - - describe('#_getReauthenticationResolver', () => { - it('calls sign in with password', async () => { - const idTokenResponse = await credential._getIdTokenResponse(auth); - expect(idTokenResponse.idToken).to.eq('id-token'); - expect(idTokenResponse.refreshToken).to.eq('refresh-token'); - expect(idTokenResponse.expiresIn).to.eq('1234'); - expect(idTokenResponse.localId).to.eq(serverUser.localId); - expect(apiMock.calls[0].request).to.eql({ - returnSecureToken: true, - email: 'some-email', - password: 'some-password' - }); - }); - }); - }); - - context('email link', () => { - const credential = EmailAuthCredential._fromEmailAndCode( - 'some-email', - 'oob-code' - ); - - beforeEach(() => { - mockFetch.setUp(); - apiMock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { - idToken: 'id-token', - refreshToken: 'refresh-token', - expiresIn: '1234', - localId: serverUser.localId! - }); - }); - afterEach(mockFetch.tearDown); - - it('should have an email provider', () => { - expect(credential.providerId).to.eq(ProviderId.PASSWORD); - }); - - it('should have an anonymous sign in method', () => { - expect(credential.signInMethod).to.eq(SignInMethod.EMAIL_LINK); - }); - - describe('#toJSON', () => { - it('throws', () => { - expect(credential.toJSON).to.throw(Error); - }); - }); - - describe('#_getIdTokenResponse', () => { - it('call sign in with email link', async () => { - const idTokenResponse = await credential._getIdTokenResponse(auth); - expect(idTokenResponse.idToken).to.eq('id-token'); - expect(idTokenResponse.refreshToken).to.eq('refresh-token'); - expect(idTokenResponse.expiresIn).to.eq('1234'); - expect(idTokenResponse.localId).to.eq(serverUser.localId); - expect(apiMock.calls[0].request).to.eql({ - email: 'some-email', - oobCode: 'oob-code' - }); - }); - }); - - describe('#_linkToIdToken', () => { - it('calls sign in with the new token', async () => { - const idTokenResponse = await credential._linkToIdToken( - auth, - 'id-token-2' - ); - expect(idTokenResponse.idToken).to.eq('id-token'); - expect(idTokenResponse.refreshToken).to.eq('refresh-token'); - expect(idTokenResponse.expiresIn).to.eq('1234'); - expect(idTokenResponse.localId).to.eq(serverUser.localId); - expect(apiMock.calls[0].request).to.eql({ - idToken: 'id-token-2', - email: 'some-email', - oobCode: 'oob-code' - }); - }); - }); - - describe('#_matchIdTokenWithUid', () => { - it('call sign in with email link', async () => { - const idTokenResponse = await credential._getIdTokenResponse(auth); - expect(idTokenResponse.idToken).to.eq('id-token'); - expect(idTokenResponse.refreshToken).to.eq('refresh-token'); - expect(idTokenResponse.expiresIn).to.eq('1234'); - expect(idTokenResponse.localId).to.eq(serverUser.localId); - expect(apiMock.calls[0].request).to.eql({ - email: 'some-email', - oobCode: 'oob-code' - }); - }); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/credentials/email.ts b/packages-exp/auth-exp/src/core/credentials/email.ts deleted file mode 100644 index f02655895bc..00000000000 --- a/packages-exp/auth-exp/src/core/credentials/email.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { updateEmailPassword } from '../../api/account_management/email_and_password'; -import { signInWithPassword } from '../../api/authentication/email_and_password'; -import { - signInWithEmailLink, - signInWithEmailLinkForLinking -} from '../../api/authentication/email_link'; -import { Auth } from '../../model/auth'; -import { IdTokenResponse } from '../../model/id_token'; -import { AuthErrorCode } from '../errors'; -import { _fail } from '../util/assert'; -import { AuthCredential } from './auth_credential'; - -/** - * Interface that represents the credentials returned by {@link EmailAuthProvider} for - * {@link @firebase/auth-types#ProviderId.PASSWORD} - * - * @remarks - * Covers both {@link @firebase/auth-types#SignInMethod.EMAIL_PASSWORD} and - * {@link @firebase/auth-types#SignInMethod.EMAIL_LINK}. - * - * @public - */ -export class EmailAuthCredential - extends AuthCredential - implements externs.AuthCredential { - /** @internal */ - private constructor( - readonly email: string, - readonly password: string, - signInMethod: externs.SignInMethod, - readonly tenantId: string | null = null - ) { - super(externs.ProviderId.PASSWORD, signInMethod); - } - - /** @internal */ - static _fromEmailAndPassword( - email: string, - password: string - ): EmailAuthCredential { - return new EmailAuthCredential( - email, - password, - externs.SignInMethod.EMAIL_PASSWORD - ); - } - - /** @internal */ - static _fromEmailAndCode( - email: string, - oobCode: string, - tenantId: string | null = null - ): EmailAuthCredential { - return new EmailAuthCredential( - email, - oobCode, - externs.SignInMethod.EMAIL_LINK, - tenantId - ); - } - - /** {@inheritdoc @firebase/auth-types#AuthCredential.toJSON} */ - toJSON(): object { - return { - email: this.email, - password: this.password, - signInMethod: this.signInMethod, - tenantId: this.tenantId - }; - } - - /** {@inheritdoc @firebase/auth-types#AuthCredential.fromJSON} */ - static fromJSON(json: object | string): EmailAuthCredential | null { - const obj = typeof json === 'string' ? JSON.parse(json) : json; - if (obj?.email && obj?.password) { - if (obj.signInMethod === externs.SignInMethod.EMAIL_PASSWORD) { - return this._fromEmailAndPassword(obj.email, obj.password); - } else if (obj.signInMethod === externs.SignInMethod.EMAIL_LINK) { - return this._fromEmailAndCode(obj.email, obj.password, obj.tenantId); - } - } - return null; - } - - /** @internal */ - async _getIdTokenResponse(auth: Auth): Promise { - switch (this.signInMethod) { - case externs.SignInMethod.EMAIL_PASSWORD: - return signInWithPassword(auth, { - returnSecureToken: true, - email: this.email, - password: this.password - }); - case externs.SignInMethod.EMAIL_LINK: - return signInWithEmailLink(auth, { - email: this.email, - oobCode: this.password - }); - default: - _fail(auth, AuthErrorCode.INTERNAL_ERROR); - } - } - - /** @internal */ - async _linkToIdToken(auth: Auth, idToken: string): Promise { - switch (this.signInMethod) { - case externs.SignInMethod.EMAIL_PASSWORD: - return updateEmailPassword(auth, { - idToken, - returnSecureToken: true, - email: this.email, - password: this.password - }); - case externs.SignInMethod.EMAIL_LINK: - return signInWithEmailLinkForLinking(auth, { - idToken, - email: this.email, - oobCode: this.password - }); - default: - _fail(auth, AuthErrorCode.INTERNAL_ERROR); - } - } - - /** @internal */ - _getReauthenticationResolver(auth: Auth): Promise { - return this._getIdTokenResponse(auth); - } -} diff --git a/packages-exp/auth-exp/src/core/credentials/index.ts b/packages-exp/auth-exp/src/core/credentials/index.ts deleted file mode 100644 index a3ec8ce895f..00000000000 --- a/packages-exp/auth-exp/src/core/credentials/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * This file is required due to the circular dependency from the parent class to its children - */ -export { AuthCredential } from './auth_credential'; -export { EmailAuthCredential } from './email'; -export { OAuthCredential } from './oauth'; -export { PhoneAuthCredential } from './phone'; diff --git a/packages-exp/auth-exp/src/core/credentials/oauth.test.ts b/packages-exp/auth-exp/src/core/credentials/oauth.test.ts deleted file mode 100644 index 93413685567..00000000000 --- a/packages-exp/auth-exp/src/core/credentials/oauth.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; - -import { ProviderId, SignInMethod } from '@firebase/auth-types-exp'; - -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { TEST_ID_TOKEN_RESPONSE } from '../../../test/helpers/id_token_response'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as fetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { SignInWithIdpRequest } from '../../api/authentication/idp'; -import { OAuthCredential, OAuthCredentialParams } from './oauth'; - -const BASE_PARAMS: OAuthCredentialParams = { - providerId: ProviderId.GOOGLE, - signInMethod: SignInMethod.GOOGLE -}; - -describe('core/credentials/oauth', () => { - let auth: TestAuth; - let signInWithIdp: fetch.Route; - - beforeEach(async () => { - auth = await testAuth(); - fetch.setUp(); - - signInWithIdp = mockEndpoint(Endpoint.SIGN_IN_WITH_IDP, { - ...TEST_ID_TOKEN_RESPONSE - }); - }); - - afterEach(() => { - fetch.tearDown(); - }); - - context('_fromParams', () => { - it('sets the idToken and accessToken', () => { - const cred = OAuthCredential._fromParams({ - ...BASE_PARAMS, - idToken: 'id-token', - accessToken: 'access-token' - }); - - expect(cred.idToken).to.eq('id-token'); - expect(cred.accessToken).to.eq('access-token'); - }); - - it('sets the nonce only if pendingToken is missing', () => { - const cred = OAuthCredential._fromParams({ - ...BASE_PARAMS, - idToken: 'id-token', - accessToken: 'access-token', - nonce: 'nonce' - }); - - expect(cred.nonce).to.eq('nonce'); - }); - - it('ignores the nonce if pendingToken set', () => { - const cred = OAuthCredential._fromParams({ - ...BASE_PARAMS, - nonce: 'nonce', - idToken: 'id-token', - accessToken: 'access-token', - pendingToken: 'pending-token' - }); - - expect(cred.nonce).to.be.undefined; - }); - - it('handles oauth1 and oauth with token secret', () => { - const cred = OAuthCredential._fromParams({ - ...BASE_PARAMS, - oauthToken: 'oauth-token', - oauthTokenSecret: 'oauth-token-secret' - }); - - expect(cred.accessToken).to.eq('oauth-token'); - expect(cred.secret).to.eq('oauth-token-secret'); - }); - }); - - context('#toJSON', () => { - it('packs up everything', () => { - const cred = OAuthCredential._fromParams({ - ...BASE_PARAMS, - idToken: 'id-token', - accessToken: 'access-token', - pendingToken: 'pending-token' - }); - - expect(cred.toJSON()).to.eql({ - ...BASE_PARAMS, - idToken: 'id-token', - accessToken: 'access-token', - pendingToken: 'pending-token', - secret: undefined, - nonce: undefined - }); - }); - }); - - context('fromJSON', () => { - it('builds the new object correctly', () => { - const cred = OAuthCredential.fromJSON({ - ...BASE_PARAMS, - idToken: 'id-token', - accessToken: 'access-token', - pendingToken: 'pending-token' - }); - - expect(cred).to.be.instanceOf(OAuthCredential); - expect(cred!.idToken).to.eq('id-token'); - expect(cred!.accessToken).to.eq('access-token'); - expect(cred!.providerId).to.eq(BASE_PARAMS.providerId); - expect(cred!.signInMethod).to.eq(BASE_PARAMS.signInMethod); - }); - }); - - context('#makeRequest', () => { - it('sets all the fields in a querystring if using nonce', async () => { - await OAuthCredential._fromParams({ - ...BASE_PARAMS, - idToken: 'id-token', - accessToken: 'access-token', - nonce: 'nonce' - })._getIdTokenResponse(auth); - - const { postBody, ...rest } = signInWithIdp.calls[0] - .request as SignInWithIdpRequest; - expect(rest.requestUri).to.eq('http://localhost'); - expect(rest.returnSecureToken).to.be.true; - expect(postBody).to.contain('id_token=id-token'); - expect(postBody).to.contain('access_token=access-token'); - expect(postBody).to.contain('nonce=nonce'); - expect(postBody).to.contain('providerId=google.com'); - }); - - it('if pendingToken is present, post body is not set', async () => { - await OAuthCredential._fromParams({ - ...BASE_PARAMS, - idToken: 'id-token', - accessToken: 'access-token', - nonce: 'nonce', - pendingToken: 'pending-token' - })._getIdTokenResponse(auth); - - const request = signInWithIdp.calls[0].request as SignInWithIdpRequest; - expect(request.requestUri).to.eq('http://localhost'); - expect(request.returnSecureToken).to.be.true; - expect(request.pendingToken).to.eq('pending-token'); - expect(request.postBody).to.be.null; - }); - }); - - context('internal methods', () => { - let cred: OAuthCredential; - - beforeEach(() => { - cred = OAuthCredential._fromParams({ - ...BASE_PARAMS, - idToken: 'id-token', - accessToken: 'access-token' - }); - }); - - it('_getIdTokenResponse calls through correctly', async () => { - await cred._getIdTokenResponse(auth); - - const request = signInWithIdp.calls[0].request as SignInWithIdpRequest; - expect(typeof request.postBody).to.eq('string'); - }); - - it('_linkToIdToken sets the idToken field on the request', async () => { - await cred._linkToIdToken(auth, 'new-id-token'); - const request = signInWithIdp.calls[0].request as SignInWithIdpRequest; - expect(typeof request.postBody).to.eq('string'); - expect(request.idToken).to.eq('new-id-token'); - }); - - it('_getReauthenticationResolver sets autoCreate to false', async () => { - await cred._getReauthenticationResolver(auth); - const request = signInWithIdp.calls[0].request as SignInWithIdpRequest; - expect(typeof request.postBody).to.eq('string'); - expect(request.autoCreate).to.be.false; - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/credentials/oauth.ts b/packages-exp/auth-exp/src/core/credentials/oauth.ts deleted file mode 100644 index 54cb110ea3e..00000000000 --- a/packages-exp/auth-exp/src/core/credentials/oauth.ts +++ /dev/null @@ -1,183 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { querystring } from '@firebase/util'; - -import { - signInWithIdp, - SignInWithIdpRequest -} from '../../api/authentication/idp'; -import { Auth } from '../../model/auth'; -import { IdTokenResponse } from '../../model/id_token'; -import { AuthErrorCode } from '../errors'; -import { _fail } from '../util/assert'; -import { AuthCredential } from './auth_credential'; - -const IDP_REQUEST_URI = 'http://localhost'; - -/** - * @internal - */ -export interface OAuthCredentialParams { - // OAuth 2 uses either id token or access token - idToken?: string | null; - accessToken?: string | null; - - // These fields are used with OAuth 1 - oauthToken?: string; - secret?: string; - oauthTokenSecret?: string; - - // Nonce is only set if pendingToken is not present - nonce?: string; - pendingToken?: string; - - // Utilities - providerId: string; - signInMethod: string; -} - -/** - * {@inheritdoc @firebase/auth-types#OAuthCredential} - * - * @public - */ -export class OAuthCredential - extends AuthCredential - implements externs.OAuthCredential { - /** {@inheritdoc @firebase/auth-types#OAuthCredential.idToken} @readonly */ - idToken?: string; - /** {@inheritdoc @firebase/auth-types#OAuthCredential.accessToken} @readonly */ - accessToken?: string; - /** {@inheritdoc @firebase/auth-types#OAuthCredential.secret} @readonly */ - secret?: string; - /** @internal */ - nonce?: string; - private pendingToken: string | null = null; - - /** @internal */ - static _fromParams(params: OAuthCredentialParams): OAuthCredential { - const cred = new OAuthCredential(params.providerId, params.signInMethod); - - if (params.idToken || params.accessToken) { - // OAuth 2 and either ID token or access token. - if (params.idToken) { - cred.idToken = params.idToken; - } - - if (params.accessToken) { - cred.accessToken = params.accessToken; - } - - // Add nonce if available and no pendingToken is present. - if (params.nonce && !params.pendingToken) { - cred.nonce = params.nonce; - } - - if (params.pendingToken) { - cred.pendingToken = params.pendingToken; - } - } else if (params.oauthToken && params.oauthTokenSecret) { - // OAuth 1 and OAuth token with token secret - cred.accessToken = params.oauthToken; - cred.secret = params.oauthTokenSecret; - } else { - _fail(AuthErrorCode.ARGUMENT_ERROR); - } - - return cred; - } - - /** {@inheritdoc @firebase/auth-types#OAuthCredential.toJSON} */ - toJSON(): object { - return { - idToken: this.idToken, - accessToken: this.accessToken, - secret: this.secret, - nonce: this.nonce, - pendingToken: this.pendingToken, - providerId: this.providerId, - signInMethod: this.signInMethod - }; - } - - /** {@inheritdoc @firebase/auth-types#OAuthCredential.fromJSON} */ - static fromJSON(json: string | object): OAuthCredential | null { - const obj = typeof json === 'string' ? JSON.parse(json) : json; - const { providerId, signInMethod, ...rest }: Partial = obj; - if (!providerId || !signInMethod) { - return null; - } - - const cred = new OAuthCredential(providerId, signInMethod); - Object.assign(cred, rest); - return cred; - } - - /** @internal */ - _getIdTokenResponse(auth: Auth): Promise { - const request = this.buildRequest(); - return signInWithIdp(auth, request); - } - - /** @internal */ - _linkToIdToken(auth: Auth, idToken: string): Promise { - const request = this.buildRequest(); - request.idToken = idToken; - return signInWithIdp(auth, request); - } - - /** @internal */ - _getReauthenticationResolver(auth: Auth): Promise { - const request = this.buildRequest(); - request.autoCreate = false; - return signInWithIdp(auth, request); - } - - private buildRequest(): SignInWithIdpRequest { - const request: SignInWithIdpRequest = { - requestUri: IDP_REQUEST_URI, - returnSecureToken: true, - postBody: null - }; - - if (this.pendingToken) { - request.pendingToken = this.pendingToken; - } else { - const postBody: Record = {}; - if (this.idToken) { - postBody['id_token'] = this.idToken; - } - if (this.accessToken) { - postBody['access_token'] = this.accessToken; - } - if (this.secret) { - postBody['oauth_token_secret'] = this.secret; - } - - postBody['providerId'] = this.providerId; - if (this.nonce && !this.pendingToken) { - postBody['nonce'] = this.nonce; - } - - request.postBody = querystring(postBody); - } - - return request; - } -} diff --git a/packages-exp/auth-exp/src/core/credentials/phone.ts b/packages-exp/auth-exp/src/core/credentials/phone.ts deleted file mode 100644 index cb8ef8c1c5f..00000000000 --- a/packages-exp/auth-exp/src/core/credentials/phone.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { PhoneOrOauthTokenResponse } from '../../api/authentication/mfa'; -import { - linkWithPhoneNumber, - signInWithPhoneNumber, - SignInWithPhoneNumberRequest, - verifyPhoneNumberForExisting -} from '../../api/authentication/sms'; -import { Auth } from '../../model/auth'; -import { IdTokenResponse } from '../../model/id_token'; -import { AuthCredential } from './auth_credential'; - -/** @internal */ -export interface PhoneAuthCredentialParameters { - verificationId?: string; - verificationCode?: string; - phoneNumber?: string; - temporaryProof?: string; -} - -/** - * {@inheritdoc @firebase/auth-types#PhoneAuthCredential} - * - * @public - */ -export class PhoneAuthCredential - extends AuthCredential - implements externs.PhoneAuthCredential { - private constructor(private readonly params: PhoneAuthCredentialParameters) { - super(externs.ProviderId.PHONE, externs.SignInMethod.PHONE); - } - - /** @internal */ - static _fromVerification( - verificationId: string, - verificationCode: string - ): PhoneAuthCredential { - return new PhoneAuthCredential({ verificationId, verificationCode }); - } - - /** @internal */ - static _fromTokenResponse( - phoneNumber: string, - temporaryProof: string - ): PhoneAuthCredential { - return new PhoneAuthCredential({ phoneNumber, temporaryProof }); - } - - /** @internal */ - _getIdTokenResponse(auth: Auth): Promise { - return signInWithPhoneNumber(auth, this._makeVerificationRequest()); - } - - /** @internal */ - _linkToIdToken(auth: Auth, idToken: string): Promise { - return linkWithPhoneNumber(auth, { - idToken, - ...this._makeVerificationRequest() - }); - } - - /** @internal */ - _getReauthenticationResolver(auth: Auth): Promise { - return verifyPhoneNumberForExisting(auth, this._makeVerificationRequest()); - } - - /** @internal */ - _makeVerificationRequest(): SignInWithPhoneNumberRequest { - const { - temporaryProof, - phoneNumber, - verificationId, - verificationCode - } = this.params; - if (temporaryProof && phoneNumber) { - return { temporaryProof, phoneNumber }; - } - - return { - sessionInfo: verificationId, - code: verificationCode - }; - } - - /** {@inheritdoc @firebase/auth-types#toJSON} */ - toJSON(): object { - const obj: Record = { - providerId: this.providerId - }; - if (this.params.phoneNumber) { - obj.phoneNumber = this.params.phoneNumber; - } - if (this.params.temporaryProof) { - obj.temporaryProof = this.params.temporaryProof; - } - if (this.params.verificationCode) { - obj.verificationCode = this.params.verificationCode; - } - if (this.params.verificationId) { - obj.verificationId = this.params.verificationId; - } - - return obj; - } - - /** {@inheritdoc @firebase/auth-types#fromJSON} */ - static fromJSON(json: object | string): PhoneAuthCredential | null { - if (typeof json === 'string') { - json = JSON.parse(json); - } - - const { - verificationId, - verificationCode, - phoneNumber, - temporaryProof - } = json as { [key: string]: string }; - if ( - !verificationCode && - !verificationId && - !phoneNumber && - !temporaryProof - ) { - return null; - } - - return new PhoneAuthCredential({ - verificationId, - verificationCode, - phoneNumber, - temporaryProof - }); - } -} diff --git a/packages-exp/auth-exp/src/core/errors.test.ts b/packages-exp/auth-exp/src/core/errors.test.ts deleted file mode 100644 index 3eb281526bf..00000000000 --- a/packages-exp/auth-exp/src/core/errors.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { - AuthErrorCode, - debugErrorMap, - prodErrorMap, - ErrorMapRetriever, - AuthErrorParams -} from './errors'; -import { AuthErrorMap } from '@firebase/auth-types-exp'; -import { ErrorFactory } from '@firebase/util'; - -function getErrorFactory( - errorMap: AuthErrorMap -): ErrorFactory { - const map = (errorMap as ErrorMapRetriever)(); - const factory = new ErrorFactory( - 'auth', - 'Firebase', - map - ); - return factory; -} - -describe('verboseErrorMap', () => { - it('should create an Auth namespaced FirebaseError with full message', () => { - const error = getErrorFactory(debugErrorMap).create( - AuthErrorCode.INTERNAL_ERROR, - {} - ); - expect(error.code).to.eq('auth/internal-error'); - expect(error.message).to.eq( - 'Firebase: An internal AuthError has occurred. (auth/internal-error).' - ); - expect(error.name).to.eq('FirebaseError'); - }); -}); - -describe('prodErrorMap', () => { - it('should create an Auth namespaced FirebaseError with full message', () => { - const error = getErrorFactory(prodErrorMap).create( - AuthErrorCode.INTERNAL_ERROR, - {} - ); - expect(error.code).to.eq('auth/internal-error'); - expect(error.message).to.eq('Firebase: Error (auth/internal-error).'); - expect(error.name).to.eq('FirebaseError'); - }); -}); diff --git a/packages-exp/auth-exp/src/core/errors.ts b/packages-exp/auth-exp/src/core/errors.ts deleted file mode 100644 index e8e24d7cab2..00000000000 --- a/packages-exp/auth-exp/src/core/errors.ts +++ /dev/null @@ -1,410 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// eslint-disable-next-line import/no-extraneous-dependencies -import * as externs from '@firebase/auth-types-exp'; -import { ErrorFactory, ErrorMap } from '@firebase/util'; - -import { IdTokenMfaResponse } from '../api/authentication/mfa'; -import { AppName } from '../model/auth'; - -/** - * Enumeration of Firebase Auth error codes. - * - * @public - */ -export const enum AuthErrorCode { - ADMIN_ONLY_OPERATION = 'admin-restricted-operation', - ARGUMENT_ERROR = 'argument-error', - APP_NOT_AUTHORIZED = 'app-not-authorized', - APP_NOT_INSTALLED = 'app-not-installed', - CAPTCHA_CHECK_FAILED = 'captcha-check-failed', - CODE_EXPIRED = 'code-expired', - CORDOVA_NOT_READY = 'cordova-not-ready', - CORS_UNSUPPORTED = 'cors-unsupported', - CREDENTIAL_ALREADY_IN_USE = 'credential-already-in-use', - CREDENTIAL_MISMATCH = 'custom-token-mismatch', - CREDENTIAL_TOO_OLD_LOGIN_AGAIN = 'requires-recent-login', - DYNAMIC_LINK_NOT_ACTIVATED = 'dynamic-link-not-activated', - EMAIL_CHANGE_NEEDS_VERIFICATION = 'email-change-needs-verification', - EMAIL_EXISTS = 'email-already-in-use', - EMULATOR_CONFIG_FAILED = 'emulator-config-failed', - EXPIRED_OOB_CODE = 'expired-action-code', - EXPIRED_POPUP_REQUEST = 'cancelled-popup-request', - INTERNAL_ERROR = 'internal-error', - INVALID_API_KEY = 'invalid-api-key', - INVALID_APP_CREDENTIAL = 'invalid-app-credential', - INVALID_APP_ID = 'invalid-app-id', - INVALID_AUTH = 'invalid-user-token', - INVALID_AUTH_EVENT = 'invalid-auth-event', - INVALID_CERT_HASH = 'invalid-cert-hash', - INVALID_CODE = 'invalid-verification-code', - INVALID_CONTINUE_URI = 'invalid-continue-uri', - INVALID_CORDOVA_CONFIGURATION = 'invalid-cordova-configuration', - INVALID_CUSTOM_TOKEN = 'invalid-custom-token', - INVALID_DYNAMIC_LINK_DOMAIN = 'invalid-dynamic-link-domain', - INVALID_EMAIL = 'invalid-email', - INVALID_EMULATOR_SCHEME = 'invalid-emulator-scheme', - INVALID_IDP_RESPONSE = 'invalid-credential', - INVALID_MESSAGE_PAYLOAD = 'invalid-message-payload', - INVALID_MFA_SESSION = 'invalid-multi-factor-session', - INVALID_OAUTH_CLIENT_ID = 'invalid-oauth-client-id', - INVALID_OAUTH_PROVIDER = 'invalid-oauth-provider', - INVALID_OOB_CODE = 'invalid-action-code', - INVALID_ORIGIN = 'unauthorized-domain', - INVALID_PASSWORD = 'wrong-password', - INVALID_PERSISTENCE = 'invalid-persistence-type', - INVALID_PHONE_NUMBER = 'invalid-phone-number', - INVALID_PROVIDER_ID = 'invalid-provider-id', - INVALID_RECIPIENT_EMAIL = 'invalid-recipient-email', - INVALID_SENDER = 'invalid-sender', - INVALID_SESSION_INFO = 'invalid-verification-id', - INVALID_TENANT_ID = 'invalid-tenant-id', - MFA_INFO_NOT_FOUND = 'multi-factor-info-not-found', - MFA_REQUIRED = 'multi-factor-auth-required', - MISSING_ANDROID_PACKAGE_NAME = 'missing-android-pkg-name', - MISSING_APP_CREDENTIAL = 'missing-app-credential', - MISSING_AUTH_DOMAIN = 'auth-domain-config-required', - MISSING_CODE = 'missing-verification-code', - MISSING_CONTINUE_URI = 'missing-continue-uri', - MISSING_IFRAME_START = 'missing-iframe-start', - MISSING_IOS_BUNDLE_ID = 'missing-ios-bundle-id', - MISSING_OR_INVALID_NONCE = 'missing-or-invalid-nonce', - MISSING_MFA_INFO = 'missing-multi-factor-info', - MISSING_MFA_SESSION = 'missing-multi-factor-session', - MISSING_PHONE_NUMBER = 'missing-phone-number', - MISSING_SESSION_INFO = 'missing-verification-id', - MODULE_DESTROYED = 'app-deleted', - NEED_CONFIRMATION = 'account-exists-with-different-credential', - NETWORK_REQUEST_FAILED = 'network-request-failed', - NULL_USER = 'null-user', - NO_AUTH_EVENT = 'no-auth-event', - NO_SUCH_PROVIDER = 'no-such-provider', - OPERATION_NOT_ALLOWED = 'operation-not-allowed', - OPERATION_NOT_SUPPORTED = 'operation-not-supported-in-this-environment', - POPUP_BLOCKED = 'popup-blocked', - POPUP_CLOSED_BY_USER = 'popup-closed-by-user', - PROVIDER_ALREADY_LINKED = 'provider-already-linked', - QUOTA_EXCEEDED = 'quota-exceeded', - REDIRECT_CANCELLED_BY_USER = 'redirect-cancelled-by-user', - REDIRECT_OPERATION_PENDING = 'redirect-operation-pending', - REJECTED_CREDENTIAL = 'rejected-credential', - SECOND_FACTOR_ALREADY_ENROLLED = 'second-factor-already-in-use', - SECOND_FACTOR_LIMIT_EXCEEDED = 'maximum-second-factor-count-exceeded', - TENANT_ID_MISMATCH = 'tenant-id-mismatch', - TIMEOUT = 'timeout', - TOKEN_EXPIRED = 'user-token-expired', - TOO_MANY_ATTEMPTS_TRY_LATER = 'too-many-requests', - UNAUTHORIZED_DOMAIN = 'unauthorized-continue-uri', - UNSUPPORTED_FIRST_FACTOR = 'unsupported-first-factor', - UNSUPPORTED_PERSISTENCE = 'unsupported-persistence-type', - UNSUPPORTED_TENANT_OPERATION = 'unsupported-tenant-operation', - UNVERIFIED_EMAIL = 'unverified-email', - USER_CANCELLED = 'user-cancelled', - USER_DELETED = 'user-not-found', - USER_DISABLED = 'user-disabled', - USER_MISMATCH = 'user-mismatch', - USER_SIGNED_OUT = 'user-signed-out', - WEAK_PASSWORD = 'weak-password', - WEB_STORAGE_UNSUPPORTED = 'web-storage-unsupported' -} - -function _debugErrorMap(): ErrorMap { - return { - [AuthErrorCode.ADMIN_ONLY_OPERATION]: - 'This operation is restricted to administrators only.', - [AuthErrorCode.ARGUMENT_ERROR]: '', - [AuthErrorCode.APP_NOT_AUTHORIZED]: - "This app, identified by the domain where it's hosted, is not " + - 'authorized to use Firebase Authentication with the provided API key. ' + - 'Review your key configuration in the Google API console.', - [AuthErrorCode.APP_NOT_INSTALLED]: - 'The requested mobile application corresponding to the identifier (' + - 'Android package name or iOS bundle ID) provided is not installed on ' + - 'this device.', - [AuthErrorCode.CAPTCHA_CHECK_FAILED]: - 'The reCAPTCHA response token provided is either invalid, expired, ' + - 'already used or the domain associated with it does not match the list ' + - 'of whitelisted domains.', - [AuthErrorCode.CODE_EXPIRED]: - 'The SMS code has expired. Please re-send the verification code to try ' + - 'again.', - [AuthErrorCode.CORDOVA_NOT_READY]: 'Cordova framework is not ready.', - [AuthErrorCode.CORS_UNSUPPORTED]: 'This browser is not supported.', - [AuthErrorCode.CREDENTIAL_ALREADY_IN_USE]: - 'This credential is already associated with a different user account.', - [AuthErrorCode.CREDENTIAL_MISMATCH]: - 'The custom token corresponds to a different audience.', - [AuthErrorCode.CREDENTIAL_TOO_OLD_LOGIN_AGAIN]: - 'This operation is sensitive and requires recent authentication. Log in ' + - 'again before retrying this request.', - [AuthErrorCode.DYNAMIC_LINK_NOT_ACTIVATED]: - 'Please activate Dynamic Links in the Firebase Console and agree to the terms and ' + - 'conditions.', - [AuthErrorCode.EMAIL_CHANGE_NEEDS_VERIFICATION]: - 'Multi-factor users must always have a verified email.', - [AuthErrorCode.EMAIL_EXISTS]: - 'The email address is already in use by another account.', - [AuthErrorCode.EMULATOR_CONFIG_FAILED]: - 'Auth instance has already been used to make a network call. Auth can ' + - 'no longer be configured to use the emulator. Try calling ' + - '"useEmulator()" sooner.', - [AuthErrorCode.EXPIRED_OOB_CODE]: 'The action code has expired.', - [AuthErrorCode.EXPIRED_POPUP_REQUEST]: - 'This operation has been cancelled due to another conflicting popup being opened.', - [AuthErrorCode.INTERNAL_ERROR]: 'An internal AuthError has occurred.', - [AuthErrorCode.INVALID_APP_CREDENTIAL]: - 'The phone verification request contains an invalid application verifier.' + - ' The reCAPTCHA token response is either invalid or expired.', - [AuthErrorCode.INVALID_APP_ID]: - 'The mobile app identifier is not registed for the current project.', - [AuthErrorCode.INVALID_AUTH]: - "This user's credential isn't valid for this project. This can happen " + - "if the user's token has been tampered with, or if the user isn't for " + - 'the project associated with this API key.', - [AuthErrorCode.INVALID_AUTH_EVENT]: 'An internal AuthError has occurred.', - [AuthErrorCode.INVALID_CODE]: - 'The SMS verification code used to create the phone auth credential is ' + - 'invalid. Please resend the verification code sms and be sure use the ' + - 'verification code provided by the user.', - [AuthErrorCode.INVALID_CONTINUE_URI]: - 'The continue URL provided in the request is invalid.', - [AuthErrorCode.INVALID_CORDOVA_CONFIGURATION]: - 'The following Cordova plugins must be installed to enable OAuth sign-in: ' + - 'cordova-plugin-buildinfo, cordova-universal-links-plugin, ' + - 'cordova-plugin-browsertab, cordova-plugin-inappbrowser and ' + - 'cordova-plugin-customurlscheme.', - [AuthErrorCode.INVALID_CUSTOM_TOKEN]: - 'The custom token format is incorrect. Please check the documentation.', - [AuthErrorCode.INVALID_DYNAMIC_LINK_DOMAIN]: - 'The provided dynamic link domain is not configured or authorized for the current project.', - [AuthErrorCode.INVALID_EMAIL]: 'The email address is badly formatted.', - [AuthErrorCode.INVALID_EMULATOR_SCHEME]: - 'Emulator URL must start with a valid scheme (http:// or https://).', - [AuthErrorCode.INVALID_API_KEY]: - 'Your API key is invalid, please check you have copied it correctly.', - [AuthErrorCode.INVALID_CERT_HASH]: - 'The SHA-1 certificate hash provided is invalid.', - [AuthErrorCode.INVALID_IDP_RESPONSE]: - 'The supplied auth credential is malformed or has expired.', - [AuthErrorCode.INVALID_MESSAGE_PAYLOAD]: - 'The email template corresponding to this action contains invalid characters in its message. ' + - 'Please fix by going to the Auth email templates section in the Firebase Console.', - [AuthErrorCode.INVALID_MFA_SESSION]: - 'The request does not contain a valid proof of first factor successful sign-in.', - [AuthErrorCode.INVALID_OAUTH_PROVIDER]: - 'EmailAuthProvider is not supported for this operation. This operation ' + - 'only supports OAuth providers.', - [AuthErrorCode.INVALID_OAUTH_CLIENT_ID]: - 'The OAuth client ID provided is either invalid or does not match the ' + - 'specified API key.', - [AuthErrorCode.INVALID_ORIGIN]: - 'This domain is not authorized for OAuth operations for your Firebase ' + - 'project. Edit the list of authorized domains from the Firebase console.', - [AuthErrorCode.INVALID_OOB_CODE]: - 'The action code is invalid. This can happen if the code is malformed, ' + - 'expired, or has already been used.', - [AuthErrorCode.INVALID_PASSWORD]: - 'The password is invalid or the user does not have a password.', - [AuthErrorCode.INVALID_PERSISTENCE]: - 'The specified persistence type is invalid. It can only be local, session or none.', - [AuthErrorCode.INVALID_PHONE_NUMBER]: - 'The format of the phone number provided is incorrect. Please enter the ' + - 'phone number in a format that can be parsed into E.164 format. E.164 ' + - 'phone numbers are written in the format [+][country code][subscriber ' + - 'number including area code].', - [AuthErrorCode.INVALID_PROVIDER_ID]: - 'The specified provider ID is invalid.', - [AuthErrorCode.INVALID_RECIPIENT_EMAIL]: - 'The email corresponding to this action failed to send as the provided ' + - 'recipient email address is invalid.', - [AuthErrorCode.INVALID_SENDER]: - 'The email template corresponding to this action contains an invalid sender email or name. ' + - 'Please fix by going to the Auth email templates section in the Firebase Console.', - [AuthErrorCode.INVALID_SESSION_INFO]: - 'The verification ID used to create the phone auth credential is invalid.', - [AuthErrorCode.INVALID_TENANT_ID]: - "The Auth instance's tenant ID is invalid.", - [AuthErrorCode.MISSING_ANDROID_PACKAGE_NAME]: - 'An Android Package Name must be provided if the Android App is required to be installed.', - [AuthErrorCode.MISSING_AUTH_DOMAIN]: - 'Be sure to include authDomain when calling firebase.initializeApp(), ' + - 'by following the instructions in the Firebase console.', - [AuthErrorCode.MISSING_APP_CREDENTIAL]: - 'The phone verification request is missing an application verifier ' + - 'assertion. A reCAPTCHA response token needs to be provided.', - [AuthErrorCode.MISSING_CODE]: - 'The phone auth credential was created with an empty SMS verification code.', - [AuthErrorCode.MISSING_CONTINUE_URI]: - 'A continue URL must be provided in the request.', - [AuthErrorCode.MISSING_IFRAME_START]: 'An internal AuthError has occurred.', - [AuthErrorCode.MISSING_IOS_BUNDLE_ID]: - 'An iOS Bundle ID must be provided if an App Store ID is provided.', - [AuthErrorCode.MISSING_OR_INVALID_NONCE]: - 'The request does not contain a valid nonce. This can occur if the ' + - 'SHA-256 hash of the provided raw nonce does not match the hashed nonce ' + - 'in the ID token payload.', - [AuthErrorCode.MISSING_MFA_INFO]: - 'No second factor identifier is provided.', - [AuthErrorCode.MISSING_MFA_SESSION]: - 'The request is missing proof of first factor successful sign-in.', - [AuthErrorCode.MISSING_PHONE_NUMBER]: - 'To send verification codes, provide a phone number for the recipient.', - [AuthErrorCode.MISSING_SESSION_INFO]: - 'The phone auth credential was created with an empty verification ID.', - [AuthErrorCode.MODULE_DESTROYED]: - 'This instance of FirebaseApp has been deleted.', - [AuthErrorCode.MFA_INFO_NOT_FOUND]: - 'The user does not have a second factor matching the identifier provided.', - [AuthErrorCode.MFA_REQUIRED]: - 'Proof of ownership of a second factor is required to complete sign-in.', - [AuthErrorCode.NEED_CONFIRMATION]: - 'An account already exists with the same email address but different ' + - 'sign-in credentials. Sign in using a provider associated with this ' + - 'email address.', - [AuthErrorCode.NETWORK_REQUEST_FAILED]: - 'A network AuthError (such as timeout, interrupted connection or unreachable host) has occurred.', - [AuthErrorCode.NO_AUTH_EVENT]: 'An internal AuthError has occurred.', - [AuthErrorCode.NO_SUCH_PROVIDER]: - 'User was not linked to an account with the given provider.', - [AuthErrorCode.NULL_USER]: - 'A null user object was provided as the argument for an operation which ' + - 'requires a non-null user object.', - [AuthErrorCode.OPERATION_NOT_ALLOWED]: - 'The given sign-in provider is disabled for this Firebase project. ' + - 'Enable it in the Firebase console, under the sign-in method tab of the ' + - 'Auth section.', - [AuthErrorCode.OPERATION_NOT_SUPPORTED]: - 'This operation is not supported in the environment this application is ' + - 'running on. "location.protocol" must be http, https or chrome-extension' + - ' and web storage must be enabled.', - [AuthErrorCode.POPUP_BLOCKED]: - 'Unable to establish a connection with the popup. It may have been blocked by the browser.', - [AuthErrorCode.POPUP_CLOSED_BY_USER]: - 'The popup has been closed by the user before finalizing the operation.', - [AuthErrorCode.PROVIDER_ALREADY_LINKED]: - 'User can only be linked to one identity for the given provider.', - [AuthErrorCode.QUOTA_EXCEEDED]: - "The project's quota for this operation has been exceeded.", - [AuthErrorCode.REDIRECT_CANCELLED_BY_USER]: - 'The redirect operation has been cancelled by the user before finalizing.', - [AuthErrorCode.REDIRECT_OPERATION_PENDING]: - 'A redirect sign-in operation is already pending.', - [AuthErrorCode.REJECTED_CREDENTIAL]: - 'The request contains malformed or mismatching credentials.', - [AuthErrorCode.SECOND_FACTOR_ALREADY_ENROLLED]: - 'The second factor is already enrolled on this account.', - [AuthErrorCode.SECOND_FACTOR_LIMIT_EXCEEDED]: - 'The maximum allowed number of second factors on a user has been exceeded.', - [AuthErrorCode.TENANT_ID_MISMATCH]: - "The provided tenant ID does not match the Auth instance's tenant ID", - [AuthErrorCode.TIMEOUT]: 'The operation has timed out.', - [AuthErrorCode.TOKEN_EXPIRED]: - "The user's credential is no longer valid. The user must sign in again.", - [AuthErrorCode.TOO_MANY_ATTEMPTS_TRY_LATER]: - 'We have blocked all requests from this device due to unusual activity. ' + - 'Try again later.', - [AuthErrorCode.UNAUTHORIZED_DOMAIN]: - 'The domain of the continue URL is not whitelisted. Please whitelist ' + - 'the domain in the Firebase console.', - [AuthErrorCode.UNSUPPORTED_FIRST_FACTOR]: - 'Enrolling a second factor or signing in with a multi-factor account requires sign-in with a supported first factor.', - [AuthErrorCode.UNSUPPORTED_PERSISTENCE]: - 'The current environment does not support the specified persistence type.', - [AuthErrorCode.UNSUPPORTED_TENANT_OPERATION]: - 'This operation is not supported in a multi-tenant context.', - [AuthErrorCode.UNVERIFIED_EMAIL]: - 'The operation requires a verified email.', - [AuthErrorCode.USER_CANCELLED]: - 'The user did not grant your application the permissions it requested.', - [AuthErrorCode.USER_DELETED]: - 'There is no user record corresponding to this identifier. The user may ' + - 'have been deleted.', - [AuthErrorCode.USER_DISABLED]: - 'The user account has been disabled by an administrator.', - [AuthErrorCode.USER_MISMATCH]: - 'The supplied credentials do not correspond to the previously signed in user.', - [AuthErrorCode.USER_SIGNED_OUT]: '', - [AuthErrorCode.WEAK_PASSWORD]: - 'The password must be 6 characters long or more.', - [AuthErrorCode.WEB_STORAGE_UNSUPPORTED]: - 'This browser is not supported or 3rd party cookies and data may be disabled.' - }; -} - -export interface ErrorMapRetriever extends externs.AuthErrorMap { - (): ErrorMap; -} - -function _prodErrorMap(): ErrorMap { - return {} as ErrorMap; -} - -/** - * A verbose error map with detailed descriptions for most error codes. - * - * See discussion at {@link @firebase/auth-types#AuthErrorMap} - * - * @public - */ -export const debugErrorMap: externs.AuthErrorMap = _debugErrorMap; - -/** - * A minimal error map with all verbose error messages stripped. - * - * See discussion at {@link @firebase/auth-types#AuthErrorMap} - * - * @public - */ -export const prodErrorMap: externs.AuthErrorMap = _prodErrorMap; - -export interface NamedErrorParams { - appName: AppName; - credential?: externs.AuthCredential; - email?: string; - phoneNumber?: string; - tenantId?: string; - user?: externs.User; - serverResponse?: object; -} - -type GenericAuthErrorParams = { - [key in Exclude< - AuthErrorCode, - | AuthErrorCode.ARGUMENT_ERROR - | AuthErrorCode.INTERNAL_ERROR - | AuthErrorCode.MFA_REQUIRED - >]: { - appName: AppName; - email?: string; - phoneNumber?: string; - }; -}; - -export interface AuthErrorParams extends GenericAuthErrorParams { - [AuthErrorCode.ARGUMENT_ERROR]: { appName?: AppName }; - [AuthErrorCode.INTERNAL_ERROR]: { appName?: AppName }; - [AuthErrorCode.MFA_REQUIRED]: { - appName: AppName; - serverResponse: IdTokenMfaResponse; - }; -} - -export const _DEFAULT_AUTH_ERROR_FACTORY = new ErrorFactory< - AuthErrorCode, - AuthErrorParams ->('auth', 'Firebase', _prodErrorMap()); diff --git a/packages-exp/auth-exp/src/core/index.ts b/packages-exp/auth-exp/src/core/index.ts deleted file mode 100644 index 7707fa6232c..00000000000 --- a/packages-exp/auth-exp/src/core/index.ts +++ /dev/null @@ -1,216 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { CompleteFn, ErrorFn, Unsubscribe } from '@firebase/util'; - -export { debugErrorMap, prodErrorMap } from './errors'; - -// Non-optional auth methods. -/** - * Changes the type of persistence on the Auth instance for the currently saved - * Auth session and applies this type of persistence for future sign-in requests, including - * sign-in with redirect requests. - * - * @remarks - * This makes it easy for a user signing in to specify whether their session should be - * remembered or not. It also makes it easier to never persist the Auth state for applications - * that are shared by other users or have sensitive data. - * - * @example - * ```javascript - * setPersistence(auth, browserSessionPersistence); - * ``` - * - * @param auth - The Auth instance. - * @param persistence - The {@link @firebase/auth-types#Persistence} to use. - * @returns A promise that resolves once the persistence change has completed - * - * @public - */ -export function setPersistence( - auth: externs.Auth, - persistence: externs.Persistence -): Promise { - return auth.setPersistence(persistence); -} -/** - * Adds an observer for changes to the signed-in user's ID token, which includes sign-in, - * sign-out, and token refresh events. - * - * @param auth - The Auth instance. - * @param nextOrObserver - callback triggered on change. - * @param error - callback triggered on error. - * @param completed - callback triggered when observer is removed. - * - * @public - */ -export function onIdTokenChanged( - auth: externs.Auth, - nextOrObserver: externs.NextOrObserver, - error?: ErrorFn, - completed?: CompleteFn -): Unsubscribe { - return auth.onIdTokenChanged(nextOrObserver, error, completed); -} -/** - * Adds an observer for changes to the user's sign-in state. - * - * @remarks - * To keep the old behavior, see {@link onIdTokenChanged}. - * - * @param auth - The Auth instance. - * @param nextOrObserver - callback triggered on change. - * @param error - callback triggered on error. - * @param completed - callback triggered when observer is removed. - * - * @public - */ -export function onAuthStateChanged( - auth: externs.Auth, - nextOrObserver: externs.NextOrObserver, - error?: ErrorFn, - completed?: CompleteFn -): Unsubscribe { - return auth.onAuthStateChanged(nextOrObserver, error, completed); -} -/** - * Sets the current language to the default device/browser preference. - * - * @param auth - The Auth instanec. - * - * @public - */ -export function useDeviceLanguage(auth: externs.Auth): void { - auth.useDeviceLanguage(); -} -/** - * Asynchronously sets the provided user as {@link @firebase/auth-types#Auth.currentUser} on the - * {@link @firebase/auth-types#Auth} instance. - * - * @remarks - * A new instance copy of the user provided will be made and set as currentUser. - * - * This will trigger {@link onAuthStateChanged} and {@link onIdTokenChanged} listeners - * like other sign in methods. - * - * The operation fails with an error if the user to be updated belongs to a different Firebase - * project. - * - * @param auth - The Auth instance. - * @param user - The new {@link @firebase/auth-types#User}. - * - * @public - */ -export function updateCurrentUser( - auth: externs.Auth, - user: externs.User | null -): Promise { - return auth.updateCurrentUser(user); -} -/** - * Signs out the current user. - * - * @param auth - The Auth instance. - * - * @public - */ -export function signOut(auth: externs.Auth): Promise { - return auth.signOut(); -} - -export { initializeAuth } from './auth/initialize'; - -// credentials -export { AuthCredential } from './credentials'; -export { EmailAuthCredential } from './credentials/email'; -export { OAuthCredential } from './credentials/oauth'; -export { PhoneAuthCredential } from './credentials/phone'; - -// persistence -export { inMemoryPersistence } from './persistence/in_memory'; - -// providers -export { EmailAuthProvider } from './providers/email'; -export { FacebookAuthProvider } from './providers/facebook'; -export { GoogleAuthProvider } from './providers/google'; -export { GithubAuthProvider } from './providers/github'; -export { - OAuthProvider, - CustomParameters, - OAuthCredentialOptions -} from './providers/oauth'; -export { TwitterAuthProvider } from './providers/twitter'; - -// strategies -export { signInAnonymously } from './strategies/anonymous'; -export { - signInWithCredential, - linkWithCredential, - reauthenticateWithCredential -} from './strategies/credential'; -export { signInWithCustomToken } from './strategies/custom_token'; -export { - sendPasswordResetEmail, - confirmPasswordReset, - applyActionCode, - checkActionCode, - verifyPasswordResetCode, - createUserWithEmailAndPassword, - signInWithEmailAndPassword -} from './strategies/email_and_password'; -export { - sendSignInLinkToEmail, - isSignInWithEmailLink, - signInWithEmailLink -} from './strategies/email_link'; -export { - fetchSignInMethodsForEmail, - sendEmailVerification, - verifyBeforeUpdateEmail -} from './strategies/email'; - -// core -export { ActionCodeURL, parseActionCodeURL } from './action_code_url'; - -// user -export { - updateProfile, - updateEmail, - updatePassword -} from './user/account_info'; -export { getIdToken, getIdTokenResult } from './user/id_token_result'; -export { unlink } from './user/link_unlink'; -export { getAdditionalUserInfo } from './user/additional_user_info'; - -// Non-optional user methods. -export { reload } from './user/reload'; -/** - * Deletes and signs out the user. - * - * @remarks - * Important: this is a security-sensitive operation that requires the user to have recently - * signed in. If this requirement isn't met, ask the user to authenticate again and then call - * {@link reauthenticateWithCredential}. - * - * @param user - The user. - * - * @public - */ -export async function deleteUser(user: externs.User): Promise { - return user.delete(); -} diff --git a/packages-exp/auth-exp/src/core/persistence/index.ts b/packages-exp/auth-exp/src/core/persistence/index.ts deleted file mode 100644 index 71f98470d3d..00000000000 --- a/packages-exp/auth-exp/src/core/persistence/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const enum PersistenceType { - SESSION = 'SESSION', - LOCAL = 'LOCAL', - NONE = 'NONE' -} - -export type PersistedBlob = Record; - -export interface Instantiator { - (blob: PersistedBlob): T; -} - -export type PersistenceValue = PersistedBlob | string; - -export const STORAGE_AVAILABLE_KEY = '__sak'; - -export interface StorageEventListener { - (value: PersistenceValue | null): void; -} - -export interface Persistence { - type: PersistenceType; - _isAvailable(): Promise; - _set(key: string, value: PersistenceValue): Promise; - _get(key: string): Promise; - _remove(key: string): Promise; - _addListener(key: string, listener: StorageEventListener): void; - _removeListener(key: string, listener: StorageEventListener): void; -} diff --git a/packages-exp/auth-exp/src/core/persistence/persistence_user_manager.test.ts b/packages-exp/auth-exp/src/core/persistence/persistence_user_manager.test.ts deleted file mode 100644 index 1cce6c683cf..00000000000 --- a/packages-exp/auth-exp/src/core/persistence/persistence_user_manager.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as chai from 'chai'; -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { testAuth, testUser, TestAuth } from '../../../test/helpers/mock_auth'; -import { UserImpl } from '../user/user_impl'; -import { _getInstance } from '../util/instantiator'; -import { Persistence, PersistenceType, StorageEventListener } from './'; -import { inMemoryPersistence } from './in_memory'; -import { KeyName, PersistenceUserManager } from './persistence_user_manager'; - -chai.use(sinonChai); - -function makePersistence( - type = PersistenceType.NONE -): { - persistence: Persistence; - stub: sinon.SinonStubbedInstance; -} { - const persistence: Persistence = { - type, - _isAvailable: () => Promise.resolve(true), - _set: async () => {}, - _get() { - return Promise.resolve(null); - }, - _remove: async () => {}, - _addListener(_key: string, _listener: StorageEventListener) {}, - _removeListener(_key: string, _listener: StorageEventListener) {} - }; - - const stub = sinon.stub(persistence); - return { persistence, stub }; -} - -describe('core/persistence/persistence_user_manager', () => { - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - }); - - describe('.create', () => { - it('defaults to inMemory if no list provided', async () => { - const manager = await PersistenceUserManager.create(auth, []); - expect(manager.persistence).to.eq(_getInstance(inMemoryPersistence)); - }); - - it('searches in order for a user', async () => { - const a = makePersistence(); - const b = makePersistence(); - const c = makePersistence(); - const search = [a.persistence, b.persistence, c.persistence]; - const auth = await testAuth(); - b.stub._get.returns(Promise.resolve(testUser(auth, 'uid').toJSON())); - - const out = await PersistenceUserManager.create(auth, search); - expect(out.persistence).to.eq(b.persistence); - expect(a.stub._get).to.have.been.calledOnce; - expect(b.stub._get).to.have.been.calledOnce; - expect(c.stub._get).not.to.have.been.called; - }); - - it('uses default user key if none provided', async () => { - const { stub, persistence } = makePersistence(); - await PersistenceUserManager.create(auth, [persistence]); - expect(stub._get).to.have.been.calledWith( - 'firebase:authUser:test-api-key:test-app' - ); - }); - - it('uses user key if provided', async () => { - const { stub, persistence } = makePersistence(); - await PersistenceUserManager.create( - auth, - [persistence], - KeyName.REDIRECT_USER - ); - expect(stub._get).to.have.been.calledWith( - 'firebase:redirectUser:test-api-key:test-app' - ); - }); - - it('returns zeroth persistence if all else fails', async () => { - const a = makePersistence(); - const b = makePersistence(); - const c = makePersistence(); - const search = [a.persistence, b.persistence, c.persistence]; - const out = await PersistenceUserManager.create(auth, search); - expect(out.persistence).to.eq(a.persistence); - expect(a.stub._get).to.have.been.calledOnce; - expect(b.stub._get).to.have.been.calledOnce; - expect(c.stub._get).to.have.been.called; - }); - }); - - describe('manager methods', () => { - let persistenceStub: sinon.SinonStubbedInstance; - let manager: PersistenceUserManager; - - beforeEach(async () => { - const { persistence, stub } = makePersistence(PersistenceType.SESSION); - persistenceStub = stub; - manager = await PersistenceUserManager.create(auth, [persistence]); - }); - - it('#setCurrentUser calls underlying persistence w/ key', async () => { - const user = testUser(auth, 'uid'); - await manager.setCurrentUser(user); - expect(persistenceStub._set).to.have.been.calledWith( - 'firebase:authUser:test-api-key:test-app', - user.toJSON() - ); - }); - - it('#removeCurrentUser calls underlying persistence', async () => { - await manager.removeCurrentUser(); - expect(persistenceStub._remove).to.have.been.calledWith( - 'firebase:authUser:test-api-key:test-app' - ); - }); - - it('#getCurrentUser calls with instantiator', async () => { - const rawObject = {}; - const userImplStub = sinon.stub(UserImpl, '_fromJSON'); - persistenceStub._get.returns(Promise.resolve(rawObject)); - - await manager.getCurrentUser(); - expect(userImplStub).to.have.been.calledWith(auth, rawObject); - - userImplStub.restore(); - }); - - it('#savePersistenceForRedirect calls through', async () => { - await manager.savePersistenceForRedirect(); - expect(persistenceStub._set).to.have.been.calledWith( - 'firebase:persistence:test-api-key:test-app', - 'SESSION' - ); - }); - - describe('#setPersistence', () => { - it('returns immediately if types match', async () => { - const { persistence: nextPersistence } = makePersistence( - PersistenceType.SESSION - ); - const spy = sinon.spy(manager, 'getCurrentUser'); - await manager.setPersistence(nextPersistence); - expect(spy).not.to.have.been.called; - spy.restore(); - }); - - it('removes current user & sets it in the new persistene', async () => { - const { - persistence: nextPersistence, - stub: nextStub - } = makePersistence(); - const auth = await testAuth(); - const user = testUser(auth, 'uid'); - persistenceStub._get.returns(Promise.resolve(user.toJSON())); - - await manager.setPersistence(nextPersistence); - expect(persistenceStub._get).to.have.been.called; - expect(persistenceStub._remove).to.have.been.called; - expect(nextStub._set).to.have.been.calledWith( - 'firebase:authUser:test-api-key:test-app', - user.toJSON() - ); - }); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/persistence/persistence_user_manager.ts b/packages-exp/auth-exp/src/core/persistence/persistence_user_manager.ts deleted file mode 100644 index 80bf2b91abc..00000000000 --- a/packages-exp/auth-exp/src/core/persistence/persistence_user_manager.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ApiKey, AppName, Auth } from '../../model/auth'; -import { User } from '../../model/user'; -import { PersistedBlob, Persistence } from '../persistence'; -import { UserImpl } from '../user/user_impl'; -import { _getInstance } from '../util/instantiator'; -import { inMemoryPersistence } from './in_memory'; - -export const enum KeyName { - AUTH_USER = 'authUser', - REDIRECT_USER = 'redirectUser', - PERSISTENCE_USER = 'persistence' -} -export const enum Namespace { - PERSISTENCE = 'firebase' -} - -export function _persistenceKeyName( - key: string, - apiKey: ApiKey, - appName: AppName -): string { - return `${Namespace.PERSISTENCE}:${key}:${apiKey}:${appName}`; -} - -export class PersistenceUserManager { - private readonly fullUserKey: string; - private readonly fullPersistenceKey: string; - private readonly boundEventHandler: () => void; - - private constructor( - public persistence: Persistence, - private readonly auth: Auth, - private readonly userKey: string - ) { - const { config, name } = this.auth; - this.fullUserKey = _persistenceKeyName(this.userKey, config.apiKey, name); - this.fullPersistenceKey = _persistenceKeyName( - KeyName.PERSISTENCE_USER, - config.apiKey, - name - ); - this.boundEventHandler = auth._onStorageEvent.bind(auth); - this.persistence._addListener(this.fullUserKey, this.boundEventHandler); - } - - setCurrentUser(user: User): Promise { - return this.persistence._set(this.fullUserKey, user.toJSON()); - } - - async getCurrentUser(): Promise { - const blob = await this.persistence._get(this.fullUserKey); - return blob ? UserImpl._fromJSON(this.auth, blob) : null; - } - - removeCurrentUser(): Promise { - return this.persistence._remove(this.fullUserKey); - } - - savePersistenceForRedirect(): Promise { - return this.persistence._set( - this.fullPersistenceKey, - this.persistence.type - ); - } - - async setPersistence(newPersistence: Persistence): Promise { - if (this.persistence.type === newPersistence.type) { - return; - } - - const currentUser = await this.getCurrentUser(); - await this.removeCurrentUser(); - - this.persistence = newPersistence; - - if (currentUser) { - return this.setCurrentUser(currentUser); - } - } - - delete(): void { - this.persistence._removeListener(this.fullUserKey, this.boundEventHandler); - } - - static async create( - auth: Auth, - persistenceHierarchy: Persistence[], - userKey = KeyName.AUTH_USER - ): Promise { - if (!persistenceHierarchy.length) { - return new PersistenceUserManager( - _getInstance(inMemoryPersistence), - auth, - userKey - ); - } - - const key = _persistenceKeyName(userKey, auth.config.apiKey, auth.name); - for (const persistence of persistenceHierarchy) { - if (await persistence._get(key)) { - return new PersistenceUserManager(persistence, auth, userKey); - } - } - - // Check all the available storage options. - // TODO: Migrate from local storage to indexedDB - // TODO: Clear other forms once one is found - - // All else failed, fall back to zeroth persistence - // TODO: Modify this to support non-browser devices - return new PersistenceUserManager(persistenceHierarchy[0], auth, userKey); - } -} diff --git a/packages-exp/auth-exp/src/core/providers/email.test.ts b/packages-exp/auth-exp/src/core/providers/email.test.ts deleted file mode 100644 index 08aa6a9f39a..00000000000 --- a/packages-exp/auth-exp/src/core/providers/email.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { ProviderId, SignInMethod } from '@firebase/auth-types-exp'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { FirebaseError } from '@firebase/util'; - -import { EmailAuthProvider } from './email'; - -use(chaiAsPromised); - -describe('core/providers/email', () => { - describe('.credential', () => { - it('should return an email & password credential', () => { - const credential = EmailAuthProvider.credential( - 'some-email', - 'some-password' - ); - expect(credential.email).to.eq('some-email'); - expect(credential.password).to.eq('some-password'); - expect(credential.providerId).to.eq(ProviderId.PASSWORD); - expect(credential.signInMethod).to.eq(SignInMethod.EMAIL_PASSWORD); - }); - }); - - describe('.credentialWithLink', () => { - it('should return an email link credential', () => { - const continueUrl = 'https://www.example.com/path/to/file?a=1&b=2#c=3'; - const actionLink = - 'https://www.example.com/finishSignIn?' + - 'oobCode=CODE&mode=signIn&apiKey=API_KEY&' + - 'continueUrl=' + - encodeURIComponent(continueUrl) + - '&languageCode=en&state=bla'; - - const credential = EmailAuthProvider.credentialWithLink( - 'some-email', - actionLink - ); - expect(credential.email).to.eq('some-email'); - expect(credential.password).to.eq('CODE'); - expect(credential.providerId).to.eq(ProviderId.PASSWORD); - expect(credential.signInMethod).to.eq(SignInMethod.EMAIL_LINK); - }); - - context('invalid email link', () => { - it('should throw an error', () => { - const actionLink = 'https://www.example.com/finishSignIn?'; - expect(() => - EmailAuthProvider.credentialWithLink('some-email', actionLink) - ).to.throw(FirebaseError, 'Firebase: Error (auth/argument-error)'); - }); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/providers/email.ts b/packages-exp/auth-exp/src/core/providers/email.ts deleted file mode 100644 index 17ac1848555..00000000000 --- a/packages-exp/auth-exp/src/core/providers/email.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { ActionCodeURL } from '../action_code_url'; -import { EmailAuthCredential } from '../credentials/email'; -import { AuthErrorCode } from '../errors'; -import { _assert } from '../util/assert'; - -/** - * {@inheritdoc @firebase/auth-types#EmailAuthProvider} - * - * @public - */ -export class EmailAuthProvider implements externs.EmailAuthProvider { - /** {@inheritdoc @firebase/auth-types#EmailAuthProvider.PROVIDER_ID} */ - static readonly PROVIDER_ID = externs.ProviderId.PASSWORD; - /** {@inheritdoc @firebase/auth-types#EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD} */ - static readonly EMAIL_PASSWORD_SIGN_IN_METHOD = - externs.SignInMethod.EMAIL_PASSWORD; - /** {@inheritdoc @firebase/auth-types#EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD} */ - static readonly EMAIL_LINK_SIGN_IN_METHOD = externs.SignInMethod.EMAIL_LINK; - /** {@inheritdoc @firebase/auth-types#EmailAuthProvider.providerId} */ - readonly providerId = EmailAuthProvider.PROVIDER_ID; - - /** {@inheritdoc @firebase/auth-types#EmailAuthProvider.credential} */ - static credential(email: string, password: string): EmailAuthCredential { - return EmailAuthCredential._fromEmailAndPassword(email, password); - } - - /** {@inheritdoc @firebase/auth-types#EmailAuthProvider.credentialWithLink} */ - static credentialWithLink( - email: string, - emailLink: string - ): EmailAuthCredential { - const actionCodeUrl = ActionCodeURL.parseLink(emailLink); - _assert(actionCodeUrl, AuthErrorCode.ARGUMENT_ERROR); - - return EmailAuthCredential._fromEmailAndCode( - email, - actionCodeUrl.code, - actionCodeUrl.tenantId - ); - } -} diff --git a/packages-exp/auth-exp/src/core/providers/facebook.test.ts b/packages-exp/auth-exp/src/core/providers/facebook.test.ts deleted file mode 100644 index 113c1f43915..00000000000 --- a/packages-exp/auth-exp/src/core/providers/facebook.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; - -import { - OperationType, - ProviderId, - SignInMethod -} from '@firebase/auth-types-exp'; - -import { TEST_ID_TOKEN_RESPONSE } from '../../../test/helpers/id_token_response'; -import { testUser, testAuth } from '../../../test/helpers/mock_auth'; -import { TaggedWithTokenResponse } from '../../model/id_token'; -import { AuthErrorCode } from '../errors'; -import { UserCredentialImpl } from '../user/user_credential_impl'; -import { FacebookAuthProvider } from './facebook'; -import { _createError } from '../util/assert'; - -describe('core/providers/facebook', () => { - it('generates the correct type of oauth credential', () => { - const cred = FacebookAuthProvider.credential('access-token'); - expect(cred.accessToken).to.eq('access-token'); - expect(cred.providerId).to.eq(ProviderId.FACEBOOK); - expect(cred.signInMethod).to.eq(SignInMethod.FACEBOOK); - }); - - it('credentialFromResult creates the cred from a tagged result', async () => { - const auth = await testAuth(); - const userCred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.FACEBOOK, - _tokenResponse: { - ...TEST_ID_TOKEN_RESPONSE, - oauthAccessToken: 'access-token' - }, - operationType: OperationType.SIGN_IN - }); - const cred = FacebookAuthProvider.credentialFromResult(userCred)!; - expect(cred.accessToken).to.eq('access-token'); - expect(cred.providerId).to.eq(ProviderId.FACEBOOK); - expect(cred.signInMethod).to.eq(SignInMethod.FACEBOOK); - }); - - it('credentialFromError creates the cred from a tagged error', () => { - const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { - appName: 'foo' - }); - (error.customData! as TaggedWithTokenResponse)._tokenResponse = { - ...TEST_ID_TOKEN_RESPONSE, - oauthAccessToken: 'access-token' - }; - - const cred = FacebookAuthProvider.credentialFromError(error)!; - expect(cred.accessToken).to.eq('access-token'); - expect(cred.providerId).to.eq(ProviderId.FACEBOOK); - expect(cred.signInMethod).to.eq(SignInMethod.FACEBOOK); - }); -}); diff --git a/packages-exp/auth-exp/src/core/providers/facebook.ts b/packages-exp/auth-exp/src/core/providers/facebook.ts deleted file mode 100644 index e0aacc0667d..00000000000 --- a/packages-exp/auth-exp/src/core/providers/facebook.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { TaggedWithTokenResponse } from '../../model/id_token'; -import { UserCredential } from '../../model/user'; -import { OAuthCredential } from '../credentials/oauth'; -import { OAuthProvider } from './oauth'; - -/** - * Provider for generating an {@link OAuthCredential} for {@link @firebase/auth-types#ProviderId.FACEBOOK}. - * - * @example - * ```javascript - * // Sign in using a redirect. - * const provider = new FacebookAuthProvider(); - * // Start a sign in process for an unauthenticated user. - * provider.addScope('user_birthday'); - * await signInWithRedirect(auth, provider); - * // This will trigger a full page redirect away from your app - * - * // After returning from the redirect when your app initializes you can obtain the result - * const result = await getRedirectResult(auth); - * if (result) { - * // This is the signed-in user - * const user = result.user; - * // This gives you a Facebook Access Token. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * } - * ``` - * - * @example - * ```javascript - * // Sign in using a popup. - * const provider = new FacebookAuthProvider(); - * provider.addScope('user_birthday'); - * const result = await signInWithPopup(auth, provider); - * - * // The signed-in user info. - * const user = result.user; - * // This gives you a Facebook Access Token. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * ``` - * - * @public - */ -export class FacebookAuthProvider extends OAuthProvider { - /** Always set to {@link @firebase/auth-types#SignInMethod.FACEBOOK}. */ - static readonly FACEBOOK_SIGN_IN_METHOD = externs.SignInMethod.FACEBOOK; - /** Always set to {@link @firebase/auth-types#ProviderId.FACEBOOK}. */ - static readonly PROVIDER_ID = externs.ProviderId.FACEBOOK; - - constructor() { - super(externs.ProviderId.FACEBOOK); - } - - /** - * Creates a credential for Facebook. - * - * @example - * ```javascript - * // `event` from the Facebook auth.authResponseChange callback. - * const credential = FacebookAuthProvider.credential(event.authResponse.accessToken); - * const result = await signInWithCredential(credential); - * ``` - * - * @param accessToken - Facebook access token. - */ - static credential(accessToken: string): externs.OAuthCredential { - return OAuthCredential._fromParams({ - providerId: FacebookAuthProvider.PROVIDER_ID, - signInMethod: FacebookAuthProvider.FACEBOOK_SIGN_IN_METHOD, - accessToken - }); - } - - /** - * Used to extract the underlying {@link OAuthCredential} from a {@link @firebase/auth-types#UserCredential}. - * - * @param userCredential - The user credential. - */ - static credentialFromResult( - userCredential: externs.UserCredential - ): externs.OAuthCredential | null { - return FacebookAuthProvider.credentialFromTaggedObject( - userCredential as UserCredential - ); - } - - /** - * Used to extract the underlying {@link OAuthCredential} from a {@link @firebase/auth-types#AuthError} which was - * thrown during a sign-in, link, or reauthenticate operation. - * - * @param userCredential - The user credential. - */ - static credentialFromError( - error: FirebaseError - ): externs.OAuthCredential | null { - return FacebookAuthProvider.credentialFromTaggedObject( - (error.customData || {}) as TaggedWithTokenResponse - ); - } - - private static credentialFromTaggedObject({ - _tokenResponse: tokenResponse - }: TaggedWithTokenResponse): externs.OAuthCredential | null { - if (!tokenResponse || !('oauthAccessToken' in tokenResponse)) { - return null; - } - - if (!tokenResponse.oauthAccessToken) { - return null; - } - - try { - return FacebookAuthProvider.credential(tokenResponse.oauthAccessToken); - } catch { - return null; - } - } -} diff --git a/packages-exp/auth-exp/src/core/providers/github.test.ts b/packages-exp/auth-exp/src/core/providers/github.test.ts deleted file mode 100644 index 6a48da8f8ea..00000000000 --- a/packages-exp/auth-exp/src/core/providers/github.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; - -import { - OperationType, - ProviderId, - SignInMethod -} from '@firebase/auth-types-exp'; - -import { TEST_ID_TOKEN_RESPONSE } from '../../../test/helpers/id_token_response'; -import { testUser, testAuth } from '../../../test/helpers/mock_auth'; -import { TaggedWithTokenResponse } from '../../model/id_token'; -import { AuthErrorCode } from '../errors'; -import { UserCredentialImpl } from '../user/user_credential_impl'; -import { GithubAuthProvider } from './github'; -import { _createError } from '../util/assert'; - -describe('core/providers/github', () => { - it('generates the correct type of oauth credential', () => { - const cred = GithubAuthProvider.credential('access-token'); - expect(cred.accessToken).to.eq('access-token'); - expect(cred.providerId).to.eq(ProviderId.GITHUB); - expect(cred.signInMethod).to.eq(SignInMethod.GITHUB); - }); - - it('credentialFromResult creates the cred from a tagged result', async () => { - const auth = await testAuth(); - const userCred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GITHUB, - _tokenResponse: { - ...TEST_ID_TOKEN_RESPONSE, - oauthAccessToken: 'access-token' - }, - operationType: OperationType.SIGN_IN - }); - const cred = GithubAuthProvider.credentialFromResult(userCred)!; - expect(cred.accessToken).to.eq('access-token'); - expect(cred.providerId).to.eq(ProviderId.GITHUB); - expect(cred.signInMethod).to.eq(SignInMethod.GITHUB); - }); - - it('credentialFromError creates the cred from a tagged error', () => { - const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { - appName: 'foo' - }); - (error.customData! as TaggedWithTokenResponse)._tokenResponse = { - ...TEST_ID_TOKEN_RESPONSE, - oauthAccessToken: 'access-token' - }; - - const cred = GithubAuthProvider.credentialFromError(error)!; - expect(cred.accessToken).to.eq('access-token'); - expect(cred.providerId).to.eq(ProviderId.GITHUB); - expect(cred.signInMethod).to.eq(SignInMethod.GITHUB); - }); -}); diff --git a/packages-exp/auth-exp/src/core/providers/github.ts b/packages-exp/auth-exp/src/core/providers/github.ts deleted file mode 100644 index 6bfebbd6357..00000000000 --- a/packages-exp/auth-exp/src/core/providers/github.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { TaggedWithTokenResponse } from '../../model/id_token'; -import { UserCredential } from '../../model/user'; -import { OAuthCredential } from '../credentials/oauth'; -import { OAuthProvider } from './oauth'; - -/** - * Provider for generating an {@link OAuthCredential} for {@link @firebase/auth-types#ProviderId.GITHUB}. - * - * @remarks - * GitHub requires an OAuth 2.0 redirect, so you can either handle the redirect directly, or use - * the {@link signInWithPopup} handler: - * - * @example - * ```javascript - * // Sign in using a redirect. - * const provider = new GithubAuthProvider(); - * // Start a sign in process for an unauthenticated user. - * provider.addScope('repo'); - * await signInWithRedirect(auth, provider); - * // This will trigger a full page redirect away from your app - * - * // After returning from the redirect when your app initializes you can obtain the result - * const result = await getRedirectResult(auth); - * if (result) { - * // This is the signed-in user - * const user = result.user; - * // This gives you a Github Access Token. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * } - * ``` - * - * @example - * ```javascript - * // Sign in using a popup. - * const provider = new GithubAuthProvider(); - * provider.addScope('repo'); - * const result = await signInWithPopup(auth, provider); - * - * // The signed-in user info. - * const user = result.user; - * // This gives you a Github Access Token. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * ``` - * @public - */ -export class GithubAuthProvider extends OAuthProvider { - /** Always set to {@link @firebase/auth-types#SignInMethod.GITHUB}. */ - static readonly GITHUB_SIGN_IN_METHOD = externs.SignInMethod.GITHUB; - /** Always set to {@link @firebase/auth-types#ProviderId.GITHUB}. */ - static readonly PROVIDER_ID = externs.ProviderId.GITHUB; - - constructor() { - super(externs.ProviderId.GITHUB); - } - - /** - * Creates a credential for Github. - * - * @param accessToken - Github access token. - */ - static credential(accessToken: string): externs.OAuthCredential { - return OAuthCredential._fromParams({ - providerId: GithubAuthProvider.PROVIDER_ID, - signInMethod: GithubAuthProvider.GITHUB_SIGN_IN_METHOD, - accessToken - }); - } - - /** - * Used to extract the underlying {@link OAuthCredential} from a {@link @firebase/auth-types#UserCredential}. - * - * @param userCredential - The user credential. - */ - static credentialFromResult( - userCredential: externs.UserCredential - ): externs.OAuthCredential | null { - return GithubAuthProvider.credentialFromTaggedObject( - userCredential as UserCredential - ); - } - - /** - * Used to extract the underlying {@link OAuthCredential} from a {@link @firebase/auth-types#AuthError} which was - * thrown during a sign-in, link, or reauthenticate operation. - * - * @param userCredential - The user credential. - */ - static credentialFromError( - error: FirebaseError - ): externs.OAuthCredential | null { - return GithubAuthProvider.credentialFromTaggedObject( - (error.customData || {}) as TaggedWithTokenResponse - ); - } - - private static credentialFromTaggedObject({ - _tokenResponse: tokenResponse - }: TaggedWithTokenResponse): externs.OAuthCredential | null { - if (!tokenResponse || !('oauthAccessToken' in tokenResponse)) { - return null; - } - - if (!tokenResponse.oauthAccessToken) { - return null; - } - - try { - return GithubAuthProvider.credential(tokenResponse.oauthAccessToken); - } catch { - return null; - } - } -} diff --git a/packages-exp/auth-exp/src/core/providers/google.ts b/packages-exp/auth-exp/src/core/providers/google.ts deleted file mode 100644 index 0d25e2779a1..00000000000 --- a/packages-exp/auth-exp/src/core/providers/google.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { SignInWithIdpResponse } from '../../api/authentication/idp'; -import { TaggedWithTokenResponse } from '../../model/id_token'; -import { UserCredential } from '../../model/user'; -import { OAuthCredential } from '../credentials/oauth'; -import { OAuthProvider } from './oauth'; - -/** - * Provider for generating an an {@link OAuthCredential} for {@link @firebase/auth-types#ProviderId.GOOGLE}. - * - * @example - * ```javascript - * // Sign in using a redirect. - * const provider = new GoogleAuthProvider(); - * // Start a sign in process for an unauthenticated user. - * provider.addScope('profile'); - * provider.addScope('email'); - * await signInWithRedirect(auth, provider); - * // This will trigger a full page redirect away from your app - * - * // After returning from the redirect when your app initializes you can obtain the result - * const result = await getRedirectResult(auth); - * if (result) { - * // This is the signed-in user - * const user = result.user; - * // This gives you a Google Access Token. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * } - * ``` - * - * @example - * ```javascript - * // Sign in using a popup. - * const provider = new GoogleAuthProvider(); - * provider.addScope('profile'); - * provider.addScope('email'); - * const result = await signInWithPopup(auth, provider); - * - * // The signed-in user info. - * const user = result.user; - * // This gives you a Google Access Token. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * ``` - * - * @public - */ -export class GoogleAuthProvider extends OAuthProvider { - /** Always set to {@link @firebase/auth-types#SignInMethod.GOOGLE}. */ - static readonly GOOGLE_SIGN_IN_METHOD = externs.SignInMethod.GOOGLE; - /** Always set to {@link @firebase/auth-types#ProviderId.GOOGLE}. */ - static readonly PROVIDER_ID = externs.ProviderId.GOOGLE; - - constructor() { - super(externs.ProviderId.GOOGLE); - this.addScope('profile'); - } - - /** - * Creates a credential for Google. At least one of ID token and access token is required. - * - * @example - * ```javascript - * // \`googleUser\` from the onsuccess Google Sign In callback. - * const credential = GoogleAuthProvider.credential(googleUser.getAuthResponse().id_token); - * const result = await signInWithCredential(credential); - * ``` - * - * @param idToken - Google ID token. - * @param accessToken - Google access token. - */ - static credential( - idToken?: string | null, - accessToken?: string | null - ): externs.OAuthCredential { - return OAuthCredential._fromParams({ - providerId: GoogleAuthProvider.PROVIDER_ID, - signInMethod: GoogleAuthProvider.GOOGLE_SIGN_IN_METHOD, - idToken, - accessToken - }); - } - - /** - * Used to extract the underlying {@link OAuthCredential} from a {@link @firebase/auth-types#UserCredential}. - * - * @param userCredential - The user credential. - */ - static credentialFromResult( - userCredential: externs.UserCredential - ): externs.OAuthCredential | null { - return GoogleAuthProvider.credentialFromTaggedObject( - userCredential as UserCredential - ); - } - /** - * Used to extract the underlying {@link OAuthCredential} from a {@link @firebase/auth-types#AuthError} which was - * thrown during a sign-in, link, or reauthenticate operation. - * - * @param userCredential - The user credential. - */ - static credentialFromError( - error: FirebaseError - ): externs.OAuthCredential | null { - return GoogleAuthProvider.credentialFromTaggedObject( - (error.customData || {}) as TaggedWithTokenResponse - ); - } - - private static credentialFromTaggedObject({ - _tokenResponse: tokenResponse - }: TaggedWithTokenResponse): externs.OAuthCredential | null { - if (!tokenResponse) { - return null; - } - - const { - oauthIdToken, - oauthAccessToken - } = tokenResponse as SignInWithIdpResponse; - if (!oauthIdToken && !oauthAccessToken) { - // This could be an oauth 1 credential or a phone credential - return null; - } - - try { - return GoogleAuthProvider.credential(oauthIdToken, oauthAccessToken); - } catch { - return null; - } - } -} diff --git a/packages-exp/auth-exp/src/core/providers/oauth.test.ts b/packages-exp/auth-exp/src/core/providers/oauth.test.ts deleted file mode 100644 index 02ecb6c12d4..00000000000 --- a/packages-exp/auth-exp/src/core/providers/oauth.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; - -import { - OperationType, - ProviderId, - SignInMethod -} from '@firebase/auth-types-exp'; - -import { TEST_ID_TOKEN_RESPONSE } from '../../../test/helpers/id_token_response'; -import { testUser, testAuth } from '../../../test/helpers/mock_auth'; -import { TaggedWithTokenResponse } from '../../model/id_token'; -import { AuthErrorCode } from '../errors'; -import { UserCredentialImpl } from '../user/user_credential_impl'; -import { _createError } from '../util/assert'; -import { OAuthProvider } from './oauth'; - -describe('core/providers/oauth', () => { - it('generates the correct type of oauth credential', () => { - const cred = new OAuthProvider('google.com').credential({ - idToken: 'id-token', - accessToken: 'access-token' - }); - expect(cred.accessToken).to.eq('access-token'); - expect(cred.idToken).to.eq('id-token'); - expect(cred.providerId).to.eq(ProviderId.GOOGLE); - expect(cred.signInMethod).to.eq(SignInMethod.GOOGLE); - }); - - it('credentialFromResult creates the cred from a tagged result', async () => { - const auth = await testAuth(); - const userCred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GOOGLE, - _tokenResponse: { - ...TEST_ID_TOKEN_RESPONSE, - oauthAccessToken: 'access-token', - oauthIdToken: 'id-token', - providerId: ProviderId.FACEBOOK - }, - operationType: OperationType.SIGN_IN - }); - const cred = OAuthProvider.credentialFromResult(userCred)!; - expect(cred.accessToken).to.eq('access-token'); - expect(cred.idToken).to.eq('id-token'); - expect(cred.providerId).to.eq(ProviderId.FACEBOOK); - expect(cred.signInMethod).to.eq(SignInMethod.FACEBOOK); - }); - - it('credentialFromResult returns null if provider ID not specified', async () => { - const auth = await testAuth(); - const userCred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GOOGLE, - _tokenResponse: { - ...TEST_ID_TOKEN_RESPONSE, - oauthAccessToken: 'access-token', - oauthIdToken: 'id-token' - }, - operationType: OperationType.SIGN_IN - }); - expect(OAuthProvider.credentialFromResult(userCred)).to.be.null; - }); - - it('credentialFromError creates the cred from a tagged error', () => { - const error = _createError(AuthErrorCode.NEED_CONFIRMATION, { - appName: 'foo' - }); - (error.customData! as TaggedWithTokenResponse)._tokenResponse = { - ...TEST_ID_TOKEN_RESPONSE, - oauthAccessToken: 'access-token', - oauthIdToken: 'id-token', - providerId: ProviderId.FACEBOOK - }; - - const cred = OAuthProvider.credentialFromError(error)!; - expect(cred.accessToken).to.eq('access-token'); - expect(cred.idToken).to.eq('id-token'); - expect(cred.providerId).to.eq(ProviderId.FACEBOOK); - expect(cred.signInMethod).to.eq(SignInMethod.FACEBOOK); - }); -}); diff --git a/packages-exp/auth-exp/src/core/providers/oauth.ts b/packages-exp/auth-exp/src/core/providers/oauth.ts deleted file mode 100644 index 275807fb636..00000000000 --- a/packages-exp/auth-exp/src/core/providers/oauth.ts +++ /dev/null @@ -1,277 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { _assert } from '../util/assert'; -import { AuthErrorCode } from '../errors'; - -import { OAuthCredential } from '../credentials/oauth'; -import { UserCredential } from '../../model/user'; -import { FirebaseError } from '@firebase/util'; -import { TaggedWithTokenResponse } from '../../model/id_token'; -import { SignInWithIdpResponse } from '../../../internal'; - -/** - * Map of OAuth Custom Parameters. - * - * @public - */ -export type CustomParameters = Record; - -/** - * Defines the options for initializing an {@link OAuthCredential}. - * - * @remarks - * For ID tokens with nonce claim, the raw nonce has to also be provided. - * - * @public - */ -export interface OAuthCredentialOptions { - /** - * The OAuth ID token used to initialize the {@link OAuthCredential}. - */ - idToken?: string; - /** - * The OAuth access token used to initialize the {@link OAuthCredential}. - */ - accessToken?: string; - /** - * The raw nonce associated with the ID token. - * - * @remarks - * It is required when an ID token with a nonce field is provided. The SHA-256 hash of the - * raw nonce must match the nonce field in the ID token. - */ - rawNonce?: string; -} - -/** - * Provider for generating generic {@link OAuthCredential}. - * - * @example - * ```javascript - * // Sign in using a redirect. - * const provider = new OAuthProvider('google.com'); - * // Start a sign in process for an unauthenticated user. - * provider.addScope('profile'); - * provider.addScope('email'); - * await signInWithRedirect(auth, provider); - * // This will trigger a full page redirect away from your app - * - * // After returning from the redirect when your app initializes you can obtain the result - * const result = await getRedirectResult(auth); - * if (result) { - * // This is the signed-in user - * const user = result.user; - * // This gives you a OAuth Access Token for the provider. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * } - * ``` - * - * @example - * ```javascript - * // Sign in using a popup. - * const provider = new OAuthProvider('google.com'); - * provider.addScope('profile'); - * provider.addScope('email'); - * const result = await signInWithPopup(auth, provider); - * - * // The signed-in user info. - * const user = result.user; - * // This gives you a OAuth Access Token for the provider. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * ``` - * @public - */ -export class OAuthProvider implements externs.AuthProvider { - /** @internal */ - defaultLanguageCode: string | null = null; - /** @internal */ - private scopes: string[] = []; - /** @internal */ - private customParameters: CustomParameters = {}; - - /** - * Constructor for generic OAuth providers. - * - * @param providerId - Provider for which credentials should be generated. - */ - constructor(readonly providerId: string) {} - - static credentialFromJSON(json: object | string): externs.OAuthCredential { - const obj = typeof json === 'string' ? JSON.parse(json) : json; - _assert( - 'providerId' in obj && 'signInMethod' in obj, - AuthErrorCode.ARGUMENT_ERROR - ); - return OAuthCredential._fromParams(obj); - } - - /** - * Creates a {@link OAuthCredential} from a generic OAuth provider's access token or ID token. - * - * @remarks - * The raw nonce is required when an ID token with a nonce field is provided. The SHA-256 hash of - * the raw nonce must match the nonce field in the ID token. - * - * @example - * ```javascript - * // `googleUser` from the onsuccess Google Sign In callback. - * // Initialize a generate OAuth provider with a `google.com` providerId. - * const provider = new OAuthProvider('google.com'); - * const credential = provider.credential({ - * idToken: googleUser.getAuthResponse().id_token, - * }); - * const result = await signInWithCredential(credential); - * ``` - * - * @param params - Either the options object containing the ID token, access token and raw nonce - * or the ID token string. - */ - credential(params: OAuthCredentialOptions): externs.OAuthCredential { - _assert(params.idToken && params.accessToken, AuthErrorCode.ARGUMENT_ERROR); - // For OAuthCredential, sign in method is same as providerId. - return OAuthCredential._fromParams({ - providerId: this.providerId, - signInMethod: this.providerId, - ...params - }); - } - - /** - * Set the language gode. - * - * @param languageCode - language code - */ - setDefaultLanguage(languageCode: string | null): void { - this.defaultLanguageCode = languageCode; - } - - /** - * Sets the OAuth custom parameters to pass in an OAuth request for popup and redirect sign-in - * operations. - * - * @remarks - * For a detailed list, check the reserved required OAuth 2.0 parameters such as `client_id`, - * `redirect_uri`, `scope`, `response_type`, and `state` are not allowed and will be ignored. - * - * @param customOAuthParameters - The custom OAuth parameters to pass in the OAuth request. - */ - setCustomParameters( - customOAuthParameters: CustomParameters - ): externs.AuthProvider { - this.customParameters = customOAuthParameters; - return this; - } - - /** - * Retrieve the current list of {@link CustomParameters}. - */ - getCustomParameters(): CustomParameters { - return this.customParameters; - } - - /** - * Add an OAuth scope to the credential. - * - * @param scope - Provider OAuth scope to add. - */ - addScope(scope: string): externs.AuthProvider { - // If not already added, add scope to list. - if (!this.scopes.includes(scope)) { - this.scopes.push(scope); - } - return this; - } - - /** - * Retrieve the current list of OAuth scopes. - */ - getScopes(): string[] { - return [...this.scopes]; - } - - /** - * Used to extract the underlying {@link OAuthCredential} from a {@link @firebase/auth-types#UserCredential}. - * - * @param userCredential - The user credential. - */ - static credentialFromResult( - userCredential: externs.UserCredential - ): externs.OAuthCredential | null { - return OAuthProvider.oauthCredentialFromTaggedObject( - userCredential as UserCredential - ); - } - /** - * Used to extract the underlying {@link OAuthCredential} from a {@link @firebase/auth-types#AuthError} which was - * thrown during a sign-in, link, or reauthenticate operation. - * - * @param userCredential - The user credential. - */ - static credentialFromError( - error: FirebaseError - ): externs.OAuthCredential | null { - return OAuthProvider.oauthCredentialFromTaggedObject( - (error.customData || {}) as TaggedWithTokenResponse - ); - } - - // This needs to have a different name so it doesn't conflict with the - // subclasses - private static oauthCredentialFromTaggedObject({ - _tokenResponse: tokenResponse - }: TaggedWithTokenResponse): externs.OAuthCredential | null { - if (!tokenResponse) { - return null; - } - - const { - oauthIdToken, - oauthAccessToken, - oauthTokenSecret, - pendingToken, - nonce, - providerId - } = tokenResponse as SignInWithIdpResponse; - if ( - !oauthAccessToken && - !oauthTokenSecret && - !oauthIdToken && - !pendingToken - ) { - return null; - } - - if (!providerId) { - return null; - } - - try { - return new OAuthProvider(providerId).credential({ - idToken: oauthIdToken, - accessToken: oauthAccessToken, - rawNonce: nonce - }); - } catch (e) { - return null; - } - } -} diff --git a/packages-exp/auth-exp/src/core/strategies/anonymous.test.ts b/packages-exp/auth-exp/src/core/strategies/anonymous.test.ts deleted file mode 100644 index 279541f3852..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/anonymous.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; - -import { OperationType } from '@firebase/auth-types-exp'; - -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, testUser, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { APIUserInfo } from '../../api/account_management/account'; -import { signInAnonymously } from './anonymous'; - -describe('core/strategies/anonymous', () => { - let auth: TestAuth; - const serverUser: APIUserInfo = { - localId: 'local-id' - }; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - mockEndpoint(Endpoint.SIGN_UP, { - idToken: 'id-token', - refreshToken: 'refresh-token', - expiresIn: '1234', - localId: serverUser.localId! - }); - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [serverUser] - }); - }); - afterEach(mockFetch.tearDown); - - describe('signInAnonymously', () => { - it('should sign in an anonymous user', async () => { - const { user, operationType } = await signInAnonymously(auth); - expect(operationType).to.eq(OperationType.SIGN_IN); - expect(user.uid).to.eq(serverUser.localId); - expect(user.isAnonymous).to.be.true; - }); - - context('already signed in anonymously', () => { - it('should return the current user', async () => { - const userCredential = await signInAnonymously(auth); - expect(userCredential.user.isAnonymous).to.be.true; - - const { user, operationType } = await signInAnonymously(auth); - expect(operationType).to.eq(OperationType.SIGN_IN); - expect(user.uid).to.eq(userCredential.user.uid); - expect(user.isAnonymous).to.be.true; - }); - }); - - context('already signed in with a non-anonymous account', () => { - it('should sign in as a new user user', async () => { - const fakeUser = testUser(auth, 'other-uid'); - await auth._updateCurrentUser(fakeUser); - expect(fakeUser.isAnonymous).to.be.false; - - const { user, operationType } = await signInAnonymously(auth); - expect(operationType).to.eq(OperationType.SIGN_IN); - expect(user.uid).to.not.eq(fakeUser.uid); - expect(user.isAnonymous).to.be.true; - }); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/strategies/anonymous.ts b/packages-exp/auth-exp/src/core/strategies/anonymous.ts deleted file mode 100644 index 71ded6c42c6..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/anonymous.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { signUp } from '../../api/authentication/sign_up'; -import { User } from '../../model/user'; -import { UserCredentialImpl } from '../user/user_credential_impl'; -import { _castAuth } from '../auth/auth_impl'; - -/** - * Asynchronously signs in as an anonymous user. - * - * @remarks - * If there is already an anonymous user signed in, that user will be returned; otherwise, a - * new anonymous user identity will be created and returned. - * - * @param auth - The Auth instance. - * - * @public - */ -export async function signInAnonymously( - auth: externs.Auth -): Promise { - const authInternal = _castAuth(auth); - if (authInternal.currentUser?.isAnonymous) { - // If an anonymous user is already signed in, no need to sign them in again. - return new UserCredentialImpl({ - user: authInternal.currentUser as User, - providerId: null, - operationType: externs.OperationType.SIGN_IN - }); - } - const response = await signUp(authInternal, { - returnSecureToken: true - }); - const userCredential = await UserCredentialImpl._fromIdTokenResponse( - authInternal, - externs.OperationType.SIGN_IN, - response, - true - ); - await authInternal._updateCurrentUser(userCredential.user); - return userCredential; -} diff --git a/packages-exp/auth-exp/src/core/strategies/credential.ts b/packages-exp/auth-exp/src/core/strategies/credential.ts deleted file mode 100644 index e97f2880063..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/credential.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { OperationType, UserCredential } from '@firebase/auth-types-exp'; - -import { _processCredentialSavingMfaContextIfNecessary } from '../../mfa/mfa_error'; -import { Auth } from '../../model/auth'; -import { User } from '../../model/user'; -import { AuthCredential } from '../credentials'; -import { _assertLinkedStatus, _link } from '../user/link_unlink'; -import { _reauthenticate } from '../user/reauthenticate'; -import { UserCredentialImpl } from '../user/user_credential_impl'; -import { _castAuth } from '../auth/auth_impl'; - -/** @internal */ -export async function _signInWithCredential( - auth: Auth, - credential: AuthCredential, - bypassAuthState = false -): Promise { - const operationType = OperationType.SIGN_IN; - const response = await _processCredentialSavingMfaContextIfNecessary( - auth, - operationType, - credential - ); - const userCredential = await UserCredentialImpl._fromIdTokenResponse( - auth, - operationType, - response - ); - - if (!bypassAuthState) { - await auth._updateCurrentUser(userCredential.user); - } - return userCredential; -} - -/** - * Asynchronously signs in with the given credentials. - * - * @remarks - * An {@link @firebase/auth-types#AuthProvider} can be used to generate the credential. - * - * @param auth - The Auth instance. - * @param credential - The auth credential. - * - * @public - */ -export async function signInWithCredential( - auth: externs.Auth, - credential: externs.AuthCredential -): Promise { - return _signInWithCredential(_castAuth(auth), credential as AuthCredential); -} - -/** - * Links the user account with the given credentials. - * - * @remarks - * An {@link @firebase/auth-types#AuthProvider} can be used to generate the credential. - * - * @param user - The user. - * @param credential - The auth credential. - * - * @public - */ -export async function linkWithCredential( - user: externs.User, - credential: externs.AuthCredential -): Promise { - const userInternal = user as User; - - await _assertLinkedStatus(false, userInternal, credential.providerId); - - return _link(userInternal, credential as AuthCredential); -} - -/** - * Re-authenticates a user using a fresh credential. - * - * @remarks - * Use before operations such as {@link updatePassword} that require tokens from recent sign-in - * attempts. This method can be used to recover from a - * {@link AuthErrorCode.CREDENTIAL_TOO_OLD_LOGIN_AGAIN} error. - * - * @param user - The user. - * @param credential - The auth credential. - * - * @public - */ -export async function reauthenticateWithCredential( - user: externs.User, - credential: externs.AuthCredential -): Promise { - return _reauthenticate(user as User, credential as AuthCredential); -} diff --git a/packages-exp/auth-exp/src/core/strategies/custom_token.test.ts b/packages-exp/auth-exp/src/core/strategies/custom_token.test.ts deleted file mode 100644 index 39a789df543..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/custom_token.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { OperationType } from '@firebase/auth-types-exp'; - -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { APIUserInfo } from '../../api/account_management/account'; -import { IdTokenResponse, IdTokenResponseKind } from '../../model/id_token'; -import { UserCredential } from '../../model/user'; -import { signInWithCustomToken } from './custom_token'; - -use(chaiAsPromised); - -describe('core/strategies/signInWithCustomToken', () => { - const serverUser: APIUserInfo = { - localId: 'local-id', - displayName: 'display-name', - photoUrl: 'photo-url', - email: 'email', - emailVerified: true, - phoneNumber: 'phone-number', - createdAt: 123, - lastLoginAt: 456 - }; - - const idTokenResponse: IdTokenResponse = { - idToken: 'my-id-token', - refreshToken: 'my-refresh-token', - expiresIn: '1234', - localId: serverUser.localId!, - kind: IdTokenResponseKind.CreateAuthUri - }; - - let auth: TestAuth; - let signInRoute: mockFetch.Route; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - signInRoute = mockEndpoint( - Endpoint.SIGN_IN_WITH_CUSTOM_TOKEN, - idTokenResponse - ); - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [serverUser] - }); - }); - afterEach(mockFetch.tearDown); - - it('should return a valid user credential', async () => { - const { - user, - operationType, - _tokenResponse - } = (await signInWithCustomToken( - auth, - 'look-at-me-im-a-jwt' - )) as UserCredential; - expect(_tokenResponse).to.eql(idTokenResponse); - expect(user.uid).to.eq('local-id'); - expect(user.displayName).to.eq('display-name'); - expect(operationType).to.eq(OperationType.SIGN_IN); - }); - - it('should send with a valid request', async () => { - await signInWithCustomToken(auth, 'j.w.t'); - expect(signInRoute.calls[0].request).to.eql({ - token: 'j.w.t', - returnSecureToken: true - }); - }); - - it('should update the current user', async () => { - const { user } = await signInWithCustomToken(auth, 'oh.no'); - expect(auth.currentUser).to.eq(user); - }); -}); diff --git a/packages-exp/auth-exp/src/core/strategies/custom_token.ts b/packages-exp/auth-exp/src/core/strategies/custom_token.ts deleted file mode 100644 index 8ba413f6479..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/custom_token.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { signInWithCustomToken as getIdTokenResponse } from '../../api/authentication/custom_token'; -import { IdTokenResponse } from '../../model/id_token'; -import { UserCredentialImpl } from '../user/user_credential_impl'; -import { _castAuth } from '../auth/auth_impl'; - -/** - * Asynchronously signs in using a custom token. - * - * @remarks - * Custom tokens are used to integrate Firebase Auth with existing auth systems, and must - * be generated by an auth backend using the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createcustomtoken | createCustomToken} - * method in the {@link https://firebase.google.com/docs/auth/admin | Admin SDK} . - * - * Fails with an error if the token is invalid, expired, or not accepted by the Firebase Auth service. - * - * @param auth - The Auth instance. - * @param customToken - The custom token to sign in with. - * - * @public - */ -export async function signInWithCustomToken( - auth: externs.Auth, - customToken: string -): Promise { - const response: IdTokenResponse = await getIdTokenResponse(auth, { - token: customToken, - returnSecureToken: true - }); - const authInternal = _castAuth(auth); - const cred = await UserCredentialImpl._fromIdTokenResponse( - authInternal, - externs.OperationType.SIGN_IN, - response - ); - await authInternal._updateCurrentUser(cred.user); - return cred; -} diff --git a/packages-exp/auth-exp/src/core/strategies/email.test.ts b/packages-exp/auth-exp/src/core/strategies/email.test.ts deleted file mode 100644 index 4724321a1f7..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/email.test.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import { restore, SinonStub, stub } from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { ActionCodeOperation, ProviderId } from '@firebase/auth-types-exp'; -import { FirebaseError, isNode } from '@firebase/util'; - -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth, testUser } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { ServerError } from '../../api/errors'; -import { User } from '../../model/user'; -import { - fetchSignInMethodsForEmail, - sendEmailVerification, - verifyBeforeUpdateEmail -} from './email'; - -use(chaiAsPromised); -use(sinonChai); - -describe('core/strategies/fetchSignInMethodsForEmail', () => { - const email = 'foo@bar.com'; - const expectedSignInMethods = [ProviderId.PASSWORD, ProviderId.GOOGLE]; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - if (isNode()) { - context('node', () => { - it('should use localhost for the continueUri', async () => { - const mock = mockEndpoint(Endpoint.CREATE_AUTH_URI, { - signinMethods: expectedSignInMethods - }); - const response = await fetchSignInMethodsForEmail(auth, email); - expect(response).to.eql(expectedSignInMethods); - expect(mock.calls[0].request).to.eql({ - identifier: email, - continueUri: 'http://localhost' - }); - }); - }); - } else { - it('should return the sign in methods', async () => { - const mock = mockEndpoint(Endpoint.CREATE_AUTH_URI, { - signinMethods: expectedSignInMethods - }); - const response = await fetchSignInMethodsForEmail(auth, email); - expect(response).to.eql(expectedSignInMethods); - const request = mock.calls[0].request as Record; - expect(request['identifier']).to.eq(email); - // We can't rely on a fixed port number - expect(request['continueUri']).to.match( - /http:\/\/localhost:[0-9]+\/context\.html/ - ); - }); - } - - it('should surface errors', async () => { - const mock = mockEndpoint( - Endpoint.CREATE_AUTH_URI, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL - } - }, - 400 - ); - await expect(fetchSignInMethodsForEmail(auth, email)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls.length).to.eq(1); - }); -}); - -describe('core/strategies/sendEmailVerification', () => { - const email = 'foo@bar.com'; - const idToken = 'access-token'; - let user: User; - let auth: TestAuth; - let reloadStub: SinonStub; - - beforeEach(async () => { - auth = await testAuth(); - user = testUser(auth, 'my-user-uid', email, true); - mockFetch.setUp(); - reloadStub = stub(user, 'reload'); - }); - - afterEach(() => { - mockFetch.tearDown(); - restore(); - }); - - it('should send the email verification', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - requestType: ActionCodeOperation.VERIFY_EMAIL, - email - }); - - await sendEmailVerification(user); - - expect(reloadStub).to.not.have.been.called; - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.VERIFY_EMAIL, - idToken - }); - }); - - it('should reload the user if the API returns a different email', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - requestType: ActionCodeOperation.VERIFY_EMAIL, - email: 'other@email.com' - }); - - await sendEmailVerification(user); - - expect(reloadStub).to.have.been.calledOnce; - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.VERIFY_EMAIL, - idToken - }); - }); - - context('on iOS', () => { - it('should pass action code parameters', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - requestType: ActionCodeOperation.VERIFY_EMAIL, - email - }); - await sendEmailVerification(user, { - handleCodeInApp: true, - iOS: { - bundleId: 'my-bundle' - }, - url: 'my-url', - dynamicLinkDomain: 'fdl-domain' - }); - - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.VERIFY_EMAIL, - idToken, - continueUrl: 'my-url', - dynamicLinkDomain: 'fdl-domain', - canHandleCodeInApp: true, - iosBundleId: 'my-bundle' - }); - }); - }); - - context('on Android', () => { - it('should pass action code parameters', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - requestType: ActionCodeOperation.VERIFY_EMAIL, - email - }); - await sendEmailVerification(user, { - handleCodeInApp: true, - android: { - installApp: false, - minimumVersion: 'my-version', - packageName: 'my-package' - }, - url: 'my-url', - dynamicLinkDomain: 'fdl-domain' - }); - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.VERIFY_EMAIL, - idToken, - continueUrl: 'my-url', - dynamicLinkDomain: 'fdl-domain', - canHandleCodeInApp: true, - androidInstallApp: false, - androidMinimumVersionCode: 'my-version', - androidPackageName: 'my-package' - }); - }); - }); -}); - -describe('core/strategies/verifyBeforeUpdateEmail', () => { - const email = 'foo@bar.com'; - const newEmail = 'newemail@bar.com'; - const idToken = 'access-token'; - let user: User; - let auth: TestAuth; - let reloadStub: SinonStub; - - beforeEach(async () => { - auth = await testAuth(); - user = testUser(auth, 'my-user-uid', email, true); - mockFetch.setUp(); - reloadStub = stub(user, 'reload'); - }); - - afterEach(() => { - mockFetch.tearDown(); - restore(); - }); - - it('should send the email verification', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - email - }); - - await verifyBeforeUpdateEmail(user, newEmail); - - expect(reloadStub).to.not.have.been.called; - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - idToken, - newEmail - }); - }); - - it('should reload the user if the API returns a different email', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - email: 'other@email.com' - }); - - await verifyBeforeUpdateEmail(user, newEmail); - - expect(reloadStub).to.have.been.calledOnce; - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - idToken, - newEmail - }); - }); - - context('on iOS', () => { - it('should pass action code parameters', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - email - }); - await verifyBeforeUpdateEmail(user, newEmail, { - handleCodeInApp: true, - iOS: { - bundleId: 'my-bundle' - }, - url: 'my-url', - dynamicLinkDomain: 'fdl-domain' - }); - - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - idToken, - newEmail, - continueUrl: 'my-url', - dynamicLinkDomain: 'fdl-domain', - canHandleCodeInApp: true, - iosBundleId: 'my-bundle' - }); - }); - }); - - context('on Android', () => { - it('should pass action code parameters', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - email - }); - await verifyBeforeUpdateEmail(user, newEmail, { - handleCodeInApp: true, - android: { - installApp: false, - minimumVersion: 'my-version', - packageName: 'my-package' - }, - url: 'my-url', - dynamicLinkDomain: 'fdl-domain' - }); - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - idToken, - newEmail, - continueUrl: 'my-url', - dynamicLinkDomain: 'fdl-domain', - canHandleCodeInApp: true, - androidInstallApp: false, - androidMinimumVersionCode: 'my-version', - androidPackageName: 'my-package' - }); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/strategies/email.ts b/packages-exp/auth-exp/src/core/strategies/email.ts deleted file mode 100644 index 35345c688ce..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/email.ts +++ /dev/null @@ -1,177 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { - createAuthUri, - CreateAuthUriRequest -} from '../../api/authentication/create_auth_uri'; -import * as api from '../../api/authentication/email_and_password'; -import { User } from '../../model/user'; -import { _getCurrentUrl, _isHttpOrHttps } from '../util/location'; -import { _setActionCodeSettingsOnRequest } from './action_code_settings'; - -/** - * Gets the list of possible sign in methods for the given email address. - * - * @remarks - * This is useful to differentiate methods of sign-in for the same provider, eg. - * {@link EmailAuthProvider} which has 2 methods of sign-in, - * {@link @firebase/auth-types#SignInMethod.EMAIL_PASSWORD} and - * {@link @firebase/auth-types#SignInMethod.EMAIL_LINK} . - * - * @param auth - The Auth instance. - * @param email - The user's email address. - * - * @public - */ -export async function fetchSignInMethodsForEmail( - auth: externs.Auth, - email: string -): Promise { - // createAuthUri returns an error if continue URI is not http or https. - // For environments like Cordova, Chrome extensions, native frameworks, file - // systems, etc, use http://localhost as continue URL. - const continueUri = _isHttpOrHttps() ? _getCurrentUrl() : 'http://localhost'; - const request: CreateAuthUriRequest = { - identifier: email, - continueUri - }; - - const { signinMethods } = await createAuthUri(auth, request); - - return signinMethods || []; -} - -/** - * Sends a verification email to a user. - * - * @remarks - * The verification process is completed by calling {@link applyActionCode}. - * - * @example - * ```javascript - * const actionCodeSettings = { - * url: 'https://www.example.com/?email=user@example.com', - * iOS: { - * bundleId: 'com.example.ios' - * }, - * android: { - * packageName: 'com.example.android', - * installApp: true, - * minimumVersion: '12' - * }, - * handleCodeInApp: true - * }; - * await sendEmailVerification(user, actionCodeSettings); - * // Obtain code from the user. - * await applyActionCode(auth, code); - * ``` - * - * @param user - The user. - * @param actionCodeSettings - The {@link @firebase/auth-types#ActionCodeSettings}. - * - * @public - */ -export async function sendEmailVerification( - user: externs.User, - actionCodeSettings?: externs.ActionCodeSettings | null -): Promise { - const userInternal = user as User; - const idToken = await user.getIdToken(); - const request: api.VerifyEmailRequest = { - requestType: externs.ActionCodeOperation.VERIFY_EMAIL, - idToken - }; - if (actionCodeSettings) { - _setActionCodeSettingsOnRequest( - userInternal.auth, - request, - actionCodeSettings - ); - } - - const { email } = await api.sendEmailVerification(userInternal.auth, request); - - if (email !== user.email) { - await user.reload(); - } -} - -/** - * Sends a verification email to a new email address. - * - * @remarks - * The user's email will be updated to the new one after being verified. - * - * If you have a custom email action handler, you can complete the verification process by calling - * {@link applyActionCode}. - * - * @example - * ```javascript - * const actionCodeSettings = { - * url: 'https://www.example.com/?email=user@example.com', - * iOS: { - * bundleId: 'com.example.ios' - * }, - * android: { - * packageName: 'com.example.android', - * installApp: true, - * minimumVersion: '12' - * }, - * handleCodeInApp: true - * }; - * await verifyBeforeUpdateEmail(user, 'newemail@example.com', actionCodeSettings); - * // Obtain code from the user. - * await applyActionCode(auth, code); - * ``` - * - * @param user - The user. - * @param newEmail - The new email address to be verified before update. - * @param actionCodeSettings - The {@link @firebase/auth-types#ActionCodeSettings}. - * - * @public - */ -export async function verifyBeforeUpdateEmail( - user: externs.User, - newEmail: string, - actionCodeSettings?: externs.ActionCodeSettings | null -): Promise { - const userInternal = user as User; - const idToken = await user.getIdToken(); - const request: api.VerifyAndChangeEmailRequest = { - requestType: externs.ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, - idToken, - newEmail - }; - if (actionCodeSettings) { - _setActionCodeSettingsOnRequest( - userInternal.auth, - request, - actionCodeSettings - ); - } - - const { email } = await api.verifyAndChangeEmail(userInternal.auth, request); - - if (email !== user.email) { - // If the local copy of the email on user is outdated, reload the - // user. - await user.reload(); - } -} diff --git a/packages-exp/auth-exp/src/core/strategies/email_and_password.test.ts b/packages-exp/auth-exp/src/core/strategies/email_and_password.test.ts deleted file mode 100644 index 25b72d592e8..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/email_and_password.test.ts +++ /dev/null @@ -1,448 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinonChai from 'sinon-chai'; - -import { ActionCodeOperation, OperationType } from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { APIUserInfo } from '../../api/account_management/account'; -import { ServerError } from '../../api/errors'; -import { UserCredential } from '../../model/user'; -import { - applyActionCode, - checkActionCode, - confirmPasswordReset, - createUserWithEmailAndPassword, - sendPasswordResetEmail, - signInWithEmailAndPassword, - verifyPasswordResetCode -} from './email_and_password'; - -use(chaiAsPromised); -use(sinonChai); - -describe('core/strategies/sendPasswordResetEmail', () => { - const email = 'foo@bar.com'; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should send a password reset email', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - email - }); - await sendPasswordResetEmail(auth, email); - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.PASSWORD_RESET, - email - }); - }); - - it('should surface errors', async () => { - const mock = mockEndpoint( - Endpoint.SEND_OOB_CODE, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL - } - }, - 400 - ); - await expect(sendPasswordResetEmail(auth, email)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls.length).to.eq(1); - }); - - context('on iOS', () => { - it('should pass action code parameters', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - email - }); - await sendPasswordResetEmail(auth, email, { - handleCodeInApp: true, - iOS: { - bundleId: 'my-bundle' - }, - url: 'my-url', - dynamicLinkDomain: 'fdl-domain' - }); - - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.PASSWORD_RESET, - email, - continueUrl: 'my-url', - dynamicLinkDomain: 'fdl-domain', - canHandleCodeInApp: true, - iosBundleId: 'my-bundle' - }); - }); - }); - - context('on Android', () => { - it('should pass action code parameters', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - email - }); - await sendPasswordResetEmail(auth, email, { - handleCodeInApp: true, - android: { - installApp: false, - minimumVersion: 'my-version', - packageName: 'my-package' - }, - url: 'my-url', - dynamicLinkDomain: 'fdl-domain' - }); - expect(mock.calls[0].request).to.eql({ - requestType: ActionCodeOperation.PASSWORD_RESET, - email, - continueUrl: 'my-url', - dynamicLinkDomain: 'fdl-domain', - canHandleCodeInApp: true, - androidInstallApp: false, - androidMinimumVersionCode: 'my-version', - androidPackageName: 'my-package' - }); - }); - }); -}); - -describe('core/strategies/confirmPasswordReset', () => { - const oobCode = 'oob-code'; - const newPassword = 'new-password'; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should confirm the password reset and not return the email', async () => { - const mock = mockEndpoint(Endpoint.RESET_PASSWORD, { - email: 'foo@bar.com' - }); - const response = await confirmPasswordReset(auth, oobCode, newPassword); - expect(response).to.be.undefined; - expect(mock.calls[0].request).to.eql({ - oobCode, - newPassword - }); - }); - - it('should surface errors', async () => { - const mock = mockEndpoint( - Endpoint.RESET_PASSWORD, - { - error: { - code: 400, - message: ServerError.INVALID_OOB_CODE - } - }, - 400 - ); - await expect( - confirmPasswordReset(auth, oobCode, newPassword) - ).to.be.rejectedWith( - FirebaseError, - 'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).' - ); - expect(mock.calls.length).to.eq(1); - }); -}); - -describe('core/strategies/applyActionCode', () => { - const oobCode = 'oob-code'; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should apply the oob code', async () => { - const mock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {}); - await applyActionCode(auth, oobCode); - expect(mock.calls[0].request).to.eql({ - oobCode - }); - }); - - it('should surface errors', async () => { - const mock = mockEndpoint( - Endpoint.SET_ACCOUNT_INFO, - { - error: { - code: 400, - message: ServerError.INVALID_OOB_CODE - } - }, - 400 - ); - await expect(applyActionCode(auth, oobCode)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).' - ); - expect(mock.calls.length).to.eq(1); - }); -}); - -describe('core/strategies/checkActionCode', () => { - const oobCode = 'oob-code'; - const email = 'foo@bar.com'; - const newEmail = 'new@email.com'; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should verify the oob code', async () => { - const mock = mockEndpoint(Endpoint.RESET_PASSWORD, { - requestType: ActionCodeOperation.PASSWORD_RESET, - email: 'foo@bar.com' - }); - const response = await checkActionCode(auth, oobCode); - expect(response).to.eql({ - data: { - email, - previousEmail: null, - multiFactorInfo: null - }, - operation: ActionCodeOperation.PASSWORD_RESET - }); - expect(mock.calls[0].request).to.eql({ - oobCode - }); - }); - - it('should return the newEmail', async () => { - const mock = mockEndpoint(Endpoint.RESET_PASSWORD, { - requestType: ActionCodeOperation.PASSWORD_RESET, - email, - newEmail - }); - const response = await checkActionCode(auth, oobCode); - expect(response).to.eql({ - data: { - email, - previousEmail: newEmail, - multiFactorInfo: null - }, - operation: ActionCodeOperation.PASSWORD_RESET - }); - expect(mock.calls[0].request).to.eql({ - oobCode - }); - }); - - it('should expect a requestType', async () => { - const mock = mockEndpoint(Endpoint.RESET_PASSWORD, { - email - }); - await expect(checkActionCode(auth, oobCode)).to.be.rejectedWith( - FirebaseError, - 'Firebase: An internal AuthError has occurred. (auth/internal-error).' - ); - expect(mock.calls.length).to.eq(1); - }); - - it('should surface errors', async () => { - const mock = mockEndpoint( - Endpoint.RESET_PASSWORD, - { - error: { - code: 400, - message: ServerError.INVALID_OOB_CODE - } - }, - 400 - ); - await expect(checkActionCode(auth, oobCode)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).' - ); - expect(mock.calls.length).to.eq(1); - }); -}); - -describe('core/strategies/verifyPasswordResetCode', () => { - const oobCode = 'oob-code'; - const email = 'foo@bar.com'; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should verify the oob code', async () => { - const mock = mockEndpoint(Endpoint.RESET_PASSWORD, { - requestType: ActionCodeOperation.PASSWORD_RESET, - email: 'foo@bar.com', - previousEmail: null - }); - const response = await verifyPasswordResetCode(auth, oobCode); - expect(response).to.eq(email); - expect(mock.calls[0].request).to.eql({ - oobCode - }); - }); - - it('should expect a requestType', async () => { - const mock = mockEndpoint(Endpoint.RESET_PASSWORD, { - email - }); - await expect(verifyPasswordResetCode(auth, oobCode)).to.be.rejectedWith( - FirebaseError, - 'Firebase: An internal AuthError has occurred. (auth/internal-error).' - ); - expect(mock.calls.length).to.eq(1); - }); - - it('should surface errors', async () => { - const mock = mockEndpoint( - Endpoint.RESET_PASSWORD, - { - error: { - code: 400, - message: ServerError.INVALID_OOB_CODE - } - }, - 400 - ); - await expect(verifyPasswordResetCode(auth, oobCode)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).' - ); - expect(mock.calls.length).to.eq(1); - }); -}); - -describe('core/strategies/email_and_password/createUserWithEmailAndPassword', () => { - let auth: TestAuth; - const serverUser: APIUserInfo = { - localId: 'local-id' - }; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - mockEndpoint(Endpoint.SIGN_UP, { - idToken: 'id-token', - refreshToken: 'refresh-token', - expiresIn: '1234', - localId: serverUser.localId! - }); - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [serverUser] - }); - }); - afterEach(mockFetch.tearDown); - - it('should sign in the user', async () => { - const { - _tokenResponse, - user, - operationType - } = (await createUserWithEmailAndPassword( - auth, - 'some-email', - 'some-password' - )) as UserCredential; - expect(_tokenResponse).to.eql({ - idToken: 'id-token', - refreshToken: 'refresh-token', - expiresIn: '1234', - localId: serverUser.localId! - }); - expect(operationType).to.eq(OperationType.SIGN_IN); - expect(user.uid).to.eq(serverUser.localId); - expect(user.isAnonymous).to.be.false; - }); -}); - -describe('core/strategies/email_and_password/signInWithEmailAndPassword', () => { - let auth: TestAuth; - const serverUser: APIUserInfo = { - localId: 'local-id' - }; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - mockEndpoint(Endpoint.SIGN_IN_WITH_PASSWORD, { - idToken: 'id-token', - refreshToken: 'refresh-token', - expiresIn: '1234', - localId: serverUser.localId! - }); - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [serverUser] - }); - }); - afterEach(mockFetch.tearDown); - - it('should sign in the user', async () => { - const { - _tokenResponse, - user, - operationType - } = (await signInWithEmailAndPassword( - auth, - 'some-email', - 'some-password' - )) as UserCredential; - expect(_tokenResponse).to.eql({ - idToken: 'id-token', - refreshToken: 'refresh-token', - expiresIn: '1234', - localId: serverUser.localId! - }); - expect(operationType).to.eq(OperationType.SIGN_IN); - expect(user.uid).to.eq(serverUser.localId); - expect(user.isAnonymous).to.be.false; - }); -}); diff --git a/packages-exp/auth-exp/src/core/strategies/email_and_password.ts b/packages-exp/auth-exp/src/core/strategies/email_and_password.ts deleted file mode 100644 index 3c0d59fc287..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/email_and_password.ts +++ /dev/null @@ -1,263 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import * as account from '../../api/account_management/email_and_password'; -import * as authentication from '../../api/authentication/email_and_password'; -import { signUp } from '../../api/authentication/sign_up'; -import { MultiFactorInfo } from '../../mfa/mfa_info'; -import { EmailAuthProvider } from '../providers/email'; -import { UserCredentialImpl } from '../user/user_credential_impl'; -import { _assert } from '../util/assert'; -import { _setActionCodeSettingsOnRequest } from './action_code_settings'; -import { signInWithCredential } from './credential'; -import { _castAuth } from '../auth/auth_impl'; -import { AuthErrorCode } from '../errors'; - -/** - * Sends a password reset email to the given email address. - * - * @remarks - * To complete the password reset, call {@link confirmPasswordReset} with the code supplied in - * the email sent to the user, along with the new password specified by the user. - * - * @example - * ```javascript - * const actionCodeSettings = { - * url: 'https://www.example.com/?email=user@example.com', - * iOS: { - * bundleId: 'com.example.ios' - * }, - * android: { - * packageName: 'com.example.android', - * installApp: true, - * minimumVersion: '12' - * }, - * handleCodeInApp: true - * }; - * await sendPasswordResetEmail(auth, 'user@example.com', actionCodeSettings); - * // Obtain code from user. - * await confirmPasswordReset('user@example.com', code); - * ``` - * - * @param auth - The Auth instance. - * @param email - The user's email address. - * @param actionCodeSettings - The {@link @firebase/auth-types#ActionCodeSettings}. - * - * @public - */ -export async function sendPasswordResetEmail( - auth: externs.Auth, - email: string, - actionCodeSettings?: externs.ActionCodeSettings -): Promise { - const request: authentication.PasswordResetRequest = { - requestType: externs.ActionCodeOperation.PASSWORD_RESET, - email - }; - if (actionCodeSettings) { - _setActionCodeSettingsOnRequest(auth, request, actionCodeSettings); - } - - await authentication.sendPasswordResetEmail(auth, request); -} - -/** - * Completes the password reset process, given a confirmation code and new password. - * - * @param auth - The Auth instance. - * @param oobCode - A confirmation code sent to the user. - * @param newPassword - The new password. - * - * @public - */ -export async function confirmPasswordReset( - auth: externs.Auth, - oobCode: string, - newPassword: string -): Promise { - await account.resetPassword(auth, { - oobCode, - newPassword - }); - // Do not return the email. -} - -/** - * Applies a verification code sent to the user by email or other out-of-band mechanism. - * - * @param auth - The Auth instance. - * @param oobCode - A verification code sent to the user. - * - * @public - */ -export async function applyActionCode( - auth: externs.Auth, - oobCode: string -): Promise { - await account.applyActionCode(auth, { oobCode }); -} - -/** - * Checks a verification code sent to the user by email or other out-of-band mechanism. - * - * @returns metadata about the code. - * - * @param auth - The Auth instance. - * @param oobCode - A verification code sent to the user. - * - * @public - */ -export async function checkActionCode( - auth: externs.Auth, - oobCode: string -): Promise { - const response = await account.resetPassword(auth, { oobCode }); - - // Email could be empty only if the request type is EMAIL_SIGNIN or - // VERIFY_AND_CHANGE_EMAIL. - // New email should not be empty if the request type is - // VERIFY_AND_CHANGE_EMAIL. - // Multi-factor info could not be empty if the request type is - // REVERT_SECOND_FACTOR_ADDITION. - const operation = response.requestType; - _assert(operation, auth, AuthErrorCode.INTERNAL_ERROR); - switch (operation) { - case externs.ActionCodeOperation.EMAIL_SIGNIN: - break; - case externs.ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL: - _assert(response.newEmail, auth, AuthErrorCode.INTERNAL_ERROR); - break; - case externs.ActionCodeOperation.REVERT_SECOND_FACTOR_ADDITION: - _assert(response.mfaInfo, auth, AuthErrorCode.INTERNAL_ERROR); - // fall through - default: - _assert(response.email, auth, AuthErrorCode.INTERNAL_ERROR); - } - - // The multi-factor info for revert second factor addition - let multiFactorInfo: MultiFactorInfo | null = null; - if (response.mfaInfo) { - multiFactorInfo = MultiFactorInfo._fromServerResponse( - _castAuth(auth), - response.mfaInfo - ); - } - - return { - data: { - email: - (response.requestType === - externs.ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL - ? response.newEmail - : response.email) || null, - previousEmail: - (response.requestType === - externs.ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL - ? response.email - : response.newEmail) || null, - multiFactorInfo - }, - operation - }; -} - -/** - * Checks a password reset code sent to the user by email or other out-of-band mechanism. - * - * @returns the user's email address if valid. - * - * @param auth - The Auth instance. - * @param code - A verification code sent to the user. - * - * @public - */ -export async function verifyPasswordResetCode( - auth: externs.Auth, - code: string -): Promise { - const { data } = await checkActionCode(auth, code); - // Email should always be present since a code was sent to it - return data.email!; -} - -/** - * Creates a new user account associated with the specified email address and password. - * - * @remarks - * On successful creation of the user account, this user will also be signed in to your application. - * - * User account creation can fail if the account already exists or the password is invalid. - * - * Note: The email address acts as a unique identifier for the user and enables an email-based - * password reset. This function will create a new user account and set the initial user password. - * - * @param auth - The Auth instance. - * @param email - The user's email address. - * @param password - The user's chosen password. - * - * @public - */ -export async function createUserWithEmailAndPassword( - auth: externs.Auth, - email: string, - password: string -): Promise { - const authInternal = _castAuth(auth); - const response = await signUp(auth, { - returnSecureToken: true, - email, - password - }); - - const userCredential = await UserCredentialImpl._fromIdTokenResponse( - authInternal, - externs.OperationType.SIGN_IN, - response - ); - await authInternal._updateCurrentUser(userCredential.user); - - return userCredential; -} - -/** - * Asynchronously signs in using an email and password. - * - * @remarks - * Fails with an error if the email address and password do not match. - * - * Note: The user's password is NOT the password used to access the user's email account. The - * email address serves as a unique identifier for the user, and the password is used to access - * the user's account in your Firebase project. See also: {@link createUserWithEmailAndPassword}. - * - * @param auth - The Auth instance. - * @param email - The users email address. - * @param password - The users password. - * - * @public - */ -export function signInWithEmailAndPassword( - auth: externs.Auth, - email: string, - password: string -): Promise { - return signInWithCredential( - auth, - EmailAuthProvider.credential(email, password) - ); -} diff --git a/packages-exp/auth-exp/src/core/strategies/email_link.test.ts b/packages-exp/auth-exp/src/core/strategies/email_link.test.ts deleted file mode 100644 index 6ee44a338ab..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/email_link.test.ts +++ /dev/null @@ -1,277 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinonChai from 'sinon-chai'; - -import * as externs from '@firebase/auth-types-exp'; -import { OperationType } from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { APIUserInfo } from '../../api/account_management/account'; -import { ServerError } from '../../api/errors'; -import { UserCredential } from '../../model/user'; -import { - isSignInWithEmailLink, - sendSignInLinkToEmail, - signInWithEmailLink -} from './email_link'; - -use(chaiAsPromised); -use(sinonChai); - -describe('core/strategies/sendSignInLinkToEmail', () => { - const email = 'foo@bar.com'; - - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - }); - - afterEach(mockFetch.tearDown); - - it('should send a sign in link via email', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - email - }); - await sendSignInLinkToEmail(auth, email, { - handleCodeInApp: true, - url: 'continue-url' - }); - expect(mock.calls[0].request).to.eql({ - requestType: externs.ActionCodeOperation.EMAIL_SIGNIN, - email, - canHandleCodeInApp: true, - continueUrl: 'continue-url' - }); - }); - - it('should require handleCodeInApp to be true', async () => { - await expect(sendSignInLinkToEmail(auth, email)).to.be.rejectedWith( - FirebaseError, - 'auth/argument-error).' - ); - }); - - it('should surface errors', async () => { - const mock = mockEndpoint( - Endpoint.SEND_OOB_CODE, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL - } - }, - 400 - ); - await expect( - sendSignInLinkToEmail(auth, email, { - handleCodeInApp: true, - url: 'continue-url' - }) - ).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls.length).to.eq(1); - }); - - context('on iOS', () => { - it('should pass action code parameters', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - email - }); - await sendSignInLinkToEmail(auth, email, { - handleCodeInApp: true, - iOS: { - bundleId: 'my-bundle' - }, - url: 'my-url', - dynamicLinkDomain: 'fdl-domain' - }); - - expect(mock.calls[0].request).to.eql({ - requestType: externs.ActionCodeOperation.EMAIL_SIGNIN, - email, - continueUrl: 'my-url', - dynamicLinkDomain: 'fdl-domain', - canHandleCodeInApp: true, - iosBundleId: 'my-bundle' - }); - }); - }); - - context('on Android', () => { - it('should pass action code parameters', async () => { - const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, { - email - }); - await sendSignInLinkToEmail(auth, email, { - handleCodeInApp: true, - android: { - installApp: false, - minimumVersion: 'my-version', - packageName: 'my-package' - }, - url: 'my-url', - dynamicLinkDomain: 'fdl-domain' - }); - expect(mock.calls[0].request).to.eql({ - requestType: externs.ActionCodeOperation.EMAIL_SIGNIN, - email, - continueUrl: 'my-url', - dynamicLinkDomain: 'fdl-domain', - canHandleCodeInApp: true, - androidInstallApp: false, - androidMinimumVersionCode: 'my-version', - androidPackageName: 'my-package' - }); - }); - }); -}); - -describe('core/strategies/isSignInWithEmailLink', () => { - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - }); - - context('simple links', () => { - it('should recognize sign in links', () => { - const link = - 'https://www.example.com/action?mode=signIn&oobCode=oobCode&apiKey=API_KEY'; - expect(isSignInWithEmailLink(auth, link)).to.be.true; - }); - - it('should not recognize other email links', () => { - const link = - 'https://www.example.com/action?mode=verifyEmail&oobCode=oobCode&apiKey=API_KEY'; - expect(isSignInWithEmailLink(auth, link)).to.be.false; - }); - - it('should not recognize invalid links', () => { - const link = 'https://www.example.com/action?mode=signIn'; - expect(isSignInWithEmailLink(auth, link)).to.be.false; - }); - }); - - context('deep links', () => { - it('should recognize valid links', () => { - const deepLink = - 'https://www.example.com/action?mode=signIn&oobCode=oobCode&apiKey=API_KEY'; - const link = `https://example.app.goo.gl/?link=${encodeURIComponent( - deepLink - )}`; - expect(isSignInWithEmailLink(auth, link)).to.be.true; - }); - - it('should recognize valid links with deep_link_id', () => { - const deepLink = - 'https://www.example.com/action?mode=signIn&oobCode=oobCode&apiKey=API_KEY'; - const link = `somexampleiosurl://google/link?deep_link_id=${encodeURIComponent( - deepLink - )}`; - expect(isSignInWithEmailLink(auth, link)).to.be.true; - }); - - it('should reject other email links', () => { - const deepLink = - 'https://www.example.com/action?mode=verifyEmail&oobCode=oobCode&apiKey=API_KEY'; - const link = `https://example.app.goo.gl/?link=${encodeURIComponent( - deepLink - )}`; - expect(isSignInWithEmailLink(auth, link)).to.be.false; - }); - - it('should reject invalid links', () => { - const deepLink = 'https://www.example.com/action?mode=signIn'; - const link = `https://example.app.goo.gl/?link=${encodeURIComponent( - deepLink - )}`; - expect(isSignInWithEmailLink(auth, link)).to.be.false; - }); - }); -}); - -describe('core/strategies/email_and_password/signInWithEmailLink', () => { - let auth: TestAuth; - const serverUser: APIUserInfo = { - localId: 'local-id' - }; - - beforeEach(async () => { - auth = await testAuth(); - mockFetch.setUp(); - mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { - idToken: 'id-token', - refreshToken: 'refresh-token', - expiresIn: '1234', - localId: serverUser.localId! - }); - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [serverUser] - }); - }); - afterEach(mockFetch.tearDown); - - it('should sign in the user', async () => { - const continueUrl = 'https://www.example.com/path/to/file?a=1&b=2#c=3'; - const actionLink = - 'https://www.example.com/finishSignIn?' + - 'oobCode=CODE&mode=signIn&apiKey=API_KEY&' + - 'continueUrl=' + - encodeURIComponent(continueUrl) + - '&languageCode=en&state=bla'; - const { _tokenResponse, user, operationType } = (await signInWithEmailLink( - auth, - 'some-email', - actionLink - )) as UserCredential; - expect(_tokenResponse).to.eql({ - idToken: 'id-token', - refreshToken: 'refresh-token', - expiresIn: '1234', - localId: serverUser.localId! - }); - expect(operationType).to.eq(OperationType.SIGN_IN); - expect(user.uid).to.eq(serverUser.localId); - expect(user.isAnonymous).to.be.false; - }); - - context('mismatched tenant ID', () => { - it('should throw an error', async () => { - const continueUrl = 'https://www.example.com/path/to/file?a=1&b=2#c=3'; - const actionLink = - 'https://www.example.com/finishSignIn?' + - 'oobCode=CODE&mode=signIn&apiKey=API_KEY&' + - 'continueUrl=' + - encodeURIComponent(continueUrl) + - '&languageCode=en&tenantId=OTHER_TENANT_ID&state=bla'; - await expect( - signInWithEmailLink(auth, 'some-email', actionLink) - ).to.be.rejectedWith(FirebaseError, 'auth/tenant-id-mismatch'); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/strategies/email_link.ts b/packages-exp/auth-exp/src/core/strategies/email_link.ts deleted file mode 100644 index a2230beb93a..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/email_link.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import * as api from '../../api/authentication/email_and_password'; -import { ActionCodeURL } from '../action_code_url'; -import { EmailAuthProvider } from '../providers/email'; -import { _getCurrentUrl } from '../util/location'; -import { _setActionCodeSettingsOnRequest } from './action_code_settings'; -import { signInWithCredential } from './credential'; -import { AuthErrorCode } from '../errors'; -import { _assert } from '../util/assert'; - -/** - * Sends a sign-in email link to the user with the specified email. - * - * @remarks - * The sign-in operation has to always be completed in the app unlike other out of band email - * actions (password reset and email verifications). This is because, at the end of the flow, - * the user is expected to be signed in and their Auth state persisted within the app. - * - * To complete sign in with the email link, call {@link signInWithEmailLink} with the email - * address and the email link supplied in the email sent to the user. - * - * @example - * ```javascript - * const actionCodeSettings = { - * url: 'https://www.example.com/?email=user@example.com', - * iOS: { - * bundleId: 'com.example.ios' - * }, - * android: { - * packageName: 'com.example.android', - * installApp: true, - * minimumVersion: '12' - * }, - * handleCodeInApp: true - * }; - * await sendSignInLinkToEmail(auth, 'user@example.com', actionCodeSettings); - * // Obtain emailLink from the user. - * if(isSignInWithEmailLink(auth, emailLink)) { - * await signInWithEmailLink('user@example.com', 'user@example.com', emailLink); - * } - * ``` - * - * @param auth - The Auth instance. - * @param email - The user's email address. - * @param actionCodeSettings - The {@link @firebase/auth-types#ActionCodeSettings}. - * - * @public - */ -export async function sendSignInLinkToEmail( - auth: externs.Auth, - email: string, - actionCodeSettings?: externs.ActionCodeSettings -): Promise { - const request: api.EmailSignInRequest = { - requestType: externs.ActionCodeOperation.EMAIL_SIGNIN, - email - }; - _assert( - actionCodeSettings?.handleCodeInApp, - auth, - AuthErrorCode.ARGUMENT_ERROR - ); - if (actionCodeSettings) { - _setActionCodeSettingsOnRequest(auth, request, actionCodeSettings); - } - - await api.sendSignInLinkToEmail(auth, request); -} - -/** - * Checks if an incoming link is a sign-in with email link suitable for {@link signInWithEmailLink}. - * - * @param auth - The Auth instance. - * @param emailLink - The link sent to the user's email address. - * - * @public - */ -export function isSignInWithEmailLink( - auth: externs.Auth, - emailLink: string -): boolean { - const actionCodeUrl = ActionCodeURL.parseLink(emailLink); - return actionCodeUrl?.operation === externs.ActionCodeOperation.EMAIL_SIGNIN; -} - -/** - * Asynchronously signs in using an email and sign-in email link. - * - * @remarks - * If no link is passed, the link is inferred from the current URL. - * - * Fails with an error if the email address is invalid or OTP in email link expires. - * - * Note: Confirm the link is a sign-in email link before calling this method firebase.auth.Auth.isSignInWithEmailLink. - * - * @example - * ```javascript - * const actionCodeSettings = { - * url: 'https://www.example.com/?email=user@example.com', - * iOS: { - * bundleId: 'com.example.ios' - * }, - * android: { - * packageName: 'com.example.android', - * installApp: true, - * minimumVersion: '12' - * }, - * handleCodeInApp: true - * }; - * await sendSignInLinkToEmail(auth, 'user@example.com', actionCodeSettings); - * // Obtain emailLink from the user. - * if(isSignInWithEmailLink(auth, emailLink)) { - * await signInWithEmailLink('user@example.com', 'user@example.com', emailLink); - * } - * ``` - * - * @param auth - The Auth instance. - * @param email - The user's email address. - * @param emailLink - The link sent to the user's email address. - * - * @public - */ -export async function signInWithEmailLink( - auth: externs.Auth, - email: string, - emailLink?: string -): Promise { - const credential = EmailAuthProvider.credentialWithLink( - email, - emailLink || _getCurrentUrl() - ); - // Check if the tenant ID in the email link matches the tenant ID on Auth - // instance. - _assert( - credential.tenantId === (auth.tenantId || null), - auth, - AuthErrorCode.TENANT_ID_MISMATCH - ); - return signInWithCredential(auth, credential); -} diff --git a/packages-exp/auth-exp/src/core/strategies/idp.test.ts b/packages-exp/auth-exp/src/core/strategies/idp.test.ts deleted file mode 100644 index 142233ed86e..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/idp.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { OperationType } from '@firebase/auth-types-exp'; - -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { TEST_ID_TOKEN_RESPONSE } from '../../../test/helpers/id_token_response'; -import { makeJWT } from '../../../test/helpers/jwt'; -import { testAuth, testUser, TestAuth } from '../../../test/helpers/mock_auth'; -import * as fetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { User } from '../../model/user'; -import * as reauthenticate from '../../core/user/reauthenticate'; -import * as linkUnlink from '../../core/user/link_unlink'; -import * as credential from '../../core/strategies/credential'; - -import * as idpTasks from './idp'; -import { UserCredentialImpl } from '../user/user_credential_impl'; - -use(chaiAsPromised); -use(sinonChai); - -describe('core/strategies/idb', () => { - let auth: TestAuth; - let user: User; - let signInEndpoint: fetch.Route; - - beforeEach(async () => { - auth = await testAuth(); - user = testUser(auth, 'uid', 'email', true); - - fetch.setUp(); - - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [{ localId: 'uid' }] - }); - - signInEndpoint = mockEndpoint(Endpoint.SIGN_IN_WITH_IDP, { - ...TEST_ID_TOKEN_RESPONSE, - idToken: makeJWT({ sub: 'uid' }) - }); - }); - - afterEach(() => { - fetch.tearDown(); - sinon.restore(); - }); - - describe('signIn', () => { - it('builds a request and calls the endpoint', async () => { - await idpTasks._signIn({ - auth, - requestUri: 'request-uri', - sessionId: 'session-id', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - postBody: 'post-body' - }); - - expect(signInEndpoint.calls[0].request).to.eql({ - requestUri: 'request-uri', - sessionId: 'session-id', - postBody: 'post-body', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - returnSecureToken: true - }); - }); - - it('returns a user credential with the signed in user', async () => { - const userCred = await idpTasks._signIn({ - auth, - requestUri: 'request-uri', - sessionId: 'session-id', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - postBody: 'post-body' - }); - - expect(userCred.operationType).to.eq(OperationType.SIGN_IN); - expect(userCred.user.uid).to.eq('uid'); - }); - - it('passes through the bypassAuthState flag', async () => { - const stub = sinon - .stub(credential, '_signInWithCredential') - .returns(Promise.resolve(({} as unknown) as UserCredentialImpl)); - await idpTasks._signIn({ - auth, - user, - requestUri: 'request-uri', - sessionId: 'session-id', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - postBody: 'post-body', - bypassAuthState: true - }); - expect(stub.getCall(0).lastArg).to.be.true; - }); - }); - - describe('reauth', () => { - it('builds a request and calls the endpoint', async () => { - await idpTasks._reauth({ - auth, - user, - requestUri: 'request-uri', - sessionId: 'session-id', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - postBody: 'post-body' - }); - - expect(signInEndpoint.calls[0].request).to.eql({ - requestUri: 'request-uri', - sessionId: 'session-id', - postBody: 'post-body', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - returnSecureToken: true - }); - }); - - it('returns a user credential with the reauthed in user', async () => { - const userCred = await idpTasks._reauth({ - auth, - user, - requestUri: 'request-uri', - sessionId: 'session-id', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - postBody: 'post-body' - }); - - expect(userCred.operationType).to.eq(OperationType.REAUTHENTICATE); - expect(userCred.user.uid).to.eq('uid'); - }); - - it('passes through the bypassAuthState flag', async () => { - const stub = sinon - .stub(reauthenticate, '_reauthenticate') - .returns(Promise.resolve(({} as unknown) as UserCredentialImpl)); - await idpTasks._reauth({ - auth, - user, - requestUri: 'request-uri', - sessionId: 'session-id', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - postBody: 'post-body', - bypassAuthState: true - }); - expect(stub.getCall(0).lastArg).to.be.true; - }); - }); - - describe('link', () => { - it('builds a request and calls the endpoint', async () => { - const idTokenBeforeLink = await user.getIdToken(); - await idpTasks._link({ - auth, - user, - requestUri: 'request-uri', - sessionId: 'session-id', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - postBody: 'post-body' - }); - - expect(signInEndpoint.calls[0].request).to.eql({ - requestUri: 'request-uri', - sessionId: 'session-id', - postBody: 'post-body', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - returnSecureToken: true, - idToken: idTokenBeforeLink - }); - }); - - it('returns a user credential with the reauthed in user', async () => { - const userCred = await idpTasks._link({ - auth, - user, - requestUri: 'request-uri', - sessionId: 'session-id', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - postBody: 'post-body' - }); - - expect(userCred.operationType).to.eq(OperationType.LINK); - expect(userCred.user.uid).to.eq('uid'); - }); - - it('passes through the bypassAuthState flag', async () => { - const stub = sinon - .stub(linkUnlink, '_link') - .returns(Promise.resolve(({} as unknown) as UserCredentialImpl)); - await idpTasks._link({ - auth, - user, - requestUri: 'request-uri', - sessionId: 'session-id', - tenantId: 'tenant-id', - pendingToken: 'pending-token', - postBody: 'post-body', - bypassAuthState: true - }); - expect(stub.getCall(0).lastArg).to.be.true; - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/strategies/idp.ts b/packages-exp/auth-exp/src/core/strategies/idp.ts deleted file mode 100644 index ebf7e7ad0a2..00000000000 --- a/packages-exp/auth-exp/src/core/strategies/idp.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { - signInWithIdp, - SignInWithIdpRequest -} from '../../api/authentication/idp'; -import { PhoneOrOauthTokenResponse } from '../../api/authentication/mfa'; -import { Auth } from '../../model/auth'; -import { IdTokenResponse } from '../../model/id_token'; -import { User, UserCredential } from '../../model/user'; -import { AuthCredential } from '../credentials'; -import { _link as _linkUser } from '../user/link_unlink'; -import { _reauthenticate } from '../user/reauthenticate'; -import { _assert } from '../util/assert'; -import { _signInWithCredential } from './credential'; -import { AuthErrorCode } from '../errors'; - -/** @internal */ -export interface IdpTaskParams { - auth: Auth; - requestUri: string; - sessionId?: string; - tenantId?: string; - postBody?: string; - pendingToken?: string; - user?: User; - bypassAuthState?: boolean; -} - -/** @internal */ -export type IdpTask = (params: IdpTaskParams) => Promise; - -/** @internal */ -class IdpCredential extends AuthCredential { - constructor(readonly params: IdpTaskParams) { - super(externs.ProviderId.CUSTOM, externs.ProviderId.CUSTOM); - } - - _getIdTokenResponse(auth: Auth): Promise { - return signInWithIdp(auth, this._buildIdpRequest()); - } - - _linkToIdToken(auth: Auth, idToken: string): Promise { - return signInWithIdp(auth, this._buildIdpRequest(idToken)); - } - - _getReauthenticationResolver(auth: Auth): Promise { - return signInWithIdp(auth, this._buildIdpRequest()); - } - - private _buildIdpRequest(idToken?: string): SignInWithIdpRequest { - const request: SignInWithIdpRequest = { - requestUri: this.params.requestUri, - sessionId: this.params.sessionId, - postBody: this.params.postBody || null, - tenantId: this.params.tenantId, - pendingToken: this.params.pendingToken, - returnSecureToken: true - }; - - if (idToken) { - request.idToken = idToken; - } - - return request; - } -} - -/** @internal */ -export function _signIn(params: IdpTaskParams): Promise { - return _signInWithCredential( - params.auth, - new IdpCredential(params), - params.bypassAuthState - ) as Promise; -} - -/** @internal */ -export function _reauth(params: IdpTaskParams): Promise { - const { auth, user } = params; - _assert(user, auth, AuthErrorCode.INTERNAL_ERROR); - return _reauthenticate( - user, - new IdpCredential(params), - params.bypassAuthState - ); -} - -/** @internal */ -export async function _link(params: IdpTaskParams): Promise { - const { auth, user } = params; - _assert(user, auth, AuthErrorCode.INTERNAL_ERROR); - return _linkUser(user, new IdpCredential(params), params.bypassAuthState); -} diff --git a/packages-exp/auth-exp/src/core/user/account_info.ts b/packages-exp/auth-exp/src/core/user/account_info.ts deleted file mode 100644 index 7461c23f3a9..00000000000 --- a/packages-exp/auth-exp/src/core/user/account_info.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { - updateEmailPassword as apiUpdateEmailPassword, - UpdateEmailPasswordRequest -} from '../../api/account_management/email_and_password'; -import { updateProfile as apiUpdateProfile } from '../../api/account_management/profile'; -import { User } from '../../model/user'; -import { _logoutIfInvalidated } from './invalidation'; - -interface Profile { - displayName?: string | null; - photoURL?: string | null; -} - -/** - * Updates a user's profile data. - * - * @param user - The user. - * @param profile - The profile's `displayName` and `photoURL` to update. - * - * @public - */ -export async function updateProfile( - user: externs.User, - { displayName, photoURL: photoUrl }: Profile -): Promise { - if (displayName === undefined && photoUrl === undefined) { - return; - } - - const userInternal = user as User; - const idToken = await user.getIdToken(); - const profileRequest = { - idToken, - displayName, - photoUrl, - returnSecureToken: true - }; - const response = await _logoutIfInvalidated( - userInternal, - apiUpdateProfile(userInternal.auth, profileRequest) - ); - - userInternal.displayName = response.displayName || null; - userInternal.photoURL = response.photoUrl || null; - - // Update the password provider as well - const passwordProvider = userInternal.providerData.find( - ({ providerId }) => providerId === externs.ProviderId.PASSWORD - ); - if (passwordProvider) { - passwordProvider.displayName = user.displayName; - passwordProvider.photoURL = user.photoURL; - } - - await userInternal._updateTokensIfNecessary(response); -} - -/** - * Updates the user's email address. - * - * @remarks - * An email will be sent to the original email address (if it was set) that allows to revoke the - * email address change, in order to protect them from account hijacking. - * - * Important: this is a security sensitive operation that requires the user to have recently signed - * in. If this requirement isn't met, ask the user to authenticate again and then call - * {@link reauthenticateWithCredential}. - * - * @param user - The user. - * @param newEmail - The new email address. - * - * @public - */ -export function updateEmail( - user: externs.User, - newEmail: string -): Promise { - return updateEmailOrPassword(user as User, newEmail, null); -} - -/** - * Updates the user's password. - * - * @remarks - * Important: this is a security sensitive operation that requires the user to have recently signed - * in. If this requirement isn't met, ask the user to authenticate again and then call - * {@link reauthenticateWithCredential}. - * - * @param user - The user. - * @param newPassword - The new password. - * - * @public - */ -export function updatePassword( - user: externs.User, - newPassword: string -): Promise { - return updateEmailOrPassword(user as User, null, newPassword); -} - -/** @internal */ -async function updateEmailOrPassword( - user: User, - email: string | null, - password: string | null -): Promise { - const { auth } = user; - const idToken = await user.getIdToken(); - const request: UpdateEmailPasswordRequest = { - idToken, - returnSecureToken: true - }; - - if (email) { - request.email = email; - } - - if (password) { - request.password = password; - } - - const response = await _logoutIfInvalidated( - user, - apiUpdateEmailPassword(auth, request) - ); - await user._updateTokensIfNecessary(response, /* reload */ true); -} diff --git a/packages-exp/auth-exp/src/core/user/user_impl.ts b/packages-exp/auth-exp/src/core/user/user_impl.ts deleted file mode 100644 index 07e821db460..00000000000 --- a/packages-exp/auth-exp/src/core/user/user_impl.ts +++ /dev/null @@ -1,319 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { NextFn } from '@firebase/util'; - -import { - APIUserInfo, - deleteAccount -} from '../../api/account_management/account'; -import { FinalizeMfaResponse } from '../../api/authentication/mfa'; -import { Auth } from '../../model/auth'; -import { IdTokenResponse } from '../../model/id_token'; -import { MutableUserInfo, User, UserParameters } from '../../model/user'; -import { AuthErrorCode } from '../errors'; -import { PersistedBlob } from '../persistence'; -import { _assert } from '../util/assert'; -import { getIdTokenResult } from './id_token_result'; -import { _logoutIfInvalidated } from './invalidation'; -import { ProactiveRefresh } from './proactive_refresh'; -import { _reloadWithoutSaving, reload } from './reload'; -import { StsTokenManager } from './token_manager'; -import { UserMetadata } from './user_metadata'; - -function assertStringOrUndefined( - assertion: unknown, - appName: string -): asserts assertion is string | undefined { - _assert( - typeof assertion === 'string' || typeof assertion === 'undefined', - AuthErrorCode.INTERNAL_ERROR, - { appName } - ); -} - -export class UserImpl implements User { - // For the user object, provider is always Firebase. - readonly providerId = externs.ProviderId.FIREBASE; - stsTokenManager: StsTokenManager; - // Last known accessToken so we know when it changes - private accessToken: string | null; - - uid: string; - auth: Auth; - emailVerified = false; - isAnonymous = false; - tenantId: string | null = null; - readonly metadata: UserMetadata; - providerData: MutableUserInfo[] = []; - - // Optional fields from UserInfo - displayName: string | null; - email: string | null; - phoneNumber: string | null; - photoURL: string | null; - - _redirectEventId?: string; - private readonly proactiveRefresh = new ProactiveRefresh(this); - - constructor({ uid, auth, stsTokenManager, ...opt }: UserParameters) { - this.uid = uid; - this.auth = auth; - this.stsTokenManager = stsTokenManager; - this.accessToken = stsTokenManager.accessToken; - this.displayName = opt.displayName || null; - this.email = opt.email || null; - this.phoneNumber = opt.phoneNumber || null; - this.photoURL = opt.photoURL || null; - this.isAnonymous = opt.isAnonymous || false; - this.metadata = new UserMetadata( - opt.createdAt || undefined, - opt.lastLoginAt || undefined - ); - } - - async getIdToken(forceRefresh?: boolean): Promise { - const accessToken = await _logoutIfInvalidated( - this, - this.stsTokenManager.getToken(this.auth, forceRefresh) - ); - _assert(accessToken, this.auth, AuthErrorCode.INTERNAL_ERROR); - - if (this.accessToken !== accessToken) { - this.accessToken = accessToken; - await this.auth._persistUserIfCurrent(this); - this.auth._notifyListenersIfCurrent(this); - } - - return accessToken; - } - - getIdTokenResult(forceRefresh?: boolean): Promise { - return getIdTokenResult(this, forceRefresh); - } - - reload(): Promise { - return reload(this); - } - - private reloadUserInfo: APIUserInfo | null = null; - private reloadListener: NextFn | null = null; - - _assign(user: User): void { - if (this === user) { - return; - } - _assert(this.uid === user.uid, this.auth, AuthErrorCode.INTERNAL_ERROR); - this.displayName = user.displayName; - this.photoURL = user.photoURL; - this.email = user.email; - this.emailVerified = user.emailVerified; - this.phoneNumber = user.phoneNumber; - this.isAnonymous = user.isAnonymous; - this.tenantId = user.tenantId; - this.providerData = user.providerData.map(userInfo => ({ ...userInfo })); - this.metadata._copy(user.metadata); - this.stsTokenManager._assign(user.stsTokenManager); - } - - _clone(): User { - return new UserImpl({ - ...this, - stsTokenManager: this.stsTokenManager._clone() - }); - } - - _onReload(callback: NextFn): void { - // There should only ever be one listener, and that is a single instance of MultiFactorUser - _assert(!this.reloadListener, this.auth, AuthErrorCode.INTERNAL_ERROR); - this.reloadListener = callback; - if (this.reloadUserInfo) { - this._notifyReloadListener(this.reloadUserInfo); - this.reloadUserInfo = null; - } - } - - _notifyReloadListener(userInfo: APIUserInfo): void { - if (this.reloadListener) { - this.reloadListener(userInfo); - } else { - // If no listener is subscribed yet, save the result so it's available when they do subscribe - this.reloadUserInfo = userInfo; - } - } - - _startProactiveRefresh(): void { - this.proactiveRefresh._start(); - } - - _stopProactiveRefresh(): void { - this.proactiveRefresh._stop(); - } - - async _updateTokensIfNecessary( - response: IdTokenResponse | FinalizeMfaResponse, - reload = false - ): Promise { - let tokensRefreshed = false; - if ( - response.idToken && - response.idToken !== this.stsTokenManager.accessToken - ) { - this.stsTokenManager.updateFromServerResponse(response); - tokensRefreshed = true; - } - - if (reload) { - await _reloadWithoutSaving(this); - } - - await this.auth._persistUserIfCurrent(this); - if (tokensRefreshed) { - this.auth._notifyListenersIfCurrent(this); - } - } - - async delete(): Promise { - const idToken = await this.getIdToken(); - await _logoutIfInvalidated(this, deleteAccount(this.auth, { idToken })); - this.stsTokenManager.clearRefreshToken(); - - // TODO: Determine if cancellable-promises are necessary to use in this class so that delete() - // cancels pending actions... - - return this.auth.signOut(); - } - - toJSON(): PersistedBlob { - return { - uid: this.uid, - email: this.email || undefined, - emailVerified: this.emailVerified, - displayName: this.displayName || undefined, - isAnonymous: this.isAnonymous, - photoURL: this.photoURL || undefined, - phoneNumber: this.phoneNumber || undefined, - tenantId: this.tenantId || undefined, - providerData: this.providerData.map(userInfo => ({ ...userInfo })), - stsTokenManager: this.stsTokenManager.toJSON(), - // Redirect event ID must be maintained in case there is a pending - // redirect event. - _redirectEventId: this._redirectEventId, - ...this.metadata.toJSON() - }; - } - - get refreshToken(): string { - return this.stsTokenManager.refreshToken || ''; - } - - static _fromJSON(auth: Auth, object: PersistedBlob): User { - const displayName = object.displayName ?? undefined; - const email = object.email ?? undefined; - const phoneNumber = object.phoneNumber ?? undefined; - const photoURL = object.photoURL ?? undefined; - const tenantId = object.tenantId ?? undefined; - const _redirectEventId = object._redirectEventId ?? undefined; - const createdAt = object.createdAt ?? undefined; - const lastLoginAt = object.lastLoginAt ?? undefined; - const { - uid, - emailVerified, - isAnonymous, - providerData, - stsTokenManager: plainObjectTokenManager - } = object; - - _assert(uid && plainObjectTokenManager, auth, AuthErrorCode.INTERNAL_ERROR); - - const stsTokenManager = StsTokenManager.fromJSON( - this.name, - plainObjectTokenManager as PersistedBlob - ); - - _assert(typeof uid === 'string', auth, AuthErrorCode.INTERNAL_ERROR); - assertStringOrUndefined(displayName, auth.name); - assertStringOrUndefined(email, auth.name); - _assert( - typeof emailVerified === 'boolean', - auth, - AuthErrorCode.INTERNAL_ERROR - ); - _assert( - typeof isAnonymous === 'boolean', - auth, - AuthErrorCode.INTERNAL_ERROR - ); - assertStringOrUndefined(phoneNumber, auth.name); - assertStringOrUndefined(photoURL, auth.name); - assertStringOrUndefined(tenantId, auth.name); - assertStringOrUndefined(_redirectEventId, auth.name); - assertStringOrUndefined(createdAt, auth.name); - assertStringOrUndefined(lastLoginAt, auth.name); - const user = new UserImpl({ - uid, - auth, - email, - emailVerified, - displayName, - isAnonymous, - photoURL, - phoneNumber, - tenantId, - stsTokenManager, - createdAt, - lastLoginAt - }); - - if (providerData && Array.isArray(providerData)) { - user.providerData = providerData.map(userInfo => ({ ...userInfo })); - } - - if (_redirectEventId) { - user._redirectEventId = _redirectEventId; - } - - return user; - } - - /** - * Initialize a User from an idToken server response - * @param auth - * @param idTokenResponse - */ - static async _fromIdTokenResponse( - auth: Auth, - idTokenResponse: IdTokenResponse, - isAnonymous: boolean = false - ): Promise { - const stsTokenManager = new StsTokenManager(); - stsTokenManager.updateFromServerResponse(idTokenResponse); - - // Initialize the Firebase Auth user. - const user = new UserImpl({ - uid: idTokenResponse.localId, - auth, - stsTokenManager, - isAnonymous - }); - - // Updates the user info and data and resolves with a user instance. - await _reloadWithoutSaving(user); - return user; - } -} diff --git a/packages-exp/auth-exp/src/core/util/assert.test.ts b/packages-exp/auth-exp/src/core/util/assert.test.ts deleted file mode 100644 index 05ae925f84e..00000000000 --- a/packages-exp/auth-exp/src/core/util/assert.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; - -import { FirebaseError } from '@firebase/util'; - -import { assertTypes, opt } from './assert'; - -class Parent {} -class Child extends Parent {} - -describe('assertTypes', () => { - context('basic types', () => { - it('works when no arguments are present', () => { - assertTypes([]); - }); - - it('works using a basic argument', () => { - assertTypes(['foobar'], 'string'); - expect(() => assertTypes([46], 'string')).to.throw( - FirebaseError, - 'auth/argument-error' - ); - expect(() => assertTypes([], 'string')).to.throw( - FirebaseError, - 'auth/argument-error' - ); - }); - - it('works using optional types with missing value', () => { - assertTypes([], opt('string')); - assertTypes([35], 'number', opt('string')); - }); - - it('works using optional types with value set', () => { - assertTypes(['foo'], opt('string')); - expect(() => assertTypes([46], opt('string'))).to.throw( - FirebaseError, - 'auth/argument-error' - ); - }); - - it('works with multiple types', () => { - assertTypes(['foo', null], 'string', 'null'); - }); - - it("works with or'd types", () => { - assertTypes(['foo'], 'string|number'); - assertTypes([47], 'string|number'); - }); - - it('works with the arguments field from a function', () => { - function test(_name: string, _height?: unknown): void { - assertTypes(arguments, 'string', opt('number')); - } - - test('foo'); - test('foo', 11); - expect(() => test('foo', 'bar')).to.throw( - FirebaseError, - 'auth/argument-error' - ); - }); - - it('works with class types', () => { - assertTypes([new Child()], Child); - assertTypes([new Child()], Parent); - assertTypes([new Parent()], opt(Parent)); - expect(() => assertTypes([new Parent()], Child)).to.throw( - FirebaseError, - 'auth/argument-error' - ); - }); - }); - - context('record types', () => { - it('works one level deep', () => { - assertTypes([{ foo: 'bar', clazz: new Child(), test: null }], { - foo: 'string', - clazz: Parent, - test: 'null', - missing: opt('string') - }); - - expect(() => - assertTypes( - [{ foo: 'bar', clazz: new Child(), test: null, missing: 46 }], - { - foo: 'string', - clazz: Parent, - test: 'null', - missing: opt('string') - } - ) - ).to.throw(FirebaseError, 'auth/argument-error'); - }); - - it('works nested', () => { - assertTypes( - [{ name: 'foo', metadata: { height: 11, extraInfo: null } }], - { - name: 'string', - metadata: { - height: opt('number'), - extraInfo: 'string|null' - } - } - ); - - expect(() => - assertTypes( - [{ name: 'foo', metadata: { height: 11, extraInfo: null } }], - { - name: 'string', - metadata: { - height: opt('number'), - extraInfo: 'string' - } - } - ) - ).to.throw(FirebaseError, 'auth/argument-error'); - }); - - it('works with triply nested', () => { - assertTypes([{ a: { b: { c: 'test' } } }], { a: { b: { c: 'string' } } }); - - expect(() => - assertTypes([{ a: { b: { c: 'test' } } }], { - a: { b: { c: 'number' } } - }) - ).to.throw(FirebaseError, 'auth/argument-error'); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/core/util/browser.test.ts b/packages-exp/auth-exp/src/core/util/browser.test.ts deleted file mode 100644 index cea528cb6bf..00000000000 --- a/packages-exp/auth-exp/src/core/util/browser.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { _getBrowserName, BrowserName } from './browser'; - -describe('core/util/_getBrowserName', () => { - it('should recognize Opera', () => { - const userAgent = - 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36 OPR/36.0.2130.74'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.OPERA); - }); - - it('should recognize IE', () => { - const userAgent = - 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C)'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.IE); - }); - - it('should recognize Edge', () => { - const userAgent = - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.EDGE); - }); - - it('should recognize Firefox', () => { - const userAgent = - 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.FIREFOX); - }); - - it('should recognize Silk', () => { - const userAgent = - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Silk/44.1.54 like Chrome/44.0.2403.63 Safari/537.36'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.SILK); - }); - - it('should recognize Safari', () => { - const userAgent = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11-4) AppleWebKit/601.5.17 (KHTML, like Gecko) Version/9.1 Safari/601.5.17'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.SAFARI); - }); - - it('should recognize Chrome', () => { - const userAgent = - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.CHROME); - }); - - it('should recognize Android', () => { - const userAgent = - 'Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Build/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.ANDROID); - }); - - it('should recognize Blackberry', () => { - const userAgent = - 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.BLACKBERRY); - }); - - it('should recognize IE Mobile', () => { - const userAgent = - 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0;Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 920)'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.IEMOBILE); - }); - - it('should recognize WebOS', () => { - const userAgent = - 'Mozilla/5.0 (webOS/1.3; U; en-US) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/1.0 Safari/525.27.1 Desktop/1.0'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.WEBOS); - }); - - it('should recognize an unlisted browser', () => { - const userAgent = - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Awesome/2.0.012'; - expect(_getBrowserName(userAgent)).to.eq('Awesome'); - }); - - it('should default to Other', () => { - const userAgent = - 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12D508 [FBAN/FBIOS;FBAV/27.0.0.10.12;FBBV/8291884;FBDV/iPhone7,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/8.2;FBSS/3; FBCR/vodafoneIE;FBID/phone;FBLC/en_US;FBOP/5]'; - expect(_getBrowserName(userAgent)).to.eq(BrowserName.OTHER); - }); -}); diff --git a/packages-exp/auth-exp/src/core/util/browser.ts b/packages-exp/auth-exp/src/core/util/browser.ts deleted file mode 100644 index a70bde6b189..00000000000 --- a/packages-exp/auth-exp/src/core/util/browser.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { isIE, getUA } from '@firebase/util'; - -interface NavigatorStandalone extends Navigator { - standalone?: unknown; -} - -interface Document { - documentMode?: number; -} - -/** - * Enums for Browser name. - */ -export const enum BrowserName { - ANDROID = 'Android', - BLACKBERRY = 'Blackberry', - EDGE = 'Edge', - FIREFOX = 'Firefox', - IE = 'IE', - IEMOBILE = 'IEMobile', - OPERA = 'Opera', - OTHER = 'Other', - CHROME = 'Chrome', - SAFARI = 'Safari', - SILK = 'Silk', - WEBOS = 'Webos' -} - -/** - * Determine the browser for the purposes of reporting usage to the API - */ -export function _getBrowserName(userAgent: string): BrowserName | string { - const ua = userAgent.toLowerCase(); - if (ua.includes('opera/') || ua.includes('opr/') || ua.includes('opios/')) { - return BrowserName.OPERA; - } else if (_isIEMobile(ua)) { - // Windows phone IEMobile browser. - return BrowserName.IEMOBILE; - } else if (ua.includes('msie') || ua.includes('trident/')) { - return BrowserName.IE; - } else if (ua.includes('edge/')) { - return BrowserName.EDGE; - } else if (_isFirefox(ua)) { - return BrowserName.FIREFOX; - } else if (ua.includes('silk/')) { - return BrowserName.SILK; - } else if (_isBlackBerry(ua)) { - // Blackberry browser. - return BrowserName.BLACKBERRY; - } else if (_isWebOS(ua)) { - // WebOS default browser. - return BrowserName.WEBOS; - } else if (_isSafari(ua)) { - return BrowserName.SAFARI; - } else if ( - (ua.includes('chrome/') || _isChromeIOS(ua)) && - !ua.includes('edge/') - ) { - return BrowserName.CHROME; - } else if (_isAndroid(ua)) { - // Android stock browser. - return BrowserName.ANDROID; - } else { - // Most modern browsers have name/version at end of user agent string. - const re = /([a-zA-Z\d\.]+)\/[a-zA-Z\d\.]*$/; - const matches = userAgent.match(re); - if (matches?.length === 2) { - return matches[1]; - } - } - return BrowserName.OTHER; -} - -export function _isFirefox(ua: string): boolean { - return /firefox\//i.test(ua); -} - -export function _isSafari(userAgent: string): boolean { - const ua = userAgent.toLowerCase(); - return ( - ua.includes('safari/') && - !ua.includes('chrome/') && - !ua.includes('crios/') && - !ua.includes('android') - ); -} - -export function _isChromeIOS(ua: string): boolean { - return /crios\//i.test(ua); -} - -export function _isIEMobile(ua: string): boolean { - return /iemobile/i.test(ua); -} - -export function _isAndroid(ua: string): boolean { - return /android/i.test(ua); -} - -export function _isBlackBerry(ua: string): boolean { - return /blackberry/i.test(ua); -} - -export function _isWebOS(ua: string): boolean { - return /webos/i.test(ua); -} - -export function _isIOS(ua: string): boolean { - return /iphone|ipad|ipod/i.test(ua); -} - -export function _isIOSStandalone(ua: string): boolean { - return _isIOS(ua) && !!(window.navigator as NavigatorStandalone)?.standalone; -} - -export function _isIE10(): boolean { - return isIE() && (document as Document).documentMode === 10; -} - -export function _isMobileBrowser(ua: string = getUA()): boolean { - // TODO: implement getBrowserName equivalent for OS. - return ( - _isIOS(ua) || - _isAndroid(ua) || - _isWebOS(ua) || - _isBlackBerry(ua) || - /windows phone/i.test(ua) || - _isIEMobile(ua) - ); -} - -export function _isIframe(): boolean { - try { - // Check that the current window is not the top window. - // If so, return true. - return !!(window && window !== window.top); - } catch (e) { - return false; - } -} diff --git a/packages-exp/auth-exp/src/core/util/emulator.test.ts b/packages-exp/auth-exp/src/core/util/emulator.test.ts deleted file mode 100644 index e9d9a88bc8d..00000000000 --- a/packages-exp/auth-exp/src/core/util/emulator.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; - -import { ConfigInternal } from '../../model/auth'; -import { _emulatorUrl } from './emulator'; - -describe('core/util/emulator', () => { - const config: ConfigInternal = { - emulator: { - url: 'http://localhost:4000' - } - } as ConfigInternal; - - it('builds the proper URL with no path', () => { - expect(_emulatorUrl(config)).to.eq('http://localhost:4000/'); - }); - - it('builds the proper URL with a path', () => { - expect(_emulatorUrl(config, '/test/path')).to.eq( - 'http://localhost:4000/test/path' - ); - }); - - it('builds the proper URL with a path missing separator', () => { - expect(_emulatorUrl(config, 'test/path')).to.eq( - 'http://localhost:4000/test/path' - ); - }); -}); diff --git a/packages-exp/auth-exp/src/core/util/emulator.ts b/packages-exp/auth-exp/src/core/util/emulator.ts deleted file mode 100644 index 4d56687bbee..00000000000 --- a/packages-exp/auth-exp/src/core/util/emulator.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigInternal } from '../../model/auth'; -import { debugAssert } from './assert'; - -export function _emulatorUrl(config: ConfigInternal, path?: string): string { - debugAssert(config.emulator, 'Emulator should always be set here'); - const { url } = config.emulator; - const emulatorHost = new URL(url).toString(); - - if (!path) { - return emulatorHost; - } - - return `${emulatorHost}${path.startsWith('/') ? path.slice(1) : path}`; -} diff --git a/packages-exp/auth-exp/src/core/util/version.test.ts b/packages-exp/auth-exp/src/core/util/version.test.ts deleted file mode 100644 index 58228732844..00000000000 --- a/packages-exp/auth-exp/src/core/util/version.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SDK_VERSION } from '@firebase/app-exp'; -import { expect } from 'chai'; -import { ClientPlatform, _getClientVersion } from './version'; -import { isNode } from '@firebase/util'; - -describe('core/util/_getClientVersion', () => { - if (isNode()) { - context('node', () => { - it('should set the correct version', () => { - expect(_getClientVersion(ClientPlatform.NODE)).to.eq( - `Node/JsCore/${SDK_VERSION}/FirebaseCore-web` - ); - }); - }); - } else { - context('browser', () => { - it('should set the correct version', () => { - expect(_getClientVersion(ClientPlatform.BROWSER)).to.eq( - `Chrome/JsCore/${SDK_VERSION}/FirebaseCore-web` - ); - }); - }); - - context('worker', () => { - it('should set the correct version', () => { - expect(_getClientVersion(ClientPlatform.WORKER)).to.eq( - `Chrome-Worker/JsCore/${SDK_VERSION}/FirebaseCore-web` - ); - }); - }); - - context('React Native', () => { - it('should set the correct version', () => { - expect(_getClientVersion(ClientPlatform.REACT_NATIVE)).to.eq( - `ReactNative/JsCore/${SDK_VERSION}/FirebaseCore-web` - ); - }); - }); - } -}); diff --git a/packages-exp/auth-exp/src/core/util/version.ts b/packages-exp/auth-exp/src/core/util/version.ts deleted file mode 100644 index c50f01d9f3f..00000000000 --- a/packages-exp/auth-exp/src/core/util/version.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SDK_VERSION } from '@firebase/app-exp'; -import { _getBrowserName } from './browser'; -import { getUA } from '@firebase/util'; - -export const enum ClientImplementation { - CORE = 'JsCore' -} - -export const enum ClientPlatform { - BROWSER = 'Browser', - NODE = 'Node', - REACT_NATIVE = 'ReactNative', - WORKER = 'Worker' -} - -const enum ClientFramework { - // No other framework used. - DEFAULT = 'FirebaseCore-web', - // Firebase Auth used with FirebaseUI-web. - // TODO: Pass this in when used in conjunction with FirebaseUI - FIREBASEUI = 'FirebaseUI-web' -} - -/* - * Determine the SDK version string - * - * TODO: This should be set on the Auth object during initialization - */ -export function _getClientVersion(clientPlatform: ClientPlatform): string { - let reportedPlatform: string; - switch (clientPlatform) { - case ClientPlatform.BROWSER: - // In a browser environment, report the browser name. - reportedPlatform = _getBrowserName(getUA()); - break; - case ClientPlatform.WORKER: - // Technically a worker runs from a browser but we need to differentiate a - // worker from a browser. - // For example: Chrome-Worker/JsCore/4.9.1/FirebaseCore-web. - reportedPlatform = `${_getBrowserName(getUA())}-${clientPlatform}`; - break; - default: - reportedPlatform = clientPlatform; - } - return `${reportedPlatform}/${ClientImplementation.CORE}/${SDK_VERSION}/${ClientFramework.DEFAULT}`; -} diff --git a/packages-exp/auth-exp/src/mfa/mfa_error.ts b/packages-exp/auth-exp/src/mfa/mfa_error.ts deleted file mode 100644 index 9eb23fc65e1..00000000000 --- a/packages-exp/auth-exp/src/mfa/mfa_error.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; -import { Auth } from '../model/auth'; -import { IdTokenResponse } from '../model/id_token'; -import { AuthErrorCode } from '../core/errors'; -import { User } from '../model/user'; -import { AuthCredential } from '../core/credentials'; -import { IdTokenMfaResponse } from '../api/authentication/mfa'; - -export class MultiFactorError - extends FirebaseError - implements externs.MultiFactorError { - readonly name = 'FirebaseError'; - readonly code: string; - readonly appName: string; - readonly serverResponse: IdTokenMfaResponse; - - readonly tenantId?: string; - - private constructor( - auth: Auth, - error: FirebaseError, - readonly operationType: externs.OperationType, - readonly user?: User - ) { - super(error.code, error.message); - // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work - Object.setPrototypeOf(this, MultiFactorError.prototype); - this.appName = auth.name; - this.code = error.code; - this.tenantId = auth.tenantId ?? undefined; - this.serverResponse = error.customData! - .serverResponse as IdTokenMfaResponse; - } - - static _fromErrorAndOperation( - auth: Auth, - error: FirebaseError, - operationType: externs.OperationType, - user?: User - ): MultiFactorError { - return new MultiFactorError(auth, error, operationType, user); - } -} - -export function _processCredentialSavingMfaContextIfNecessary( - auth: Auth, - operationType: externs.OperationType, - credential: AuthCredential, - user?: User -): Promise { - const idTokenProvider = - operationType === externs.OperationType.REAUTHENTICATE - ? credential._getReauthenticationResolver(auth) - : credential._getIdTokenResponse(auth); - - return idTokenProvider.catch(error => { - if (error.code === `auth/${AuthErrorCode.MFA_REQUIRED}`) { - throw MultiFactorError._fromErrorAndOperation( - auth, - error, - operationType, - user - ); - } - - throw error; - }); -} diff --git a/packages-exp/auth-exp/src/mfa/mfa_info.test.ts b/packages-exp/auth-exp/src/mfa/mfa_info.test.ts deleted file mode 100644 index d3ab67158fe..00000000000 --- a/packages-exp/auth-exp/src/mfa/mfa_info.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { ProviderId } from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { testAuth, TestAuth } from '../../test/helpers/mock_auth'; -import { PhoneMfaEnrollment } from '../api/account_management/mfa'; -import { MultiFactorInfo } from './mfa_info'; - -use(chaiAsPromised); - -describe('core/mfa/mfa_info/MultiFactorInfo', () => { - let auth: TestAuth; - - beforeEach(async () => { - auth = await testAuth(); - }); - - describe('_fromServerResponse', () => { - context('phone enrollment', () => { - const date = Date.now(); - const enrollmentInfo = { - mfaEnrollmentId: 'uid', - enrolledAt: date, - displayName: 'display-name', - phoneInfo: 'phone-info' - }; - - it('should create a valid MfaInfo', () => { - const mfaInfo = MultiFactorInfo._fromServerResponse( - auth, - enrollmentInfo - ); - expect(mfaInfo.factorId).to.eq(ProviderId.PHONE); - expect(mfaInfo.uid).to.eq('uid'); - expect(mfaInfo.enrollmentTime).to.eq(new Date(date).toUTCString()); - expect(mfaInfo.displayName).to.eq('display-name'); - }); - }); - - context('Invalid enrollment', () => { - const enrollmentInfo = { - mfaEnrollmentId: 'uid', - enrolledAt: Date.now(), - displayName: 'display-name' - }; - - it('should throw an error', () => { - expect(() => - MultiFactorInfo._fromServerResponse( - auth, - enrollmentInfo as PhoneMfaEnrollment - ) - ).to.throw(FirebaseError, 'auth/internal-error'); - }); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/mfa/mfa_info.ts b/packages-exp/auth-exp/src/mfa/mfa_info.ts deleted file mode 100644 index 7e116e494d8..00000000000 --- a/packages-exp/auth-exp/src/mfa/mfa_info.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { - PhoneMfaEnrollment, - MfaEnrollment -} from '../api/account_management/mfa'; -import { AuthErrorCode } from '../core/errors'; -import { _fail } from '../core/util/assert'; -import { Auth } from '../model/auth'; - -export abstract class MultiFactorInfo implements externs.MultiFactorInfo { - readonly uid: string; - readonly displayName?: string | null; - readonly enrollmentTime: string; - - protected constructor( - readonly factorId: externs.FactorId, - response: MfaEnrollment - ) { - this.uid = response.mfaEnrollmentId; - this.enrollmentTime = new Date(response.enrolledAt).toUTCString(); - this.displayName = response.displayName; - } - - static _fromServerResponse( - auth: Auth, - enrollment: MfaEnrollment - ): MultiFactorInfo { - if ('phoneInfo' in enrollment) { - return PhoneMultiFactorInfo._fromServerResponse(auth, enrollment); - } - return _fail(auth, AuthErrorCode.INTERNAL_ERROR); - } -} - -export class PhoneMultiFactorInfo extends MultiFactorInfo { - readonly phoneNumber: string; - - private constructor(response: PhoneMfaEnrollment) { - super(externs.FactorId.PHONE, response); - this.phoneNumber = response.phoneInfo; - } - - static _fromServerResponse( - _auth: Auth, - enrollment: MfaEnrollment - ): PhoneMultiFactorInfo { - return new PhoneMultiFactorInfo(enrollment); - } -} diff --git a/packages-exp/auth-exp/src/mfa/mfa_resolver.ts b/packages-exp/auth-exp/src/mfa/mfa_resolver.ts deleted file mode 100644 index 7f714fb9daf..00000000000 --- a/packages-exp/auth-exp/src/mfa/mfa_resolver.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { _castAuth } from '../core/auth/auth_impl'; -import { AuthErrorCode } from '../core/errors'; -import { UserCredentialImpl } from '../core/user/user_credential_impl'; -import { _assert, _fail } from '../core/util/assert'; -import { UserCredential } from '../model/user'; -import { MultiFactorAssertion } from './mfa_assertion'; -import { MultiFactorError } from './mfa_error'; -import { MultiFactorInfo } from './mfa_info'; -import { MultiFactorSession } from './mfa_session'; - -export class MultiFactorResolver implements externs.MultiFactorResolver { - private constructor( - readonly session: MultiFactorSession, - readonly hints: MultiFactorInfo[], - private readonly signInResolver: ( - assertion: MultiFactorAssertion - ) => Promise - ) {} - - /** @internal */ - static _fromError( - authExtern: externs.Auth, - error: MultiFactorError - ): MultiFactorResolver { - const auth = _castAuth(authExtern); - const hints = (error.serverResponse.mfaInfo || []).map(enrollment => - MultiFactorInfo._fromServerResponse(auth, enrollment) - ); - - _assert( - error.serverResponse.mfaPendingCredential, - auth, - AuthErrorCode.INTERNAL_ERROR - ); - const session = MultiFactorSession._fromMfaPendingCredential( - error.serverResponse.mfaPendingCredential - ); - - return new MultiFactorResolver( - session, - hints, - async (assertion: MultiFactorAssertion): Promise => { - const mfaResponse = await assertion._process(auth, session); - // Clear out the unneeded fields from the old login response - delete error.serverResponse.mfaInfo; - delete error.serverResponse.mfaPendingCredential; - - // Use in the new token & refresh token in the old response - const idTokenResponse = { - ...error.serverResponse, - idToken: mfaResponse.idToken, - refreshToken: mfaResponse.refreshToken - }; - - // TODO: we should collapse this switch statement into UserCredentialImpl._forOperation and have it support the SIGN_IN case - switch (error.operationType) { - case externs.OperationType.SIGN_IN: - const userCredential = await UserCredentialImpl._fromIdTokenResponse( - auth, - error.operationType, - idTokenResponse - ); - await auth._updateCurrentUser(userCredential.user); - return userCredential; - case externs.OperationType.REAUTHENTICATE: - _assert(error.user, auth, AuthErrorCode.INTERNAL_ERROR); - return UserCredentialImpl._forOperation( - error.user, - error.operationType, - idTokenResponse - ); - default: - _fail(auth, AuthErrorCode.INTERNAL_ERROR); - } - } - ); - } - - async resolveSignIn( - assertionExtern: externs.MultiFactorAssertion - ): Promise { - const assertion = assertionExtern as MultiFactorAssertion; - return this.signInResolver(assertion); - } -} - -/** - * Provides a {@link @firebase/auth-types#MultiFactorResolver} suitable for completion of a - * multi-factor flow. - * - * @param auth - The auth instance. - * @param error - The {@link @firebase/auth-types#MultiFactorError} raised during a sign-in, or - * reauthentication operation. - * - * @public - */ -export function getMultiFactorResolver( - auth: externs.Auth, - error: externs.MultiFactorError -): externs.MultiFactorResolver { - const errorInternal = error as MultiFactorError; - _assert(error.operationType, auth, AuthErrorCode.ARGUMENT_ERROR); - _assert( - errorInternal.serverResponse?.mfaPendingCredential, - auth, - AuthErrorCode.ARGUMENT_ERROR - ); - - return MultiFactorResolver._fromError(auth, errorInternal); -} diff --git a/packages-exp/auth-exp/src/mfa/mfa_session.ts b/packages-exp/auth-exp/src/mfa/mfa_session.ts deleted file mode 100644 index 4748efd4035..00000000000 --- a/packages-exp/auth-exp/src/mfa/mfa_session.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import * as externs from '@firebase/auth-types-exp'; - -export const enum MultiFactorSessionType { - ENROLL = 'enroll', - SIGN_IN = 'signin' -} - -interface SerializedMultiFactorSession { - multiFactorSession: { - idToken?: string; - pendingCredential?: string; - }; -} - -export class MultiFactorSession implements externs.MultiFactorSession { - private constructor( - readonly type: MultiFactorSessionType, - readonly credential: string - ) {} - - static _fromIdtoken(idToken: string): MultiFactorSession { - return new MultiFactorSession(MultiFactorSessionType.ENROLL, idToken); - } - - static _fromMfaPendingCredential( - mfaPendingCredential: string - ): MultiFactorSession { - return new MultiFactorSession( - MultiFactorSessionType.SIGN_IN, - mfaPendingCredential - ); - } - - toJSON(): SerializedMultiFactorSession { - const key = - this.type === MultiFactorSessionType.ENROLL - ? 'idToken' - : 'pendingCredential'; - return { - multiFactorSession: { - [key]: this.credential - } - }; - } - - static fromJSON( - obj: Partial - ): MultiFactorSession | null { - if (obj?.multiFactorSession) { - if (obj.multiFactorSession?.pendingCredential) { - return MultiFactorSession._fromMfaPendingCredential( - obj.multiFactorSession.pendingCredential - ); - } else if (obj.multiFactorSession?.idToken) { - return MultiFactorSession._fromIdtoken(obj.multiFactorSession.idToken); - } - } - return null; - } -} diff --git a/packages-exp/auth-exp/src/mfa/mfa_user.ts b/packages-exp/auth-exp/src/mfa/mfa_user.ts deleted file mode 100644 index be2ff51b59c..00000000000 --- a/packages-exp/auth-exp/src/mfa/mfa_user.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import * as externs from '@firebase/auth-types-exp'; - -import { withdrawMfa } from '../api/account_management/mfa'; -import { AuthErrorCode } from '../core/errors'; -import { _logoutIfInvalidated } from '../core/user/invalidation'; -import { User } from '../model/user'; -import { MultiFactorAssertion } from './mfa_assertion'; -import { MultiFactorInfo } from './mfa_info'; -import { MultiFactorSession } from './mfa_session'; - -export class MultiFactorUser implements externs.MultiFactorUser { - enrolledFactors: externs.MultiFactorInfo[] = []; - - private constructor(readonly user: User) { - user._onReload(userInfo => { - if (userInfo.mfaInfo) { - this.enrolledFactors = userInfo.mfaInfo.map(enrollment => - MultiFactorInfo._fromServerResponse(user.auth, enrollment) - ); - } - }); - } - - static _fromUser(user: User): MultiFactorUser { - return new MultiFactorUser(user); - } - - async getSession(): Promise { - return MultiFactorSession._fromIdtoken(await this.user.getIdToken()); - } - - async enroll( - assertionExtern: externs.MultiFactorAssertion, - displayName?: string | null - ): Promise { - const assertion = assertionExtern as MultiFactorAssertion; - const session = (await this.getSession()) as MultiFactorSession; - const finalizeMfaResponse = await _logoutIfInvalidated( - this.user, - assertion._process(this.user.auth, session, displayName) - ); - // New tokens will be issued after enrollment of the new second factors. - // They need to be updated on the user. - await this.user._updateTokensIfNecessary(finalizeMfaResponse); - // The user needs to be reloaded to get the new multi-factor information - // from server. USER_RELOADED event will be triggered and `enrolledFactors` - // will be updated. - return this.user.reload(); - } - - async unenroll(infoOrUid: externs.MultiFactorInfo | string): Promise { - const mfaEnrollmentId = - typeof infoOrUid === 'string' ? infoOrUid : infoOrUid.uid; - const idToken = await this.user.getIdToken(); - const idTokenResponse = await _logoutIfInvalidated( - this.user, - withdrawMfa(this.user.auth, { - idToken, - mfaEnrollmentId - }) - ); - // Remove the second factor from the user's list. - this.enrolledFactors = this.enrolledFactors.filter( - ({ uid }) => uid !== mfaEnrollmentId - ); - // Depending on whether the backend decided to revoke the user's session, - // the tokenResponse may be empty. If the tokens were not updated (and they - // are now invalid), reloading the user will discover this and invalidate - // the user's state accordingly. - await this.user._updateTokensIfNecessary(idTokenResponse); - try { - await this.user.reload(); - } catch (e) { - if (e.code !== `auth/${AuthErrorCode.TOKEN_EXPIRED}`) { - throw e; - } - } - } -} - -const multiFactorUserCache = new WeakMap< - externs.User, - externs.MultiFactorUser ->(); - -/** - * The {@link @firebase/auth-types#MultiFactorUser} corresponding to the user. - * - * @remarks - * This is used to access all multi-factor properties and operations related to the user. - * - * @param user - The user. - * - * @public - */ -export function multiFactor(user: externs.User): externs.MultiFactorUser { - if (!multiFactorUserCache.has(user)) { - multiFactorUserCache.set(user, MultiFactorUser._fromUser(user as User)); - } - return multiFactorUserCache.get(user)!; -} diff --git a/packages-exp/auth-exp/src/model/auth.ts b/packages-exp/auth-exp/src/model/auth.ts deleted file mode 100644 index 083af819c98..00000000000 --- a/packages-exp/auth-exp/src/model/auth.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { ErrorFactory } from '@firebase/util'; -import { AuthErrorCode, AuthErrorParams } from '../core/errors'; - -import { PopupRedirectResolver } from './popup_redirect'; -import { User } from './user'; - -/** @internal */ -export type AppName = string; -/** @internal */ -export type ApiKey = string; -/** @internal */ -export type AuthDomain = string; - -/** @internal */ -export interface ConfigInternal extends externs.Config { - /** - * @internal - * @readonly - */ - emulator?: { - url: string; - }; -} - -/** @internal */ -export interface Auth extends externs.Auth { - /** @internal */ - currentUser: externs.User | null; - /** @internal */ - _canInitEmulator: boolean; - /** @internal */ - _isInitialized: boolean; - /** @internal */ - _initializationPromise: Promise | null; - /** @internal */ - _updateCurrentUser(user: User | null): Promise; - - /** @internal */ - _onStorageEvent(): void; - - /** @internal */ - _notifyListenersIfCurrent(user: User): void; - /** @internal */ - _persistUserIfCurrent(user: User): Promise; - /** @internal */ - _setRedirectUser( - user: User | null, - popupRedirectResolver?: externs.PopupRedirectResolver - ): Promise; - /** @internal */ - _redirectUserForId(id: string): Promise; - /** @internal */ - _popupRedirectResolver: PopupRedirectResolver | null; - /** @internal */ - _key(): string; - /** @internal */ - _startProactiveRefresh(): void; - /** @internal */ - _stopProactiveRefresh(): void; - _getPersistence(): string; - - /** @internal */ - readonly name: AppName; - /** @internal */ - readonly config: ConfigInternal; - /** @internal */ - languageCode: string | null; - /** @internal */ - tenantId: string | null; - /** @internal */ - readonly settings: externs.AuthSettings; - _errorFactory: ErrorFactory; - - /** @internal */ - useDeviceLanguage(): void; - /** @internal */ - signOut(): Promise; -} - -export interface Dependencies { - persistence?: externs.Persistence | externs.Persistence[]; - popupRedirectResolver?: externs.PopupRedirectResolver; - errorMap?: externs.AuthErrorMap; -} diff --git a/packages-exp/auth-exp/src/model/popup_redirect.ts b/packages-exp/auth-exp/src/model/popup_redirect.ts deleted file mode 100644 index 0376b866350..00000000000 --- a/packages-exp/auth-exp/src/model/popup_redirect.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { AuthPopup } from '../platform_browser/util/popup'; -import { Auth } from './auth'; - -/** @internal */ -export const enum EventFilter { - POPUP, - REDIRECT -} - -/** @internal */ -export const enum GapiOutcome { - ACK = 'ACK', - ERROR = 'ERROR' -} - -/** @internal */ -export interface GapiAuthEvent extends gapi.iframes.Message { - authEvent: AuthEvent; -} - -/** @internal */ -export const enum AuthEventType { - LINK_VIA_POPUP = 'linkViaPopup', - LINK_VIA_REDIRECT = 'linkViaRedirect', - REAUTH_VIA_POPUP = 'reauthViaPopup', - REAUTH_VIA_REDIRECT = 'reauthViaRedirect', - SIGN_IN_VIA_POPUP = 'signInViaPopup', - SIGN_IN_VIA_REDIRECT = 'signInViaRedirect', - UNKNOWN = 'unknown', - VERIFY_APP = 'verifyApp' -} - -/** @internal */ -export interface AuthEventError extends Error { - code: string; // in the form of auth/${AuthErrorCode} - message: string; -} - -/** @internal */ -export interface AuthEvent { - type: AuthEventType; - eventId: string | null; - urlResponse: string | null; - sessionId: string | null; - postBody: string | null; - tenantId: string | null; - error?: AuthEventError; -} - -/** @internal */ -export interface AuthEventConsumer { - readonly filter: AuthEventType[]; - eventId: string | null; - onAuthEvent(event: AuthEvent): unknown; - onError(error: FirebaseError): unknown; -} - -/** @internal */ -export interface EventManager { - registerConsumer(authEventConsumer: AuthEventConsumer): void; - unregisterConsumer(authEventConsumer: AuthEventConsumer): void; -} - -/** @internal */ -export interface PopupRedirectResolver extends externs.PopupRedirectResolver { - /** @internal */ - _initialize(auth: Auth): Promise; - /** @internal */ - _openPopup( - auth: Auth, - provider: externs.AuthProvider, - authType: AuthEventType, - eventId?: string - ): Promise; - /** @internal */ - _openRedirect( - auth: Auth, - provider: externs.AuthProvider, - authType: AuthEventType, - eventId?: string - ): Promise; - /** @internal */ - _isIframeWebStorageSupported( - auth: Auth, - cb: (support: boolean) => unknown - ): void; - _redirectPersistence: externs.Persistence; - - // This is needed so that auth does not have a hard dependency on redirect - _completeRedirectFn: ( - auth: externs.Auth, - resolver: externs.PopupRedirectResolver, - bypassAuthState: boolean - ) => Promise; -} diff --git a/packages-exp/auth-exp/src/model/user.ts b/packages-exp/auth-exp/src/model/user.ts deleted file mode 100644 index 286f2a0c97e..00000000000 --- a/packages-exp/auth-exp/src/model/user.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { NextFn } from '@firebase/util'; -import { APIUserInfo } from '../api/account_management/account'; -import { FinalizeMfaResponse } from '../api/authentication/mfa'; -import { PersistedBlob } from '../core/persistence'; -import { StsTokenManager } from '../core/user/token_manager'; -import { UserMetadata } from '../core/user/user_metadata'; -import { Auth } from './auth'; -import { IdTokenResponse, TaggedWithTokenResponse } from './id_token'; - -/** @internal */ -export type MutableUserInfo = { - -readonly [K in keyof externs.UserInfo]: externs.UserInfo[K]; -}; - -/** @internal */ -export interface UserParameters { - uid: string; - auth: Auth; - stsTokenManager: StsTokenManager; - - displayName?: string | null; - email?: string | null; - phoneNumber?: string | null; - photoURL?: string | null; - isAnonymous?: boolean | null; - emailVerified?: boolean | null; - tenantId?: string | null; - - createdAt?: string | null; - lastLoginAt?: string | null; -} - -/** @internal */ -export interface User extends externs.User { - /** @internal */ - displayName: string | null; - /** @internal */ - email: string | null; - /** @internal */ - phoneNumber: string | null; - /** @internal */ - photoURL: string | null; - - /** @internal */ - auth: Auth; - /** @internal */ - providerId: externs.ProviderId.FIREBASE; - /** @internal */ - refreshToken: string; - /** @internal */ - emailVerified: boolean; - /** @internal */ - tenantId: string | null; - /** @internal */ - providerData: MutableUserInfo[]; - /** @internal */ - metadata: UserMetadata; - - /** @internal */ - stsTokenManager: StsTokenManager; - /** @internal */ - _redirectEventId?: string; - - /** @internal */ - _updateTokensIfNecessary( - response: IdTokenResponse | FinalizeMfaResponse, - reload?: boolean - ): Promise; - - /** @internal */ - _assign(user: User): void; - /** @internal */ - _clone(): User; - /** @internal */ - _onReload: (cb: NextFn) => void; - /** @internal */ - _notifyReloadListener: NextFn; - /** @internal */ - _startProactiveRefresh: () => void; - /** @internal */ - _stopProactiveRefresh: () => void; - - /** @internal */ - getIdToken(forceRefresh?: boolean): Promise; - /** @internal */ - getIdTokenResult(forceRefresh?: boolean): Promise; - /** @internal */ - reload(): Promise; - /** @internal */ - delete(): Promise; - /** @internal */ - toJSON(): PersistedBlob; -} - -/** @internal */ -export interface UserCredential - extends externs.UserCredential, - TaggedWithTokenResponse { - /** @internal */ - user: User; -} diff --git a/packages-exp/auth-exp/src/platform_browser/auth.test.ts b/packages-exp/auth-exp/src/platform_browser/auth.test.ts deleted file mode 100644 index b92be189fb0..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/auth.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { FirebaseApp } from '@firebase/app-types-exp'; -import * as externs from '@firebase/auth-types-exp'; - -import { testAuth, testUser } from '../../test/helpers/mock_auth'; -import { AuthImpl, DefaultConfig } from '../core/auth/auth_impl'; -import { _initializeAuthInstance } from '../core/auth/initialize'; -import { AuthErrorCode } from '../core/errors'; -import { Persistence } from '../core/persistence'; -import { browserLocalPersistence } from './persistence/local_storage'; -import { browserSessionPersistence } from './persistence/session_storage'; -import { inMemoryPersistence } from '../core/persistence/in_memory'; -import { PersistenceUserManager } from '../core/persistence/persistence_user_manager'; -import * as reload from '../core/user/reload'; -import { _getInstance } from '../core/util/instantiator'; -import { _getClientVersion, ClientPlatform } from '../core/util/version'; -import { Auth } from '../model/auth'; -import { browserPopupRedirectResolver } from './popup_redirect'; -import { PopupRedirectResolver } from '../model/popup_redirect'; -import { UserCredentialImpl } from '../core/user/user_credential_impl'; -import { User } from '../model/user'; -import { _createError } from '../core/util/assert'; - -use(sinonChai); -use(chaiAsPromised); - -const FAKE_APP: FirebaseApp = { - name: 'test-app', - options: { - apiKey: 'api-key', - authDomain: 'auth-domain' - }, - automaticDataCollectionEnabled: false -}; - -describe('core/auth/auth_impl', () => { - let auth: Auth; - let persistenceStub: sinon.SinonStubbedInstance; - - beforeEach(async () => { - persistenceStub = sinon.stub(_getInstance(inMemoryPersistence)); - const authImpl = new AuthImpl(FAKE_APP, { - apiKey: FAKE_APP.options.apiKey!, - apiHost: DefaultConfig.API_HOST, - apiScheme: DefaultConfig.API_SCHEME, - tokenApiHost: DefaultConfig.TOKEN_API_HOST, - sdkClientVersion: 'v' - }); - - _initializeAuthInstance(authImpl, { persistence: inMemoryPersistence }); - auth = authImpl; - }); - - afterEach(sinon.restore); - - describe('#setPersistence', () => { - it('swaps underlying persistence', async () => { - const newPersistence = browserLocalPersistence; - const newStub = sinon.stub(_getInstance(newPersistence)); - persistenceStub._get.returns( - Promise.resolve(testUser(auth, 'test').toJSON()) - ); - - await auth.setPersistence(newPersistence); - expect(persistenceStub._get).to.have.been.called; - expect(persistenceStub._remove).to.have.been.called; - expect(newStub._set).to.have.been.calledWith( - sinon.match.any, - testUser(auth, 'test').toJSON() - ); - }); - }); -}); - -describe('core/auth/initializeAuth', () => { - afterEach(sinon.restore); - - describe('persistence manager creation', () => { - let createManagerStub: sinon.SinonSpy; - let reloadStub: sinon.SinonStub; - let oldAuth: Auth; - let completeRedirectFnStub: sinon.SinonStub; - - beforeEach(async () => { - oldAuth = await testAuth(); - createManagerStub = sinon.spy(PersistenceUserManager, 'create'); - reloadStub = sinon - .stub(reload, '_reloadWithoutSaving') - .returns(Promise.resolve()); - completeRedirectFnStub = sinon - .stub( - _getInstance(browserPopupRedirectResolver), - '_completeRedirectFn' - ) - .returns(Promise.resolve(null)); - }); - - async function initAndWait( - persistence: externs.Persistence | externs.Persistence[], - popupRedirectResolver?: externs.PopupRedirectResolver, - authDomain = FAKE_APP.options.authDomain - ): Promise { - const auth = new AuthImpl(FAKE_APP, { - apiKey: FAKE_APP.options.apiKey!, - apiHost: DefaultConfig.API_HOST, - apiScheme: DefaultConfig.API_SCHEME, - tokenApiHost: DefaultConfig.TOKEN_API_HOST, - authDomain, - sdkClientVersion: _getClientVersion(ClientPlatform.BROWSER) - }); - - _initializeAuthInstance(auth, { - persistence, - popupRedirectResolver - }); - // Auth initializes async. We can make sure the initialization is - // flushed by awaiting a method on the queue. - await auth.setPersistence(inMemoryPersistence); - return auth; - } - - it('converts single persistence to array', async () => { - const auth = await initAndWait(inMemoryPersistence); - expect(createManagerStub).to.have.been.calledWith(auth, [ - _getInstance(inMemoryPersistence) - ]); - }); - - it('pulls the user from storage', async () => { - sinon - .stub(_getInstance(inMemoryPersistence), '_get') - .returns(Promise.resolve(testUser(oldAuth, 'uid').toJSON())); - const auth = await initAndWait(inMemoryPersistence); - expect(auth.currentUser!.uid).to.eq('uid'); - }); - - it('calls create with the persistence in order', async () => { - const auth = await initAndWait([ - inMemoryPersistence, - browserLocalPersistence - ]); - expect(createManagerStub).to.have.been.calledWith(auth, [ - _getInstance(inMemoryPersistence), - _getInstance(browserLocalPersistence) - ]); - }); - - it('does not reload redirect users', async () => { - const user = testUser(oldAuth, 'uid'); - user._redirectEventId = 'event-id'; - sinon - .stub(_getInstance(inMemoryPersistence), '_get') - .returns(Promise.resolve(user.toJSON())); - sinon - .stub(_getInstance(browserSessionPersistence), '_get') - .returns(Promise.resolve(user.toJSON())); - await initAndWait(inMemoryPersistence); - expect(reload._reloadWithoutSaving).not.to.have.been.called; - }); - - it('reloads non-redirect users', async () => { - sinon - .stub(_getInstance(inMemoryPersistence), '_get') - .returns(Promise.resolve(testUser(oldAuth, 'uid').toJSON())); - sinon - .stub(_getInstance(browserSessionPersistence), '_get') - .returns(Promise.resolve(null)); - - await initAndWait(inMemoryPersistence); - expect(reload._reloadWithoutSaving).to.have.been.called; - }); - - it('Does not reload if the event ids match', async () => { - const user = testUser(oldAuth, 'uid'); - user._redirectEventId = 'event-id'; - - sinon - .stub(_getInstance(inMemoryPersistence), '_get') - .returns(Promise.resolve(user.toJSON())); - sinon - .stub(_getInstance(browserSessionPersistence), '_get') - .returns(Promise.resolve(user.toJSON())); - - await initAndWait(inMemoryPersistence, browserPopupRedirectResolver); - expect(reload._reloadWithoutSaving).not.to.have.been.called; - }); - - it('Reloads if the event ids do not match', async () => { - const user = testUser(oldAuth, 'uid'); - user._redirectEventId = 'event-id'; - - sinon - .stub(_getInstance(inMemoryPersistence), '_get') - .returns(Promise.resolve(user.toJSON())); - - user._redirectEventId = 'some-other-id'; - sinon - .stub(_getInstance(browserSessionPersistence), '_get') - .returns(Promise.resolve(user.toJSON())); - - await initAndWait(inMemoryPersistence, browserPopupRedirectResolver); - expect(reload._reloadWithoutSaving).to.have.been.called; - }); - - it('Nulls out the current user if reload fails', async () => { - const stub = sinon.stub(_getInstance(inMemoryPersistence)); - stub._get.returns(Promise.resolve(testUser(oldAuth, 'uid').toJSON())); - stub._remove.returns(Promise.resolve()); - reloadStub.returns( - Promise.reject( - _createError(AuthErrorCode.TOKEN_EXPIRED, { - appName: 'app' - }) - ) - ); - - await initAndWait(inMemoryPersistence); - expect(stub._remove).to.have.been.called; - }); - - it('Keeps current user if reload fails with network error', async () => { - const stub = sinon.stub(_getInstance(inMemoryPersistence)); - stub._get.returns(Promise.resolve(testUser(oldAuth, 'uid').toJSON())); - stub._remove.returns(Promise.resolve()); - reloadStub.returns( - Promise.reject( - _createError(AuthErrorCode.NETWORK_REQUEST_FAILED, { - appName: 'app' - }) - ) - ); - - await initAndWait(inMemoryPersistence); - expect(stub._remove).not.to.have.been.called; - }); - - it('sets auth name and config', async () => { - const auth = await initAndWait(inMemoryPersistence); - expect(auth.name).to.eq(FAKE_APP.name); - expect(auth.config).to.eql({ - apiKey: FAKE_APP.options.apiKey, - authDomain: FAKE_APP.options.authDomain, - apiHost: DefaultConfig.API_HOST, - apiScheme: DefaultConfig.API_SCHEME, - tokenApiHost: DefaultConfig.TOKEN_API_HOST, - sdkClientVersion: _getClientVersion(ClientPlatform.BROWSER) - }); - }); - - context('#tryRedirectSignIn', () => { - it('returns null and clears the redirect user in case of error', async () => { - const stub = sinon.stub( - _getInstance(browserSessionPersistence) - ); - stub._remove.returns(Promise.resolve()); - completeRedirectFnStub.returns(Promise.reject(new Error('no'))); - - await initAndWait([inMemoryPersistence], browserPopupRedirectResolver); - expect(stub._remove).to.have.been.called; - }); - - it('does not run redirect sign in attempt if authDomain not set', async () => { - await initAndWait( - [inMemoryPersistence], - browserPopupRedirectResolver, - '' - ); - expect(completeRedirectFnStub).not.to.have.been.called; - }); - - it('signs in the redirect user if found', async () => { - let user: User | null = null; - completeRedirectFnStub.callsFake((auth: Auth) => { - user = testUser(auth, 'uid', 'redirectUser@test.com'); - return Promise.resolve( - new UserCredentialImpl({ - operationType: externs.OperationType.SIGN_IN, - user, - providerId: null - }) - ); - }); - - const auth = await initAndWait( - [inMemoryPersistence], - browserPopupRedirectResolver - ); - expect(user).not.to.be.null; - expect(auth.currentUser).to.eq(user); - }); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/platform_browser/iframe/iframe.ts b/packages-exp/auth-exp/src/platform_browser/iframe/iframe.ts deleted file mode 100644 index e8c819bd277..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/iframe/iframe.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SDK_VERSION } from '@firebase/app-exp'; -import { querystring } from '@firebase/util'; - -import { AuthErrorCode } from '../../core/errors'; -import { _assert, _createError } from '../../core/util/assert'; -import { Delay } from '../../core/util/delay'; -import { _emulatorUrl } from '../../core/util/emulator'; -import { Auth } from '../../model/auth'; -import { _window } from '../auth_window'; -import * as gapiLoader from './gapi'; - -const PING_TIMEOUT = new Delay(5000, 15000); -const IFRAME_PATH = '__/auth/iframe'; -const EMULATED_IFRAME_PATH = 'emulator/auth/iframe'; - -const IFRAME_ATTRIBUTES = { - style: { - position: 'absolute', - top: '-100px', - width: '1px', - height: '1px' - } -}; - -function getIframeUrl(auth: Auth): string { - const config = auth.config; - _assert(config.authDomain, auth, AuthErrorCode.MISSING_AUTH_DOMAIN); - const url = config.emulator - ? _emulatorUrl(config, EMULATED_IFRAME_PATH) - : `https://${auth.config.authDomain}/${IFRAME_PATH}`; - - const params = { - apiKey: config.apiKey, - appName: auth.name, - v: SDK_VERSION - }; - // Can pass 'eid' as one of 'p' (production), 's' (staging), or 't' (test) - // TODO: do we care about frameworks? pass them as fw= - - return `${url}?${querystring(params).slice(1)}`; -} - -export async function _openIframe(auth: Auth): Promise { - const context = await gapiLoader._loadGapi(auth); - const gapi = _window().gapi; - _assert(gapi, auth, AuthErrorCode.INTERNAL_ERROR); - return context.open( - { - where: document.body, - url: getIframeUrl(auth), - messageHandlersFilter: gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER, - attributes: IFRAME_ATTRIBUTES, - dontclear: true - }, - (iframe: gapi.iframes.Iframe) => - new Promise(async (resolve, reject) => { - await iframe.restyle({ - // Prevent iframe from closing on mouse out. - setHideOnLeave: false - }); - - const networkError = _createError( - auth, - AuthErrorCode.NETWORK_REQUEST_FAILED - ); - // Confirm iframe is correctly loaded. - // To fallback on failure, set a timeout. - const networkErrorTimer = _window().setTimeout(() => { - reject(networkError); - }, PING_TIMEOUT.get()); - // Clear timer and resolve pending iframe ready promise. - function clearTimerAndResolve(): void { - _window().clearTimeout(networkErrorTimer); - resolve(iframe); - } - // This returns an IThenable. However the reject part does not call - // when the iframe is not loaded. - iframe.ping(clearTimerAndResolve).then(clearTimerAndResolve, () => { - reject(networkError); - }); - }) - ); -} diff --git a/packages-exp/auth-exp/src/platform_browser/load_js.test.ts b/packages-exp/auth-exp/src/platform_browser/load_js.test.ts deleted file mode 100644 index 9d0d0028169..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/load_js.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { _generateCallbackName, _loadJS } from './load_js'; - -use(sinonChai); - -describe('platform-browser/load_js', () => { - afterEach(() => sinon.restore()); - - describe('_generateCallbackName', () => { - it('generates a callback with a prefix and a number', () => { - expect(_generateCallbackName('foo')).to.match(/__foo\d+/); - }); - }); - - describe('_loadJS', () => { - it('sets the appropriate properties', () => { - const el = document.createElement('script'); - sinon.stub(el); // Prevent actually setting the src attribute - sinon.stub(document, 'createElement').returns(el); - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - _loadJS('http://localhost/url'); - expect(el.setAttribute).to.have.been.calledWith( - 'src', - 'http://localhost/url' - ); - expect(el.type).to.eq('text/javascript'); - expect(el.charset).to.eq('UTF-8'); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/platform_browser/load_js.ts b/packages-exp/auth-exp/src/platform_browser/load_js.ts deleted file mode 100644 index c94ce8a38fb..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/load_js.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -function getScriptParentElement(): HTMLDocument | HTMLHeadElement { - return document.getElementsByTagName('head')?.[0] ?? document; -} - -export function _loadJS(url: string): Promise { - // TODO: consider adding timeout support & cancellation - return new Promise((resolve, reject) => { - const el = document.createElement('script'); - el.setAttribute('src', url); - el.onload = resolve; - el.onerror = reject; - el.type = 'text/javascript'; - el.charset = 'UTF-8'; - getScriptParentElement().appendChild(el); - }); -} - -export function _generateCallbackName(prefix: string): string { - return `__${prefix}${Math.floor(Math.random() * 1000000)}`; -} diff --git a/packages-exp/auth-exp/src/platform_browser/messagechannel/promise.test.ts b/packages-exp/auth-exp/src/platform_browser/messagechannel/promise.test.ts deleted file mode 100644 index 0bf17f50772..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/messagechannel/promise.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { _allSettled } from './promise'; - -describe('platform_browser/messagechannel/promise', () => { - describe('_allSettled', () => { - it('should work with a single successfull promise', async () => { - const result = await _allSettled([Promise.resolve('foo')]); - expect(result).to.have.deep.members([ - { - fulfilled: true, - value: 'foo' - } - ]); - }); - - it('should work with a failed promise', async () => { - const result = await _allSettled([Promise.reject('bar')]); - expect(result).to.have.deep.members([ - { - fulfilled: false, - reason: 'bar' - } - ]); - }); - - it('should work with mixed promises', async () => { - const result = await _allSettled([ - Promise.resolve('foo'), - Promise.reject('bar') - ]); - expect(result).to.have.deep.members([ - { - fulfilled: true, - value: 'foo' - }, - { - fulfilled: false, - reason: 'bar' - } - ]); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/platform_browser/mfa/assertions/phone.test.ts b/packages-exp/auth-exp/src/platform_browser/mfa/assertions/phone.test.ts deleted file mode 100644 index cf72fa883e4..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/mfa/assertions/phone.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ProviderId } from '@firebase/auth-types-exp'; -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { mockEndpoint } from '../../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../../test/helpers/mock_auth'; -import * as mockFetch from '../../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../../api'; -import { FinalizeMfaResponse } from '../../../api/authentication/mfa'; -import { PhoneAuthCredential } from '../../../core/credentials/phone'; -import { MultiFactorSession } from '../../../mfa/mfa_session'; -import { PhoneAuthProvider } from '../../providers/phone'; -import { PhoneMultiFactorAssertion, PhoneMultiFactorGenerator } from './phone'; - -use(chaiAsPromised); - -describe('platform_browser/mfa/phone', () => { - let auth: TestAuth; - let credential: PhoneAuthCredential; - let assertion: PhoneMultiFactorAssertion; - let session: MultiFactorSession; - - const serverResponse: FinalizeMfaResponse = { - idToken: 'final-id-token', - refreshToken: 'refresh-token' - }; - - beforeEach(async () => { - mockFetch.setUp(); - auth = await testAuth(); - credential = PhoneAuthProvider.credential( - 'verification-id', - 'verification-code' - ); - assertion = PhoneMultiFactorAssertion._fromCredential(credential); - }); - afterEach(mockFetch.tearDown); - - describe('enroll', () => { - beforeEach(() => { - session = MultiFactorSession._fromIdtoken('enrollment-id-token'); - }); - - it('should finalize the MFA enrollment', async () => { - const mock = mockEndpoint( - Endpoint.FINALIZE_PHONE_MFA_ENROLLMENT, - serverResponse - ); - const response = await assertion._process(auth, session); - expect(response).to.eql(serverResponse); - expect(mock.calls[0].request).to.eql({ - idToken: 'enrollment-id-token', - tenantId: auth.tenantId, - phoneVerificationInfo: { - code: 'verification-code', - sessionInfo: 'verification-id' - } - }); - }); - - context('with display name', () => { - it('should set the display name', async () => { - const mock = mockEndpoint( - Endpoint.FINALIZE_PHONE_MFA_ENROLLMENT, - serverResponse - ); - const response = await assertion._process( - auth, - session, - 'display-name' - ); - expect(response).to.eql(serverResponse); - expect(mock.calls[0].request).to.eql({ - idToken: 'enrollment-id-token', - displayName: 'display-name', - tenantId: auth.tenantId, - phoneVerificationInfo: { - code: 'verification-code', - sessionInfo: 'verification-id' - } - }); - }); - }); - }); - - describe('sign_in', () => { - beforeEach(() => { - session = MultiFactorSession._fromMfaPendingCredential( - 'mfa-pending-credential' - ); - }); - - it('should finalize the MFA sign in', async () => { - const mock = mockEndpoint( - Endpoint.FINALIZE_PHONE_MFA_SIGN_IN, - serverResponse - ); - const response = await assertion._process(auth, session); - expect(response).to.eql(serverResponse); - expect(mock.calls[0].request).to.eql({ - mfaPendingCredential: 'mfa-pending-credential', - tenantId: null, - phoneVerificationInfo: { - code: 'verification-code', - sessionInfo: 'verification-id' - } - }); - }); - }); -}); - -describe('core/mfa/phone/PhoneMultiFactorGenerator', () => { - describe('.assertion', () => { - let credential: PhoneAuthCredential; - - beforeEach(async () => { - credential = PhoneAuthProvider.credential( - 'verification-id', - 'verification-code' - ); - }); - - it('can be used to create an assertion', () => { - const assertion = PhoneMultiFactorGenerator.assertion(credential); - expect(assertion.factorId).to.eq(ProviderId.PHONE); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/platform_browser/mfa/assertions/phone.ts b/packages-exp/auth-exp/src/platform_browser/mfa/assertions/phone.ts deleted file mode 100644 index 226552f3add..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/mfa/assertions/phone.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import * as externs from '@firebase/auth-types-exp'; - -import { MultiFactorAssertion } from '../../../mfa/mfa_assertion'; -import { Auth } from '../../../model/auth'; -import { finalizeEnrollPhoneMfa } from '../../../api/account_management/mfa'; -import { PhoneAuthCredential } from '../../../core/credentials/phone'; -import { - finalizeSignInPhoneMfa, - FinalizeMfaResponse -} from '../../../api/authentication/mfa'; - -/** - * {@inheritdoc @firebase/auth-types#PhoneMultiFactorAssertion} - * - * @public - */ -export class PhoneMultiFactorAssertion - extends MultiFactorAssertion - implements externs.PhoneMultiFactorAssertion { - private constructor(private readonly credential: PhoneAuthCredential) { - super(externs.FactorId.PHONE); - } - - /** @internal */ - static _fromCredential( - credential: PhoneAuthCredential - ): PhoneMultiFactorAssertion { - return new PhoneMultiFactorAssertion(credential); - } - - /** @internal */ - _finalizeEnroll( - auth: Auth, - idToken: string, - displayName?: string | null - ): Promise { - return finalizeEnrollPhoneMfa(auth, { - idToken, - displayName, - phoneVerificationInfo: this.credential._makeVerificationRequest() - }); - } - - /** @internal */ - _finalizeSignIn( - auth: Auth, - mfaPendingCredential: string - ): Promise { - return finalizeSignInPhoneMfa(auth, { - mfaPendingCredential, - phoneVerificationInfo: this.credential._makeVerificationRequest() - }); - } -} - -/** - * {@inheritdoc @firebase/auth-types#PhoneMultiFactorGenerator} - * @public - */ -export class PhoneMultiFactorGenerator - implements externs.PhoneMultiFactorGenerator { - private constructor() {} - - /** {@inheritdoc @firebase/auth-types#PhoneMultiFactorGenerator.assertion} */ - static assertion( - credential: externs.PhoneAuthCredential - ): externs.PhoneMultiFactorAssertion { - return PhoneMultiFactorAssertion._fromCredential( - credential as PhoneAuthCredential - ); - } -} diff --git a/packages-exp/auth-exp/src/platform_browser/persistence/browser.ts b/packages-exp/auth-exp/src/platform_browser/persistence/browser.ts deleted file mode 100644 index b27af2a1ef8..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/persistence/browser.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - PersistenceValue, - STORAGE_AVAILABLE_KEY, - PersistenceType -} from '../../core/persistence'; - -// There are two different browser persistence types: local and session. -// Both have the same implementation but use a different underlying storage -// object. - -export abstract class BrowserPersistenceClass { - protected constructor( - protected readonly storage: Storage, - readonly type: PersistenceType - ) {} - - _isAvailable(this: BrowserPersistenceClass): Promise { - try { - if (!this.storage) { - return Promise.resolve(false); - } - this.storage.setItem(STORAGE_AVAILABLE_KEY, '1'); - this.storage.removeItem(STORAGE_AVAILABLE_KEY); - return Promise.resolve(true); - } catch { - return Promise.resolve(false); - } - } - - _set(key: string, value: PersistenceValue): Promise { - this.storage.setItem(key, JSON.stringify(value)); - return Promise.resolve(); - } - - _get(key: string): Promise { - const json = this.storage.getItem(key); - return Promise.resolve(json ? JSON.parse(json) : null); - } - - _remove(key: string): Promise { - this.storage.removeItem(key); - return Promise.resolve(); - } -} diff --git a/packages-exp/auth-exp/src/platform_browser/persistence/local_storage.ts b/packages-exp/auth-exp/src/platform_browser/persistence/local_storage.ts deleted file mode 100644 index c3b911315f5..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/persistence/local_storage.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { getUA } from '@firebase/util'; -import { - _isSafari, - _isIOS, - _isIframe, - _isMobileBrowser, - _isIE10 -} from '../../core/util/browser'; -import { - Persistence, - PersistenceType, - StorageEventListener -} from '../../core/persistence'; -import { BrowserPersistenceClass } from './browser'; - -function _iframeCannotSyncWebStorage(): boolean { - const ua = getUA(); - return _isSafari(ua) || _isIOS(ua); -} - -// The polling period in case events are not supported -export const _POLLING_INTERVAL_MS = 1000; - -// The IE 10 localStorage cross tab synchronization delay in milliseconds -const IE10_LOCAL_STORAGE_SYNC_DELAY = 10; - -class BrowserLocalPersistence - extends BrowserPersistenceClass - implements Persistence { - static type: 'LOCAL' = 'LOCAL'; - - constructor() { - super(localStorage, PersistenceType.LOCAL); - this.boundEventHandler = this.onStorageEvent.bind(this); - } - - private readonly boundEventHandler: ( - event: StorageEvent, - poll?: boolean - ) => void; - private readonly listeners: Record> = {}; - private readonly localCache: Record = {}; - // setTimeout return value is platform specific - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private pollTimer: any | null = null; - - // Safari or iOS browser and embedded in an iframe. - private readonly safariLocalStorageNotSynced = - _iframeCannotSyncWebStorage() && _isIframe(); - // Whether to use polling instead of depending on window events - private readonly fallbackToPolling = _isMobileBrowser(); - - private forAllChangedKeys( - cb: (key: string, oldValue: string | null, newValue: string | null) => void - ): void { - // Check all keys with listeners on them. - for (const key of Object.keys(this.listeners)) { - // Get value from localStorage. - const newValue = this.storage.getItem(key); - const oldValue = this.localCache[key]; - // If local map value does not match, trigger listener with storage event. - // Differentiate this simulated event from the real storage event. - if (newValue !== oldValue) { - cb(key, oldValue, newValue); - } - } - } - - private onStorageEvent(event: StorageEvent, poll: boolean = false): void { - // Key would be null in some situations, like when localStorage is cleared - if (!event.key) { - this.forAllChangedKeys( - (key: string, _oldValue: string | null, newValue: string | null) => { - this.notifyListeners(key, newValue); - } - ); - return; - } - - const key = event.key; - - // Ignore keys that have no listeners. - if (!this.listeners[key]) { - return; - } - - // Check the mechanism how this event was detected. - // The first event will dictate the mechanism to be used. - if (poll) { - // Environment detects storage changes via polling. - // Remove storage event listener to prevent possible event duplication. - this.detachListener(); - } else { - // Environment detects storage changes via storage event listener. - // Remove polling listener to prevent possible event duplication. - this.stopPolling(); - } - - // Safari embedded iframe. Storage event will trigger with the delta - // changes but no changes will be applied to the iframe localStorage. - if (this.safariLocalStorageNotSynced) { - // Get current iframe page value. - const storedValue = this.storage.getItem(key); - // Value not synchronized, synchronize manually. - if (event.newValue !== storedValue) { - if (event.newValue !== null) { - // Value changed from current value. - this.storage.setItem(key, event.newValue); - } else { - // Current value deleted. - this.storage.removeItem(key); - } - } else if (this.localCache[key] === event.newValue && !poll) { - // Already detected and processed, do not trigger listeners again. - return; - } - } - - const triggerListeners = (): void => { - // Keep local map up to date in case storage event is triggered before - // poll. - const storedValue = this.storage.getItem(key); - if (!poll && this.localCache[key] === storedValue) { - // Real storage event which has already been detected, do nothing. - // This seems to trigger in some IE browsers for some reason. - return; - } - this.notifyListeners(key, storedValue); - }; - - const storedValue = this.storage.getItem(key); - if ( - _isIE10() && - storedValue !== event.newValue && - event.newValue !== event.oldValue - ) { - // IE 10 has this weird bug where a storage event would trigger with the - // correct key, oldValue and newValue but localStorage.getItem(key) does - // not yield the updated value until a few milliseconds. This ensures - // this recovers from that situation. - setTimeout(triggerListeners, IE10_LOCAL_STORAGE_SYNC_DELAY); - } else { - triggerListeners(); - } - } - - private notifyListeners(key: string, value: string | null): void { - if (!this.listeners[key]) { - return; - } - this.localCache[key] = value; - for (const listener of Array.from(this.listeners[key])) { - listener(value ? JSON.parse(value) : value); - } - } - - private startPolling(): void { - this.stopPolling(); - - this.pollTimer = setInterval(() => { - this.forAllChangedKeys( - (key: string, oldValue: string | null, newValue: string | null) => { - this.onStorageEvent( - new StorageEvent('storage', { - key, - oldValue, - newValue - }), - /* poll */ true - ); - } - ); - }, _POLLING_INTERVAL_MS); - } - - private stopPolling(): void { - if (this.pollTimer) { - clearInterval(this.pollTimer); - this.pollTimer = null; - } - } - - private attachListener(): void { - window.addEventListener('storage', this.boundEventHandler); - } - - private detachListener(): void { - window.removeEventListener('storage', this.boundEventHandler); - } - - _addListener(key: string, listener: StorageEventListener): void { - this.localCache[key] = this.storage.getItem(key); - if (Object.keys(this.listeners).length === 0) { - // Whether browser can detect storage event when it had already been pushed to the background. - // This may happen in some mobile browsers. A localStorage change in the foreground window - // will not be detected in the background window via the storage event. - // This was detected in iOS 7.x mobile browsers - if (this.fallbackToPolling) { - this.startPolling(); - } else { - this.attachListener(); - } - } - this.listeners[key] = this.listeners[key] || new Set(); - this.listeners[key].add(listener); - } - - _removeListener(key: string, listener: StorageEventListener): void { - if (this.listeners[key]) { - this.listeners[key].delete(listener); - - if (this.listeners[key].size === 0) { - delete this.listeners[key]; - delete this.localCache[key]; - } - } - - if (Object.keys(this.listeners).length === 0) { - this.detachListener(); - this.stopPolling(); - } - } -} - -/** - * An implementation of {@link @firebase/auth-types#Persistence} of type 'LOCAL' using `localStorage` - * for the underlying storage. - * - * @public - */ -export const browserLocalPersistence: externs.Persistence = BrowserLocalPersistence; diff --git a/packages-exp/auth-exp/src/platform_browser/popup_redirect.test.ts b/packages-exp/auth-exp/src/platform_browser/popup_redirect.test.ts deleted file mode 100644 index fa314cf8597..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/popup_redirect.test.ts +++ /dev/null @@ -1,341 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { SDK_VERSION } from '@firebase/app-exp'; -import { Config, ProviderId } from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { - TEST_AUTH_DOMAIN, - TEST_KEY, - testAuth, - TestAuth -} from '../../test/helpers/mock_auth'; -import { AuthEventManager } from '../core/auth/auth_event_manager'; -import { OAuthProvider } from '../core/providers/oauth'; -import { SingletonInstantiator } from '../core/util/instantiator'; -import * as validateOrigin from '../core/util/validate_origin'; -import { - AuthEvent, - AuthEventType, - GapiAuthEvent, - PopupRedirectResolver -} from '../model/popup_redirect'; -import * as authWindow from './auth_window'; -import * as gapiLoader from './iframe/gapi'; -import { browserPopupRedirectResolver } from './popup_redirect'; - -use(chaiAsPromised); -use(sinonChai); - -describe('platform_browser/popup_redirect', () => { - let resolver: PopupRedirectResolver; - let auth: TestAuth; - let onIframeMessage: (event: GapiAuthEvent) => Promise; - let iframeSendStub: sinon.SinonStub; - - beforeEach(async () => { - auth = await testAuth(); - resolver = new (browserPopupRedirectResolver as SingletonInstantiator< - PopupRedirectResolver - >)(); - - sinon.stub(validateOrigin, '_validateOrigin').returns(Promise.resolve()); - iframeSendStub = sinon.stub(); - - sinon.stub(gapiLoader, '_loadGapi').returns( - Promise.resolve(({ - open: () => - Promise.resolve({ - register: ( - _message: string, - cb: (event: GapiAuthEvent) => Promise - ) => (onIframeMessage = cb), - send: iframeSendStub - }) - } as unknown) as gapi.iframes.Context) - ); - }); - - afterEach(() => { - sinon.restore(); - }); - - context('#_openPopup', () => { - let popupUrl: string | undefined; - let provider: OAuthProvider; - const event = AuthEventType.LINK_VIA_POPUP; - - beforeEach(async () => { - sinon.stub(window, 'open').callsFake(url => { - popupUrl = url; - return {} as Window; - }); - provider = new OAuthProvider(ProviderId.GOOGLE); - }); - - it('builds the correct url', async () => { - await resolver._initialize(auth); - provider.addScope('some-scope-a'); - provider.addScope('some-scope-b'); - provider.setCustomParameters({ foo: 'bar' }); - - await resolver._openPopup(auth, provider, event); - expect(popupUrl).to.include( - `https://${TEST_AUTH_DOMAIN}/__/auth/handler` - ); - expect(popupUrl).to.include(`apiKey=${TEST_KEY}`); - expect(popupUrl).to.include('appName=test-app'); - expect(popupUrl).to.include(`authType=${AuthEventType.LINK_VIA_POPUP}`); - expect(popupUrl).to.include(`v=${SDK_VERSION}`); - expect(popupUrl).to.include('scopes=some-scope-a%2Csome-scope-b'); - expect(popupUrl).to.include( - 'customParameters=%7B%22foo%22%3A%22bar%22%7D' - ); - }); - - it('throws an error if authDomain is unspecified', async () => { - delete auth.config.authDomain; - await resolver._initialize(auth); - - await expect( - resolver._openPopup(auth, provider, event) - ).to.be.rejectedWith(FirebaseError, 'auth/auth-domain-config-required'); - }); - - it('throws an error if apiKey is unspecified', async () => { - delete (auth.config as Partial).apiKey; - await resolver._initialize(auth); - - await expect( - resolver._openPopup(auth, provider, event) - ).to.be.rejectedWith(FirebaseError, 'auth/invalid-api-key'); - }); - - it('rejects immediately if origin validation fails', async () => { - await resolver._initialize(auth); - (validateOrigin._validateOrigin as sinon.SinonStub).returns( - Promise.reject(new Error('invalid-origin')) - ); - - await expect( - resolver._openPopup(auth, provider, event) - ).to.be.rejectedWith(Error, 'invalid-origin'); - }); - }); - - context('#_openRedirect', () => { - let newWindowLocation: string; - let provider: OAuthProvider; - const event = AuthEventType.LINK_VIA_POPUP; - - beforeEach(async () => { - provider = new OAuthProvider(ProviderId.GOOGLE); - await resolver._initialize(auth); - sinon.stub(authWindow, '_setWindowLocation').callsFake(url => { - newWindowLocation = url; - }); - }); - - it('builds the correct url', async () => { - provider.addScope('some-scope-a'); - provider.addScope('some-scope-b'); - provider.setCustomParameters({ foo: 'bar' }); - - // This promise will never resolve on purpose - // eslint-disable-next-line @typescript-eslint/no-floating-promises - resolver._openRedirect(auth, provider, event); - - // Delay one tick - await Promise.resolve(); - - expect(newWindowLocation).to.include( - `https://${TEST_AUTH_DOMAIN}/__/auth/handler` - ); - expect(newWindowLocation).to.include(`apiKey=${TEST_KEY}`); - expect(newWindowLocation).to.include('appName=test-app'); - expect(newWindowLocation).to.include( - `authType=${AuthEventType.LINK_VIA_POPUP}` - ); - expect(newWindowLocation).to.include(`v=${SDK_VERSION}`); - expect(newWindowLocation).to.include( - 'scopes=some-scope-a%2Csome-scope-b' - ); - expect(newWindowLocation).to.include( - 'customParameters=%7B%22foo%22%3A%22bar%22%7D' - ); - }); - - it('throws an error if authDomain is unspecified', async () => { - delete auth.config.authDomain; - - await expect( - resolver._openRedirect(auth, provider, event) - ).to.be.rejectedWith(FirebaseError, 'auth/auth-domain-config-required'); - }); - - it('throws an error if apiKey is unspecified', async () => { - delete (auth.config as Partial).apiKey; - - await expect( - resolver._openRedirect(auth, provider, event) - ).to.be.rejectedWith(FirebaseError, 'auth/invalid-api-key'); - }); - - it('rejects immediately if origin validation fails', async () => { - (validateOrigin._validateOrigin as sinon.SinonStub).returns( - Promise.reject(new Error('invalid-origin')) - ); - await expect( - resolver._openRedirect(auth, provider, event) - ).to.be.rejectedWith(Error, 'invalid-origin'); - }); - }); - - context('#_initialize', () => { - it('returns different manager for a different auth', async () => { - const manager = await resolver._initialize(auth); - expect(await resolver._initialize(auth)).to.eq(manager); - - const secondAuth = await testAuth(); - secondAuth.config.authDomain = 'something-else'; - const secondManager = await resolver._initialize(secondAuth); - expect(secondManager).not.to.eq(manager); - expect(await resolver._initialize(secondAuth)).to.eq(secondManager); - }); - - it('initialization promise is cached as well for diff auths', async () => { - const promise = resolver._initialize(auth); - expect(resolver._initialize(auth)).to.eq(promise); - - const secondAuth = await testAuth(); - secondAuth.config.authDomain = 'something-else'; - const secondPromise = resolver._initialize(secondAuth); - expect(secondPromise).not.to.eq(promise); - expect(resolver._initialize(secondAuth)).to.eq(secondPromise); - }); - - it('iframe event goes through to the manager', async () => { - const manager = (await resolver._initialize(auth)) as AuthEventManager; - sinon.stub(manager, 'onEvent').returns(true); - const response = await onIframeMessage({ - type: 'authEvent', - authEvent: { type: AuthEventType.LINK_VIA_POPUP } as AuthEvent - }); - - expect(manager.onEvent).to.have.been.calledWith({ - type: AuthEventType.LINK_VIA_POPUP - }); - expect(response).to.eql({ - status: 'ACK' - }); - }); - - it('errors with invalid event if null event', async () => { - const manager = (await resolver._initialize(auth)) as AuthEventManager; - sinon.stub(manager, 'onEvent').returns(true); - - expect(() => - onIframeMessage({ - type: 'authEvent', - authEvent: (null as unknown) as AuthEvent - }) - ).to.throw(FirebaseError, 'auth/invalid-auth-event'); - }); - - it('errors with invalid event if everything is null', async () => { - const manager = (await resolver._initialize(auth)) as AuthEventManager; - sinon.stub(manager, 'onEvent').returns(true); - expect(() => - onIframeMessage((null as unknown) as GapiAuthEvent) - ).to.throw(FirebaseError, 'auth/invalid-auth-event'); - }); - - it('returns error to the iframe if the event was not handled', async () => { - const manager = (await resolver._initialize(auth)) as AuthEventManager; - sinon.stub(manager, 'onEvent').returns(false); - const response = await onIframeMessage({ - type: 'authEvent', - authEvent: { type: AuthEventType.LINK_VIA_POPUP } as AuthEvent - }); - - expect(manager.onEvent).to.have.been.calledWith({ - type: AuthEventType.LINK_VIA_POPUP - }); - expect(response).to.eql({ - status: 'ERROR' - }); - }); - }); - - context('#_isIframeWebStorageSupported', () => { - beforeEach(async () => { - await resolver._initialize(auth); - }); - - function setIframeResponse(value: unknown): void { - iframeSendStub.callsFake( - ( - _message: string, - _event: unknown, - callback: (response: unknown) => void - ) => { - callback(value); - } - ); - } - - it('calls the iframe send method with the correct parameters', () => { - resolver._isIframeWebStorageSupported(auth, () => {}); - expect(iframeSendStub).to.have.been.calledOnce; - const args = iframeSendStub.getCalls()[0].args; - expect(args[0]).to.eq('webStorageSupport'); - expect(args[1]).to.eql({ - type: 'webStorageSupport' - }); - expect(args[3]).to.eq(gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER); - }); - - it('passes through true value from the response to the callback', done => { - setIframeResponse([{ webStorageSupport: true }]); - resolver._isIframeWebStorageSupported(auth, supported => { - expect(supported).to.be.true; - done(); - }); - }); - - it('passes through false value from the response to callback', done => { - setIframeResponse([{ webStorageSupport: false }]); - resolver._isIframeWebStorageSupported(auth, supported => { - expect(supported).to.be.false; - done(); - }); - }); - - it('throws an error if the response is malformed', () => { - setIframeResponse({}); - expect(() => - resolver._isIframeWebStorageSupported(auth, () => {}) - ).to.throw(FirebaseError, 'auth/internal-error'); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts b/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts deleted file mode 100644 index cb3e4431f44..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts +++ /dev/null @@ -1,287 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SDK_VERSION } from '@firebase/app-exp'; -import * as externs from '@firebase/auth-types-exp'; -import { isEmpty, querystring } from '@firebase/util'; -import { _getInstance } from '../core/util/instantiator'; - -import { AuthEventManager } from '../core/auth/auth_event_manager'; -import { AuthErrorCode } from '../core/errors'; -import { OAuthProvider } from '../core/providers/oauth'; -import { _assert, debugAssert, _fail } from '../core/util/assert'; -import { _emulatorUrl } from '../core/util/emulator'; -import { _generateEventId } from '../core/util/event_id'; -import { _getCurrentUrl } from '../core/util/location'; -import { _validateOrigin } from '../core/util/validate_origin'; -import { ApiKey, AppName, Auth } from '../model/auth'; -import { - AuthEventType, - EventManager, - GapiAuthEvent, - GapiOutcome, - PopupRedirectResolver -} from '../model/popup_redirect'; -import { _setWindowLocation } from './auth_window'; -import { _openIframe } from './iframe/iframe'; -import { browserSessionPersistence } from './persistence/session_storage'; -import { _open, AuthPopup } from './util/popup'; -import { _getRedirectResult } from './strategies/redirect'; - -/** - * URL for Authentication widget which will initiate the OAuth handshake - * - * @internal - */ -const WIDGET_PATH = '__/auth/handler'; - -/** - * URL for emulated environment - * - * @internal - */ -const EMULATOR_WIDGET_PATH = 'emulator/auth/handler'; - -/** - * The special web storage event - * - * @internal - */ -const WEB_STORAGE_SUPPORT_KEY = 'webStorageSupport'; - -interface WebStorageSupportMessage extends gapi.iframes.Message { - [index: number]: Record; -} - -interface ManagerOrPromise { - manager?: EventManager; - promise?: Promise; -} - -/** - * Chooses a popup/redirect resolver to use. This prefers the override (which - * is directly passed in), and falls back to the property set on the auth - * object. If neither are available, this function errors w/ an argument error. - * - * @internal - */ -export function _withDefaultResolver( - auth: Auth, - resolverOverride: externs.PopupRedirectResolver | undefined -): PopupRedirectResolver { - if (resolverOverride) { - return _getInstance(resolverOverride); - } - - _assert(auth._popupRedirectResolver, auth, AuthErrorCode.ARGUMENT_ERROR); - - return auth._popupRedirectResolver; -} - -class BrowserPopupRedirectResolver implements PopupRedirectResolver { - private readonly eventManagers: Record = {}; - private readonly iframes: Record = {}; - private readonly originValidationPromises: Record> = {}; - - readonly _redirectPersistence = browserSessionPersistence; - - // Wrapping in async even though we don't await anywhere in order - // to make sure errors are raised as promise rejections - async _openPopup( - auth: Auth, - provider: externs.AuthProvider, - authType: AuthEventType, - eventId?: string - ): Promise { - debugAssert( - this.eventManagers[auth._key()]?.manager, - '_initialize() not called before _openPopup()' - ); - - await this.originValidation(auth); - const url = getRedirectUrl(auth, provider, authType, eventId); - return _open(auth, url, _generateEventId()); - } - - async _openRedirect( - auth: Auth, - provider: externs.AuthProvider, - authType: AuthEventType, - eventId?: string - ): Promise { - await this.originValidation(auth); - _setWindowLocation(getRedirectUrl(auth, provider, authType, eventId)); - return new Promise(() => {}); - } - - _initialize(auth: Auth): Promise { - const key = auth._key(); - if (this.eventManagers[key]) { - const { manager, promise } = this.eventManagers[key]; - if (manager) { - return Promise.resolve(manager); - } else { - debugAssert(promise, 'If manager is not set, promise should be'); - return promise; - } - } - - const promise = this.initAndGetManager(auth); - this.eventManagers[key] = { promise }; - return promise; - } - - private async initAndGetManager(auth: Auth): Promise { - const iframe = await _openIframe(auth); - const manager = new AuthEventManager(auth); - iframe.register( - 'authEvent', - (iframeEvent: GapiAuthEvent | null) => { - _assert(iframeEvent?.authEvent, auth, AuthErrorCode.INVALID_AUTH_EVENT); - // TODO: Consider splitting redirect and popup events earlier on - - const handled = manager.onEvent(iframeEvent.authEvent); - return { status: handled ? GapiOutcome.ACK : GapiOutcome.ERROR }; - }, - gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER - ); - - this.eventManagers[auth._key()] = { manager }; - this.iframes[auth._key()] = iframe; - return manager; - } - - _isIframeWebStorageSupported( - auth: Auth, - cb: (supported: boolean) => unknown - ): void { - const iframe = this.iframes[auth._key()]; - iframe.send( - WEB_STORAGE_SUPPORT_KEY, - { type: WEB_STORAGE_SUPPORT_KEY }, - result => { - const isSupported = result?.[0]?.[WEB_STORAGE_SUPPORT_KEY]; - if (isSupported !== undefined) { - cb(!!isSupported); - } - - _fail(auth, AuthErrorCode.INTERNAL_ERROR); - }, - gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER - ); - } - - private originValidation(auth: Auth): Promise { - const key = auth._key(); - if (!this.originValidationPromises[key]) { - this.originValidationPromises[key] = _validateOrigin(auth); - } - - return this.originValidationPromises[key]; - } - - _completeRedirectFn = _getRedirectResult; -} - -/** - * An implementation of {@link @firebase/auth-types#PopupRedirectResolver} suitable for browser - * based applications. - * - * @public - */ -export const browserPopupRedirectResolver: externs.PopupRedirectResolver = BrowserPopupRedirectResolver; - -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type WidgetParams = { - apiKey: ApiKey; - appName: AppName; - authType: AuthEventType; - redirectUrl: string; - v: string; - providerId?: string; - scopes?: string; - customParameters?: string; - eventId?: string; - tid?: string; -}; - -function getRedirectUrl( - auth: Auth, - provider: externs.AuthProvider, - authType: AuthEventType, - eventId?: string -): string { - _assert(auth.config.authDomain, auth, AuthErrorCode.MISSING_AUTH_DOMAIN); - _assert(auth.config.apiKey, auth, AuthErrorCode.INVALID_API_KEY); - - const params: WidgetParams = { - apiKey: auth.config.apiKey, - appName: auth.name, - authType, - redirectUrl: _getCurrentUrl(), - v: SDK_VERSION, - eventId - }; - - if (provider instanceof OAuthProvider) { - provider.setDefaultLanguage(auth.languageCode); - params.providerId = provider.providerId || ''; - if (!isEmpty(provider.getCustomParameters())) { - params.customParameters = JSON.stringify(provider.getCustomParameters()); - } - const scopes = provider.getScopes().filter(scope => scope !== ''); - if (scopes.length > 0) { - params.scopes = scopes.join(','); - } - // TODO set additionalParams? - // let additionalParams = provider.getAdditionalParams(); - // for (let key in additionalParams) { - // if (!params.hasOwnProperty(key)) { - // params[key] = additionalParams[key] - // } - // } - } - - if (auth.tenantId) { - params.tid = auth.tenantId; - } - - for (const key of Object.keys(params)) { - if ((params as Record)[key] === undefined) { - delete (params as Record)[key]; - } - } - - // TODO: maybe set eid as endipointId - // TODO: maybe set fw as Frameworks.join(",") - - const url = new URL( - `${getHandlerBase(auth)}?${querystring( - params as Record - ).slice(1)}` - ); - - return url.toString(); -} - -function getHandlerBase({ config }: Auth): string { - if (!config.emulator) { - return `https://${config.authDomain}/${WIDGET_PATH}`; - } - - return _emulatorUrl(config, EMULATOR_WIDGET_PATH); -} diff --git a/packages-exp/auth-exp/src/platform_browser/providers/phone.test.ts b/packages-exp/auth-exp/src/platform_browser/providers/phone.test.ts deleted file mode 100644 index 25383a0deb4..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/providers/phone.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import * as sinon from 'sinon'; - -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; -import * as fetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { RecaptchaVerifier } from '../../platform_browser/recaptcha/recaptcha_verifier'; -import { PhoneAuthProvider } from './phone'; - -describe('platform_browser/providers/phone', () => { - let auth: TestAuth; - - beforeEach(async () => { - fetch.setUp(); - auth = await testAuth(); - }); - - afterEach(() => { - fetch.tearDown(); - sinon.restore(); - }); - - context('#verifyPhoneNumber', () => { - it('calls verify on the appVerifier and then calls the server', async () => { - const route = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, { - sessionInfo: 'verification-id' - }); - - const verifier = new RecaptchaVerifier( - document.createElement('div'), - {}, - auth - ); - sinon - .stub(verifier, 'verify') - .returns(Promise.resolve('verification-code')); - - const provider = new PhoneAuthProvider(auth); - const result = await provider.verifyPhoneNumber('+15105550000', verifier); - expect(result).to.eq('verification-id'); - expect(route.calls[0].request).to.eql({ - phoneNumber: '+15105550000', - recaptchaToken: 'verification-code' - }); - }); - }); - - context('.credential', () => { - it('creates a phone auth credential', () => { - const credential = PhoneAuthProvider.credential('id', 'code'); - - // Allows us to inspect the object - const blob = credential.toJSON() as Record; - - expect(blob.verificationId).to.eq('id'); - expect(blob.verificationCode).to.eq('code'); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/platform_browser/providers/phone.ts b/packages-exp/auth-exp/src/platform_browser/providers/phone.ts deleted file mode 100644 index 9700cca1ff6..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/providers/phone.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { SignInWithPhoneNumberResponse } from '../../api/authentication/sms'; -import { ApplicationVerifier } from '../../model/application_verifier'; -import { Auth } from '../../model/auth'; -import { UserCredential } from '../../model/user'; -import { PhoneAuthCredential } from '../../core/credentials/phone'; -import { AuthErrorCode } from '../../core/errors'; -import { _verifyPhoneNumber } from '../strategies/phone'; -import { _assert, _fail } from '../../core/util/assert'; -import { _castAuth } from '../../core/auth/auth_impl'; - -/** - * {@inheritdoc @firebase/auth-types#PhoneAuthProvider} - * @public - */ -export class PhoneAuthProvider implements externs.PhoneAuthProvider { - /** {@inheritdoc @firebase/auth-types#PhoneAuthProvider.PROVIDER_ID} */ - static readonly PROVIDER_ID = externs.ProviderId.PHONE; - /** {@inheritdoc @firebase/auth-types#PhoneAuthProvider.PHONE_SIGN_IN_METHOD} */ - static readonly PHONE_SIGN_IN_METHOD = externs.SignInMethod.PHONE; - - /** {@inheritdoc @firebase/auth-types#PhoneAuthProvider.providerId} */ - readonly providerId = PhoneAuthProvider.PROVIDER_ID; - private readonly auth: Auth; - - constructor(auth: externs.Auth) { - this.auth = _castAuth(auth); - } - - /** {@inheritdoc @firebase/auth-types#PhoneAuthProvider.verifyPhoneNumber} */ - verifyPhoneNumber( - phoneOptions: externs.PhoneInfoOptions | string, - applicationVerifier: externs.ApplicationVerifier - ): Promise { - return _verifyPhoneNumber( - this.auth, - phoneOptions, - applicationVerifier as ApplicationVerifier - ); - } - - /** {@inheritdoc @firebase/auth-types#PhoneAuthProvider.credential} */ - static credential( - verificationId: string, - verificationCode: string - ): PhoneAuthCredential { - return PhoneAuthCredential._fromVerification( - verificationId, - verificationCode - ); - } - - static credentialFromResult( - userCredential: externs.UserCredential - ): externs.AuthCredential | null { - const credential = userCredential as UserCredential; - _assert( - credential._tokenResponse, - credential.user.auth, - AuthErrorCode.ARGUMENT_ERROR - ); - const { - phoneNumber, - temporaryProof - } = credential._tokenResponse as SignInWithPhoneNumberResponse; - if (phoneNumber && temporaryProof) { - return PhoneAuthCredential._fromTokenResponse( - phoneNumber, - temporaryProof - ); - } - - _fail(credential.user.auth, AuthErrorCode.ARGUMENT_ERROR); - } -} diff --git a/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha.ts b/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha.ts deleted file mode 100644 index aad5cfc5420..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface Parameters { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -export interface Recaptcha { - render: (container: HTMLElement, parameters: Parameters) => number; - getResponse: (id: number) => string; - execute: (id: number) => unknown; - reset: (id: number) => unknown; -} diff --git a/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_loader.ts b/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_loader.ts deleted file mode 100644 index 97afe888579..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_loader.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { querystring } from '@firebase/util'; - -import { AuthErrorCode } from '../../core/errors'; -import { _assert, _createError } from '../../core/util/assert'; -import { Delay } from '../../core/util/delay'; -import { Auth } from '../../model/auth'; -import { _window } from '../auth_window'; -import * as jsHelpers from '../load_js'; -import { Recaptcha } from './recaptcha'; -import { MockReCaptcha } from './recaptcha_mock'; - -// ReCaptcha will load using the same callback, so the callback function needs -// to be kept around -export const _JSLOAD_CALLBACK = jsHelpers._generateCallbackName('rcb'); -const NETWORK_TIMEOUT_DELAY = new Delay(30000, 60000); -const RECAPTCHA_BASE = 'https://www.google.com/recaptcha/api.js?'; - -export interface ReCaptchaLoader { - load(auth: Auth, hl?: string): Promise; - clearedOneInstance(): void; -} - -/** - * Loader for the GReCaptcha library. There should only ever be one of this. - */ -export class ReCaptchaLoaderImpl implements ReCaptchaLoader { - private hostLanguage = ''; - private counter = 0; - private readonly librarySeparatelyLoaded = !!_window().grecaptcha; - - load(auth: Auth, hl = ''): Promise { - _assert(isHostLanguageValid(hl), auth, AuthErrorCode.ARGUMENT_ERROR); - - if (this.shouldResolveImmediately(hl)) { - return Promise.resolve(_window().grecaptcha!); - } - return new Promise((resolve, reject) => { - const networkTimeout = _window().setTimeout(() => { - reject(_createError(auth, AuthErrorCode.NETWORK_REQUEST_FAILED)); - }, NETWORK_TIMEOUT_DELAY.get()); - - _window()[_JSLOAD_CALLBACK] = () => { - _window().clearTimeout(networkTimeout); - delete _window()[_JSLOAD_CALLBACK]; - - const recaptcha = _window().grecaptcha; - - if (!recaptcha) { - reject(_createError(auth, AuthErrorCode.INTERNAL_ERROR)); - return; - } - - // Wrap the greptcha render function so that we know if the developer has - // called it separately - const render = recaptcha.render; - recaptcha.render = (container, params) => { - const widgetId = render(container, params); - this.counter++; - return widgetId; - }; - - this.hostLanguage = hl; - resolve(recaptcha); - }; - - const url = `${RECAPTCHA_BASE}?${querystring({ - onload: _JSLOAD_CALLBACK, - render: 'explicit', - hl - })}`; - - jsHelpers._loadJS(url).catch(() => { - clearTimeout(networkTimeout); - reject(_createError(auth, AuthErrorCode.INTERNAL_ERROR)); - }); - }); - } - - clearedOneInstance(): void { - this.counter--; - } - - private shouldResolveImmediately(hl: string): boolean { - // We can resolve immediately if: - // • grecaptcha is already defined AND ( - // 1. the requested language codes are the same OR - // 2. there exists already a ReCaptcha on the page - // 3. the library was already loaded by the app - // In cases (2) and (3), we _can't_ reload as it would break the recaptchas - // that are already in the page - return ( - !!_window().grecaptcha && - (hl === this.hostLanguage || - this.counter > 0 || - this.librarySeparatelyLoaded) - ); - } -} - -function isHostLanguageValid(hl: string): boolean { - return hl.length <= 6 && /^\s*[a-zA-Z0-9\-]*\s*$/.test(hl); -} - -export class MockReCaptchaLoaderImpl implements ReCaptchaLoader { - async load(auth: Auth): Promise { - return new MockReCaptcha(auth); - } - - clearedOneInstance(): void {} -} diff --git a/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_mock.ts b/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_mock.ts deleted file mode 100644 index 2f78e9509a9..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_mock.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AuthErrorCode } from '../../core/errors'; -import { _assert } from '../../core/util/assert'; -import { Auth } from '../../model/auth'; -import { Parameters, Recaptcha } from './recaptcha'; - -export const _SOLVE_TIME_MS = 500; -export const _EXPIRATION_TIME_MS = 60_000; -export const _WIDGET_ID_START = 1_000_000_000_000; - -export interface Widget { - getResponse: () => string | null; - delete: () => void; - execute: () => void; -} - -export class MockReCaptcha implements Recaptcha { - private counter = _WIDGET_ID_START; - _widgets = new Map(); - - constructor(private readonly auth: Auth) {} - - render(container: string | HTMLElement, parameters?: Parameters): number { - const id = this.counter; - this._widgets.set( - id, - new MockWidget(container, this.auth.name, parameters || {}) - ); - this.counter++; - return id; - } - - reset(optWidgetId?: number): void { - const id = optWidgetId || _WIDGET_ID_START; - void this._widgets.get(id)?.delete(); - this._widgets.delete(id); - } - - getResponse(optWidgetId?: number): string { - const id = optWidgetId || _WIDGET_ID_START; - return this._widgets.get(id)?.getResponse() || ''; - } - - async execute(optWidgetId?: number | string): Promise { - const id: number = (optWidgetId as number) || _WIDGET_ID_START; - void this._widgets.get(id)?.execute(); - return ''; - } -} - -export class MockWidget { - private readonly container: HTMLElement; - private readonly isVisible: boolean; - private timerId: number | null = null; - private deleted = false; - private responseToken: string | null = null; - private readonly clickHandler = (): void => { - this.execute(); - }; - - constructor( - containerOrId: string | HTMLElement, - appName: string, - private readonly params: Parameters - ) { - const container = - typeof containerOrId === 'string' - ? document.getElementById(containerOrId) - : containerOrId; - _assert(container, AuthErrorCode.ARGUMENT_ERROR, { appName }); - - this.container = container; - this.isVisible = this.params.size !== 'invisible'; - if (this.isVisible) { - this.execute(); - } else { - this.container.addEventListener('click', this.clickHandler); - } - } - - getResponse(): string | null { - this.checkIfDeleted(); - return this.responseToken; - } - - delete(): void { - this.checkIfDeleted(); - this.deleted = true; - if (this.timerId) { - clearTimeout(this.timerId); - this.timerId = null; - } - this.container.removeEventListener('click', this.clickHandler); - } - - execute(): void { - this.checkIfDeleted(); - if (this.timerId) { - return; - } - - this.timerId = window.setTimeout(() => { - this.responseToken = generateRandomAlphaNumericString(50); - const { callback, 'expired-callback': expiredCallback } = this.params; - if (callback) { - try { - callback(this.responseToken); - } catch (e) {} - } - - this.timerId = window.setTimeout(() => { - this.timerId = null; - this.responseToken = null; - if (expiredCallback) { - try { - expiredCallback(); - } catch (e) {} - } - - if (this.isVisible) { - this.execute(); - } - }, _EXPIRATION_TIME_MS); - }, _SOLVE_TIME_MS); - } - - private checkIfDeleted(): void { - if (this.deleted) { - throw new Error('reCAPTCHA mock was already deleted!'); - } - } -} - -function generateRandomAlphaNumericString(len: number): string { - const chars = []; - const allowedChars = - '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - for (let i = 0; i < len; i++) { - chars.push( - allowedChars.charAt(Math.floor(Math.random() * allowedChars.length)) - ); - } - return chars.join(''); -} diff --git a/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_verifier.ts b/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_verifier.ts deleted file mode 100644 index 8ca52237ceb..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/recaptcha/recaptcha_verifier.ts +++ /dev/null @@ -1,256 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; -import { getRecaptchaParams } from '../../api/authentication/recaptcha'; -import { _castAuth } from '../../core/auth/auth_impl'; -import { AuthErrorCode } from '../../core/errors'; -import { _assert } from '../../core/util/assert'; -import { _isHttpOrHttps } from '../../core/util/location'; -import { ApplicationVerifier } from '../../model/application_verifier'; -import { Auth } from '../../model/auth'; -import { _window } from '../auth_window'; -import { _isWorker } from '../util/worker'; -import { Parameters, Recaptcha } from './recaptcha'; -import { - MockReCaptchaLoaderImpl, - ReCaptchaLoader, - ReCaptchaLoaderImpl -} from './recaptcha_loader'; - -export const RECAPTCHA_VERIFIER_TYPE = 'recaptcha'; - -const DEFAULT_PARAMS: Parameters = { - theme: 'light', - type: 'image' -}; - -type TokenCallback = (token: string) => void; - -/** - * {@inheritdoc @firebase/auth-types#RecaptchaVerifier} - * @public - */ -export class RecaptchaVerifier - implements externs.RecaptchaVerifier, ApplicationVerifier { - readonly type = RECAPTCHA_VERIFIER_TYPE; - private destroyed = false; - private widgetId: number | null = null; - private readonly container: HTMLElement; - private readonly isInvisible: boolean; - private readonly tokenChangeListeners = new Set(); - private renderPromise: Promise | null = null; - private readonly auth: Auth; - - /** @internal */ - readonly _recaptchaLoader: ReCaptchaLoader; - private recaptcha: Recaptcha | null = null; - - constructor( - containerOrId: HTMLElement | string, - private readonly parameters: Parameters = { - ...DEFAULT_PARAMS - }, - authExtern: externs.Auth - ) { - this.auth = _castAuth(authExtern); - this.isInvisible = this.parameters.size === 'invisible'; - _assert( - typeof document !== 'undefined', - this.auth, - AuthErrorCode.OPERATION_NOT_SUPPORTED - ); - const container = - typeof containerOrId === 'string' - ? document.getElementById(containerOrId) - : containerOrId; - _assert(container, this.auth, AuthErrorCode.ARGUMENT_ERROR); - - this.container = container; - this.parameters.callback = this.makeTokenCallback(this.parameters.callback); - - this._recaptchaLoader = this.auth.settings.appVerificationDisabledForTesting - ? new MockReCaptchaLoaderImpl() - : new ReCaptchaLoaderImpl(); - - this.validateStartingState(); - // TODO: Figure out if sdk version is needed - } - - /** {@inheritdoc @firebase/auth-types#RecaptchaVerifier.verify} */ - async verify(): Promise { - this.assertNotDestroyed(); - const id = await this.render(); - const recaptcha = this.getAssertedRecaptcha(); - - const response = recaptcha.getResponse(id); - if (response) { - return response; - } - - return new Promise(resolve => { - const tokenChange = (token: string): void => { - if (!token) { - return; // Ignore token expirations. - } - this.tokenChangeListeners.delete(tokenChange); - resolve(token); - }; - - this.tokenChangeListeners.add(tokenChange); - if (this.isInvisible) { - recaptcha.execute(id); - } - }); - } - - /** {@inheritdoc @firebase/auth-types#RecaptchaVerifier.render} */ - render(): Promise { - try { - this.assertNotDestroyed(); - } catch (e) { - // This method returns a promise. Since it's not async (we want to return the - // _same_ promise if rendering is still occurring), the API surface should - // reject with the error rather than just throw - return Promise.reject(e); - } - - if (this.renderPromise) { - return this.renderPromise; - } - - this.renderPromise = this.makeRenderPromise().catch(e => { - this.renderPromise = null; - throw e; - }); - - return this.renderPromise; - } - - /** @internal */ - _reset(): void { - this.assertNotDestroyed(); - if (this.widgetId !== null) { - this.getAssertedRecaptcha().reset(this.widgetId); - } - } - - /** {@inheritdoc @firebase/auth-types#RecaptchaVerifier.clear} */ - clear(): void { - this.assertNotDestroyed(); - this.destroyed = true; - this._recaptchaLoader.clearedOneInstance(); - if (!this.isInvisible) { - this.container.childNodes.forEach(node => { - this.container.removeChild(node); - }); - } - } - - private validateStartingState(): void { - _assert(!this.parameters.sitekey, this.auth, AuthErrorCode.ARGUMENT_ERROR); - _assert( - this.isInvisible || !this.container.hasChildNodes(), - this.auth, - AuthErrorCode.ARGUMENT_ERROR - ); - } - - private makeTokenCallback( - existing: TokenCallback | string | undefined - ): TokenCallback { - return token => { - this.tokenChangeListeners.forEach(listener => listener(token)); - if (typeof existing === 'function') { - existing(token); - } else if (typeof existing === 'string') { - const globalFunc = _window()[existing]; - if (typeof globalFunc === 'function') { - globalFunc(token); - } - } - }; - } - - private assertNotDestroyed(): void { - _assert(!this.destroyed, this.auth, AuthErrorCode.INTERNAL_ERROR); - } - - private async makeRenderPromise(): Promise { - await this.init(); - if (!this.widgetId) { - let container = this.container; - if (!this.isInvisible) { - const guaranteedEmpty = document.createElement('div'); - container.appendChild(guaranteedEmpty); - container = guaranteedEmpty; - } - - this.widgetId = this.getAssertedRecaptcha().render( - container, - this.parameters - ); - } - - return this.widgetId; - } - - private async init(): Promise { - _assert( - _isHttpOrHttps() && !_isWorker(), - this.auth, - AuthErrorCode.INTERNAL_ERROR - ); - - await domReady(); - this.recaptcha = await this._recaptchaLoader.load( - this.auth, - this.auth.languageCode || undefined - ); - - const siteKey = await getRecaptchaParams(this.auth); - _assert(siteKey, this.auth, AuthErrorCode.INTERNAL_ERROR); - this.parameters.sitekey = siteKey; - } - - private getAssertedRecaptcha(): Recaptcha { - _assert(this.recaptcha, this.auth, AuthErrorCode.INTERNAL_ERROR); - return this.recaptcha; - } -} - -function domReady(): Promise { - let resolver: (() => void) | null = null; - return new Promise(resolve => { - if (document.readyState === 'complete') { - resolve(); - return; - } - - // Document not ready, wait for load before resolving. - // Save resolver, so we can remove listener in case it was externally - // cancelled. - resolver = () => resolve(); - window.addEventListener('load', resolver); - }).catch(e => { - if (resolver) { - window.removeEventListener('load', resolver); - } - - throw e; - }); -} diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.ts b/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.ts deleted file mode 100644 index f476d6d15a8..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseError } from '@firebase/util'; - -import { - AuthEvent, - AuthEventConsumer, - AuthEventType, - EventManager, - PopupRedirectResolver -} from '../../model/popup_redirect'; -import { User, UserCredential } from '../../model/user'; -import { AuthErrorCode } from '../../core/errors'; -import { debugAssert, _fail } from '../../core/util/assert'; -import { - _link, - _reauth, - _signIn, - IdpTask, - IdpTaskParams -} from '../../core/strategies/idp'; -import { Auth } from '../../model/auth'; - -interface PendingPromise { - resolve: (cred: UserCredential | null) => void; - reject: (error: Error) => void; -} - -/** - * Popup event manager. Handles the popup's entire lifecycle; listens to auth - * events - */ -export abstract class AbstractPopupRedirectOperation - implements AuthEventConsumer { - private pendingPromise: PendingPromise | null = null; - private eventManager: EventManager | null = null; - readonly filter: AuthEventType[]; - - abstract eventId: string | null; - - constructor( - protected readonly auth: Auth, - filter: AuthEventType | AuthEventType[], - protected readonly resolver: PopupRedirectResolver, - protected user?: User, - private readonly bypassAuthState = false - ) { - this.filter = Array.isArray(filter) ? filter : [filter]; - } - - abstract onExecution(): Promise; - - execute(): Promise { - return new Promise(async (resolve, reject) => { - this.pendingPromise = { resolve, reject }; - - try { - this.eventManager = await this.resolver._initialize(this.auth); - await this.onExecution(); - this.eventManager.registerConsumer(this); - } catch (e) { - this.reject(e); - } - }); - } - - async onAuthEvent(event: AuthEvent): Promise { - const { urlResponse, sessionId, postBody, tenantId, error, type } = event; - if (error) { - this.reject(error); - return; - } - - const params: IdpTaskParams = { - auth: this.auth, - requestUri: urlResponse!, - sessionId: sessionId!, - tenantId: tenantId || undefined, - postBody: postBody || undefined, - user: this.user, - bypassAuthState: this.bypassAuthState - }; - - try { - this.resolve(await this.getIdpTask(type)(params)); - } catch (e) { - this.reject(e); - } - } - - onError(error: FirebaseError): void { - this.reject(error); - } - - private getIdpTask(type: AuthEventType): IdpTask { - switch (type) { - case AuthEventType.SIGN_IN_VIA_POPUP: - case AuthEventType.SIGN_IN_VIA_REDIRECT: - return _signIn; - case AuthEventType.LINK_VIA_POPUP: - case AuthEventType.LINK_VIA_REDIRECT: - return _link; - case AuthEventType.REAUTH_VIA_POPUP: - case AuthEventType.REAUTH_VIA_REDIRECT: - return _reauth; - default: - _fail(this.auth, AuthErrorCode.INTERNAL_ERROR); - } - } - - protected resolve(cred: UserCredential | null): void { - debugAssert(this.pendingPromise, 'Pending promise was never set'); - this.pendingPromise.resolve(cred); - this.unregisterAndCleanUp(); - } - - protected reject(error: Error): void { - debugAssert(this.pendingPromise, 'Pending promise was never set'); - this.pendingPromise.reject(error); - this.unregisterAndCleanUp(); - } - - private unregisterAndCleanUp(): void { - if (this.eventManager) { - this.eventManager.unregisterConsumer(this); - } - - this.pendingPromise = null; - this.cleanUp(); - } - - abstract cleanUp(): void; -} diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/phone.test.ts b/packages-exp/auth-exp/src/platform_browser/strategies/phone.test.ts deleted file mode 100644 index c4974872e9f..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/strategies/phone.test.ts +++ /dev/null @@ -1,458 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { OperationType, ProviderId } from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { mockEndpoint } from '../../../test/helpers/api/helper'; -import { makeJWT } from '../../../test/helpers/jwt'; -import { testAuth, testUser, TestAuth } from '../../../test/helpers/mock_auth'; -import * as fetch from '../../../test/helpers/mock_fetch'; -import { Endpoint } from '../../api'; -import { MultiFactorInfo } from '../../mfa/mfa_info'; -import { MultiFactorSession } from '../../mfa/mfa_session'; -import { multiFactor, MultiFactorUser } from '../../mfa/mfa_user'; -import { ApplicationVerifier } from '../../model/application_verifier'; -import { IdTokenResponse, IdTokenResponseKind } from '../../model/id_token'; -import { User } from '../../model/user'; -import { RecaptchaVerifier } from '../../platform_browser/recaptcha/recaptcha_verifier'; -import { PhoneAuthCredential } from '../../core/credentials/phone'; -import { - _verifyPhoneNumber, - linkWithPhoneNumber, - reauthenticateWithPhoneNumber, - signInWithPhoneNumber, - updatePhoneNumber -} from './phone'; - -use(chaiAsPromised); -use(sinonChai); - -describe('platform_browser/strategies/phone', () => { - let auth: TestAuth; - let verifier: ApplicationVerifier; - let sendCodeEndpoint: fetch.Route; - - beforeEach(async () => { - auth = await testAuth(); - fetch.setUp(); - - sendCodeEndpoint = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, { - sessionInfo: 'session-info' - }); - - verifier = new RecaptchaVerifier(document.createElement('div'), {}, auth); - sinon.stub(verifier, 'verify').returns(Promise.resolve('recaptcha-token')); - }); - - afterEach(() => { - fetch.tearDown(); - sinon.restore(); - }); - - describe('signInWithPhoneNumber', () => { - it('calls verify phone number', async () => { - await signInWithPhoneNumber(auth, '+15105550000', verifier); - - expect(sendCodeEndpoint.calls[0].request).to.eql({ - recaptchaToken: 'recaptcha-token', - phoneNumber: '+15105550000' - }); - }); - - context('ConfirmationResult', () => { - it('result contains verification id baked in', async () => { - const result = await signInWithPhoneNumber(auth, 'number', verifier); - expect(result.verificationId).to.eq('session-info'); - }); - - it('calling #confirm finishes the sign in flow', async () => { - const idTokenResponse: IdTokenResponse = { - idToken: 'my-id-token', - refreshToken: 'my-refresh-token', - expiresIn: '1234', - localId: 'uid', - kind: IdTokenResponseKind.CreateAuthUri - }; - - // This endpoint is called from within the callback, in - // signInWithCredential - const signInEndpoint = mockEndpoint( - Endpoint.SIGN_IN_WITH_PHONE_NUMBER, - idTokenResponse - ); - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [{ localId: 'uid' }] - }); - - const result = await signInWithPhoneNumber(auth, 'number', verifier); - const userCred = await result.confirm('6789'); - expect(userCred.user.uid).to.eq('uid'); - expect(userCred.operationType).to.eq(OperationType.SIGN_IN); - expect(signInEndpoint.calls[0].request).to.eql({ - sessionInfo: 'session-info', - code: '6789' - }); - }); - }); - }); - - describe('linkWithPhoneNumber', () => { - let getAccountInfoEndpoint: fetch.Route; - let user: User; - - beforeEach(() => { - getAccountInfoEndpoint = mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [{ uid: 'uid' }] - }); - - user = testUser(auth, 'uid', 'email', true); - }); - - it('rejects if a phone provider is already linked', async () => { - getAccountInfoEndpoint.response = { - users: [ - { - uid: 'uid', - providerUserInfo: [{ providerId: ProviderId.PHONE }] - } - ] - }; - - await expect( - linkWithPhoneNumber(user, 'number', verifier) - ).to.be.rejectedWith( - FirebaseError, - 'Firebase: User can only be linked to one identity for the given provider. (auth/provider-already-linked).' - ); - }); - - it('calls verify phone number', async () => { - await linkWithPhoneNumber(user, '+15105550000', verifier); - - expect(sendCodeEndpoint.calls[0].request).to.eql({ - recaptchaToken: 'recaptcha-token', - phoneNumber: '+15105550000' - }); - }); - - context('ConfirmationResult', () => { - it('result contains verification id baked in', async () => { - const result = await linkWithPhoneNumber(user, 'number', verifier); - expect(result.verificationId).to.eq('session-info'); - }); - - it('calling #confirm finishes the sign in flow', async () => { - const idTokenResponse: IdTokenResponse = { - idToken: 'my-id-token', - refreshToken: 'my-refresh-token', - expiresIn: '1234', - localId: 'uid', - kind: IdTokenResponseKind.CreateAuthUri - }; - - // This endpoint is called from within the callback, in - // signInWithCredential - const signInEndpoint = mockEndpoint( - Endpoint.SIGN_IN_WITH_PHONE_NUMBER, - idTokenResponse - ); - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [{ localId: 'uid' }] - }); - - const initialIdToken = await user.getIdToken(); - - const result = await linkWithPhoneNumber(user, 'number', verifier); - const userCred = await result.confirm('6789'); - expect(userCred.user.uid).to.eq('uid'); - expect(userCred.operationType).to.eq(OperationType.LINK); - expect(signInEndpoint.calls[0].request).to.eql({ - sessionInfo: 'session-info', - code: '6789', - idToken: initialIdToken - }); - }); - }); - }); - - describe('reauthenticateWithPhoneNumber', () => { - let user: User; - - beforeEach(() => { - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [{ uid: 'uid' }] - }); - - user = testUser(auth, 'uid', 'email', true); - }); - - it('calls verify phone number', async () => { - await reauthenticateWithPhoneNumber(user, '+15105550000', verifier); - - expect(sendCodeEndpoint.calls[0].request).to.eql({ - recaptchaToken: 'recaptcha-token', - phoneNumber: '+15105550000' - }); - }); - - context('ConfirmationResult', () => { - it('result contains verification id baked in', async () => { - const result = await reauthenticateWithPhoneNumber( - user, - 'number', - verifier - ); - expect(result.verificationId).to.eq('session-info'); - }); - - it('calling #confirm finishes the sign in flow', async () => { - const idTokenResponse: IdTokenResponse = { - idToken: makeJWT({ 'sub': 'uid' }), - refreshToken: 'my-refresh-token', - expiresIn: '1234', - localId: 'uid', - kind: IdTokenResponseKind.CreateAuthUri - }; - - // This endpoint is called from within the callback, in - // signInWithCredential - const signInEndpoint = mockEndpoint( - Endpoint.SIGN_IN_WITH_PHONE_NUMBER, - idTokenResponse - ); - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [{ localId: 'uid' }] - }); - - const result = await reauthenticateWithPhoneNumber( - user, - 'number', - verifier - ); - const userCred = await result.confirm('6789'); - expect(userCred.user.uid).to.eq('uid'); - expect(userCred.operationType).to.eq(OperationType.REAUTHENTICATE); - expect(signInEndpoint.calls[0].request).to.eql({ - sessionInfo: 'session-info', - code: '6789', - operation: 'REAUTH' - }); - }); - - it('rejects if the uid mismatches', async () => { - const idTokenResponse: IdTokenResponse = { - idToken: makeJWT({ 'sub': 'different-uid' }), - refreshToken: 'my-refresh-token', - expiresIn: '1234', - localId: 'uid', - kind: IdTokenResponseKind.CreateAuthUri - }; - // This endpoint is called from within the callback, in - // signInWithCredential - mockEndpoint(Endpoint.SIGN_IN_WITH_PHONE_NUMBER, idTokenResponse); - - const result = await reauthenticateWithPhoneNumber( - user, - 'number', - verifier - ); - await expect(result.confirm('code')).to.be.rejectedWith( - FirebaseError, - 'Firebase: The supplied credentials do not correspond to the previously signed in user. (auth/user-mismatch)' - ); - }); - }); - }); - - describe('_verifyPhoneNumber', () => { - it('works with a string phone number', async () => { - const sessionInfo = await _verifyPhoneNumber(auth, 'number', verifier); - expect(sessionInfo).to.eq('session-info'); - expect(sendCodeEndpoint.calls[0].request).to.eql({ - recaptchaToken: 'recaptcha-token', - phoneNumber: 'number' - }); - }); - - it('works with an options object', async () => { - const sessionInfo = await _verifyPhoneNumber( - auth, - { - phoneNumber: 'number' - }, - verifier - ); - expect(sessionInfo).to.eq('session-info'); - expect(sendCodeEndpoint.calls[0].request).to.eql({ - recaptchaToken: 'recaptcha-token', - phoneNumber: 'number' - }); - }); - - context('MFA', () => { - let user: User; - let mfaUser: MultiFactorUser; - - beforeEach(() => { - mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [{ uid: 'uid' }] - }); - - user = testUser(auth, 'uid', 'email', true); - mfaUser = multiFactor(user) as MultiFactorUser; - }); - - it('works with an enrollment flow', async () => { - const endpoint = mockEndpoint(Endpoint.START_PHONE_MFA_ENROLLMENT, { - phoneSessionInfo: { - sessionInfo: 'session-info' - } - }); - const session = (await mfaUser.getSession()) as MultiFactorSession; - const sessionInfo = await _verifyPhoneNumber( - auth, - { phoneNumber: 'number', session }, - verifier - ); - expect(sessionInfo).to.eq('session-info'); - expect(endpoint.calls[0].request).to.eql({ - tenantId: auth.tenantId, - idToken: session.credential, - phoneEnrollmentInfo: { - phoneNumber: 'number', - recaptchaToken: 'recaptcha-token' - } - }); - }); - - it('works when completing the sign in flow', async () => { - const endpoint = mockEndpoint(Endpoint.START_PHONE_MFA_SIGN_IN, { - phoneResponseInfo: { - sessionInfo: 'session-info' - } - }); - const session = MultiFactorSession._fromMfaPendingCredential( - 'mfa-pending-credential' - ); - const mfaInfo = MultiFactorInfo._fromServerResponse(auth, { - mfaEnrollmentId: 'mfa-enrollment-id', - enrolledAt: Date.now(), - phoneInfo: 'phone-number-from-enrollment' - }); - const sessionInfo = await _verifyPhoneNumber( - auth, - { - session, - multiFactorHint: mfaInfo - }, - verifier - ); - expect(sessionInfo).to.eq('session-info'); - expect(endpoint.calls[0].request).to.eql({ - tenantId: auth.tenantId, - mfaPendingCredential: 'mfa-pending-credential', - mfaEnrollmentId: 'mfa-enrollment-id', - phoneSignInInfo: { - recaptchaToken: 'recaptcha-token' - } - }); - }); - }); - - it('throws if the verifier does not return a string', async () => { - (verifier.verify as sinon.SinonStub).returns(Promise.resolve(123)); - await expect( - _verifyPhoneNumber(auth, 'number', verifier) - ).to.be.rejectedWith(FirebaseError, 'auth/argument-error'); - }); - - it('throws if the verifier type is not recaptcha', async () => { - const mutVerifier: { - -readonly [K in keyof ApplicationVerifier]: ApplicationVerifier[K]; - } = verifier; - mutVerifier.type = 'not-recaptcha-thats-for-sure'; - await expect( - _verifyPhoneNumber(auth, 'number', mutVerifier) - ).to.be.rejectedWith(FirebaseError, 'auth/argument-error'); - }); - - it('resets the verifer after successful verification', async () => { - sinon.spy(verifier, '_reset'); - expect(await _verifyPhoneNumber(auth, 'number', verifier)).to.eq( - 'session-info' - ); - expect(verifier._reset).to.have.been.called; - }); - - it('resets the verifer after a failed verification', async () => { - sinon.spy(verifier, '_reset'); - (verifier.verify as sinon.SinonStub).returns(Promise.resolve(123)); - - await expect(_verifyPhoneNumber(auth, 'number', verifier)).to.be.rejected; - expect(verifier._reset).to.have.been.called; - }); - }); - - describe('updatePhoneNumber', () => { - let user: User; - let reloadMock: fetch.Route; - let signInMock: fetch.Route; - let credential: PhoneAuthCredential; - - beforeEach(() => { - reloadMock = mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { - users: [{ uid: 'uid' }] - }); - signInMock = mockEndpoint(Endpoint.SIGN_IN_WITH_PHONE_NUMBER, { - idToken: 'new-access-token', - refreshToken: 'refresh-token' - }); - credential = PhoneAuthCredential._fromVerification( - 'session-info', - 'code' - ); - - user = testUser(auth, 'uid', 'email', true); - }); - - it('should link the phone number to the user', async () => { - await updatePhoneNumber(user, credential); - expect(signInMock.calls[0].request).to.eql({ - idToken: 'access-token', - sessionInfo: 'session-info', - code: 'code' - }); - }); - - it('should update the access token', async () => { - await updatePhoneNumber(user, credential); - const idToken = await user.getIdToken(); - expect(idToken).to.eq('new-access-token'); - }); - - it('should reload the user', async () => { - await updatePhoneNumber(user, credential); - expect(reloadMock.calls.length).to.eq(1); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/phone.ts b/packages-exp/auth-exp/src/platform_browser/strategies/phone.ts deleted file mode 100644 index 17381d76bf0..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/strategies/phone.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { startEnrollPhoneMfa } from '../../api/account_management/mfa'; -import { startSignInPhoneMfa } from '../../api/authentication/mfa'; -import { sendPhoneVerificationCode } from '../../api/authentication/sms'; -import { ApplicationVerifier } from '../../model/application_verifier'; -import { PhoneAuthCredential } from '../../core/credentials/phone'; -import { AuthErrorCode } from '../../core/errors'; -import { _assertLinkedStatus, _link } from '../../core/user/link_unlink'; -import { _assert } from '../../core/util/assert'; -import { Auth } from '../../model/auth'; -import { - linkWithCredential, - reauthenticateWithCredential, - signInWithCredential -} from '../../core/strategies/credential'; -import { - MultiFactorSession, - MultiFactorSessionType -} from '../../mfa/mfa_session'; -import { User } from '../../model/user'; -import { RECAPTCHA_VERIFIER_TYPE } from '../recaptcha/recaptcha_verifier'; -import { _castAuth } from '../../core/auth/auth_impl'; - -interface OnConfirmationCallback { - (credential: PhoneAuthCredential): Promise; -} - -class ConfirmationResult implements externs.ConfirmationResult { - constructor( - readonly verificationId: string, - private readonly onConfirmation: OnConfirmationCallback - ) {} - - confirm(verificationCode: string): Promise { - const authCredential = PhoneAuthCredential._fromVerification( - this.verificationId, - verificationCode - ); - return this.onConfirmation(authCredential); - } -} - -/** - * Asynchronously signs in using a phone number. - * - * @remarks - * This method sends a code via SMS to the given - * phone number, and returns a {@link @firebase/auth-types#ConfirmationResult}. After the user - * provides the code sent to their phone, call {@link @firebase/auth-types#ConfirmationResult.confirm} - * with the code to sign the user in. - * - * For abuse prevention, this method also requires a {@link @firebase/auth-types#ApplicationVerifier}. - * This SDK includes a reCAPTCHA-based implementation, {@link RecaptchaVerifier}. - * - * @example - * ```javascript - * // 'recaptcha-container' is the ID of an element in the DOM. - * const applicationVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container'); - * const confirmationResult = await signInWithPhoneNumber(auth, phoneNumber, applicationVerifier); - * // Obtain a verificationCode from the user. - * const credential = await confirmationResult.confirm(verificationCode); - * ``` - * - * @param auth - The Auth instance. - * @param phoneNumber - The user's phone number in E.164 format (e.g. +16505550101). - * @param appVerifier - The {@link @firebase/auth-types#ApplicationVerifier}. - * - * @public - */ -export async function signInWithPhoneNumber( - auth: externs.Auth, - phoneNumber: string, - appVerifier: externs.ApplicationVerifier -): Promise { - const verificationId = await _verifyPhoneNumber( - _castAuth(auth), - phoneNumber, - appVerifier as ApplicationVerifier - ); - return new ConfirmationResult(verificationId, cred => - signInWithCredential(auth, cred) - ); -} - -/** - * Links the user account with the given phone number. - * - * @param user - The user. - * @param phoneNumber - The user's phone number in E.164 format (e.g. +16505550101). - * @param appVerifier - The {@link @firebase/auth-types#ApplicationVerifier}. - * - * @public - */ -export async function linkWithPhoneNumber( - user: externs.User, - phoneNumber: string, - appVerifier: externs.ApplicationVerifier -): Promise { - const userInternal = user as User; - await _assertLinkedStatus(false, userInternal, externs.ProviderId.PHONE); - const verificationId = await _verifyPhoneNumber( - userInternal.auth, - phoneNumber, - appVerifier as ApplicationVerifier - ); - return new ConfirmationResult(verificationId, cred => - linkWithCredential(user, cred) - ); -} - -/** - * Re-authenticates a user using a fresh phne credential. - * - * @remarks Use before operations such as {@link updatePassword} that require tokens from recent sign-in attempts. - * - * @param user - The user. - * @param phoneNumber - The user's phone number in E.164 format (e.g. +16505550101). - * @param appVerifier - The {@link @firebase/auth-types#ApplicationVerifier}. - * - * @public - */ -export async function reauthenticateWithPhoneNumber( - user: externs.User, - phoneNumber: string, - appVerifier: externs.ApplicationVerifier -): Promise { - const userInternal = user as User; - const verificationId = await _verifyPhoneNumber( - userInternal.auth, - phoneNumber, - appVerifier as ApplicationVerifier - ); - return new ConfirmationResult(verificationId, cred => - reauthenticateWithCredential(user, cred) - ); -} - -/** - * Returns a verification ID to be used in conjunction with the SMS code that is sent. - * - * @internal - */ -export async function _verifyPhoneNumber( - auth: Auth, - options: externs.PhoneInfoOptions | string, - verifier: ApplicationVerifier -): Promise { - const recaptchaToken = await verifier.verify(); - - try { - _assert( - typeof recaptchaToken === 'string', - auth, - AuthErrorCode.ARGUMENT_ERROR - ); - _assert( - verifier.type === RECAPTCHA_VERIFIER_TYPE, - auth, - AuthErrorCode.ARGUMENT_ERROR - ); - - let phoneInfoOptions: externs.PhoneInfoOptions; - - if (typeof options === 'string') { - phoneInfoOptions = { - phoneNumber: options - }; - } else { - phoneInfoOptions = options; - } - - if ('session' in phoneInfoOptions) { - const session = phoneInfoOptions.session as MultiFactorSession; - - if ('phoneNumber' in phoneInfoOptions) { - _assert( - session.type === MultiFactorSessionType.ENROLL, - auth, - AuthErrorCode.INTERNAL_ERROR - ); - const response = await startEnrollPhoneMfa(auth, { - idToken: session.credential, - phoneEnrollmentInfo: { - phoneNumber: phoneInfoOptions.phoneNumber, - recaptchaToken - } - }); - return response.phoneSessionInfo.sessionInfo; - } else { - _assert( - session.type === MultiFactorSessionType.SIGN_IN, - auth, - AuthErrorCode.INTERNAL_ERROR - ); - const mfaEnrollmentId = - phoneInfoOptions.multiFactorHint?.uid || - phoneInfoOptions.multiFactorUid; - _assert(mfaEnrollmentId, auth, AuthErrorCode.MISSING_MFA_INFO); - const response = await startSignInPhoneMfa(auth, { - mfaPendingCredential: session.credential, - mfaEnrollmentId, - phoneSignInInfo: { - recaptchaToken - } - }); - return response.phoneResponseInfo.sessionInfo; - } - } else { - const { sessionInfo } = await sendPhoneVerificationCode(auth, { - phoneNumber: phoneInfoOptions.phoneNumber, - recaptchaToken - }); - return sessionInfo; - } - } finally { - verifier._reset(); - } -} - -/** - * Updates the user's phone number. - * - * @example - * ``` - * // 'recaptcha-container' is the ID of an element in the DOM. - * const applicationVerifier = new RecaptchaVerifier('recaptcha-container'); - * const provider = new PhoneAuthProvider(auth); - * const verificationId = await provider.verifyPhoneNumber('+16505550101', applicationVerifier); - * // Obtain the verificationCode from the user. - * const phoneCredential = PhoneAuthProvider.credential(verificationId, verificationCode); - * await updatePhoneNumber(user, phoneCredential); - * ``` - * - * @param user - The user. - * @param credential - A credential authenticating the new phone number. - * - * @public - */ -export async function updatePhoneNumber( - user: externs.User, - credential: externs.PhoneAuthCredential -): Promise { - await _link(user as User, credential as PhoneAuthCredential); -} diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/popup.test.ts b/packages-exp/auth-exp/src/platform_browser/strategies/popup.test.ts deleted file mode 100644 index 070b937b79c..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/strategies/popup.test.ts +++ /dev/null @@ -1,633 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { - OperationType, - PopupRedirectResolver, - ProviderId -} from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { delay } from '../../../test/helpers/delay'; -import { BASE_AUTH_EVENT } from '../../../test/helpers/iframe_event'; -import { testAuth, testUser, TestAuth } from '../../../test/helpers/mock_auth'; -import { makeMockPopupRedirectResolver } from '../../../test/helpers/mock_popup_redirect_resolver'; -import { stubTimeouts, TimerMap } from '../../../test/helpers/timeout_stub'; -import { AuthEvent, AuthEventType } from '../../model/popup_redirect'; -import { User } from '../../model/user'; -import { AuthEventManager } from '../../core/auth/auth_event_manager'; -import { AuthErrorCode } from '../../core/errors'; -import { OAuthProvider } from '../../core/providers/oauth'; -import { UserCredentialImpl } from '../../core/user/user_credential_impl'; -import * as eid from '../../core/util/event_id'; -import { AuthPopup } from '../util/popup'; -import * as idpTasks from '../../core/strategies/idp'; -import { - _Timeout, - _POLL_WINDOW_CLOSE_TIMEOUT, - linkWithPopup, - reauthenticateWithPopup, - signInWithPopup -} from './popup'; -import { _getInstance } from '../../../internal'; -import { _createError } from '../../core/util/assert'; - -use(sinonChai); -use(chaiAsPromised); - -const MATCHING_EVENT_ID = 'matching-event-id'; -const OTHER_EVENT_ID = 'wrong-id'; - -describe('platform_browser/strategies/popup', () => { - let resolver: PopupRedirectResolver; - let provider: OAuthProvider; - let eventManager: AuthEventManager; - let authPopup: AuthPopup; - let underlyingWindow: { closed: boolean }; - let auth: TestAuth; - let idpStubs: sinon.SinonStubbedInstance; - let pendingTimeouts: TimerMap; - - beforeEach(async () => { - auth = await testAuth(); - eventManager = new AuthEventManager(auth); - underlyingWindow = { closed: false }; - authPopup = new AuthPopup(underlyingWindow as Window); - provider = new OAuthProvider(ProviderId.GOOGLE); - resolver = makeMockPopupRedirectResolver(eventManager, authPopup); - idpStubs = sinon.stub(idpTasks); - sinon.stub(eid, '_generateEventId').returns(MATCHING_EVENT_ID); - pendingTimeouts = stubTimeouts(); - sinon.stub(window, 'clearTimeout'); - }); - - afterEach(() => { - sinon.restore(); - }); - - function iframeEvent(event: Partial): void { - // Push the dispatch out of the synchronous flow - delay(() => { - eventManager.onEvent({ - ...BASE_AUTH_EVENT, - eventId: MATCHING_EVENT_ID, - ...event - }); - }); - } - - context('signInWithPopup', () => { - it('completes the full flow', async () => { - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GOOGLE, - operationType: OperationType.SIGN_IN - }); - idpStubs._signIn.returns(Promise.resolve(cred)); - const promise = signInWithPopup(auth, provider, resolver); - iframeEvent({ - type: AuthEventType.SIGN_IN_VIA_POPUP - }); - expect(await promise).to.eq(cred); - }); - - it('completes the full flow with default resolver', async () => { - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GOOGLE, - operationType: OperationType.SIGN_IN - }); - auth._popupRedirectResolver = _getInstance(resolver); - idpStubs._signIn.returns(Promise.resolve(cred)); - const promise = signInWithPopup(auth, provider); - iframeEvent({ - type: AuthEventType.SIGN_IN_VIA_POPUP - }); - expect(await promise).to.eq(cred); - }); - - it('errors if resolver not provided and not on auth', async () => { - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GOOGLE, - operationType: OperationType.SIGN_IN - }); - idpStubs._signIn.returns(Promise.resolve(cred)); - await expect(signInWithPopup(auth, provider)).to.be.rejectedWith( - FirebaseError, - 'auth/argument-error' - ); - }); - - it('ignores events for another event id', async () => { - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GOOGLE, - operationType: OperationType.SIGN_IN - }); - idpStubs._signIn.returns(Promise.resolve(cred)); - const promise = signInWithPopup(auth, provider, resolver); - iframeEvent({ - type: AuthEventType.SIGN_IN_VIA_POPUP, - eventId: OTHER_EVENT_ID, - error: { - code: 'auth/internal-error', - message: '', - name: '' - } - }); - - iframeEvent({ - type: AuthEventType.SIGN_IN_VIA_POPUP, - eventId: MATCHING_EVENT_ID - }); - expect(await promise).to.eq(cred); - }); - - it('does not call idp tasks if event is error', async () => { - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GOOGLE, - operationType: OperationType.SIGN_IN - }); - idpStubs._signIn.returns(Promise.resolve(cred)); - const promise = signInWithPopup(auth, provider, resolver); - iframeEvent({ - type: AuthEventType.SIGN_IN_VIA_POPUP, - eventId: MATCHING_EVENT_ID, - error: { - code: 'auth/invalid-app-credential', - message: '', - name: '' - } - }); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/invalid-app-credential' - ); - expect(idpStubs._signIn).not.to.have.been.called; - }); - - it('does not error if the poll timeout trips', async () => { - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GOOGLE, - operationType: OperationType.SIGN_IN - }); - idpStubs._signIn.returns(Promise.resolve(cred)); - const promise = signInWithPopup(auth, provider, resolver); - delay(() => { - underlyingWindow.closed = true; - pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()](); - }); - iframeEvent({ - type: AuthEventType.SIGN_IN_VIA_POPUP - }); - expect(await promise).to.eq(cred); - }); - - it('does error if the poll timeout and event timeout trip', async () => { - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GOOGLE, - operationType: OperationType.SIGN_IN - }); - idpStubs._signIn.returns(Promise.resolve(cred)); - const promise = signInWithPopup(auth, provider, resolver); - delay(() => { - underlyingWindow.closed = true; - pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()](); - pendingTimeouts[_Timeout.AUTH_EVENT](); - }); - iframeEvent({ - type: AuthEventType.SIGN_IN_VIA_POPUP - }); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/popup-closed-by-user' - ); - }); - - it('errors if webstorage support comes back negative', async () => { - resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false); - await expect( - signInWithPopup(auth, provider, resolver) - ).to.be.rejectedWith(FirebaseError, 'auth/web-storage-unsupported'); - }); - - it('passes any errors from idp task', async () => { - idpStubs._signIn.returns( - Promise.reject(_createError(auth, AuthErrorCode.INVALID_APP_ID)) - ); - const promise = signInWithPopup(auth, provider, resolver); - iframeEvent({ - eventId: MATCHING_EVENT_ID, - type: AuthEventType.SIGN_IN_VIA_POPUP - }); - - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/invalid-app-id' - ); - }); - - it('cancels the task if called consecutively', async () => { - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: ProviderId.GOOGLE, - operationType: OperationType.SIGN_IN - }); - idpStubs._signIn.returns(Promise.resolve(cred)); - const firstPromise = signInWithPopup(auth, provider, resolver); - const secondPromise = signInWithPopup(auth, provider, resolver); - iframeEvent({ - type: AuthEventType.SIGN_IN_VIA_POPUP - }); - await expect(firstPromise).to.be.rejectedWith( - FirebaseError, - 'auth/cancelled-popup-request' - ); - expect(await secondPromise).to.eq(cred); - }); - }); - - context('linkWithPopup', () => { - let user: User; - beforeEach(() => { - user = testUser(auth, 'uid'); - }); - - it('completes the full flow', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - const promise = linkWithPopup(user, provider, resolver); - iframeEvent({ - type: AuthEventType.LINK_VIA_POPUP - }); - expect(await promise).to.eq(cred); - }); - - it('completes the full flow with default resolver', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.LINK - }); - user.auth._popupRedirectResolver = _getInstance(resolver); - idpStubs._link.returns(Promise.resolve(cred)); - const promise = linkWithPopup(user, provider); - iframeEvent({ - type: AuthEventType.LINK_VIA_POPUP - }); - expect(await promise).to.eq(cred); - }); - - it('errors if resolver not provided and not on auth', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - await expect(linkWithPopup(user, provider)).to.be.rejectedWith( - FirebaseError, - 'auth/argument-error' - ); - }); - - it('ignores events for another event id', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - const promise = linkWithPopup(user, provider, resolver); - iframeEvent({ - type: AuthEventType.LINK_VIA_POPUP, - eventId: OTHER_EVENT_ID, - error: { - code: 'auth/internal-error', - message: '', - name: '' - } - }); - - iframeEvent({ - type: AuthEventType.LINK_VIA_POPUP, - eventId: MATCHING_EVENT_ID - }); - expect(await promise).to.eq(cred); - }); - - it('does not call idp tasks if event is error', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - const promise = linkWithPopup(user, provider, resolver); - iframeEvent({ - type: AuthEventType.LINK_VIA_POPUP, - eventId: MATCHING_EVENT_ID, - error: { - code: 'auth/invalid-app-credential', - message: '', - name: '' - } - }); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/invalid-app-credential' - ); - expect(idpStubs._link).not.to.have.been.called; - }); - - it('does not error if the poll timeout trips', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - const promise = linkWithPopup(user, provider, resolver); - delay(() => { - underlyingWindow.closed = true; - pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()](); - }); - iframeEvent({ - type: AuthEventType.LINK_VIA_POPUP - }); - expect(await promise).to.eq(cred); - }); - - it('does error if the poll timeout and event timeout trip', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - const promise = linkWithPopup(user, provider, resolver); - delay(() => { - underlyingWindow.closed = true; - pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()](); - pendingTimeouts[_Timeout.AUTH_EVENT](); - }); - iframeEvent({ - type: AuthEventType.LINK_VIA_POPUP - }); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/popup-closed-by-user' - ); - }); - - it('errors if webstorage support comes back negative', async () => { - resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false); - await expect(linkWithPopup(user, provider, resolver)).to.be.rejectedWith( - FirebaseError, - 'auth/web-storage-unsupported' - ); - }); - - it('passes any errors from idp task', async () => { - idpStubs._link.returns( - Promise.reject(_createError(auth, AuthErrorCode.INVALID_APP_ID)) - ); - const promise = linkWithPopup(user, provider, resolver); - iframeEvent({ - eventId: MATCHING_EVENT_ID, - type: AuthEventType.LINK_VIA_POPUP - }); - - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/invalid-app-id' - ); - }); - - it('cancels the task if called consecutively', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - const firstPromise = linkWithPopup(user, provider, resolver); - const secondPromise = linkWithPopup(user, provider, resolver); - iframeEvent({ - type: AuthEventType.LINK_VIA_POPUP - }); - await expect(firstPromise).to.be.rejectedWith( - FirebaseError, - 'auth/cancelled-popup-request' - ); - expect(await secondPromise).to.eq(cred); - }); - }); - - context('reauthenticateWithPopup', () => { - let user: User; - beforeEach(() => { - user = testUser(auth, 'uid'); - }); - - it('completes the full flow', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.REAUTHENTICATE - }); - idpStubs._reauth.returns(Promise.resolve(cred)); - const promise = reauthenticateWithPopup(user, provider, resolver); - iframeEvent({ - type: AuthEventType.REAUTH_VIA_POPUP - }); - expect(await promise).to.eq(cred); - }); - - it('completes the full flow with default resolver', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.REAUTHENTICATE - }); - user.auth._popupRedirectResolver = _getInstance(resolver); - idpStubs._reauth.returns(Promise.resolve(cred)); - const promise = reauthenticateWithPopup(user, provider); - iframeEvent({ - type: AuthEventType.REAUTH_VIA_POPUP - }); - expect(await promise).to.eq(cred); - }); - - it('errors if resolver not provided and not on auth', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.REAUTHENTICATE - }); - idpStubs._reauth.returns(Promise.resolve(cred)); - await expect(reauthenticateWithPopup(user, provider)).to.be.rejectedWith( - FirebaseError, - 'auth/argument-error' - ); - }); - - it('ignores events for another event id', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.REAUTHENTICATE - }); - idpStubs._reauth.returns(Promise.resolve(cred)); - const promise = reauthenticateWithPopup(user, provider, resolver); - iframeEvent({ - type: AuthEventType.REAUTH_VIA_POPUP, - eventId: OTHER_EVENT_ID, - error: { - code: 'auth/internal-error', - message: '', - name: '' - } - }); - - iframeEvent({ - type: AuthEventType.REAUTH_VIA_POPUP, - eventId: MATCHING_EVENT_ID - }); - expect(await promise).to.eq(cred); - }); - - it('does not call idp tasks if event is error', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.REAUTHENTICATE - }); - idpStubs._reauth.returns(Promise.resolve(cred)); - const promise = reauthenticateWithPopup(user, provider, resolver); - iframeEvent({ - type: AuthEventType.REAUTH_VIA_POPUP, - eventId: MATCHING_EVENT_ID, - error: { - code: 'auth/invalid-app-credential', - message: '', - name: '' - } - }); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/invalid-app-credential' - ); - expect(idpStubs._reauth).not.to.have.been.called; - }); - - it('does not error if the poll timeout trips', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.REAUTHENTICATE - }); - idpStubs._reauth.returns(Promise.resolve(cred)); - const promise = reauthenticateWithPopup(user, provider, resolver); - delay(() => { - underlyingWindow.closed = true; - pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()](); - }); - iframeEvent({ - type: AuthEventType.REAUTH_VIA_POPUP - }); - expect(await promise).to.eq(cred); - }); - - it('errors if webstorage support comes back negative', async () => { - resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false); - await expect( - reauthenticateWithPopup(user, provider, resolver) - ).to.be.rejectedWith(FirebaseError, 'auth/web-storage-unsupported'); - }); - - it('does error if the poll timeout and event timeout trip', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.REAUTHENTICATE - }); - idpStubs._reauth.returns(Promise.resolve(cred)); - const promise = reauthenticateWithPopup(user, provider, resolver); - delay(() => { - underlyingWindow.closed = true; - pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()](); - pendingTimeouts[_Timeout.AUTH_EVENT](); - }); - iframeEvent({ - type: AuthEventType.REAUTH_VIA_POPUP - }); - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/popup-closed-by-user' - ); - }); - - it('passes any errors from idp task', async () => { - idpStubs._reauth.returns( - Promise.reject(_createError(auth, AuthErrorCode.INVALID_APP_ID)) - ); - const promise = reauthenticateWithPopup(user, provider, resolver); - iframeEvent({ - eventId: MATCHING_EVENT_ID, - type: AuthEventType.REAUTH_VIA_POPUP - }); - - await expect(promise).to.be.rejectedWith( - FirebaseError, - 'auth/invalid-app-id' - ); - }); - - it('cancels the task if called consecutively', async () => { - const cred = new UserCredentialImpl({ - user, - providerId: ProviderId.GOOGLE, - operationType: OperationType.REAUTHENTICATE - }); - idpStubs._reauth.returns(Promise.resolve(cred)); - const firstPromise = reauthenticateWithPopup(user, provider, resolver); - const secondPromise = reauthenticateWithPopup(user, provider, resolver); - iframeEvent({ - type: AuthEventType.REAUTH_VIA_POPUP - }); - await expect(firstPromise).to.be.rejectedWith( - FirebaseError, - 'auth/cancelled-popup-request' - ); - expect(await secondPromise).to.eq(cred); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/popup.ts b/packages-exp/auth-exp/src/platform_browser/strategies/popup.ts deleted file mode 100644 index 72e50ebfc6b..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/strategies/popup.ts +++ /dev/null @@ -1,301 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { _castAuth } from '../../core/auth/auth_impl'; -import { AuthErrorCode } from '../../core/errors'; -import { OAuthProvider } from '../../core/providers/oauth'; -import { _assert, debugAssert, _createError } from '../../core/util/assert'; -import { Delay } from '../../core/util/delay'; -import { _generateEventId } from '../../core/util/event_id'; -import { Auth } from '../../model/auth'; -import { - AuthEventType, - PopupRedirectResolver -} from '../../model/popup_redirect'; -import { User } from '../../model/user'; -import { _withDefaultResolver } from '../popup_redirect'; -import { AuthPopup } from '../util/popup'; -import { AbstractPopupRedirectOperation } from './abstract_popup_redirect_operation'; - -/* - * The event timeout is the same on mobile and desktop, no need for Delay. - * @internal - */ -export const enum _Timeout { - AUTH_EVENT = 2000 -} -export const _POLL_WINDOW_CLOSE_TIMEOUT = new Delay(2000, 10000); - -/** - * Authenticates a Firebase client using a popup-based OAuth authentication flow. - * - * @remarks - * If succeeds, returns the signed in user along with the provider's credential. If sign in was - * unsuccessful, returns an error object containing additional information about the error. - * - * @example - * ```javascript - * // Sign in using a popup. - * const provider = new FacebookAuthProvider(); - * const result = await signInWithPopup(auth, provider); - * - * // The signed-in user info. - * const user = result.user; - * // This gives you a Facebook Access Token. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * ``` - * - * @param auth - The Auth instance. - * @param provider - The provider to authenticate. The provider has to be an {@link OAuthProvider}. - * Non-OAuth providers like {@link EmailAuthProvider} will throw an error. - * @param resolver - An instance of {@link @firebase/auth-types#PopupRedirectResolver}, optional - * if already supplied to {@link initializeAuth} or provided by {@link getAuth}. - * - * - * @public - */ -export async function signInWithPopup( - auth: externs.Auth, - provider: externs.AuthProvider, - resolver?: externs.PopupRedirectResolver -): Promise { - const authInternal = _castAuth(auth); - _assert( - provider instanceof OAuthProvider, - auth, - AuthErrorCode.ARGUMENT_ERROR - ); - - const resolverInternal = _withDefaultResolver(authInternal, resolver); - const action = new PopupOperation( - authInternal, - AuthEventType.SIGN_IN_VIA_POPUP, - provider, - resolverInternal - ); - return action.executeNotNull(); -} - -/** - * Reauthenticates the current user with the specified {@link OAuthProvider} using a pop-up based - * OAuth flow. - * - * @remarks - * If the reauthentication is successful, the returned result will contain the user and the - * provider's credential. - * - * @example - * ```javascript - * // Sign in using a popup. - * const provider = new FacebookAuthProvider(); - * const result = await signInWithPopup(auth, provider); - * // Reauthenticate using a popup. - * await reauthenticateWithPopup(result.user, provider); - * ``` - * - * @param user - The user. - * @param provider - The provider to authenticate. The provider has to be an {@link OAuthProvider}. - * Non-OAuth providers like {@link EmailAuthProvider} will throw an error. - * @param resolver - An instance of {@link @firebase/auth-types#PopupRedirectResolver}, optional - * if already supplied to {@link initializeAuth} or provided by {@link getAuth}. - * - * @public - */ -export async function reauthenticateWithPopup( - user: externs.User, - provider: externs.AuthProvider, - resolver?: externs.PopupRedirectResolver -): Promise { - const userInternal = user as User; - _assert( - provider instanceof OAuthProvider, - userInternal.auth, - AuthErrorCode.ARGUMENT_ERROR - ); - - const resolverInternal = _withDefaultResolver(userInternal.auth, resolver); - const action = new PopupOperation( - userInternal.auth, - AuthEventType.REAUTH_VIA_POPUP, - provider, - resolverInternal, - userInternal - ); - return action.executeNotNull(); -} - -/** - * Links the authenticated provider to the user account using a pop-up based OAuth flow. - * - * @remarks - * If the linking is successful, the returned result will contain the user and the provider's credential. - * - * - * @example - * ```javascript - * // Sign in using some other provider. - * const result = await signInWithEmailAndPassword(auth, email, password); - * // Link using a popup. - * const provider = new FacebookAuthProvider(); - * await linkWithPopup(result.user, provider); - * ``` - * - * @param user - The user. - * @param provider - The provider to authenticate. The provider has to be an {@link OAuthProvider}. - * Non-OAuth providers like {@link EmailAuthProvider} will throw an error. - * @param resolver - An instance of {@link @firebase/auth-types#PopupRedirectResolver}, optional - * if already supplied to {@link initializeAuth} or provided by {@link getAuth}. - * - * @public - */ -export async function linkWithPopup( - user: externs.User, - provider: externs.AuthProvider, - resolver?: externs.PopupRedirectResolver -): Promise { - const userInternal = user as User; - _assert( - provider instanceof OAuthProvider, - userInternal.auth, - AuthErrorCode.ARGUMENT_ERROR - ); - - const resolverInternal = _withDefaultResolver(userInternal.auth, resolver); - - const action = new PopupOperation( - userInternal.auth, - AuthEventType.LINK_VIA_POPUP, - provider, - resolverInternal, - userInternal - ); - return action.executeNotNull(); -} - -/** - * Popup event manager. Handles the popup's entire lifecycle; listens to auth - * events - * - * @internal - */ -class PopupOperation extends AbstractPopupRedirectOperation { - // Only one popup is ever shown at once. The lifecycle of the current popup - // can be managed / cancelled by the constructor. - private static currentPopupAction: PopupOperation | null = null; - private authWindow: AuthPopup | null = null; - private pollId: number | null = null; - - constructor( - auth: Auth, - filter: AuthEventType, - private readonly provider: externs.AuthProvider, - resolver: PopupRedirectResolver, - user?: User - ) { - super(auth, filter, resolver, user); - if (PopupOperation.currentPopupAction) { - PopupOperation.currentPopupAction.cancel(); - } - - PopupOperation.currentPopupAction = this; - } - - async executeNotNull(): Promise { - const result = await this.execute(); - _assert(result, this.auth, AuthErrorCode.INTERNAL_ERROR); - return result; - } - - async onExecution(): Promise { - debugAssert( - this.filter.length === 1, - 'Popup operations only handle one event' - ); - const eventId = _generateEventId(); - this.authWindow = await this.resolver._openPopup( - this.auth, - this.provider, - this.filter[0], // There's always one, see constructor - eventId - ); - this.authWindow.associatedEvent = eventId; - - // Check for web storage support _after_ the popup is loaded. Checking for - // web storage is slow (on the order of a second or so). Rather than - // waiting on that before opening the window, optimistically open the popup - // and check for storage support at the same time. If storage support is - // not available, this will cause the whole thing to reject properly. It - // will also close the popup, but since the promise has already rejected, - // the popup closed by user poll will reject into the void. - this.resolver._isIframeWebStorageSupported(this.auth, isSupported => { - if (!isSupported) { - this.reject( - _createError(this.auth, AuthErrorCode.WEB_STORAGE_UNSUPPORTED) - ); - } - }); - - // Handle user closure. Notice this does *not* use await - this.pollUserCancellation(); - } - - get eventId(): string | null { - return this.authWindow?.associatedEvent || null; - } - - cancel(): void { - this.reject(_createError(this.auth, AuthErrorCode.EXPIRED_POPUP_REQUEST)); - } - - cleanUp(): void { - if (this.authWindow) { - this.authWindow.close(); - } - - if (this.pollId) { - window.clearTimeout(this.pollId); - } - - this.authWindow = null; - this.pollId = null; - PopupOperation.currentPopupAction = null; - } - - private pollUserCancellation(): void { - const poll = (): void => { - if (this.authWindow?.window?.closed) { - // Make sure that there is sufficient time for whatever action to - // complete. The window could have closed but the sign in network - // call could still be in flight. - this.pollId = window.setTimeout(() => { - this.pollId = null; - this.reject( - _createError(this.auth, AuthErrorCode.POPUP_CLOSED_BY_USER) - ); - }, _Timeout.AUTH_EVENT); - return; - } - - this.pollId = window.setTimeout(poll, _POLL_WINDOW_CLOSE_TIMEOUT.get()); - }; - - poll(); - } -} diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/redirect.test.ts b/packages-exp/auth-exp/src/platform_browser/strategies/redirect.test.ts deleted file mode 100644 index a1ea059b1a7..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/strategies/redirect.test.ts +++ /dev/null @@ -1,455 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import * as externs from '@firebase/auth-types-exp'; - -import { delay } from '../../../test/helpers/delay'; -import { BASE_AUTH_EVENT } from '../../../test/helpers/iframe_event'; -import { - MockPersistenceLayer, - testAuth, - TestAuth, - testUser -} from '../../../test/helpers/mock_auth'; -import { makeMockPopupRedirectResolver } from '../../../test/helpers/mock_popup_redirect_resolver'; -import { - AuthEvent, - AuthEventType, - PopupRedirectResolver -} from '../../model/popup_redirect'; -import { User } from '../../model/user'; -import { AuthEventManager } from '../../core/auth/auth_event_manager'; -import { AuthErrorCode } from '../../core/errors'; -import { Persistence } from '../../core/persistence'; -import { InMemoryPersistence } from '../../core/persistence/in_memory'; -import { OAuthProvider } from '../../core/providers/oauth'; -import * as link from '../../core/user/link_unlink'; -import { UserCredentialImpl } from '../../core/user/user_credential_impl'; -import { _getInstance } from '../../core/util/instantiator'; -import * as idpTasks from '../../core/strategies/idp'; -import { - _clearOutcomes, - getRedirectResult, - linkWithRedirect, - reauthenticateWithRedirect, - signInWithRedirect, - _getRedirectResult -} from './redirect'; -import { FirebaseError } from '@firebase/util'; - -use(sinonChai); -use(chaiAsPromised); - -const MATCHING_EVENT_ID = 'matching-event-id'; -const OTHER_EVENT_ID = 'wrong-id'; - -class RedirectPersistence extends InMemoryPersistence {} - -describe('platform_browser/strategies/redirect', () => { - let auth: TestAuth; - let eventManager: AuthEventManager; - let provider: OAuthProvider; - let resolver: externs.PopupRedirectResolver; - let idpStubs: sinon.SinonStubbedInstance; - - beforeEach(async () => { - eventManager = new AuthEventManager(({} as unknown) as TestAuth); - provider = new OAuthProvider(externs.ProviderId.GOOGLE); - resolver = makeMockPopupRedirectResolver(eventManager); - _getInstance( - resolver - )._redirectPersistence = RedirectPersistence; - auth = await testAuth(resolver); - idpStubs = sinon.stub(idpTasks); - }); - - afterEach(() => { - sinon.restore(); - _clearOutcomes(); - }); - - context('signInWithRedirect', () => { - it('redirects the window', async () => { - const spy = sinon.spy( - _getInstance(resolver), - '_openRedirect' - ); - await signInWithRedirect(auth, provider, resolver); - expect(spy).to.have.been.calledWith( - auth, - provider, - AuthEventType.SIGN_IN_VIA_REDIRECT - ); - }); - - it('redirects the window with auth fallback resolver', async () => { - const spy = sinon.spy( - _getInstance(resolver), - '_openRedirect' - ); - await signInWithRedirect(auth, provider); - expect(spy).to.have.been.calledWith( - auth, - provider, - AuthEventType.SIGN_IN_VIA_REDIRECT - ); - }); - - it('errors if no resolver available', async () => { - auth._popupRedirectResolver = null; - await expect(signInWithRedirect(auth, provider)).to.be.rejectedWith( - FirebaseError, - 'auth/argument-error' - ); - }); - }); - - context('linkWithRedirect', () => { - let user: User; - - beforeEach(async () => { - user = testUser(auth, 'uid', 'email', true); - await auth._updateCurrentUser(user); - sinon.stub(link, '_assertLinkedStatus').returns(Promise.resolve()); - }); - - it('redirects the window', async () => { - const spy = sinon.spy( - _getInstance(resolver), - '_openRedirect' - ); - await linkWithRedirect(user, provider, resolver); - expect(spy).to.have.been.calledWith( - auth, - provider, - AuthEventType.LINK_VIA_REDIRECT - ); - }); - - it('redirects the window with auth fallback resolver', async () => { - const spy = sinon.spy( - _getInstance(resolver), - '_openRedirect' - ); - await linkWithRedirect(user, provider); - expect(spy).to.have.been.calledWith( - auth, - provider, - AuthEventType.LINK_VIA_REDIRECT - ); - }); - - it('errors if no resolver available', async () => { - auth._popupRedirectResolver = null; - await expect(linkWithRedirect(user, provider)).to.be.rejectedWith( - FirebaseError, - 'auth/argument-error' - ); - }); - - it('persists the redirect user and current user', async () => { - const redirectPersistence: Persistence = _getInstance( - RedirectPersistence - ); - sinon.spy(redirectPersistence, '_set'); - sinon.spy(auth.persistenceLayer, '_set'); - - await linkWithRedirect(user, provider, resolver); - expect(redirectPersistence._set).to.have.been.calledWith( - 'firebase:redirectUser:test-api-key:test-app', - user.toJSON() - ); - expect(auth.persistenceLayer._set).to.have.been.calledWith( - 'firebase:authUser:test-api-key:test-app', - user.toJSON() - ); - expect(typeof user._redirectEventId).to.eq('string'); - }); - - it('persists the redirect user but not current user if diff currentUser', async () => { - await auth._updateCurrentUser(testUser(auth, 'not-uid', 'email', true)); - const redirectPersistence: Persistence = _getInstance( - RedirectPersistence - ); - sinon.spy(redirectPersistence, '_set'); - sinon.spy(auth.persistenceLayer, '_set'); - - await linkWithRedirect(user, provider, resolver); - expect(redirectPersistence._set).to.have.been.calledWith( - 'firebase:redirectUser:test-api-key:test-app', - user.toJSON() - ); - expect(auth.persistenceLayer._set).not.to.have.been.called; - expect(typeof user._redirectEventId).to.eq('string'); - }); - }); - - context('reauthenticateWithRedirect', () => { - let user: User; - - beforeEach(async () => { - user = testUser(auth, 'uid', 'email', true); - await auth._updateCurrentUser(user); - }); - - it('redirects the window', async () => { - const spy = sinon.spy( - _getInstance(resolver), - '_openRedirect' - ); - await reauthenticateWithRedirect(user, provider, resolver); - expect(spy).to.have.been.calledWith( - auth, - provider, - AuthEventType.REAUTH_VIA_REDIRECT - ); - }); - - it('redirects the window with auth fallback resolver', async () => { - const spy = sinon.spy( - _getInstance(resolver), - '_openRedirect' - ); - await reauthenticateWithRedirect(user, provider); - expect(spy).to.have.been.calledWith( - auth, - provider, - AuthEventType.REAUTH_VIA_REDIRECT - ); - }); - - it('errors if no resolver available', async () => { - auth._popupRedirectResolver = null; - await expect( - reauthenticateWithRedirect(user, provider) - ).to.be.rejectedWith(FirebaseError, 'auth/argument-error'); - }); - - it('persists the redirect user and current user', async () => { - const redirectPersistence: Persistence = _getInstance( - RedirectPersistence - ); - sinon.spy(redirectPersistence, '_set'); - sinon.spy(auth.persistenceLayer, '_set'); - - await reauthenticateWithRedirect(user, provider, resolver); - expect(redirectPersistence._set).to.have.been.calledWith( - 'firebase:redirectUser:test-api-key:test-app', - user.toJSON() - ); - expect(auth.persistenceLayer._set).to.have.been.calledWith( - 'firebase:authUser:test-api-key:test-app', - user.toJSON() - ); - expect(typeof user._redirectEventId).to.eq('string'); - }); - - it('persists the redirect user but not current user if diff currentUser', async () => { - await auth._updateCurrentUser(testUser(auth, 'not-uid', 'email', true)); - const redirectPersistence: Persistence = _getInstance( - RedirectPersistence - ); - sinon.spy(redirectPersistence, '_set'); - sinon.spy(auth.persistenceLayer, '_set'); - - await reauthenticateWithRedirect(user, provider, resolver); - expect(redirectPersistence._set).to.have.been.calledWith( - 'firebase:redirectUser:test-api-key:test-app', - user.toJSON() - ); - expect(auth.persistenceLayer._set).not.to.have.been.called; - expect(typeof user._redirectEventId).to.eq('string'); - }); - }); - - context('getRedirectResult', () => { - function iframeEvent(event: Partial): void { - // Push the dispatch out of the synchronous flow - delay(() => { - eventManager.onEvent({ - ...BASE_AUTH_EVENT, - eventId: MATCHING_EVENT_ID, - ...event - }); - }); - } - - async function reInitAuthWithRedirectUser(eventId: string): Promise { - const redirectPersistence: Persistence = _getInstance( - RedirectPersistence - ); - const mainPersistence = new MockPersistenceLayer(); - const oldAuth = await testAuth(); - const user = testUser(oldAuth, 'uid'); - user._redirectEventId = eventId; - sinon - .stub(redirectPersistence, '_get') - .returns(Promise.resolve(user.toJSON())); - sinon - .stub(mainPersistence, '_get') - .returns(Promise.resolve(user.toJSON())); - - auth = await testAuth(resolver, mainPersistence); - } - - it('completes the proper flow', async () => { - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: externs.ProviderId.GOOGLE, - operationType: externs.OperationType.SIGN_IN - }); - idpStubs._signIn.returns(Promise.resolve(cred)); - const promise = getRedirectResult(auth, resolver); - iframeEvent({ - type: AuthEventType.SIGN_IN_VIA_REDIRECT - }); - expect(await promise).to.eq(cred); - }); - - it('returns the same value if called multiple times', async () => { - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: externs.ProviderId.GOOGLE, - operationType: externs.OperationType.SIGN_IN - }); - idpStubs._signIn.returns(Promise.resolve(cred)); - const promise = getRedirectResult(auth, resolver); - iframeEvent({ - type: AuthEventType.SIGN_IN_VIA_REDIRECT - }); - expect(await promise).to.eq(cred); - expect(await getRedirectResult(auth, resolver)).to.eq(cred); - }); - - it('interacts with redirectUser loading from auth object', async () => { - // We need to re-initialize auth since it pulls the redirect user at - // auth load - await reInitAuthWithRedirectUser(MATCHING_EVENT_ID); - - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: externs.ProviderId.GOOGLE, - operationType: externs.OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - const promise = getRedirectResult(auth, resolver); - iframeEvent({ - type: AuthEventType.LINK_VIA_REDIRECT - }); - expect(await promise).to.eq(cred); - }); - - it('returns null if the event id mismatches', async () => { - // We need to re-initialize auth since it pulls the redirect user at - // auth load - await reInitAuthWithRedirectUser(OTHER_EVENT_ID); - - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: externs.ProviderId.GOOGLE, - operationType: externs.OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - const promise = getRedirectResult(auth, resolver); - iframeEvent({ - type: AuthEventType.LINK_VIA_REDIRECT - }); - expect(await promise).to.be.null; - }); - - it('returns null if there is no pending redirect', async () => { - const promise = getRedirectResult(auth, resolver); - iframeEvent({ - type: AuthEventType.UNKNOWN, - error: { - code: `auth/${AuthErrorCode.NO_AUTH_EVENT}` - } as externs.AuthError - }); - expect(await promise).to.be.null; - }); - - it('works with reauthenticate', async () => { - await reInitAuthWithRedirectUser(MATCHING_EVENT_ID); - - const cred = new UserCredentialImpl({ - user: testUser(auth, 'uid'), - providerId: externs.ProviderId.GOOGLE, - operationType: externs.OperationType.REAUTHENTICATE - }); - idpStubs._reauth.returns(Promise.resolve(cred)); - const promise = getRedirectResult(auth, resolver); - iframeEvent({ - type: AuthEventType.REAUTH_VIA_REDIRECT - }); - expect(await promise).to.eq(cred); - expect(await getRedirectResult(auth, resolver)).to.eq(cred); - }); - - it('removes the redirect user and clears eventId from currentuser', async () => { - await reInitAuthWithRedirectUser(MATCHING_EVENT_ID); - const redirectPersistence: Persistence = _getInstance( - RedirectPersistence - ); - sinon.spy(redirectPersistence, '_remove'); - - const cred = new UserCredentialImpl({ - user: auth._currentUser!, - providerId: externs.ProviderId.GOOGLE, - operationType: externs.OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - const promise = getRedirectResult(auth, resolver); - iframeEvent({ - type: AuthEventType.LINK_VIA_REDIRECT - }); - expect(await promise).to.eq(cred); - expect(redirectPersistence._remove).to.have.been.called; - expect(auth._currentUser?._redirectEventId).to.be.undefined; - expect(auth.persistenceLayer.lastObjectSet?._redirectEventId).to.be - .undefined; - }); - - it('does not mutate authstate if bypassAuthState is true', async () => { - await reInitAuthWithRedirectUser(MATCHING_EVENT_ID); - const redirectPersistence: Persistence = _getInstance( - RedirectPersistence - ); - sinon.spy(redirectPersistence, '_remove'); - - const cred = new UserCredentialImpl({ - user: auth._currentUser!, - providerId: externs.ProviderId.GOOGLE, - operationType: externs.OperationType.LINK - }); - idpStubs._link.returns(Promise.resolve(cred)); - const promise = _getRedirectResult(auth, resolver, true); - iframeEvent({ - type: AuthEventType.LINK_VIA_REDIRECT - }); - expect(await promise).to.eq(cred); - expect(redirectPersistence._remove).not.to.have.been.called; - expect(auth._currentUser?._redirectEventId).not.to.be.undefined; - expect(auth.persistenceLayer.lastObjectSet?._redirectEventId).not.to.be - .undefined; - }); - }); -}); diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts b/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts deleted file mode 100644 index 45f944224df..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts +++ /dev/null @@ -1,348 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { OAuthProvider } from '../../core'; -import { _castAuth } from '../../core/auth/auth_impl'; -import { AuthErrorCode } from '../../core/errors'; -import { _assertLinkedStatus } from '../../core/user/link_unlink'; -import { _assert } from '../../core/util/assert'; -import { _generateEventId } from '../../core/util/event_id'; -import { Auth } from '../../model/auth'; -import { - AuthEvent, - AuthEventType, - PopupRedirectResolver -} from '../../model/popup_redirect'; -import { User, UserCredential } from '../../model/user'; -import { _withDefaultResolver } from '../popup_redirect'; -import { AbstractPopupRedirectOperation } from './abstract_popup_redirect_operation'; - -/** - * Authenticates a Firebase client using a full-page redirect flow. - * - * @remarks - * To handle the results and errors for this operation, refer to {@link getRedirectResult}. - * - * @example - * ```javascript - * // Sign in using a redirect. - * const provider = new FacebookAuthProvider(); - * // You can add additional scopes to the provider: - * provider.addScope('user_birthday'); - * // Start a sign in process for an unauthenticated user. - * await signInWithRedirect(auth, provider); - * // This will trigger a full page redirect away from your app - * - * // After returning from the redirect when your app initializes you can obtain the result - * const result = await getRedirectResult(auth); - * if (result) { - * // This is the signed-in user - * const user = result.user; - * // This gives you a Facebook Access Token. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * } - * // As this API can be used for sign-in, linking and reauthentication, - * // check the operationType to determine what triggered this redirect - * // operation. - * const operationType = result.operationType; - * ``` - * - * @param auth - The Auth instance. - * @param provider - The provider to authenticate. The provider has to be an {@link OAuthProvider}. - * Non-OAuth providers like {@link EmailAuthProvider} will throw an error. - * @param resolver - An instance of {@link @firebase/auth-types#PopupRedirectResolver}, optional - * if already supplied to {@link initializeAuth} or provided by {@link getAuth}. - * - * @public - */ -export async function signInWithRedirect( - auth: externs.Auth, - provider: externs.AuthProvider, - resolver?: externs.PopupRedirectResolver -): Promise { - const authInternal = _castAuth(auth); - _assert( - provider instanceof OAuthProvider, - auth, - AuthErrorCode.ARGUMENT_ERROR - ); - - return _withDefaultResolver(authInternal, resolver)._openRedirect( - authInternal, - provider, - AuthEventType.SIGN_IN_VIA_REDIRECT - ); -} - -/** - * Reauthenticates the current user with the specified {@link OAuthProvider} using a full-page redirect flow. - * - * @example - * ```javascript - * // Sign in using a redirect. - * const provider = new FacebookAuthProvider(); - * const result = await signInWithRedirect(auth, provider); - * // This will trigger a full page redirect away from your app - * - * // After returning from the redirect when your app initializes you can obtain the result - * const result = await getRedirectResult(auth); - * // Link using a redirect. - * await linkWithRedirect(result.user, provider); - * // This will again trigger a full page redirect away from your app - * - * // After returning from the redirect when your app initializes you can obtain the result - * const result = await getRedirectResult(auth); - * ``` - * - * @param user - The user. - * @param provider - The provider to authenticate. The provider has to be an {@link OAuthProvider}. - * Non-OAuth providers like {@link EmailAuthProvider} will throw an error. - * @param resolver - An instance of {@link @firebase/auth-types#PopupRedirectResolver}, optional - * if already supplied to {@link initializeAuth} or provided by {@link getAuth}. - * - * @public - */ -export async function reauthenticateWithRedirect( - user: externs.User, - provider: externs.AuthProvider, - resolver?: externs.PopupRedirectResolver -): Promise { - const userInternal = user as User; - _assert( - provider instanceof OAuthProvider, - userInternal.auth, - AuthErrorCode.ARGUMENT_ERROR - ); - - // Allow the resolver to error before persisting the redirect user - const resolverInternal = _withDefaultResolver(userInternal.auth, resolver); - - const eventId = await prepareUserForRedirect(userInternal); - return resolverInternal._openRedirect( - userInternal.auth, - provider, - AuthEventType.REAUTH_VIA_REDIRECT, - eventId - ); -} - -/** - * Links the {@link OAuthProvider} to the user account using a full-page redirect flow. - * - * @example - * ```javascript - * // Sign in using some other provider. - * const result = await signInWithEmailAndPassword(auth, email, password); - * // Link using a redirect. - * const provider = new FacebookAuthProvider(); - * await linkWithRedirect(result.user, provider); - * // This will trigger a full page redirect away from your app - * - * // After returning from the redirect when your app initializes you can obtain the result - * const result = await getRedirectResult(auth); - * ``` - * - * @param user - The user. - * @param provider - The provider to authenticate. The provider has to be an {@link OAuthProvider}. - * Non-OAuth providers like {@link EmailAuthProvider} will throw an error. - * @param resolver - An instance of {@link @firebase/auth-types#PopupRedirectResolver}, optional - * if already supplied to {@link initializeAuth} or provided by {@link getAuth}. - * - * - * @public - */ -export async function linkWithRedirect( - user: externs.User, - provider: externs.AuthProvider, - resolver?: externs.PopupRedirectResolver -): Promise { - const userInternal = user as User; - _assert( - provider instanceof OAuthProvider, - userInternal.auth, - AuthErrorCode.ARGUMENT_ERROR - ); - - // Allow the resolver to error before persisting the redirect user - const resolverInternal = _withDefaultResolver(userInternal.auth, resolver); - - await _assertLinkedStatus(false, userInternal, provider.providerId); - const eventId = await prepareUserForRedirect(userInternal); - return resolverInternal._openRedirect( - userInternal.auth, - provider, - AuthEventType.LINK_VIA_REDIRECT, - eventId - ); -} - -/** - * Returns a {@link @firebase/auth-types#UserCredential} from the redirect-based sign-in flow. - * - * @remarks - * If sign-in succeeded, returns the signed in user. If sign-in was unsuccessful, fails with an - * error. If no redirect operation was called, returns a {@link @firebase/auth-types#UserCredential} - * with a null `user`. - * - * @example - * ```javascript - * // Sign in using a redirect. - * const provider = new FacebookAuthProvider(); - * // You can add additional scopes to the provider: - * provider.addScope('user_birthday'); - * // Start a sign in process for an unauthenticated user. - * await signInWithRedirect(auth, provider); - * // This will trigger a full page redirect away from your app - * - * // After returning from the redirect when your app initializes you can obtain the result - * const result = await getRedirectResult(auth); - * if (result) { - * // This is the signed-in user - * const user = result.user; - * // This gives you a Facebook Access Token. - * const credential = provider.credentialFromResult(auth, result); - * const token = credential.accessToken; - * } - * // As this API can be used for sign-in, linking and reauthentication, - * // check the operationType to determine what triggered this redirect - * // operation. - * const operationType = result.operationType; - * ``` - * - * @param auth - The Auth instance. - * @param resolver - An instance of {@link @firebase/auth-types#PopupRedirectResolver}, optional - * if already supplied to {@link initializeAuth} or provided by {@link getAuth}. - * - * @public - */ -export async function getRedirectResult( - auth: externs.Auth, - resolver?: externs.PopupRedirectResolver -): Promise { - await _castAuth(auth)._initializationPromise; - return _getRedirectResult(auth, resolver, false); -} - -export async function _getRedirectResult( - auth: externs.Auth, - resolverExtern?: externs.PopupRedirectResolver, - bypassAuthState = false -): Promise { - const authInternal = _castAuth(auth); - const resolver = _withDefaultResolver(authInternal, resolverExtern); - const action = new RedirectAction(authInternal, resolver, bypassAuthState); - const result = await action.execute(); - - if (result && !bypassAuthState) { - delete result.user._redirectEventId; - await authInternal._persistUserIfCurrent(result.user as User); - await authInternal._setRedirectUser(null, resolverExtern); - } - - return result; -} - -/** @internal */ -async function prepareUserForRedirect(user: User): Promise { - const eventId = _generateEventId(`${user.uid}:::`); - user._redirectEventId = eventId; - await user.auth._setRedirectUser(user); - await user.auth._persistUserIfCurrent(user); - return eventId; -} - -// We only get one redirect outcome for any one auth, so just store it -// in here. -const redirectOutcomeMap: Map< - string, - () => Promise -> = new Map(); - -class RedirectAction extends AbstractPopupRedirectOperation { - eventId = null; - - constructor( - auth: Auth, - resolver: PopupRedirectResolver, - bypassAuthState = false - ) { - super( - auth, - [ - AuthEventType.SIGN_IN_VIA_REDIRECT, - AuthEventType.LINK_VIA_REDIRECT, - AuthEventType.REAUTH_VIA_REDIRECT, - AuthEventType.UNKNOWN - ], - resolver, - undefined, - bypassAuthState - ); - } - - /** - * Override the execute function; if we already have a redirect result, then - * just return it. - */ - async execute(): Promise { - let readyOutcome = redirectOutcomeMap.get(this.auth._key()); - if (!readyOutcome) { - try { - const result = await super.execute(); - readyOutcome = () => Promise.resolve(result); - } catch (e) { - readyOutcome = () => Promise.reject(e); - } - - redirectOutcomeMap.set(this.auth._key(), readyOutcome); - } - - return readyOutcome(); - } - - async onAuthEvent(event: AuthEvent): Promise { - if (event.type === AuthEventType.SIGN_IN_VIA_REDIRECT) { - return super.onAuthEvent(event); - } else if (event.type === AuthEventType.UNKNOWN) { - // This is a sentinel value indicating there's no pending redirect - this.resolve(null); - return; - } - - if (event.eventId) { - const user = await this.auth._redirectUserForId(event.eventId); - if (user) { - this.user = user; - return super.onAuthEvent(event); - } else { - this.resolve(null); - } - } - } - - async onExecution(): Promise {} - - cleanUp(): void {} -} - -/** @internal */ -export function _clearOutcomes(): void { - redirectOutcomeMap.clear(); -} diff --git a/packages-exp/auth-exp/src/platform_browser/util/popup.test.ts b/packages-exp/auth-exp/src/platform_browser/util/popup.test.ts deleted file mode 100644 index 001c528916e..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/util/popup.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -import { FirebaseError } from '@firebase/util'; -import * as utils from '@firebase/util'; - -import { _open, AuthPopup } from './popup'; -import { Auth } from '../../model/auth'; -import { testAuth } from '../../../test/helpers/mock_auth'; - -use(sinonChai); - -describe('platform_browser/util/popup', () => { - let windowOpenStub: sinon.SinonStub; - let auth: Auth; - let popupStub: sinon.SinonStubbedInstance; - - function setUA(ua: string): void { - sinon.stub(utils, 'getUA').returns(ua); - } - - function windowTarget(): string { - return windowOpenStub.firstCall.args[1]; - } - - function windowURL(): string { - return windowOpenStub.firstCall.args[0]; - } - - function windowOptions(): string { - return windowOpenStub.firstCall.args[2]; - } - - beforeEach(async () => { - windowOpenStub = sinon.stub(window, 'open'); - popupStub = sinon.stub(({ - focus: () => {}, - close: () => {} - } as unknown) as Window); - windowOpenStub.returns(popupStub); - auth = await testAuth(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it('sets target to name param if not chrome UA', () => { - setUA('notchrome'); - _open(auth, 'url', 'name'); - expect(windowTarget()).to.eq('name'); - }); - - it('sets target to _blank if on chrome IOS', () => { - setUA('crios/'); - _open(auth, 'url', 'name'); - expect(windowTarget()).to.eq('_blank'); - }); - - it('sets the firefox url to a default if not provided', () => { - setUA('firefox/'); - _open(auth); - expect(windowURL()).to.eq('http://localhost'); - }); - - it('sets the firefox url to the value provided', () => { - setUA('firefox/'); - _open(auth, 'url'); - expect(windowURL()).to.eq('url'); - }); - - it('sets non-firefox url to empty if not provided', () => { - setUA('not-ff/'); - _open(auth); - expect(windowURL()).to.eq(''); - }); - - it('sets non-firefox url to url if not provided', () => { - setUA('not-ff/'); - _open(auth, 'url'); - expect(windowURL()).to.eq('url'); - }); - - it('sets scrollbars to yes in popup', () => { - setUA('firefox/'); - _open(auth); - expect(windowOptions()).to.include('scrollbars=yes'); - }); - - it('errors if the popup is blocked', () => { - setUA(''); - windowOpenStub.returns(undefined); - expect(() => _open(auth)).to.throw(FirebaseError, 'auth/popup-blocked'); - }); - - it('builds the proper options string', () => { - const screen = window.screen; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - (window as any).sreen = { - availHeight: 1000, - availWidth: 2000 - }; - - setUA(''); - _open(auth); - const options = windowOptions() - .split(',') - .filter(s => !!s) - .map(prop => prop.split('=')) - .reduce>((accum, [prop, val]) => { - accum[prop] = val; - return accum; - }, {}); - - expect(options).to.eql({ - location: 'yes', - resizable: 'yes', - statusbar: 'yes', - toolbar: 'no', - width: '500', - height: '600', - top: '0', - left: '0' - }); - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - (window as any).screen = screen; - }); - - it('calls focus on the new popup', () => { - setUA(''); - _open(auth); - expect(popupStub.focus).to.have.been.called; - }); - - it('does not fail if window.focus errors', () => { - popupStub.focus.throws(new Error('lol no')); - setUA(''); - expect(() => _open(auth)).not.to.throw(Error); - }); - - context('resulting popup object', () => { - let authPopup: AuthPopup; - beforeEach(() => { - setUA(''); - authPopup = _open(auth); - }); - - it('has a window object', () => { - expect(authPopup.window).to.eq(popupStub); - }); - - it('calls through to the popup close', () => { - authPopup.close(); - expect(popupStub.close).to.have.been.called; - }); - - it('close() does not error if underlying call errors', () => { - popupStub.close.throws(new Error('not this time')); - expect(() => authPopup.close()).not.to.throw(Error); - }); - }); -}); diff --git a/packages-exp/auth-exp/src/platform_browser/util/popup.ts b/packages-exp/auth-exp/src/platform_browser/util/popup.ts deleted file mode 100644 index da37f4b30b7..00000000000 --- a/packages-exp/auth-exp/src/platform_browser/util/popup.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getUA } from '@firebase/util'; - -import { AuthErrorCode } from '../../core/errors'; -import { _assert } from '../../core/util/assert'; -import { - _isChromeIOS, - _isFirefox, - _isIOSStandalone -} from '../../core/util/browser'; -import { Auth } from '../../model/auth'; - -const BASE_POPUP_OPTIONS = { - location: 'yes', - resizable: 'yes', - statusbar: 'yes', - toolbar: 'no' -}; - -const DEFAULT_WIDTH = 500; -const DEFAULT_HEIGHT = 600; -const TARGET_BLANK = '_blank'; - -const FIREFOX_EMPTY_URL = 'http://localhost'; - -export class AuthPopup { - associatedEvent: string | null = null; - - constructor(readonly window: Window | null) {} - - close(): void { - if (this.window) { - try { - this.window.close(); - } catch (e) {} - } - } -} - -export function _open( - auth: Auth, - url?: string, - name?: string, - width = DEFAULT_WIDTH, - height = DEFAULT_HEIGHT -): AuthPopup { - const top = Math.min((window.screen.availHeight - height) / 2, 0).toString(); - const left = Math.min((window.screen.availWidth - width) / 2, 0).toString(); - let target = ''; - - const options: { [key: string]: string } = { - ...BASE_POPUP_OPTIONS, - width: width.toString(), - height: height.toString(), - top, - left - }; - - // Chrome iOS 7 and 8 is returning an undefined popup win when target is - // specified, even though the popup is not necessarily blocked. - const ua = getUA().toLowerCase(); - - if (name) { - target = _isChromeIOS(ua) ? TARGET_BLANK : name; - } - - if (_isFirefox(ua)) { - // Firefox complains when invalid URLs are popped out. Hacky way to bypass. - url = url || FIREFOX_EMPTY_URL; - // Firefox disables by default scrolling on popup windows, which can create - // issues when the user has many Google accounts, for instance. - options.scrollbars = 'yes'; - } - - const optionsString = Object.entries(options).reduce( - (accum, [key, value]) => `${accum}${key}=${value},`, - '' - ); - - if (_isIOSStandalone(ua) && target !== '_self') { - openAsNewWindowIOS(url || '', target); - return new AuthPopup(null); - } - - // about:blank getting sanitized causing browsers like IE/Edge to display - // brief error message before redirecting to handler. - const newWin = window.open(url || '', target, optionsString); - _assert(newWin, auth, AuthErrorCode.POPUP_BLOCKED); - - // Flaky on IE edge, encapsulate with a try and catch. - try { - newWin.focus(); - } catch (e) {} - - return new AuthPopup(newWin); -} - -function openAsNewWindowIOS(url: string, target: string): void { - const el = document.createElement('a'); - el.href = url; - el.target = target; - const click = document.createEvent('MouseEvent'); - click.initMouseEvent( - 'click', - true, - true, - window, - 1, - 0, - 0, - 0, - 0, - false, - false, - false, - false, - 1, - null - ); - el.dispatchEvent(click); -} diff --git a/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts b/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts deleted file mode 100644 index 8d349a60a23..00000000000 --- a/packages-exp/auth-exp/src/platform_react_native/persistence/react_native.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as externs from '@firebase/auth-types-exp'; - -import { - Persistence, - PersistenceType, - PersistenceValue, - STORAGE_AVAILABLE_KEY, - StorageEventListener -} from '../../core/persistence'; -import { debugFail } from '../../core/util/assert'; - -/** - * Returns a persistence class that wraps AsyncStorage imported from - * `react-native` or `@react-native-community/async-storage`. - * - * Creates a "new"-able subclass on the fly that has an empty constructor. - * - * In the _getInstance() implementation (see src/core/persistence/index.ts), - * we expect each "externs.Persistence" object passed to us by the user to - * be able to be instantiated (as a class) using "new". That function also - * expects the constructor to be empty. Since ReactNativeStorage requires the - * underlying storage layer, we need to be able to create subclasses - * (closures, esentially) that have the storage layer but empty constructor. - */ - -export function getReactNativePersistence( - storage: externs.ReactNativeAsyncStorage -): externs.Persistence { - return class implements Persistence { - static type: 'LOCAL' = 'LOCAL'; - readonly type: PersistenceType = PersistenceType.LOCAL; - - async _isAvailable(): Promise { - try { - if (!storage) { - return false; - } - await storage.setItem(STORAGE_AVAILABLE_KEY, '1'); - await storage.removeItem(STORAGE_AVAILABLE_KEY); - return true; - } catch { - return false; - } - } - - _set(key: string, value: PersistenceValue): Promise { - return storage.setItem(key, JSON.stringify(value)); - } - - async _get(key: string): Promise { - const json = await storage.getItem(key); - return json ? JSON.parse(json) : null; - } - - _remove(key: string): Promise { - return storage.removeItem(key); - } - - _addListener(_key: string, _listener: StorageEventListener): void { - debugFail('not implemented'); - } - - _removeListener(_key: string, _listener: StorageEventListener): void { - debugFail('not implemented'); - } - }; -} diff --git a/packages-exp/auth-exp/test/helpers/api/helper.ts b/packages-exp/auth-exp/test/helpers/api/helper.ts deleted file mode 100644 index 0385ef62f66..00000000000 --- a/packages-exp/auth-exp/test/helpers/api/helper.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Endpoint } from '../../../src/api'; -import { TEST_HOST, TEST_KEY, TEST_SCHEME } from '../mock_auth'; -import { mock, Route } from '../mock_fetch'; - -export function endpointUrl(endpoint: Endpoint): string { - return `${TEST_SCHEME}://${TEST_HOST}${endpoint}?key=${TEST_KEY}`; -} - -export function mockEndpoint( - endpoint: Endpoint, - response: object, - status = 200 -): Route { - return mock(endpointUrl(endpoint), response, status); -} diff --git a/packages-exp/auth-exp/test/helpers/integration/helpers.ts b/packages-exp/auth-exp/test/helpers/integration/helpers.ts deleted file mode 100644 index dfc05cffe31..00000000000 --- a/packages-exp/auth-exp/test/helpers/integration/helpers.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { deleteApp, initializeApp } from '@firebase/app-exp'; -import { Auth, User } from '@firebase/auth-types-exp'; - -import { getAuth } from '../../../index'; -import { _generateEventId } from '../../../src/core/util/event_id'; - -// eslint-disable-next-line @typescript-eslint/no-require-imports -const PROJECT_CONFIG = require('../../../../../config/project.json'); - -export const PROJECT_ID = PROJECT_CONFIG.projectId; -export const AUTH_DOMAIN = PROJECT_CONFIG.authDomain; -export const API_KEY = PROJECT_CONFIG.apiKey; - -interface IntegrationTestAuth extends Auth { - cleanUp(): Promise; -} - -export function randomEmail(): string { - return `${_generateEventId('test.email.')}@test.com`; -} - -export function getTestInstance(): Auth { - const app = initializeApp({ - apiKey: API_KEY, - projectId: PROJECT_ID, - authDomain: AUTH_DOMAIN - }); - - const createdUsers: User[] = []; - const auth = getAuth(app) as IntegrationTestAuth; - auth.settings.appVerificationDisabledForTesting = true; - - auth.onAuthStateChanged(user => { - if (user) { - createdUsers.push(user); - } - }); - - auth.cleanUp = async () => { - // Clear out any new users that were created in the course of the test - for (const user of createdUsers) { - try { - await user.delete(); - } catch { - // Best effort. Maybe the test already deleted the user ¯\_(ツ)_/¯ - } - } - - await deleteApp(app); - }; - - return auth; -} - -export async function cleanUpTestInstance(auth: Auth): Promise { - await auth.signOut(); - await (auth as IntegrationTestAuth).cleanUp(); -} diff --git a/packages-exp/auth-exp/test/helpers/mock_auth.ts b/packages-exp/auth-exp/test/helpers/mock_auth.ts deleted file mode 100644 index 5d9becda6a7..00000000000 --- a/packages-exp/auth-exp/test/helpers/mock_auth.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { PopupRedirectResolver } from '@firebase/auth-types-exp'; -import { debugErrorMap } from '../../src'; - -import { AuthImpl } from '../../src/core/auth/auth_impl'; -import { PersistedBlob } from '../../src/core/persistence'; -import { InMemoryPersistence } from '../../src/core/persistence/in_memory'; -import { StsTokenManager } from '../../src/core/user/token_manager'; -import { UserImpl } from '../../src/core/user/user_impl'; -import { Auth } from '../../src/model/auth'; -import { User } from '../../src/model/user'; - -export const TEST_HOST = 'localhost'; -export const TEST_TOKEN_HOST = 'localhost/token'; -export const TEST_AUTH_DOMAIN = 'localhost'; -export const TEST_SCHEME = 'mock'; -export const TEST_KEY = 'test-api-key'; - -export interface TestAuth extends AuthImpl { - persistenceLayer: MockPersistenceLayer; -} - -const FAKE_APP: FirebaseApp = { - name: 'test-app', - options: {}, - automaticDataCollectionEnabled: false -}; - -export class MockPersistenceLayer extends InMemoryPersistence { - lastObjectSet: PersistedBlob | null = null; - - _set(key: string, object: PersistedBlob): Promise { - this.lastObjectSet = object; - return super._set(key, object); - } - - _remove(key: string): Promise { - this.lastObjectSet = null; - return super._remove(key); - } -} - -export async function testAuth( - popupRedirectResolver?: PopupRedirectResolver, - persistence = new MockPersistenceLayer() -): Promise { - const auth: TestAuth = new AuthImpl(FAKE_APP, { - apiKey: TEST_KEY, - authDomain: TEST_AUTH_DOMAIN, - apiHost: TEST_HOST, - apiScheme: TEST_SCHEME, - tokenApiHost: TEST_TOKEN_HOST, - sdkClientVersion: 'testSDK/0.0.0' - }) as TestAuth; - auth._updateErrorMap(debugErrorMap); - - await auth._initializeWithPersistence([persistence], popupRedirectResolver); - auth.persistenceLayer = persistence; - auth.settings.appVerificationDisabledForTesting = true; - return auth; -} - -export function testUser( - auth: Auth, - uid: string, - email?: string, - fakeTokens = false -): User { - // Create a token manager that's valid off the bat to avoid refresh calls - const stsTokenManager = new StsTokenManager(); - if (fakeTokens) { - Object.assign>(stsTokenManager, { - expirationTime: Date.now() + 100_000, - accessToken: 'access-token', - refreshToken: 'refresh-token' - }); - } - - return new UserImpl({ - uid, - auth: auth as Auth, - stsTokenManager, - email - }); -} diff --git a/packages-exp/auth-exp/test/integration/flows/anonymous.test.ts b/packages-exp/auth-exp/test/integration/flows/anonymous.test.ts deleted file mode 100644 index 615cf3f488e..00000000000 --- a/packages-exp/auth-exp/test/integration/flows/anonymous.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { - createUserWithEmailAndPassword, - EmailAuthProvider, - linkWithCredential, - signInAnonymously, - signInWithEmailAndPassword, - updateEmail, - updatePassword - // eslint-disable-next-line import/no-extraneous-dependencies -} from '@firebase/auth-exp'; -import { Auth, OperationType } from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { - cleanUpTestInstance, - getTestInstance, - randomEmail -} from '../../helpers/integration/helpers'; - -use(chaiAsPromised); - -describe('Integration test: anonymous auth', () => { - let auth: Auth; - beforeEach(() => (auth = getTestInstance())); - afterEach(() => cleanUpTestInstance(auth)); - - it('signs in anonymously', async () => { - const userCred = await signInAnonymously(auth); - expect(auth.currentUser).to.eq(userCred.user); - expect(userCred.operationType).to.eq(OperationType.SIGN_IN); - - const user = userCred.user; - expect(user.isAnonymous).to.be.true; - expect(user.uid).to.be.a('string'); - }); - - it('second sign in on the same device yields same user', async () => { - const { user: userA } = await signInAnonymously(auth); - const { user: userB } = await signInAnonymously(auth); - - expect(userA.uid).to.eq(userB.uid); - }); - - context('email/password interaction', () => { - let email: string; - - beforeEach(() => { - email = randomEmail(); - }); - - it('anonymous / email-password accounts remain independent', async () => { - let anonCred = await signInAnonymously(auth); - const emailCred = await createUserWithEmailAndPassword( - auth, - email, - 'password' - ); - expect(emailCred.user.uid).not.to.eql(anonCred.user.uid); - - await auth.signOut(); - anonCred = await signInAnonymously(auth); - const emailSignIn = await signInWithEmailAndPassword( - auth, - email, - 'password' - ); - expect(emailCred.user.uid).to.eql(emailSignIn.user.uid); - expect(emailSignIn.user.uid).not.to.eql(anonCred.user.uid); - }); - - it('account can be upgraded by setting email and password', async () => { - const { user: anonUser } = await signInAnonymously(auth); - await updateEmail(anonUser, email); - await updatePassword(anonUser, 'password'); - - await auth.signOut(); - - const { user: emailPassUser } = await signInWithEmailAndPassword( - auth, - email, - 'password' - ); - expect(emailPassUser.uid).to.eq(anonUser.uid); - }); - - it('account can be linked using email and password', async () => { - const { user: anonUser } = await signInAnonymously(auth); - const cred = EmailAuthProvider.credential(email, 'password'); - await linkWithCredential(anonUser, cred); - await auth.signOut(); - - const { user: emailPassUser } = await signInWithEmailAndPassword( - auth, - email, - 'password' - ); - expect(emailPassUser.uid).to.eq(anonUser.uid); - }); - - it('account cannot be linked with existing email/password', async () => { - await createUserWithEmailAndPassword(auth, email, 'password'); - const { user: anonUser } = await signInAnonymously(auth); - const cred = EmailAuthProvider.credential(email, 'password'); - await expect(linkWithCredential(anonUser, cred)).to.be.rejectedWith( - FirebaseError, - 'auth/email-already-in-use' - ); - }); - }); -}); diff --git a/packages-exp/auth-exp/test/integration/flows/email.test.ts b/packages-exp/auth-exp/test/integration/flows/email.test.ts deleted file mode 100644 index 574caaae96b..00000000000 --- a/packages-exp/auth-exp/test/integration/flows/email.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { - createUserWithEmailAndPassword, - EmailAuthProvider, - reload, - signInWithCredential, - signInWithEmailAndPassword, - updateProfile - // eslint-disable-next-line import/no-extraneous-dependencies -} from '@firebase/auth-exp'; -import { Auth, OperationType, UserCredential } from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { - cleanUpTestInstance, - getTestInstance, - randomEmail -} from '../../helpers/integration/helpers'; - -use(chaiAsPromised); - -describe('Integration test: email/password auth', () => { - let auth: Auth; - let email: string; - beforeEach(() => { - auth = getTestInstance(); - email = randomEmail(); - }); - - afterEach(() => cleanUpTestInstance(auth)); - - it('allows user to sign up', async () => { - const userCred = await createUserWithEmailAndPassword( - auth, - email, - 'password' - ); - expect(auth.currentUser).to.eq(userCred.user); - expect(userCred.operationType).to.eq(OperationType.SIGN_IN); - - const user = userCred.user; - expect(user.isAnonymous).to.be.false; - expect(user.uid).to.be.a('string'); - expect(user.email).to.eq(email); - expect(user.emailVerified).to.be.false; - }); - - it('errors when createUser called twice', async () => { - await createUserWithEmailAndPassword(auth, email, 'password'); - await expect( - createUserWithEmailAndPassword(auth, email, 'password') - ).to.be.rejectedWith(FirebaseError, 'auth/email-already-in-use'); - }); - - context('with existing user', () => { - let signUpCred: UserCredential; - - beforeEach(async () => { - signUpCred = await createUserWithEmailAndPassword( - auth, - email, - 'password' - ); - await auth.signOut(); - }); - - it('allows the user to sign in with signInWithEmailAndPassword', async () => { - const signInCred = await signInWithEmailAndPassword( - auth, - email, - 'password' - ); - expect(auth.currentUser).to.eq(signInCred.user); - - expect(signInCred.operationType).to.eq(OperationType.SIGN_IN); - expect(signInCred.user.uid).to.eq(signUpCred.user.uid); - }); - - it('allows the user to sign in with signInWithCredential', async () => { - const credential = EmailAuthProvider.credential(email, 'password'); - const signInCred = await signInWithCredential(auth, credential); - expect(auth.currentUser).to.eq(signInCred.user); - - expect(signInCred.operationType).to.eq(OperationType.SIGN_IN); - expect(signInCred.user.uid).to.eq(signUpCred.user.uid); - }); - - it('allows the user to update profile', async () => { - let { user } = await signInWithEmailAndPassword(auth, email, 'password'); - await updateProfile(user, { - displayName: 'Display Name', - photoURL: 'photo-url' - }); - expect(user.displayName).to.eq('Display Name'); - expect(user.photoURL).to.eq('photo-url'); - - await auth.signOut(); - - user = (await signInWithEmailAndPassword(auth, email, 'password')).user; - expect(user.displayName).to.eq('Display Name'); - expect(user.photoURL).to.eq('photo-url'); - }); - - it('allows the user to delete the account', async () => { - const { user } = await signInWithEmailAndPassword( - auth, - email, - 'password' - ); - await user.delete(); - - await expect(reload(user)).to.be.rejectedWith( - FirebaseError, - 'auth/user-token-expired' - ); - - expect(auth.currentUser).to.be.null; - await expect( - signInWithEmailAndPassword(auth, email, 'password') - ).to.be.rejectedWith(FirebaseError, 'auth/user-not-found'); - }); - - it('sign in can be called twice successively', async () => { - const { user: userA } = await signInWithEmailAndPassword( - auth, - email, - 'password' - ); - const { user: userB } = await signInWithEmailAndPassword( - auth, - email, - 'password' - ); - expect(userA.uid).to.eq(userB.uid); - }); - }); -}); diff --git a/packages-exp/auth-exp/test/integration/flows/phone.test.ts b/packages-exp/auth-exp/test/integration/flows/phone.test.ts deleted file mode 100644 index 48dd3bd888c..00000000000 --- a/packages-exp/auth-exp/test/integration/flows/phone.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { - linkWithPhoneNumber, - PhoneAuthProvider, - reauthenticateWithPhoneNumber, - RecaptchaVerifier, - signInAnonymously, - signInWithPhoneNumber, - unlink, - updatePhoneNumber - // eslint-disable-next-line import/no-extraneous-dependencies -} from '@firebase/auth-exp'; -import { - Auth, - OperationType, - ProviderId, - UserCredential -} from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; - -import { - cleanUpTestInstance, - getTestInstance -} from '../../helpers/integration/helpers'; - -use(chaiAsPromised); - -// NOTE: These tests don't use a real phone number. In order to run these tests -// you must whitelist the following phone numbers as "testing" numbers in the -// auth console -// https://console.firebase.google.com/u/0/project/_/authentication/providers -// • +1 (555) 555-1000, SMS code 123456 -// • +1 (555) 555-2000, SMS code 654321 - -const PHONE_A = { - phoneNumber: '+15555551000', - code: '123456' -}; - -const PHONE_B = { - phoneNumber: '+15555552000', - code: '654321' -}; - -describe('Integration test: phone auth', () => { - let auth: Auth; - let verifier: RecaptchaVerifier; - let fakeRecaptchaContainer: HTMLElement; - - beforeEach(() => { - auth = getTestInstance(); - fakeRecaptchaContainer = document.createElement('div'); - document.body.appendChild(fakeRecaptchaContainer); - verifier = new RecaptchaVerifier( - fakeRecaptchaContainer, - undefined as any, - auth - ); - }); - - afterEach(async () => { - await cleanUpTestInstance(auth); - document.body.removeChild(fakeRecaptchaContainer); - }); - - it('allows user to sign up', async () => { - const cr = await signInWithPhoneNumber(auth, PHONE_A.phoneNumber, verifier); - const userCred = await cr.confirm(PHONE_A.code); - - expect(auth.currentUser).to.eq(userCred.user); - expect(userCred.operationType).to.eq(OperationType.SIGN_IN); - - const user = userCred.user; - expect(user.isAnonymous).to.be.false; - expect(user.uid).to.be.a('string'); - expect(user.phoneNumber).to.eq(PHONE_A.phoneNumber); - }); - - it('anonymous users can link (and unlink) phone number', async () => { - const { user } = await signInAnonymously(auth); - const { uid: anonId } = user; - - const cr = await linkWithPhoneNumber(user, PHONE_A.phoneNumber, verifier); - const linkResult = await cr.confirm(PHONE_A.code); - expect(linkResult.operationType).to.eq(OperationType.LINK); - expect(linkResult.user.uid).to.eq(user.uid); - expect(linkResult.user.phoneNumber).to.eq(PHONE_A.phoneNumber); - - await unlink(user, ProviderId.PHONE); - expect(auth.currentUser!.uid).to.eq(anonId); - // Is anonymous stays false even after unlinking - expect(auth.currentUser!.isAnonymous).to.be.false; - expect(auth.currentUser!.phoneNumber).to.be.null; - }); - - context('with already-created user', () => { - let signUpCred: UserCredential; - - function resetVerifier(): void { - verifier.clear(); - verifier = new RecaptchaVerifier( - fakeRecaptchaContainer, - undefined as any, - auth - ); - } - - beforeEach(async () => { - const cr = await signInWithPhoneNumber( - auth, - PHONE_A.phoneNumber, - verifier - ); - signUpCred = await cr.confirm(PHONE_A.code); - resetVerifier(); - await auth.signOut(); - }); - - it('allows the user to sign in again', async () => { - const cr = await signInWithPhoneNumber( - auth, - PHONE_A.phoneNumber, - verifier - ); - const signInCred = await cr.confirm(PHONE_A.code); - - expect(signInCred.user.uid).to.eq(signUpCred.user.uid); - }); - - it('allows the user to update their phone number', async () => { - let cr = await signInWithPhoneNumber(auth, PHONE_A.phoneNumber, verifier); - const { user } = await cr.confirm(PHONE_A.code); - - resetVerifier(); - - const provider = new PhoneAuthProvider(auth); - const verificationId = await provider.verifyPhoneNumber( - PHONE_B.phoneNumber, - verifier - ); - - await updatePhoneNumber( - user, - PhoneAuthProvider.credential(verificationId, PHONE_B.code) - ); - expect(user.phoneNumber).to.eq(PHONE_B.phoneNumber); - - await auth.signOut(); - resetVerifier(); - - cr = await signInWithPhoneNumber(auth, PHONE_B.phoneNumber, verifier); - const { user: secondSignIn } = await cr.confirm(PHONE_B.code); - expect(secondSignIn.uid).to.eq(user.uid); - }); - - it('allows the user to reauthenticate with phone number', async () => { - let cr = await signInWithPhoneNumber(auth, PHONE_A.phoneNumber, verifier); - const { user } = await cr.confirm(PHONE_A.code); - const oldToken = await user.getIdToken(); - - resetVerifier(); - - cr = await reauthenticateWithPhoneNumber( - user, - PHONE_A.phoneNumber, - verifier - ); - await cr.confirm(PHONE_A.code); - - expect(await user.getIdToken()).not.to.eq(oldToken); - }); - - it('prevents reauthentication with wrong phone number', async () => { - let cr = await signInWithPhoneNumber(auth, PHONE_A.phoneNumber, verifier); - const { user } = await cr.confirm(PHONE_A.code); - - resetVerifier(); - - cr = await reauthenticateWithPhoneNumber( - user, - PHONE_B.phoneNumber, - verifier - ); - await expect(cr.confirm(PHONE_B.code)).to.be.rejectedWith( - FirebaseError, - 'auth/user-mismatch' - ); - - // We need to manually delete PHONE_B number since a failed - // reauthenticateWithPhoneNumber does not trigger a state change - resetVerifier(); - cr = await signInWithPhoneNumber(auth, PHONE_B.phoneNumber, verifier); - const { user: otherUser } = await cr.confirm(PHONE_B.code); - await otherUser.delete(); - }); - }); -}); diff --git a/packages-exp/auth-exp/tsconfig.json b/packages-exp/auth-exp/tsconfig.json deleted file mode 100644 index 03897eed09c..00000000000 --- a/packages-exp/auth-exp/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist" - }, - "exclude": [ - "dist/**/*", - "demo/**/*" - ] -} \ No newline at end of file diff --git a/packages-exp/auth-types-exp/README.md b/packages-exp/auth-types-exp/README.md deleted file mode 100644 index e63106206bf..00000000000 --- a/packages-exp/auth-types-exp/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @firebase/auth-types-exp - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/auth-types-exp/api-extractor.json b/packages-exp/auth-types-exp/api-extractor.json deleted file mode 100644 index 42f37a88c4b..00000000000 --- a/packages-exp/auth-types-exp/api-extractor.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/index.d.ts" -} \ No newline at end of file diff --git a/packages-exp/auth-types-exp/index.d.ts b/packages-exp/auth-types-exp/index.d.ts deleted file mode 100644 index da6a2cbd8de..00000000000 --- a/packages-exp/auth-types-exp/index.d.ts +++ /dev/null @@ -1,1545 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - CompleteFn, - ErrorFn, - FirebaseError, - NextFn, - Observer, - Unsubscribe -} from '@firebase/util'; - -/** - * Enumeration of supported providers. - * - * @public - */ -export const enum ProviderId { - ANONYMOUS = 'anonymous', - CUSTOM = 'custom', - FACEBOOK = 'facebook.com', - FIREBASE = 'firebase', - GITHUB = 'github.com', - GOOGLE = 'google.com', - PASSWORD = 'password', - PHONE = 'phone', - TWITTER = 'twitter.com' -} - -/** - * Enumeration of supported sign-in methods. - * - * @public - */ -export const enum SignInMethod { - ANONYMOUS = 'anonymous', - EMAIL_LINK = 'emailLink', - EMAIL_PASSWORD = 'password', - FACEBOOK = 'facebook.com', - GITHUB = 'github.com', - GOOGLE = 'google.com', - PHONE = 'phone', - TWITTER = 'twitter.com' -} - -/** - * Enumeration of supported operation types. - * - * @public - */ -export const enum OperationType { - /** Operation involving linking an additional provider to an already signed-in user. */ - LINK = 'link', - /** Operation involving using a provider to reauthenticate an already signed-in user. */ - REAUTHENTICATE = 'reauthenticate', - /** Operation involving signing in a user. */ - SIGN_IN = 'signIn' -} - -/** - * Interface representing the Auth config. - * - * @public - */ -export interface Config { - /** - * The API Key used to communicate with the Firebase Auth backend. - */ - apiKey: string; - /** - * The host at which the Firebase Auth backend is running. - */ - apiHost: string; - /** - * The scheme used to communicate with the Firebase Auth backend. - */ - apiScheme: string; - /** - * The host at which the Secure Token API is running. - */ - tokenApiHost: string; - /** - * The SDK Client Version. - */ - sdkClientVersion: string; - /** - * The domain at which the web widgets are hosted (provided via Firebase Config). - */ - authDomain?: string; -} - -/** - * Interface representing a parsed ID token. - * - * @privateRemarks TODO(avolkovi): consolidate with parsed_token in implementation. - * - * @public - */ -export interface ParsedToken { - /** Expiration time of the token. */ - 'exp'?: string; - /** UID of the user. */ - 'sub'?: string; - /** Time at which authentication was performed. */ - 'auth_time'?: string; - /** Issuance time of the token. */ - 'iat'?: string; - /** Firebase specific claims, containing the provider(s) used to authenticate the user. */ - 'firebase'?: { - 'sign_in_provider'?: string; - 'sign_in_second_factor'?: string; - }; - /** Map of any additional custom claims. */ - [key: string]: string | object | undefined; -} - -/** - * Type definition for an event callback. - * - * @privateRemarks TODO(avolkovi): should we consolidate with Subscribe since we're changing the API anyway? - * - * @public - */ -export type NextOrObserver = NextFn | Observer; - -/** - * Interface for an Auth error. - * - * @public - */ -export interface AuthError extends FirebaseError { - /** The name of the Firebase App which triggered this error. */ - readonly appName: string; - /** The email of the user's account, used for sign-in/linking. */ - readonly email?: string; - /** The phone number of the user's account, used for sign-in/linking. */ - readonly phoneNumber?: string; - /** - * The tenant ID being used for sign-in/linking. - * - * @remarks - * If you use {@link @firebase/auth#signInWithRedirect} to sign in, - * you have to set the tenant ID on {@link Auth} instance again as the tenant ID is not persisted - * after redirection. - */ - readonly tenantid?: string; -} - -/** - * Interface representing an Auth instance's settings. - * - * @remarks Currently used for enabling/disabling app verification for phone Auth testing. - * - * @public - */ -export interface AuthSettings { - /** - * When set, this property disables app verification for the purpose of testing phone - * authentication. For this property to take effect, it needs to be set before rendering a - * reCAPTCHA app verifier. When this is disabled, a mock reCAPTCHA is rendered instead. This is - * useful for manual testing during development or for automated integration tests. - * - * In order to use this feature, you will need to - * {@link https://firebase.google.com/docs/auth/web/phone-auth#test-with-whitelisted-phone-numbers | whitelist your phone number} - * via the Firebase Console. - * - * The default value is false (app verification is enabled). - */ - appVerificationDisabledForTesting: boolean; -} - -/** - * Interface representing Firebase Auth service. - * - * @remarks - * See {@link https://firebase.google.com/docs/auth/ | Firebase Authentication} for a full guide - * on how to use the Firebase Auth service. - * - * @public - */ -export interface Auth { - /** The name of the app associated with the Auth service instance. */ - readonly name: string; - /** The {@link Config} used to initialize this instance. */ - readonly config: Config; - /** - * Changes the type of persistence on the Auth instance. - * - * @remarks - * This will affect the currently saved Auth session and applies this type of persistence for - * future sign-in requests, including sign-in with redirect requests. - * - * This makes it easy for a user signing in to specify whether their session should be - * remembered or not. It also makes it easier to never persist the Auth state for applications - * that are shared by other users or have sensitive data. - * - * @example - * ```javascript - * auth.setPersistence(browserSessionPersistence); - * ``` - * - * @param persistence - The {@link Persistence} to use. - */ - setPersistence(persistence: Persistence): Promise; - /** - * The Auth instance's language code. - * - * @remarks - * This is a readable/writable property. When set to null, the default Firebase Console language - * setting is applied. The language code will propagate to email action templates (password - * reset, email verification and email change revocation), SMS templates for phone authentication, - * reCAPTCHA verifier and OAuth popup/redirect operations provided the specified providers support - * localization with the language code specified. - */ - languageCode: string | null; - /** - * The Auth instance's tenant ID. - * - * @remarks - * This is a readable/writable property. When you set the tenant ID of an Auth instance, all - * future sign-in/sign-up operations will pass this tenant ID and sign in or sign up users to - * the specified tenant project. When set to null, users are signed in to the parent project. - * - * @example - * ```javascript - * // Set the tenant ID on Auth instance. - * auth.tenantId = 'TENANT_PROJECT_ID'; - * - * // All future sign-in request now include tenant ID. - * const result = await signInWithEmailAndPassword(auth, email, password); - * // result.user.tenantId should be 'TENANT_PROJECT_ID'. - * ``` - * - * @defaultValue null - */ - tenantId: string | null; - /** - * The Auth instance's settings. - * - * @remarks - * This is used to edit/read configuration related options such as app verification mode for - * phone authentication. - */ - readonly settings: AuthSettings; - /** - * Adds an observer for changes to the user's sign-in state. - * - * @remarks - * To keep the old behavior, see {@link Auth.onIdTokenChanged}. - * - * @param nextOrObserver - callback triggered on change. - * @param error - callback triggered on error. - * @param completed - callback triggered when observer is removed. - */ - onAuthStateChanged( - nextOrObserver: NextOrObserver, - error?: ErrorFn, - completed?: CompleteFn - ): Unsubscribe; - /** - * Adds an observer for changes to the signed-in user's ID token. - * - * @remarks - * This includes sign-in, sign-out, and token refresh events. - * - * @param nextOrObserver - callback triggered on change. - * @param error - callback triggered on error. - * @param completed - callback triggered when observer is removed. - */ - onIdTokenChanged( - nextOrObserver: NextOrObserver, - error?: ErrorFn, - completed?: CompleteFn - ): Unsubscribe; - /** The currently signed-in user (or null). */ - readonly currentUser: User | null; - /** - * Asynchronously sets the provided user as {@link Auth.currentUser} on the {@link Auth} instance. - * - * @remarks - * A new instance copy of the user provided will be made and set as currentUser. - * - * This will trigger {@link Auth.onAuthStateChanged} and {@link Auth.onIdTokenChanged} listeners - * like other sign in methods. - * - * The operation fails with an error if the user to be updated belongs to a different Firebase - * project. - * - * @param user - The new {@link User}. - */ - updateCurrentUser(user: User | null): Promise; - /** - * Sets the current language to the default device/browser preference. - */ - useDeviceLanguage(): void; - /** - * Modify this Auth instance to communicate with the Firebase Auth emulator. - * - * @remarks - * This must be called synchronously immediately following the first call to - * {@link @firebase/auth#initializeAuth}. Do not use with production credentials as emulator - * traffic is not encrypted. - * - * @param url - The URL at which the emulator is running (eg, 'http://localhost:9099'). - * @param disableBanner - (Optional: default false) Disable the warning banner attached to the DOM - */ - useEmulator(url: string, options?: { disableWarnings: boolean }): void; - /** - * Signs out the current user. - */ - signOut(): Promise; -} - -/** - * An interface covering the possible persistence mechanism types. - * - * @public - */ -export interface Persistence { - /** - * Type of Persistence. - * - 'SESSION' is used for temporary persistence such as `sessionStorage`. - * - 'LOCAL' is used for long term persistence such as `localStorage` or `IndexedDB`. - * - 'NONE' is used for in-memory, or no persistence. - */ - readonly type: 'SESSION' | 'LOCAL' | 'NONE'; -} - -/** - * Interface representing ID token result obtained from {@link User.getIdTokenResult}. - * - * @remarks - * It contains the ID token JWT string and other helper properties for getting different data - * associated with the token as well as all the decoded payload claims. - * - * Note that these claims are not to be trusted as they are parsed client side. Only server side - * verification can guarantee the integrity of the token claims. - * - * @public - */ -export interface IdTokenResult { - /** - * The authentication time formatted as a UTC string. - * - * @remarks - * This is the time the user authenticated (signed in) and not the time the token was refreshed. - */ - authTime: string; - /** The ID token expiration time formatted as a UTC string. */ - expirationTime: string; - /** The ID token issuance time formatted as a UTC string. */ - issuedAtTime: string; - /** - * The sign-in provider through which the ID token was obtained (anonymous, custom, phone, - * password, etc). - * - * @remarks - * Note, this does not map to provider IDs. - */ - signInProvider: string | null; - /** - * The type of second factor associated with this session, provided the user was multi-factor - * authenticated (eg. phone, etc). - */ - signInSecondFactor: string | null; - /** The Firebase Auth ID token JWT string. */ - token: string; - /** - * The entire payload claims of the ID token including the standard reserved claims as well as - * the custom claims. - */ - claims: ParsedToken; -} - -/** - * A response from {@link @firebase/auth#checkActionCode}. - * - * @public - */ -export interface ActionCodeInfo { - /** - * The data associated with the action code. - * - * @remarks - * For the {@link Operation.PASSWORD_RESET}, {@link Operation.VERIFY_EMAIL}, and - * {@link Operation.RECOVER_EMAIL} actions, this object contains an email field with the address - * the email was sent to. - * - * For the {@link Operation.RECOVER_EMAIL} action, which allows a user to undo an email address - * change, this object also contains a `previousEmail` field with the user account's current - * email address. After the action completes, the user's email address will revert to the value - * in the `email` field from the value in `previousEmail` field. - * - * For the {@link Operation.VERIFY_AND_CHANGE_EMAIL} action, which allows a user to verify the - * email before updating it, this object contains a `previousEmail` field with the user account's - * email address before updating. After the action completes, the user's email address will be - * updated to the value in the `email` field from the value in `previousEmail` field. - * - * For the {@link Operation.REVERT_SECOND_FACTOR_ADDITION} action, which allows a user to - * unenroll a newly added second factor, this object contains a `multiFactorInfo` field with - * the information about the second factor. For phone second factor, the `multiFactorInfo` - * is a {@link MultiFactorInfo} object, which contains the phone number. - */ - data: { - email?: string | null; - multiFactorInfo?: MultiFactorInfo | null; - previousEmail?: string | null; - }; - /** - * The type of operation that generated the action code. - */ - operation: ActionCodeOperation; -} - -/** - * An enumeration of the possible email action types. - * - * @public - */ -export const enum ActionCodeOperation { - /** The email link sign-in action. */ - EMAIL_SIGNIN = 'EMAIL_SIGNIN', - /** The password reset action. */ - PASSWORD_RESET = 'PASSWORD_RESET', - /** The email revocation action. */ - RECOVER_EMAIL = 'RECOVER_EMAIL', - /** The revert second factor addition email action. */ - REVERT_SECOND_FACTOR_ADDITION = 'REVERT_SECOND_FACTOR_ADDITION', - /** The revert second factor addition email action. */ - VERIFY_AND_CHANGE_EMAIL = 'VERIFY_AND_CHANGE_EMAIL', - /** The email verification action. */ - VERIFY_EMAIL = 'VERIFY_EMAIL' -} - -/** - * An interface that defines the required continue/state URL with optional Android and iOS - * bundle identifiers. - * - * @public - */ -export interface ActionCodeSettings { - /** - * Sets the Android package name. - * - * @remarks - * This will try to open the link in an android app if it is - * installed. If `installApp` is passed, it specifies whether to install the Android app if the - * device supports it and the app is not already installed. If this field is provided without - * a `packageName`, an error is thrown explaining that the `packageName` must be provided in - * conjunction with this field. If `minimumVersion` is specified, and an older version of the - * app is installed, the user is taken to the Play Store to upgrade the app. - */ - android?: { - installApp?: boolean; - minimumVersion?: string; - packageName: string; - }; - /** - * When set to true, the action code link will be be sent as a Universal Link or Android App - * Link and will be opened by the app if installed. - * - * @remarks - * In the false case, the code will be sent to the web widget first and then on continue will - * redirect to the app if installed. - * - * @defaultValue false - */ - handleCodeInApp?: boolean; - /** - * Sets the iOS bundle ID. - * - * @remarks - * This will try to open the link in an iOS app if it is installed. - * - * App installation is not supported for iOS. - */ - iOS?: { - bundleId: string; - }; - /** - * Sets the link continue/state URL. - * - * @remarks - * This has different meanings in different contexts: - * - When the link is handled in the web action widgets, this is the deep link in the - * `continueUrl` query parameter. - * - When the link is handled in the app directly, this is the `continueUrl` query parameter in - * the deep link of the Dynamic Link. - */ - url: string; - /** - * When multiple custom dynamic link domains are defined for a project, specify which one to use - * when the link is to be opened via a specified mobile app (for example, `example.page.link`). - * - * - * @defaultValue The first domain is automatically selected. - */ - dynamicLinkDomain?: string; -} - -/** - * A utility class to parse email action URLs such as password reset, email verification, - * email link sign in, etc. - * - * @public - */ -export abstract class ActionCodeURL { - /** - * The API key of the email action link. - */ - readonly apiKey: string; - /** - * The action code of the email action link. - */ - readonly code: string; - /** - * The continue URL of the email action link. Null if not provided. - */ - readonly continueUrl: string | null; - /** - * The language code of the email action link. Null if not provided. - */ - readonly languageCode: string | null; - /** - * The action performed by the email action link. It returns from one of the types from - * {@link @firebase/auth-types#ActionCodeInfo} - */ - readonly operation: ActionCodeOperation; - /** - * The tenant ID of the email action link. Null if the email action is from the parent project. - */ - readonly tenantId: string | null; - - /** - * Parses the email action link string and returns an {@link ActionCodeURL} if the link is valid, - * otherwise returns null. - * - * @param link - The email action link string. - * @returns The ActionCodeURL object, or null if the link is invalid. - * - * @public - */ - static parseLink(link: string): ActionCodeURL | null; -} - -/** - * A verifier for domain verification and abuse prevention. - * - * @remarks - * Currently, the only implementation is {@link @firebase/auth#RecaptchaVerifier}. - * - * @public - */ -export interface ApplicationVerifier { - /** - * Identifies the type of application verifier (e.g. "recaptcha"). - */ - readonly type: string; - /** - * Executes the verification process. - * - * @returns A Promise for a token that can be used to assert the validity of a request. - */ - verify(): Promise; -} - -/** - * An {@link https://www.google.com/recaptcha/ | reCAPTCHA}-based application verifier. - * - * @public - */ -export abstract class RecaptchaVerifier implements ApplicationVerifier { - constructor( - /** - * The reCAPTCHA container parameter. - * - * @remarks - * This has different meaning depending on whether the reCAPTCHA is hidden or visible. For a - * visible reCAPTCHA the container must be empty. If a string is used, it has to correspond to - * an element ID. The corresponding element must also must be in the DOM at the time of - * initialization. - */ - container: any | string, - /** - * The optional reCAPTCHA parameters. - * - * @remarks - * Check the reCAPTCHA docs for a comprehensive list. All parameters are accepted except for - * the sitekey. Firebase Auth backend provisions a reCAPTCHA for each project and will - * configure this upon rendering. For an invisible reCAPTCHA, a size key must have the value - * 'invisible'. - */ - parameters?: Object | null, - /** - * The corresponding Firebase Auth instance. - * - * @remarks - * If none is provided, the default Firebase Auth instance is used. A Firebase Auth instance - * must be initialized with an API key, otherwise an error will be thrown. - */ - auth?: Auth | null - ); - /** - * Clears the reCAPTCHA widget from the page and destroys the instance. - */ - clear(): void; - /** - * Renders the reCAPTCHA widget on the page. - * - * @returns A Promise that resolves with the reCAPTCHA widget ID. - */ - render(): Promise; - /** - * The application verifier type. - * - * @remarks - * For a reCAPTCHA verifier, this is 'recaptcha'. - */ - readonly type: string; - /** - * Waits for the user to solve the reCAPTCHA and resolves with the reCAPTCHA token. - * - * @returns A Promise for the reCAPTCHA token. - */ - verify(): Promise; -} - -/** - * Interface that represents the credentials returned by an {@link @firebase/auth-types#AuthProvider}. - * - * @remarks - * Implementations specify the details about each auth provider's credential requirements. - * - * @public - */ -export abstract class AuthCredential { - /** - * Static method to deserialize a JSON representation of an object into an {@link @firebase/auth-types#AuthCredential}. - * - * @param json - Either `object` or the stringified representation of the object. When string is - * provided, `JSON.parse` would be called first. - * - * @returns If the JSON input does not represent an {@link @firebase/auth-types#AuthCredential}, null is returned. - */ - static fromJSON(json: object | string): AuthCredential | null; - - /** - * The authentication provider ID for the credential. - * - * @remarks - * For example, 'facebook.com', or 'google.com'. - */ - readonly providerId: string; - /** - * The authentication sign in method for the credential. - * - * @remarks - * For example, {@link @firebase/auth-types#SignInMethod.EMAIL_PASSWORD}, or - * {@link @firebase/auth-types#SignInMethod.EMAIL_LINK}. This corresponds to the sign-in method - * identifier as returned in {@link @firebase/auth#fetchSignInMethodsForEmail}. - */ - readonly signInMethod: string; - /** - * Returns a JSON-serializable representation of this object. - * - * @returns a JSON-serializable representation of this object. - */ - toJSON(): object; -} - -/** - * Interface that represents the OAuth credentials returned by an {@link @firebase/auth#OAuthProvider}. - * - * @remarks - * Implementations specify the details about each auth provider's credential requirements. - * - * @public - */ -export abstract class OAuthCredential extends AuthCredential { - /** - * Static method to deserialize a JSON representation of an object into an - * {@link @firebase/auth-types#AuthCredential}. - * - * @param json - Input can be either Object or the stringified representation of the object. - * When string is provided, JSON.parse would be called first. - * - * @returns If the JSON input does not represent an {@link @firebase/auth-types#AuthCredential}, null is returned. - */ - static fromJSON(json: object | string): OAuthCredential | null; - - /** - * The OAuth access token associated with the credential if it belongs to an - * {@link @firebase/auth#OAuthProvider}, such as `facebook.com`, `twitter.com`, etc. - */ - readonly accessToken?: string; - /** - * The OAuth ID token associated with the credential if it belongs to an OIDC provider, - * such as `google.com`. - */ - readonly idToken?: string; - /** - * The OAuth access token secret associated with the credential if it belongs to an OAuth 1.0 - * provider, such as `twitter.com`. - */ - readonly secret?: string; -} - -/** - * Interface that represents the credentials returned by a - * {@link @firebase/auth#PhoneAuthProvider}. - * - * @public - */ -export abstract class PhoneAuthCredential extends AuthCredential { - /** {@inheritdoc @firebase/auth-types#AuthCredential.fromJSON} */ - static fromJSON(json: object | string): PhoneAuthCredential | null; - /** {@inheritdoc @firebase/auth-types#AuthCredential.toJSON} */ - toJSON(): object; -} - -/** - * Interface that represents an auth provider, used to facilitate creating {@link @firebase/auth-types#AuthCredential}. - * - * @public - */ -export interface AuthProvider { - /** - * Provider for which credentials can be constructed. - */ - readonly providerId: string; -} - -/** - * Provider for generating {@link @firebase/auth#EmailAuthCredential}. - * - * @public - */ -export abstract class EmailAuthProvider implements AuthProvider { - private constructor(); - /** - * Always set to {@link @firebase/auth-types#ProviderId.PASSWORD}, even for email link. - */ - static readonly PROVIDER_ID: ProviderId; - /** - * Always set to {@link @firebase/auth-types#SignInMethod.EMAIL_PASSWORD}. - */ - static readonly EMAIL_PASSWORD_SIGN_IN_METHOD: SignInMethod; - /** - * Always set to {@link @firebase/auth-types#SignInMethod.EMAIL_LINK}. - */ - static readonly EMAIL_LINK_SIGN_IN_METHOD: SignInMethod; - /** - * Initialize an {@link @firebase/auth-types#AuthCredential} using an email and password. - * - * @example - * ```javascript - * const authCredential = EmailAuthProvider.credential(email, password); - * const userCredential = await signInWithCredential(auth, authCredential); - * ``` - * - * @example - * ```javascript - * const userCredential = await signInWithEmailAndPassword(auth, email, password); - * ``` - * - * @param email - Email address. - * @param password - User account password. - * @returns The auth provider credential. - */ - static credential(email: string, password: string): AuthCredential; - /** - * Initialize an {@link @firebase/auth-types#AuthCredential} using an email and an email link after a sign in with - * email link operation. - * - * @example - * ```javascript - * const authCredential = EmailAuthProvider.credentialWithLink(auth, email, emailLink); - * const userCredential = await signInWithCredential(auth, authCredential); - * ``` - * - * @example - * ```javascript - * await sendSignInLinkToEmail(auth, email); - * // Obtain emailLink from user. - * const userCredential = await signInWithEmailLink(auth, email, emailLink); - * ``` - * - * @param auth - The Auth instance used to verify the link. - * @param email - Email address. - * @param emailLink - Sign-in email link. - * @returns - The auth provider credential. - */ - static credentialWithLink( - auth: Auth, - email: string, - emailLink: string - ): AuthCredential; - /** - * Always set to {@link @firebase/auth-types#ProviderId.PASSWORD}, even for email link. - */ - readonly providerId: ProviderId; -} - -/** - * Provider for generating an {@link @firebase/auth#PhoneAuthCredential}. - * - * @example - * ```javascript - * // 'recaptcha-container' is the ID of an element in the DOM. - * const applicationVerifier = new RecaptchaVerifier('recaptcha-container'); - * const provider = new PhoneAuthProvider(auth); - * const verificationId = await provider.verifyPhoneNumber('+16505550101', applicationVerifier); - * // Obtain the verificationCode from the user. - * const phoneCredential = PhoneAuthProvider.credential(verificationId, verificationCode); - * const userCredential = await signInWithCredential(auth, phoneCredential); - * ``` - * - * @public - */ -export class PhoneAuthProvider implements AuthProvider { - /** Always set to {@link @firebase/auth-types#ProviderId.PHONE}. */ - static readonly PROVIDER_ID: ProviderId; - /** Always set to {@link @firebase/auth-types#SignInMethod.PHONE}. */ - static readonly PHONE_SIGN_IN_METHOD: SignInMethod; - /** - * Creates a phone auth credential, given the verification ID from - * {@link @firebase/auth#PhoneAuthProvider.verifyPhoneNumber} and the code that was sent to the user's - * mobile device. - * - * @example - * ```javascript - * const provider = new PhoneAuthProvider(auth); - * const verificationId = provider.verifyPhoneNumber(phoneNumber, applicationVerifier); - * // Obtain verificationCode from the user. - * const authCredential = PhoneAuthProvider.credential(verificationId, verificationCode); - * const userCredential = signInWithCredential(auth, authCredential); - * ``` - * - * @example - * An alternative flow is provided using the `signInWithPhoneNumber` method. - * ```javascript - * const confirmationResult = await signInWithPhoneNumber(auth, phoneNumber, applicationVerifier); - * // Obtain verificationCode from the user. - * const userCredential = await confirmationResult.confirm(verificationCode); - * ``` - * - * @param verificationId - The verification ID returned from {@link @firebase/auth#PhoneAuthProvider.verifyPhoneNumber}. - * @param verificationCode - The verification code sent to the user's mobile device. - * - * @returns The auth provider credential. - */ - static credential( - verificationId: string, - verificationCode: string - ): AuthCredential; - /** - * @param auth - The Firebase Auth instance in which sign-ins should occur. - * - * @remarks - * Uses the default Auth instance if unspecified. - */ - constructor(auth?: Auth | null); - /** Always set to {@link @firebase/auth-types#ProviderId.PHONE}. */ - readonly providerId: ProviderId; - - /** - * - * Starts a phone number authentication flow by sending a verification code to the given phone - * number. - * - * @example - * ```javascript - * const provider = new PhoneAuthProvider(auth); - * const verificationId = await provider.verifyPhoneNumber(phoneNumber, applicationVerifier); - * // Obtain verificationCode from the user. - * const authCredential = PhoneAuthProvider.credential(verificationId, verificationCode); - * const userCredential = await signInWithCredential(auth, authCredential); - * ``` - * - * @example - * An alternative flow is provided using the `signInWithPhoneNumber` method. - * ```javascript - * const confirmationResult = signInWithPhoneNumber(auth, phoneNumber, applicationVerifier); - * // Obtain verificationCode from the user. - * const userCredential = confirmationResult.confirm(verificationCode); - * ``` - * - * @param phoneInfoOptions - The user's {@link @firebase/auth-types#PhoneInfoOptions}. The phone number should be in - * E.164 format (e.g. +16505550101). - * @param applicationVerifier - For abuse prevention, this method also requires a - * {@link @firebase/auth-types#ApplicationVerifier}. This SDK includes a reCAPTCHA-based implementation, - * {@link RecaptchaVerifier}. - * - * @returns A Promise for a verification ID that can be passed to - * {@link @firebase/auth#PhoneAuthProvider.credential} to identify this flow.. - */ - verifyPhoneNumber( - phoneInfoOptions: PhoneInfoOptions | string, - applicationVerifier: ApplicationVerifier - ): Promise; -} - -/** - * An enum of factors that may be used for multifactor authentication. - * - * @public - */ -export const enum FactorId { - /** Phone as second factor */ - PHONE = 'phone' -} - -/** - * A result from a phone number sign-in, link, or reauthenticate call. - * - * @public - */ -export interface ConfirmationResult { - /** - * The phone number authentication operation's verification ID. - * - * @remarks - * This can be used along with the verification code to initialize a - * {@link @firebase/auth-types#PhoneAuthCredential}. - */ - readonly verificationId: string; - /** - * Finishes a phone number sign-in, link, or reauthentication. - * - * @example - * ```javascript - * const confirmationResult = await signInWithPhoneNumber(auth, phoneNumber, applicationVerifier); - * // Obtain verificationCode from the user. - * const userCredential = await confirmationResult.confirm(verificationCode); - * ``` - * - * @param verificationCode - The code that was sent to the user's mobile device. - */ - confirm(verificationCode: string): Promise; -} - -/** - * The base class for asserting ownership of a second factor. - * - * @remarks - * This is used to facilitate enrollment of a second factor on an existing user or sign-in of a - * user who already verified the first factor. - * - * @public - */ -export interface MultiFactorAssertion { - /** The identifier of the second factor. */ - readonly factorId: FactorId; -} - -/** - * The error thrown when the user needs to provide a second factor to sign in successfully. - * - * @remarks - * The error code for this error is `auth/multi-factor-auth-required`. - * - * @example - * ```javascript - * let resolver; - * let multiFactorHints; - * - * signInWithEmailAndPassword(auth, email, password) - * .then((result) => { - * // User signed in. No 2nd factor challenge is needed. - * }) - * .catch((error) => { - * if (error.code == 'auth/multi-factor-auth-required') { - * resolver = getMultiFactorResolver(auth, error); - * multiFactorHints = resolver.hints; - * } else { - * // Handle other errors. - * } - * }); - * - * // Obtain a multiFactorAssertion by verifying the second factor. - * - * const userCredential = await resolver.resolveSignIn(multiFactorAssertion); - * ``` - * - * @public - */ -export interface MultiFactorError extends AuthError { - /** - * The type of operation (e.g., sign-in, link, or reauthenticate) during which the error was raised. - */ - readonly operationType: OperationType; -} - -/** - * A structure containing the information of a second factor entity. - * - * @public - */ -export interface MultiFactorInfo { - /** The multi-factor enrollment ID. */ - readonly uid: string; - /** The user friendly name of the current second factor. */ - readonly displayName?: string | null; - /** The enrollment date of the second factor formatted as a UTC string. */ - readonly enrollmentTime: string; - /** The identifier of the second factor. */ - readonly factorId: FactorId; -} - -/** - * The class used to facilitate recovery from {@link @firebase/auth-types#MultiFactorError} when a user needs to - * provide a second factor to sign in. - * - * @example - * ```javascript - * let resolver; - * let multiFactorHints; - * - * signInWithEmailAndPassword(auth, email, password) - * .then((result) => { - * // User signed in. No 2nd factor challenge is needed. - * }) - * .catch((error) => { - * if (error.code == 'auth/multi-factor-auth-required') { - * resolver = getMultiFactorResolver(auth, error); - * // Show UI to let user select second factor. - * multiFactorHints = resolver.hints; - * } else { - * // Handle other errors. - * } - * }); - * - * // The enrolled second factors that can be used to complete - * // sign-in are returned in the `MultiFactorResolver.hints` list. - * // UI needs to be presented to allow the user to select a second factor - * // from that list. - * - * const selectedHint = // ; selected from multiFactorHints - * const phoneAuthProvider = new PhoneAuthProvider(auth); - * const phoneInfoOptions = { - * multiFactorHint: selectedHint, - * session: resolver.session - * }; - * const verificationId = phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, appVerifier); - * // Store `verificationId` and show UI to let user enter verification code. - * - * // UI to enter verification code and continue. - * // Continue button click handler - * const phoneAuthCredential = PhoneAuthProvider.credential(verificationId, verificationCode); - * const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(phoneAuthCredential); - * const userCredential = await resolver.resolveSignIn(multiFactorAssertion); - * ``` - * - * @public - */ -export abstract class MultiFactorResolver { - /** - * The list of hints for the second factors needed to complete the sign-in for the current - * session. - */ - readonly hints: MultiFactorInfo[]; - /** - * The session identifier for the current sign-in flow, which can be used to complete the second - * factor sign-in. - */ - readonly session: MultiFactorSession; - /** - * A helper function to help users complete sign in with a second factor using an - * {@link @firebase/auth-types#MultiFactorAssertion} confirming the user successfully completed the second factor - * challenge. - * - * @example - * ```javascript - * const phoneAuthCredential = PhoneAuthProvider.credential(verificationId, verificationCode); - * const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(phoneAuthCredential); - * const userCredential = await resolver.resolveSignIn(multiFactorAssertion); - * ``` - * - * @param assertion - The multi-factor assertion to resolve sign-in with. - * @returns The promise that resolves with the user credential object. - */ - resolveSignIn(assertion: MultiFactorAssertion): Promise; -} - -/** - * An interface defining the multi-factor session object used for enrolling a second factor on a - * user or helping sign in an enrolled user with a second factor. - * - * @public - */ -export interface MultiFactorSession {} - -/** - * An interface that defines the multi-factor related properties and operations pertaining - * to a {@link User}. - * - * @public - */ -export interface MultiFactorUser { - /** Returns a list of the user's enrolled second factors. */ - readonly enrolledFactors: MultiFactorInfo[]; - /** - * Returns the session identifier for a second factor enrollment operation. This is used to - * identify the user trying to enroll a second factor. - * - * @example - * ```javascript - * const multiFactorUser = multiFactor(auth.currentUser); - * const multiFactorSession = await multiFactorUser.getSession(); - * - * // Send verification code. - * const phoneAuthProvider = new PhoneAuthProvider(auth); - * const phoneInfoOptions = { - * phoneNumber: phoneNumber, - * session: multiFactorSession - * }; - * const verificationId = await phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, appVerifier); - * - * // Obtain verification code from user. - * const phoneAuthCredential = PhoneAuthProvider.credential(verificationId, verificationCode); - * const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(phoneAuthCredential); - * await multiFactorUser.enroll(multiFactorAssertion); - * ``` - * - * @returns The promise that resolves with the {@link @firebase/auth-types#MultiFactorSession}. - */ - getSession(): Promise; - /** - * - * Enrolls a second factor as identified by the {@link @firebase/auth-types#MultiFactorAssertion} for the - * user. - * - * @remarks - * On resolution, the user tokens are updated to reflect the change in the JWT payload. - * Accepts an additional display name parameter used to identify the second factor to the end - * user. Recent re-authentication is required for this operation to succeed. On successful - * enrollment, existing Firebase sessions (refresh tokens) are revoked. When a new factor is - * enrolled, an email notification is sent to the user’s email. - * - * @example - * ```javascript - * const multiFactorUser = multiFactor(auth.currentUser); - * const multiFactorSession = await multiFactorUser.getSession(); - * - * // Send verification code. - * const phoneAuthProvider = new PhoneAuthProvider(auth); - * const phoneInfoOptions = { - * phoneNumber: phoneNumber, - * session: multiFactorSession - * }; - * const verificationId = await phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, appVerifier); - * - * // Obtain verification code from user. - * const phoneAuthCredential = PhoneAuthProvider.credential(verificationId, verificationCode); - * const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(phoneAuthCredential); - * await multiFactorUser.enroll(multiFactorAssertion); - * // Second factor enrolled. - * ``` - * - * @param assertion - The multi-factor assertion to enroll with. - * @param displayName - The display name of the second factor. - */ - enroll( - assertion: MultiFactorAssertion, - displayName?: string | null - ): Promise; - /** - * Unenrolls the specified second factor. - * - * @remarks - * To specify the factor to remove, pass a {@link MultiFactorInfo} object (retrieved from - * {@link MultiFactorUser.enrolledFactors}) or the - * factor's UID string. Sessions are not revoked when the account is unenrolled. An email - * notification is likely to be sent to the user notifying them of the change. Recent - * re-authentication is required for this operation to succeed. When an existing factor is - * unenrolled, an email notification is sent to the user’s email. - * - * @example - * ```javascript - * const multiFactorUser = multiFactor(auth.currentUser); - * // Present user the option to choose which factor to unenroll. - * await multiFactorUser.unenroll(multiFactorUser.enrolledFactors[i]) - * ``` - * - * @param option - The multi-factor option to unenroll. - * @returns - A promise which resolves when the unenroll operation is complete. - */ - unenroll(option: MultiFactorInfo | string): Promise; -} - -/** - * The class for asserting ownership of a phone second factor. Provided by - * {@link @firebase/auth-types#PhoneMultiFactorGenerator.assertion}. - * - * @public - */ -export interface PhoneMultiFactorAssertion extends MultiFactorAssertion {} - -/** - * Provider for generating a {@link @firebase/auth-types#PhoneMultiFactorAssertion}. - * - * @public - */ -export abstract class PhoneMultiFactorGenerator { - /** - * The identifier of the phone second factor: {@link @firebase/auth-types#ProviderId.PHONE}. - */ - static FACTOR_ID: ProviderId; - /** - * Provides a {@link @firebase/auth-types#PhoneMultiFactorAssertion} to confirm ownership of the phone second factor. - * - * @param phoneAuthCredential - A credential provided by {@link @firebase/auth#PhoneAuthProvider.credential}. - * @returns A {@link @firebase/auth-types#PhoneMultiFactorAssertion} which can be used with - * {@link @firebase/auth-types#MultiFactorResolver.resolveSignIn} - */ - static assertion( - phoneAuthCredential: PhoneAuthCredential - ): PhoneMultiFactorAssertion; -} - -/** - * The information required to verify the ownership of a phone number. - * - * @remarks - * The information that's required depends on whether you are doing single-factor sign-in, - * multi-factor enrollment or multi-factor sign-in. - * - * @public - */ -export type PhoneInfoOptions = - | PhoneSingleFactorInfoOptions - | PhoneMultiFactorEnrollInfoOptions - | PhoneMultiFactorSignInInfoOptions; - -/** - * Options used for single-factor sign-in. - * - * @public - */ -export interface PhoneSingleFactorInfoOptions { - /** Phone number to send a verification code to. */ - phoneNumber: string; -} - -/** - * Options used for enrolling a second factor. - * - * @public - */ -export interface PhoneMultiFactorEnrollInfoOptions { - /** Phone number to send a verification code to. */ - phoneNumber: string; - /** The {@link MultiFactorSession} obtained via {@link MultiFactorUser.getSession}. */ - session: MultiFactorSession; -} -/** - * Options used for signing-in with a second factor. - * - * @public - */ -export interface PhoneMultiFactorSignInInfoOptions { - /** - * The {@link MultiFactorInfo} obtained via {@link MultiFactorResolver.hints}. - * - * One of `multiFactorHint` or `multiFactorUid` is required. - */ - multiFactorHint?: MultiFactorInfo; - /** - * The uid of the second factor. - * - * One of `multiFactorHint` or `multiFactorUid` is required. - */ - multiFactorUid?: string; - /** The {@link MultiFactorSession} obtained via {@link MultiFactorResolver.session}. */ - session: MultiFactorSession; -} - -/** - * Interface for a supplied AsyncStorage. - * - * @public - */ -export interface ReactNativeAsyncStorage { - /** - * Persist an item in storage. - * - * @param key - storage key. - * @param value - storage value. - */ - setItem(key: string, value: string): Promise; - /** - * Retrieve an item from storage. - * - * @param key - storage key. - */ - getItem(key: string): Promise; - /** - * Remove an item from storage. - * - * @param key - storage key. - */ - removeItem(key: string): Promise; -} - -/** - * A user account. - * - * @public - */ -export interface User extends UserInfo { - /** - * Whether the email has been verified with {@link @firebase/auth#sendEmailVerification} and - * {@link @firebase/auth#applyActionCode}. - */ - readonly emailVerified: boolean; - /** - * Whether the user is authenticated using the {@link @firebase/auth-types@ProviderId.ANONYMOUS} provider. - */ - readonly isAnonymous: boolean; - /** - * Additional metadata around user creation and sign-in times. - */ - readonly metadata: UserMetadata; - /** - * Additional per provider such as displayName and profile information. - */ - readonly providerData: UserInfo[]; - /** - * Refresh token used to reauthenticate the user. Avoid using this directly and prefer - * {@link User.getIdToken} to refresh the ID token instead. - */ - readonly refreshToken: string; - /** - * The user's tenant ID. - * - * @remarks - * This is a read-only property, which indicates the tenant ID - * used to sign in the user. This is null if the user is signed in from the parent - * project. - * - * @example - * ```javascript - * // Set the tenant ID on Auth instance. - * auth.tenantId = 'TENANT_PROJECT_ID'; - * - * // All future sign-in request now include tenant ID. - * const result = await signInWithEmailAndPassword(auth, email, password); - * // result.user.tenantId should be 'TENANT_PROJECT_ID'. - * ``` - */ - readonly tenantId: string | null; - /** - * Deletes and signs out the user. - * - * @remarks - * Important: this is a security-sensitive operation that requires the user to have recently - * signed in. If this requirement isn't met, ask the user to authenticate again and then call - * one of the reauthentication methods like {@link @firebase/auth#reauthenticateWithCredential}. - */ - delete(): Promise; - /** - * Returns a JSON Web Token (JWT) used to identify the user to a Firebase service. - * - * @remarks - * Returns the current token if it has not expired or if it will not expire in the next five - * minutes. Otherwise, this will refresh the token and return a new one. - * - * @param forceRefresh - Force refresh regardless of token expiration. - */ - getIdToken(forceRefresh?: boolean): Promise; - /** - * Returns a deserialized JSON Web Token (JWT) used to identitfy the user to a Firebase service. - * - * @remarks - * Returns the current token if it has not expired or if it will not expire in the next five - * minutes. Otherwise, this will refresh the token and return a new one. - * - * @param forceRefresh - Force refresh regardless of token expiration. - */ - getIdTokenResult(forceRefresh?: boolean): Promise; - /** - * Refreshes the user, if signed in. - */ - reload(): Promise; - /** - * Returns a JSON-serializable representation of this object. - * - * @returns A JSON-serializable representation of this object. - */ - toJSON(): object; -} - -/** - * A structure containing a {@link User}, an {@link AuthCredential}, the {@link OperationType}, - * and any additional user information that was returned from the identity provider. - * - * @remarks - * `operationType` could be {@link OperationType.SIGN_IN} for a sign-in operation, - * {@link OperationType.LINK} for a linking operation and {@link OperationType.REAUTHENTICATE} for - * a reauthentication operation. - * - * @public - */ -export interface UserCredential { - /** - * The user authenticated by this credential. - */ - user: User; - /** - * The provider which was used to authenticate the user. - */ - providerId: string | null; - /** - * The type of operation which was used to authenticate the user (such as sign-in or link). - */ - operationType: OperationType; -} - -/** - * User profile information, visible only to the Firebase project's apps. - * - * @public - */ -export interface UserInfo { - /** - * The display name of the user. - */ - readonly displayName: string | null; - /** - * The email of the user. - */ - readonly email: string | null; - /** - * The phone number normalized based on the E.164 standard (e.g. +16505550101) for the - * user. - * - * @remarks - * This is null if the user has no phone credential linked to the account. - */ - readonly phoneNumber: string | null; - /** - * The profile photo URL of the user. - */ - readonly photoURL: string | null; - /** - * The provider used to authenticate the user. - */ - readonly providerId: string; - /** - * The user's unique ID, scoped to the project. - */ - readonly uid: string; -} - -/** - * Interface representing a user's metadata. - * - * @public - */ -export interface UserMetadata { - /** Time the user was created. */ - readonly creationTime?: string; - /** Time the user last signed in. */ - readonly lastSignInTime?: string; -} - -/** - * A structure containing additional user information from a federated identity provider. - * - * @public - */ -export interface AdditionalUserInfo { - /** - * Whether the user is new (created via sign-up) or existing (authenticated using sign-in). - */ - readonly isNewUser: boolean; - /** - * Map containing IDP-specific user data. - */ - readonly profile: Record | null; - /** - * Identifier for the provider used to authenticate this user. - */ - readonly providerId: string | null; - /** - * The username if the provider is GitHub or Twitter. - */ - readonly username?: string | null; -} - -/** - * User profile used in {@link AdditionalUserInfo}. - * - * @public - */ -export type UserProfile = Record; - -/** - * A resolver used for handling DOM specific operations like {@link @firebase/auth#signInWithPopup} - * or {@link @firebase/auth#signInWithRedirect}. - * - * @public - */ -export interface PopupRedirectResolver {} - -declare module '@firebase/component' { - interface NameServiceMapping { - 'auth-exp': Auth; - } -} - -/** - * A mapping of error codes to error messages. - * - * @discussion - * - * While error messages are useful for debugging (providing verbose textual - * context around what went wrong), these strings take up a lot of space in the - * compiled code. When deploying code in production, using {@link prodErrorMap} - * will save you roughly 10k compressed/gzipped over {@link debugErrorMap}. You - * can select the error map during initialization: - * - * ```javascript - * initializeAuth(app, {errorMap: debugErrorMap}) - * ``` - * - * When initializing Auth, {@link prodErrorMap} is default. - * - * @public - */ -export interface AuthErrorMap {} diff --git a/packages-exp/auth-types-exp/package.json b/packages-exp/auth-types-exp/package.json deleted file mode 100644 index ae035c14da0..00000000000 --- a/packages-exp/auth-types-exp/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@firebase/auth-types-exp", - "private": true, - "version": "0.0.900", - "description": "@firebase/auth-exp Types", - "author": "Firebase (https://firebase.google.com/)", - "license": "Apache-2.0", - "scripts": { - "test": "tsc", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn api-report && yarn doc" - }, - "files": [ - "index.d.ts" - ], - "repository": { - "directory": "packages-exp/auth-types-exp", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "devDependencies": { - "typescript": "4.0.5" - } -} diff --git a/packages-exp/auth-types-exp/tsconfig.json b/packages-exp/auth-types-exp/tsconfig.json deleted file mode 100644 index 9a785433d90..00000000000 --- a/packages-exp/auth-types-exp/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "noEmit": true - }, - "exclude": [ - "dist/**/*" - ] -} diff --git a/packages-exp/firebase-exp/.gitignore b/packages-exp/firebase-exp/.gitignore deleted file mode 100644 index 228443ec909..00000000000 --- a/packages-exp/firebase-exp/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/firebase*.js -/firebase*.map -/firebase*.gz -/firebase*.tgz \ No newline at end of file diff --git a/packages-exp/firebase-exp/README.md b/packages-exp/firebase-exp/README.md deleted file mode 100644 index 0569dc7ea39..00000000000 --- a/packages-exp/firebase-exp/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Firebase - App success made simple - -## Overview - -TODO \ No newline at end of file diff --git a/packages-exp/firebase-exp/app/index.cdn.ts b/packages-exp/firebase-exp/app/index.cdn.ts deleted file mode 100644 index 008e95af754..00000000000 --- a/packages-exp/firebase-exp/app/index.cdn.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { registerVersion } from '@firebase/app-exp'; -import { name, version } from '../package.json'; - -registerVersion(name, version, 'cdn'); -export * from '@firebase/app-exp'; diff --git a/packages-exp/firebase-exp/app/index.ts b/packages-exp/firebase-exp/app/index.ts deleted file mode 100644 index 0ecb5e28753..00000000000 --- a/packages-exp/firebase-exp/app/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { registerVersion } from '@firebase/app-exp'; -import { name, version } from '../package.json'; - -registerVersion(name, version, 'app'); -export * from '@firebase/app-exp'; diff --git a/packages-exp/firebase-exp/app/package.json b/packages-exp/firebase-exp/app/package.json deleted file mode 100644 index 318577de23d..00000000000 --- a/packages-exp/firebase-exp/app/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "firebase-exp/app", - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "typings": "dist/app/index.d.ts" -} diff --git a/packages-exp/firebase-exp/auth/index.ts b/packages-exp/firebase-exp/auth/index.ts deleted file mode 100644 index 2505d8e8794..00000000000 --- a/packages-exp/firebase-exp/auth/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export * from '@firebase/auth-exp'; diff --git a/packages-exp/firebase-exp/auth/package.json b/packages-exp/firebase-exp/auth/package.json deleted file mode 100644 index a3874cfe19b..00000000000 --- a/packages-exp/firebase-exp/auth/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "firebase-exp/auth", - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "typings": "dist/auth/index.d.ts" -} diff --git a/packages-exp/firebase-exp/compat/app/package.json b/packages-exp/firebase-exp/compat/app/package.json deleted file mode 100644 index b4e10f5a47c..00000000000 --- a/packages-exp/firebase-exp/compat/app/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "firebase-exp/compat/app", - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "typings": "dist/compat/app/index.d.ts" - } - \ No newline at end of file diff --git a/packages-exp/firebase-exp/compat/index.cdn.ts b/packages-exp/firebase-exp/compat/index.cdn.ts deleted file mode 100644 index 387883d1afd..00000000000 --- a/packages-exp/firebase-exp/compat/index.cdn.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -console.warn(` -It looks like you're using the development build of the Firebase JS SDK. -When deploying Firebase apps to production, it is advisable to only import -the individual SDK components you intend to use. - -For the CDN builds, these are available in the following manner -(replace with the name of a component - i.e. auth, database, etc): - -https://www.gstatic.com/firebasejs/5.0.0/firebase-.js -`); - -import '@firebase/polyfill'; -import firebase from './app'; -import { name, version } from '../package.json'; - -// import './auth'; -// import './database'; -// import './firestore'; -// import './functions'; -// import './messaging'; -// import './storage'; -// import './performance'; -// import './analytics'; -// import './remote-config'; - -firebase.registerVersion(name, version, 'compat-cdn'); - -export default firebase; diff --git a/packages-exp/firebase-exp/compat/index.node.ts b/packages-exp/firebase-exp/compat/index.node.ts deleted file mode 100644 index ad532d78a4a..00000000000 --- a/packages-exp/firebase-exp/compat/index.node.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import firebase from './app'; -import { name, version } from '../package.json'; - -// import './auth'; -// import './database'; -// import './firestore'; -// import './functions'; - -firebase.registerVersion(name, version, 'compat-node'); - -export default firebase; diff --git a/packages-exp/firebase-exp/compat/index.perf.ts b/packages-exp/firebase-exp/compat/index.perf.ts deleted file mode 100644 index e2efa3bfdd0..00000000000 --- a/packages-exp/firebase-exp/compat/index.perf.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import firebase from './app'; -// import './performance'; -import { name, version } from '../package.json'; - -firebase.registerVersion(name, version, 'compat-lite'); - -export default firebase; diff --git a/packages-exp/firebase-exp/compat/index.rn.ts b/packages-exp/firebase-exp/compat/index.rn.ts deleted file mode 100644 index 0bfc80dd3ce..00000000000 --- a/packages-exp/firebase-exp/compat/index.rn.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import firebase from './app'; -import { name, version } from '../package.json'; - -// import './auth'; -// import './database'; -// // TODO(b/158625454): Storage doesn't actually work by default in RN (it uses -// // `atob`). We should provide a RN build that works out of the box. -// import './storage'; -// import './firestore'; - -firebase.registerVersion(name, version, 'compat-rn'); - -export default firebase; diff --git a/packages-exp/firebase-exp/compat/index.ts b/packages-exp/firebase-exp/compat/index.ts deleted file mode 100644 index 074d7223507..00000000000 --- a/packages-exp/firebase-exp/compat/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -console.warn(` -It looks like you're using the development build of the Firebase JS SDK. -When deploying Firebase apps to production, it is advisable to only import -the individual SDK components you intend to use. - -For the module builds, these are available in the following manner -(replace with the name of a component - i.e. auth, database, etc): - -CommonJS Modules: -const firebase = require('firebase/app'); -require('firebase/'); - -ES Modules: -import firebase from 'firebase/app'; -import 'firebase/'; - -Typescript: -import firebase from 'firebase/app'; -import 'firebase/'; -`); - -import firebase from './app'; -import { name, version } from '../package.json'; - -// import './auth'; -// import './database'; -// import './firestore'; -// import './functions'; -// import './messaging'; -// import './storage'; -// import './performance'; -// import './analytics'; -// import './remote-config'; - -firebase.registerVersion(name, version, 'compat'); - -export default firebase; diff --git a/packages-exp/firebase-exp/compat/package.json b/packages-exp/firebase-exp/compat/package.json deleted file mode 100644 index ba13f52a70f..00000000000 --- a/packages-exp/firebase-exp/compat/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "firebase-exp/compat", - "main": "dist/index.node.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "react-native": "dist/index.rn.cjs.js", - "typings": "index.d.ts", - "components": [ - "app" - ] -} \ No newline at end of file diff --git a/packages-exp/firebase-exp/compat/rollup.config.js b/packages-exp/firebase-exp/compat/rollup.config.js deleted file mode 100644 index 8c01a151955..00000000000 --- a/packages-exp/firebase-exp/compat/rollup.config.js +++ /dev/null @@ -1,264 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { resolve } from 'path'; -import resolveModule from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import sourcemaps from 'rollup-plugin-sourcemaps'; -import rollupTypescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import { uglify } from 'rollup-plugin-uglify'; -import { terser } from 'rollup-plugin-terser'; -import json from '@rollup/plugin-json'; -import pkg from '../package.json'; -import compatPkg from './package.json'; -import appPkg from './app/package.json'; - -const external = Object.keys(pkg.dependencies || {}); - -/** - * Global UMD Build - */ -const GLOBAL_NAME = 'firebase'; - -function createUmdOutputConfig(output) { - return { - file: output, - format: 'umd', - sourcemap: true, - extend: true, - name: GLOBAL_NAME, - globals: { - '@firebase/app-compat': GLOBAL_NAME - }, - - /** - * use iife to avoid below error in the old Safari browser - * SyntaxError: Functions cannot be declared in a nested block in strict mode - * https://github.com/firebase/firebase-js-sdk/issues/1228 - * - */ - intro: ` - try { - (function() {`, - outro: ` - }).apply(this, arguments); - } catch(err) { - console.error(err); - throw new Error( - 'Cannot instantiate ${output} - ' + - 'be sure to load firebase-app.js first.' - ); - }` - }; -} - -const plugins = [sourcemaps(), resolveModule(), json(), commonjs()]; - -const typescriptPlugin = rollupTypescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - declaration: false - } - } -}); - -/** - * Individual Component Builds - */ -const appBuilds = [ - /** - * App Browser Builds - */ - { - input: `${__dirname}/app/index.ts`, - output: [ - { - file: resolve(__dirname, 'app', appPkg.main), - format: 'cjs', - sourcemap: true - }, - { - file: resolve(__dirname, 'app', appPkg.module), - format: 'es', - sourcemap: true - } - ], - plugins: [...plugins, typescriptPlugin], - external - }, - /** - * App UMD Builds - */ - { - input: `${__dirname}/app/index.cdn.ts`, - output: { - file: 'firebase-app-compat.js', - sourcemap: true, - format: 'umd', - name: GLOBAL_NAME - }, - plugins: [...plugins, typescriptPlugin, uglify()] - } -]; - -const componentBuilds = compatPkg.components - // The "app" component is treated differently because it doesn't depend on itself. - .filter(component => component !== 'app') - .map(component => { - const pkg = require(`${__dirname}/${component}/package.json`); - return [ - { - input: `${__dirname}/${component}/index.ts`, - output: [ - { - file: resolve(__dirname, component, pkg.main), - format: 'cjs', - sourcemap: true - }, - { - file: resolve(__dirname, component, pkg.module), - format: 'es', - sourcemap: true - } - ], - plugins: [...plugins, typescriptPlugin], - external - }, - { - input: `${__dirname}/${component}/index.ts`, - output: createUmdOutputConfig(`firebase-${component}-compat.js`), - plugins: [...plugins, typescriptPlugin, uglify()], - external: ['@firebase/app-compat'] - } - ]; - }) - .reduce((a, b) => a.concat(b), []); - -/** - * Complete Package Builds - */ -const completeBuilds = [ - /** - * App Browser Builds - */ - { - input: `${__dirname}/index.ts`, - output: [ - { - file: resolve(__dirname, compatPkg.module), - format: 'es', - sourcemap: true - } - ], - plugins: [...plugins, typescriptPlugin], - external - }, - { - input: `${__dirname}/index.cdn.ts`, - output: { - file: 'firebase-compat.js', - format: 'umd', - sourcemap: true, - name: GLOBAL_NAME - }, - plugins: [...plugins, typescriptPlugin, uglify()] - }, - /** - * App Node.js Builds - */ - { - input: `${__dirname}/index.node.ts`, - output: { - file: resolve(__dirname, compatPkg.main), - format: 'cjs', - sourcemap: true - }, - plugins: [...plugins, typescriptPlugin], - external - }, - /** - * App React Native Builds - */ - { - input: `${__dirname}/index.rn.ts`, - output: { - file: resolve(__dirname, compatPkg['react-native']), - format: 'cjs', - sourcemap: true - }, - plugins: [...plugins, typescriptPlugin], - external - }, - /** - * Performance script Build - */ - { - input: `${__dirname}/index.perf.ts`, - output: { - file: 'firebase-performance-standalone-compat.js', - format: 'umd', - sourcemap: true, - name: GLOBAL_NAME - }, - plugins: [ - sourcemaps(), - resolveModule({ - mainFields: ['lite', 'module', 'main'] - }), - typescriptPlugin, - json(), - commonjs(), - uglify() - ] - }, - /** - * Performance script Build in ES2017 - */ - { - input: `${__dirname}/index.perf.ts`, - output: { - file: 'firebase-performance-standalone-compat.es2017.js', - format: 'umd', - sourcemap: true, - name: GLOBAL_NAME - }, - plugins: [ - sourcemaps(), - resolveModule({ - mainFields: ['lite-esm2017', 'esm2017', 'module', 'main'] - }), - rollupTypescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017', - declaration: false - } - } - }), - json({ - preferConst: true - }), - commonjs(), - terser() - ] - } -]; - -export default [...appBuilds, ...componentBuilds, ...completeBuilds]; diff --git a/packages-exp/firebase-exp/compat/rollup.config.release.js b/packages-exp/firebase-exp/compat/rollup.config.release.js deleted file mode 100644 index c9d5204647a..00000000000 --- a/packages-exp/firebase-exp/compat/rollup.config.release.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { resolve } from 'path'; -import sourcemaps from 'rollup-plugin-sourcemaps'; -import rollupTypescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import json from '@rollup/plugin-json'; -import pkg from '../package.json'; -import compatPkg from './package.json'; -import appPkg from './app/package.json'; - -const external = Object.keys(pkg.dependencies || {}); - -const plugins = [sourcemaps(), json()]; - -const typescriptPlugin = rollupTypescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - declaration: false - } - } -}); - -/** - * Individual Component Builds - */ -const appBuilds = [ - /** - * App Browser Builds - */ - { - input: `${__dirname}/app/index.ts`, - output: [ - { - file: resolve(__dirname, 'app', appPkg.main), - format: 'cjs', - sourcemap: true - }, - { - file: resolve(__dirname, 'app', appPkg.module), - format: 'es', - sourcemap: true - } - ], - plugins: [...plugins, typescriptPlugin], - external - } -]; - -const componentBuilds = compatPkg.components - // The "app" component is treated differently because it doesn't depend on itself. - .filter(component => component !== 'app') - .map(component => { - const pkg = require(`${__dirname}/${component}/package.json`); - return [ - { - input: `${__dirname}/${component}/index.ts`, - output: [ - { - file: resolve(__dirname, component, pkg.main), - format: 'cjs', - sourcemap: true - }, - { - file: resolve(__dirname, component, pkg.module), - format: 'es', - sourcemap: true - } - ], - plugins: [...plugins, typescriptPlugin], - external - } - ]; - }) - .reduce((a, b) => a.concat(b), []); - -export default [...appBuilds, ...componentBuilds]; diff --git a/packages-exp/firebase-exp/firestore/index.ts b/packages-exp/firebase-exp/firestore/index.ts deleted file mode 100644 index 5d721c4e3b3..00000000000 --- a/packages-exp/firebase-exp/firestore/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from '@firebase/firestore'; diff --git a/packages-exp/firebase-exp/firestore/lite/package.json b/packages-exp/firebase-exp/firestore/lite/package.json deleted file mode 100644 index d2ef7841699..00000000000 --- a/packages-exp/firebase-exp/firestore/lite/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "firebase-exp/firestore/lite", - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "typings": "dist/firestore/lite/index.d.ts" -} diff --git a/packages-exp/firebase-exp/firestore/package.json b/packages-exp/firebase-exp/firestore/package.json deleted file mode 100644 index 3aeb4159404..00000000000 --- a/packages-exp/firebase-exp/firestore/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "firebase-exp/firestore", - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "typings": "dist/firestore/index.d.ts" -} diff --git a/packages-exp/firebase-exp/functions/index.ts b/packages-exp/firebase-exp/functions/index.ts deleted file mode 100644 index 7b0fa9efc45..00000000000 --- a/packages-exp/firebase-exp/functions/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export * from '@firebase/functions-exp'; diff --git a/packages-exp/firebase-exp/functions/package.json b/packages-exp/firebase-exp/functions/package.json deleted file mode 100644 index 1bdc6028a27..00000000000 --- a/packages-exp/firebase-exp/functions/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "firebase-exp/functions", - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "typings": "dist/functions/index.d.ts" -} diff --git a/packages-exp/firebase-exp/gulpfile.js b/packages-exp/firebase-exp/gulpfile.js deleted file mode 100644 index a2ceb544e81..00000000000 --- a/packages-exp/firebase-exp/gulpfile.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var gulp = require('gulp'); -var concat = require('gulp-concat'); -var sourcemaps = require('gulp-sourcemaps'); - -const OUTPUT_FILE = 'firebase.js'; -const pkgJson = require('./package.json'); -const files = pkgJson.components.map(component => { - const componentName = component.replace('/', '-'); - return `firebase-${componentName}.js`; -}); - -gulp.task('firebase-js', function () { - return gulp - .src(files) - .pipe(sourcemaps.init({ loadMaps: true })) - .pipe(concat(OUTPUT_FILE)) - .pipe(sourcemaps.write('.')) - .pipe(gulp.dest('.')); -}); diff --git a/packages-exp/firebase-exp/messaging/index.ts b/packages-exp/firebase-exp/messaging/index.ts deleted file mode 100644 index 94c90207d03..00000000000 --- a/packages-exp/firebase-exp/messaging/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export * from '@firebase/messaging-exp'; diff --git a/packages-exp/firebase-exp/messaging/package.json b/packages-exp/firebase-exp/messaging/package.json deleted file mode 100644 index c2e46721890..00000000000 --- a/packages-exp/firebase-exp/messaging/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "firebase-exp/messaging", - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "typings": "dist/messaging/index.d.ts" -} diff --git a/packages-exp/firebase-exp/package.json b/packages-exp/firebase-exp/package.json deleted file mode 100644 index 79ced03afa3..00000000000 --- a/packages-exp/firebase-exp/package.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "name": "firebase-exp", - "version": "0.900.4", - "private": true, - "description": "Firebase JavaScript library for web and Node.js", - "author": "Firebase (https://firebase.google.com/)", - "license": "Apache-2.0", - "homepage": "https://firebase.google.com/", - "keywords": [ - "authentication", - "database", - "Firebase", - "firebase", - "realtime", - "storage", - "performance", - "remote-config" - ], - "files": [ - "**/dist/", - "**/package.json", - "/firebase*.js", - "/firebase*.map" - ], - "repository": { - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "scripts": { - "build": "rollup -c && gulp firebase-js && yarn build:compat", - "build:release": "rollup -c rollup.config.release.js && gulp firebase-js && yarn build:compat:release", - "build:compat": "rollup -c compat/rollup.config.js", - "build:compat:release": "rollup -c compat/rollup.config.release.js", - "dev": "rollup -c -w", - "test": "echo 'No test suite for firebase wrapper'", - "test:ci": "echo 'No test suite for firebase wrapper'" - }, - "dependencies": { - "@firebase/app-exp": "0.0.900", - "@firebase/app-compat": "0.0.900", - "@firebase/auth-exp": "0.0.900", - "@firebase/functions-exp": "0.0.900", - "@firebase/firestore": "2.1.2", - "@firebase/performance-exp": "0.0.900", - "@firebase/remote-config-exp": "0.0.900", - "@firebase/messaging-exp": "0.0.900" - }, - "devDependencies": { - "rollup": "2.35.1", - "@rollup/plugin-commonjs": "15.1.0", - "rollup-plugin-license": "2.2.0", - "@rollup/plugin-node-resolve": "9.0.0", - "rollup-plugin-sourcemaps": "0.6.3", - "rollup-plugin-terser": "7.0.2", - "rollup-plugin-typescript2": "0.29.0", - "rollup-plugin-uglify": "6.0.4", - "gulp": "4.0.2", - "gulp-sourcemaps": "2.6.5", - "gulp-concat": "2.6.1", - "typescript": "4.0.5" - }, - "components": [ - "app", - "auth", - "functions", - "firestore", - "firestore/lite", - "performance", - "remote-config", - "messaging" - ] -} diff --git a/packages-exp/firebase-exp/performance/index.ts b/packages-exp/firebase-exp/performance/index.ts deleted file mode 100644 index e1ea2b74db5..00000000000 --- a/packages-exp/firebase-exp/performance/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export * from '@firebase/performance-exp'; diff --git a/packages-exp/firebase-exp/performance/package.json b/packages-exp/firebase-exp/performance/package.json deleted file mode 100644 index ad326e66470..00000000000 --- a/packages-exp/firebase-exp/performance/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "firebase-exp/performance", - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "typings": "dist/performance/index.d.ts" -} diff --git a/packages-exp/firebase-exp/remote-config/index.ts b/packages-exp/firebase-exp/remote-config/index.ts deleted file mode 100644 index 73b26c811e9..00000000000 --- a/packages-exp/firebase-exp/remote-config/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export * from '@firebase/remote-config-exp'; diff --git a/packages-exp/firebase-exp/remote-config/package.json b/packages-exp/firebase-exp/remote-config/package.json deleted file mode 100644 index 86878ce0089..00000000000 --- a/packages-exp/firebase-exp/remote-config/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "firebase-exp/remote-config", - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "typings": "dist/remote-config/index.d.ts" -} diff --git a/packages-exp/firebase-exp/rollup.config.js b/packages-exp/firebase-exp/rollup.config.js deleted file mode 100644 index 27b15a7cddd..00000000000 --- a/packages-exp/firebase-exp/rollup.config.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { resolve } from 'path'; -import resolveModule from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import sourcemaps from 'rollup-plugin-sourcemaps'; -import rollupTypescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import { uglify } from 'rollup-plugin-uglify'; -import json from '@rollup/plugin-json'; -import pkg from './package.json'; -import appPkg from './app/package.json'; - -const external = Object.keys(pkg.dependencies || {}); - -/** - * Global UMD Build - */ -const GLOBAL_NAME = 'firebase'; - -function createUmdOutputConfig(output, componentName) { - return { - file: output, - format: 'umd', - sourcemap: true, - extend: true, - name: `${GLOBAL_NAME}.${camelize(componentName)}`, - globals: { - '@firebase/app-exp': `${GLOBAL_NAME}.app` - }, - - /** - * use iife to avoid below error in the old Safari browser - * SyntaxError: Functions cannot be declared in a nested block in strict mode - * https://github.com/firebase/firebase-js-sdk/issues/1228 - * - */ - intro: ` - try { - (function() {`, - outro: ` - }).apply(this, arguments); - } catch(err) { - console.error(err); - throw new Error( - 'Cannot instantiate ${output} - ' + - 'be sure to load firebase-app.js first.' - ); - }` - }; -} - -function camelize(str) { - const arr = str.split('-'); - const capital = arr.map((item, index) => - index > 0 - ? item.charAt(0).toUpperCase() + item.slice(1).toLowerCase() - : item.toLowerCase() - ); - return capital.join(''); -} - -const plugins = [sourcemaps(), resolveModule(), json(), commonjs()]; - -const typescriptPlugin = rollupTypescriptPlugin({ - typescript -}); - -const typescriptPluginUMD = rollupTypescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - declaration: false - } - } -}); - -/** - * Individual Component Builds - */ -const appBuilds = [ - /** - * App Browser Builds - */ - { - input: 'app/index.ts', - output: [ - { file: resolve('app', appPkg.main), format: 'cjs', sourcemap: true }, - { file: resolve('app', appPkg.module), format: 'es', sourcemap: true } - ], - plugins: [...plugins, typescriptPlugin], - external - }, - /** - * App UMD Builds - */ - { - input: 'app/index.cdn.ts', - output: { - file: 'firebase-app.js', - sourcemap: true, - format: 'umd', - name: `${GLOBAL_NAME}.app` - }, - plugins: [...plugins, typescriptPluginUMD, uglify()] - } -]; - -const componentBuilds = pkg.components - // The "app" component is treated differently because it doesn't depend on itself. - .filter(component => component !== 'app') - .map(component => { - const pkg = require(`./${component}/package.json`); - // It is needed for handling sub modules, for example firestore/lite which should produce firebase-firestore-lite.js - // Otherwise, we will create a directory with '/' in the name. - const componentName = component.replace('/', '-'); - return [ - { - input: `${component}/index.ts`, - output: [ - { - file: resolve(component, pkg.main), - format: 'cjs', - sourcemap: true - }, - { - file: resolve(component, pkg.module), - format: 'es', - sourcemap: true - } - ], - plugins: [...plugins, typescriptPlugin], - external - }, - { - input: `${component}/index.ts`, - output: createUmdOutputConfig( - `firebase-${componentName}.js`, - componentName - ), - plugins: [...plugins, typescriptPluginUMD, uglify()], - external: ['@firebase/app-exp'] - } - ]; - }) - .reduce((a, b) => a.concat(b), []); - -export default [...appBuilds, ...componentBuilds]; diff --git a/packages-exp/firebase-exp/rollup.config.release.js b/packages-exp/firebase-exp/rollup.config.release.js deleted file mode 100644 index 2af5a3183de..00000000000 --- a/packages-exp/firebase-exp/rollup.config.release.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { resolve } from 'path'; -import resolveModule from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import sourcemaps from 'rollup-plugin-sourcemaps'; -import rollupTypescriptPlugin from 'rollup-plugin-typescript2'; -import alias from '@rollup/plugin-alias'; -import typescript from 'typescript'; -import { uglify } from 'rollup-plugin-uglify'; -import json from '@rollup/plugin-json'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import pkg from './package.json'; -import appPkg from './app/package.json'; - -// remove -exp from dependencies name -const deps = Object.keys(pkg.dependencies || {}).map(name => - name.replace('-exp', '') -); - -/** - * Global UMD Build - */ -const GLOBAL_NAME = 'firebase'; - -function createUmdOutputConfig(output, componentName) { - return { - file: output, - format: 'umd', - sourcemap: true, - extend: true, - name: `${GLOBAL_NAME}.${componentName}`, - globals: { - '@firebase/app': `${GLOBAL_NAME}.app` - }, - - /** - * use iife to avoid below error in the old Safari browser - * SyntaxError: Functions cannot be declared in a nested block in strict mode - * https://github.com/firebase/firebase-js-sdk/issues/1228 - * - */ - intro: ` - try { - (function() {`, - outro: ` - }).apply(this, arguments); - } catch(err) { - console.error(err); - throw new Error( - 'Cannot instantiate ${output} - ' + - 'be sure to load firebase-app.js first.' - ); - }` - }; -} - -const plugins = [sourcemaps(), resolveModule(), json(), commonjs()]; - -const typescriptPlugin = rollupTypescriptPlugin({ - typescript, - transformers: [importPathTransformer] -}); - -const typescriptPluginUMD = rollupTypescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - declaration: false - } - } -}); - -/** - * Individual Component Builds - */ -const appBuilds = [ - /** - * App Browser Builds - */ - { - input: 'app/index.ts', - output: [ - { file: resolve('app', appPkg.main), format: 'cjs', sourcemap: true }, - { file: resolve('app', appPkg.module), format: 'es', sourcemap: true } - ], - plugins: [...plugins, typescriptPlugin], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - /** - * App UMD Builds - */ - { - input: 'app/index.ts', - output: { - file: 'firebase-app.js', - sourcemap: true, - format: 'umd', - name: `${GLOBAL_NAME}.app` - }, - plugins: [...plugins, typescriptPluginUMD, uglify()] - } -]; - -const componentBuilds = pkg.components - // The "app" component is treated differently because it doesn't depend on itself. - .filter(component => component !== 'app') - .map(component => { - const pkg = require(`./${component}/package.json`); - // It is needed for handling sub modules, for example firestore/lite which should produce firebase-firestore-lite.js - // Otherwise, we will create a directory with '/' in the name. - const componentName = component.replace('/', '-'); - return [ - { - input: `${component}/index.ts`, - output: [ - { - file: resolve(component, pkg.main), - format: 'cjs', - sourcemap: true - }, - { - file: resolve(component, pkg.module), - format: 'es', - sourcemap: true - } - ], - plugins: [...plugins, typescriptPlugin], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - { - input: `${component}/index.ts`, - output: createUmdOutputConfig( - `firebase-${componentName}.js`, - componentName - ), - plugins: [ - ...plugins, - typescriptPluginUMD, - /** - * Hack to bundle @firebase/installations-exp - */ - alias({ - entries: [ - { - find: '@firebase/installations', - replacement: '@firebase/installations-exp' - } - ] - }) - ], - external: ['@firebase/app'] - } - ]; - }) - .reduce((a, b) => a.concat(b), []); - -export default [...appBuilds, ...componentBuilds]; diff --git a/packages-exp/firebase-exp/tsconfig.json b/packages-exp/firebase-exp/tsconfig.json deleted file mode 100644 index a06ed9a374c..00000000000 --- a/packages-exp/firebase-exp/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist" - }, - "exclude": [ - "dist/**/*" - ] -} \ No newline at end of file diff --git a/packages-exp/functions-compat/package.json b/packages-exp/functions-compat/package.json deleted file mode 100644 index b4b2f5bab9a..00000000000 --- a/packages-exp/functions-compat/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@firebase/functions-compat", - "version": "0.0.900", - "description": "", - "private": true, - "author": "Firebase (https://firebase.google.com/)", - "main": "dist/index.node.cjs.js", - "browser": "dist/index.esm5.js", - "module": "dist/index.esm5.js", - "esm2017": "dist/index.esm2017.js", - "files": ["dist"], - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-compat": "0.x" - }, - "devDependencies": { - "@firebase/app-compat": "0.0.900", - "rollup": "2.35.1", - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-typescript2": "0.29.0", - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages-exp/functions-compat", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c", - "build:deps": "lerna run --scope @firebase/functions-compat --include-dependencies build", - "build:release": "rollup -c rollup.config.release.js", - "dev": "rollup -c -w", - "test": "run-p lint test:all", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", - "test:all": "run-p test:browser test:node", - "test:browser": "karma start --single-run", - "test:browser:debug": "karma start --browsers=Chrome --auto-watch", - "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'src/{,!(browser)/**/}*.test.ts' --file src/index.node.ts --config ../../config/mocharc.node.js", - "test:emulator": "env FIREBASE_FUNCTIONS_HOST=http://localhost FIREBASE_FUNCTIONS_PORT=5005 run-p test:node" - }, - "typings": "dist/functions-compat-public.d.ts", - "dependencies": { - "@firebase/component": "0.1.21", - "@firebase/functions-exp": "0.0.900", - "@firebase/functions-types-exp": "0.0.900", - "@firebase/messaging-types": "0.5.0", - "@firebase/util": "0.3.4", - "tslib": "^1.11.1" - }, - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/functions-compat/rollup.config.base.js b/packages-exp/functions-compat/rollup.config.base.js deleted file mode 100644 index ebb941e4225..00000000000 --- a/packages-exp/functions-compat/rollup.config.base.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import pkg from './package.json'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * ES5 Builds - */ -export function getEs5Builds(additionalTypescriptPlugins = {}) { - const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - abortOnError: false, - ...additionalTypescriptPlugins - }), - json() - ]; - return [ - /** - * Browser Builds - */ - { - input: 'src/index.ts', - output: [{ file: pkg.module, format: 'es', sourcemap: true }], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - /** - * Node.js Build - */ - { - input: 'src/index.node.ts', - output: [{ file: pkg.main, format: 'cjs', sourcemap: true }], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } - ]; -} - -/** - * ES2017 Builds - */ -export function getEs2017Builds(additionalTypescriptPlugins = {}) { - const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - abortOnError: false, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - ...additionalTypescriptPlugins - }), - json({ preferConst: true }) - ]; - return [ - { - /** - * Browser Build - */ - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - plugins: es2017BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } - ]; -} - -export function getAllBuilds(additionalTypescriptPlugins = {}) { - return [ - ...getEs5Builds(additionalTypescriptPlugins), - ...getEs2017Builds(additionalTypescriptPlugins) - ]; -} diff --git a/packages-exp/functions-compat/rollup.config.js b/packages-exp/functions-compat/rollup.config.js deleted file mode 100644 index 7746175a9a6..00000000000 --- a/packages-exp/functions-compat/rollup.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getAllBuilds } from './rollup.config.base'; - -// eslint-disable-next-line import/no-default-export -export default getAllBuilds({}); diff --git a/packages-exp/functions-compat/rollup.config.release.js b/packages-exp/functions-compat/rollup.config.release.js deleted file mode 100644 index d364683678a..00000000000 --- a/packages-exp/functions-compat/rollup.config.release.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import { getAllBuilds } from './rollup.config.base'; - -// eslint-disable-next-line import/no-default-export -export default getAllBuilds({ - clean: true, - transformers: [importPathTransformer] -}); diff --git a/packages-exp/functions-compat/src/callable.test.ts b/packages-exp/functions-compat/src/callable.test.ts deleted file mode 100644 index 5eaf2db5714..00000000000 --- a/packages-exp/functions-compat/src/callable.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { expect } from 'chai'; -import { FirebaseApp } from '@firebase/app-types'; -import { FunctionsErrorCode } from '@firebase/functions-types-exp'; -import { createTestService } from '../test/utils'; -import { firebase } from '@firebase/app-compat'; - -// eslint-disable-next-line @typescript-eslint/no-require-imports -export const TEST_PROJECT = require('../../../config/project.json'); - -// Chai doesn't handle Error comparisons in a useful way. -// https://github.com/chaijs/chai/issues/608 -async function expectError( - promise: Promise, - code: FunctionsErrorCode, - message: string, - details?: any -): Promise { - let failed = false; - try { - await promise; - } catch (e) { - failed = true; - // Errors coming from callable functions usually have the functions-exp - // code in the message since it's thrown inside functions-exp. - expect(e.code).to.match(new RegExp(`functions.*/${code}`)); - expect(e.message).to.equal(message); - expect(e.details).to.deep.equal(details); - } - if (!failed) { - expect(false, 'Promise should have failed.').to.be.true; - } -} - -describe('Firebase Functions > Call', () => { - let app: FirebaseApp; - const region = 'us-central1'; - - before(() => { - const useEmulator = !!process.env.HOST; - const projectId = useEmulator - ? 'functions-integration-test' - : TEST_PROJECT.projectId; - const messagingSenderId = 'messaging-sender-id'; - - app = firebase.initializeApp({ projectId, messagingSenderId }); - }); - - after(async () => { - await app.delete(); - }); - - it('simple data', async () => { - const functions = createTestService(app, region); - // TODO(klimt): Should we add an API to create a "long" in JS? - const data = { - bool: true, - int: 2, - str: 'four', - array: [5, 6], - null: null - }; - - const func = functions.httpsCallable('dataTest'); - const result = await func(data); - - expect(result.data).to.deep.equal({ - message: 'stub response', - code: 42, - long: 420 - }); - }); - - it('scalars', async () => { - const functions = createTestService(app, region); - const func = functions.httpsCallable('scalarTest'); - const result = await func(17); - expect(result.data).to.equal(76); - }); - - it('null', async () => { - const functions = createTestService(app, region); - const func = functions.httpsCallable('nullTest'); - let result = await func(null); - expect(result.data).to.be.null; - - // Test with void arguments version. - result = await func(); - expect(result.data).to.be.null; - }); - - it('missing result', async () => { - const functions = createTestService(app, region); - const func = functions.httpsCallable('missingResultTest'); - await expectError(func(), 'internal', 'Response is missing data field.'); - }); - - it('unhandled error', async () => { - const functions = createTestService(app, region); - const func = functions.httpsCallable('unhandledErrorTest'); - await expectError(func(), 'internal', 'internal'); - }); - - it('unknown error', async () => { - const functions = createTestService(app, region); - const func = functions.httpsCallable('unknownErrorTest'); - await expectError(func(), 'internal', 'internal'); - }); - - it('explicit error', async () => { - const functions = createTestService(app, region); - const func = functions.httpsCallable('explicitErrorTest'); - await expectError(func(), 'out-of-range', 'explicit nope', { - start: 10, - end: 20, - long: 30 - }); - }); - - it('http error', async () => { - const functions = createTestService(app, region); - const func = functions.httpsCallable('httpErrorTest'); - await expectError(func(), 'invalid-argument', 'invalid-argument'); - }); - - it('timeout', async () => { - const functions = createTestService(app, region); - const func = functions.httpsCallable('timeoutTest', { timeout: 10 }); - await expectError(func(), 'deadline-exceeded', 'deadline-exceeded'); - }); -}); diff --git a/packages-exp/functions-compat/src/index.node.ts b/packages-exp/functions-compat/src/index.node.ts deleted file mode 100644 index f560f4aa251..00000000000 --- a/packages-exp/functions-compat/src/index.node.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { firebase } from '@firebase/app-compat'; -import { name, version } from '../package.json'; -import { registerFunctions } from './register'; - -registerFunctions(); -firebase.registerVersion(name, version, 'node'); diff --git a/packages-exp/functions-compat/src/index.ts b/packages-exp/functions-compat/src/index.ts deleted file mode 100644 index 159d9a6590e..00000000000 --- a/packages-exp/functions-compat/src/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { firebase } from '@firebase/app-compat'; -import { name, version } from '../package.json'; -import { registerFunctions } from './register'; - -registerFunctions(); -firebase.registerVersion(name, version); diff --git a/packages-exp/functions-compat/src/register.ts b/packages-exp/functions-compat/src/register.ts deleted file mode 100644 index 47829cb45b7..00000000000 --- a/packages-exp/functions-compat/src/register.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import firebase from '@firebase/app-compat'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { FunctionsService } from './service'; -import { - Component, - ComponentType, - InstanceFactory, - ComponentContainer -} from '@firebase/component'; -import { FirebaseApp } from '@firebase/app-types'; -import { Functions as FunctionsServiceExp } from '@firebase/functions-types-exp'; - -declare module '@firebase/component' { - interface NameServiceMapping { - 'app-compat': FirebaseApp; - 'functions-compat': FunctionsService; - 'functions-exp': FunctionsServiceExp; - } -} - -const factory: InstanceFactory<'functions-compat'> = ( - container: ComponentContainer, - regionOrCustomDomain?: string -) => { - // Dependencies - const app = container.getProvider('app-compat').getImmediate(); - const functionsServiceExp = container - .getProvider('functions-exp') - .getImmediate({ - identifier: regionOrCustomDomain - }); - - return new FunctionsService(app as FirebaseApp, functionsServiceExp); -}; - -export function registerFunctions(): void { - const namespaceExports = { - // no-inline - Functions: FunctionsService - }; - (firebase as _FirebaseNamespace).INTERNAL.registerComponent( - new Component('functions-compat', factory, ComponentType.PUBLIC) - .setServiceProps(namespaceExports) - .setMultipleInstances(true) - ); -} diff --git a/packages-exp/functions-compat/src/service.test.ts b/packages-exp/functions-compat/src/service.test.ts deleted file mode 100644 index e81b6ac7f0c..00000000000 --- a/packages-exp/functions-compat/src/service.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { expect, use } from 'chai'; -import { createTestService } from '../test/utils'; -import { FunctionsService } from './service'; -import { firebase } from '@firebase/app-compat'; -import { FirebaseApp } from '@firebase/app-types'; -import * as functionsExp from '@firebase/functions-exp'; -import { stub, match, SinonStub } from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -use(sinonChai); - -describe('Firebase Functions > Service', () => { - let app: FirebaseApp; - let service: FunctionsService; - let functionsEmulatorStub: SinonStub = stub(); - let httpsCallableStub: SinonStub = stub(); - - before(() => { - functionsEmulatorStub = stub(functionsExp, 'useFunctionsEmulator'); - httpsCallableStub = stub(functionsExp, 'httpsCallable'); - }); - - beforeEach(() => { - app = firebase.initializeApp({ - projectId: 'my-project', - messagingSenderId: 'messaging-sender-id' - }); - }); - - afterEach(async () => { - await app.delete(); - }); - - after(() => { - functionsEmulatorStub.restore(); - httpsCallableStub.restore(); - }); - - it('useFunctionsEmulator (deprecated) calls modular useEmulator', () => { - service = createTestService(app); - service.useFunctionsEmulator('http://localhost:5005'); - expect(functionsEmulatorStub).to.be.calledWith( - match.any, - 'localhost', - 5005 - ); - functionsEmulatorStub.resetHistory(); - }); - - it('useEmulator calls modular useEmulator', () => { - service = createTestService(app); - service.useEmulator('otherlocalhost', 5006); - expect(functionsEmulatorStub).to.be.calledWith( - match.any, - 'otherlocalhost', - 5006 - ); - functionsEmulatorStub.resetHistory(); - }); - - it('httpsCallable calls modular httpsCallable', () => { - service = createTestService(app); - service.httpsCallable('blah', { timeout: 2000 }); - expect(httpsCallableStub).to.be.calledWith(match.any, 'blah', { - timeout: 2000 - }); - httpsCallableStub.resetHistory(); - }); - - it('correctly sets region', () => { - service = createTestService(app, 'my-region'); - expect(service._region).to.equal('my-region'); - }); - - it('correctly sets custom domain', () => { - service = createTestService(app, 'https://mydomain.com'); - expect(service._customDomain).to.equal('https://mydomain.com'); - }); -}); diff --git a/packages-exp/functions-compat/src/service.ts b/packages-exp/functions-compat/src/service.ts deleted file mode 100644 index de71ceb1e45..00000000000 --- a/packages-exp/functions-compat/src/service.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - httpsCallable as httpsCallableExp, - useFunctionsEmulator as useFunctionsEmulatorExp -} from '@firebase/functions-exp'; -import { FirebaseFunctions, HttpsCallable } from '@firebase/functions-types'; -import { - HttpsCallableOptions, - Functions as FunctionsServiceExp -} from '@firebase/functions-types-exp'; -import { FirebaseApp } from '@firebase/app-types'; -import { FirebaseError } from '@firebase/util'; - -export class FunctionsService implements FirebaseFunctions { - /** - * For testing. - * @internal - */ - _region: string; - /** - * For testing. - * @internal - */ - _customDomain: string | null; - - constructor( - public app: FirebaseApp, - private _functionsInstance: FunctionsServiceExp - ) { - this._region = this._functionsInstance.region; - this._customDomain = this._functionsInstance.customDomain; - } - httpsCallable(name: string, options?: HttpsCallableOptions): HttpsCallable { - return httpsCallableExp(this._functionsInstance, name, options); - } - /** - * Deprecated in pre-modularized repo, does not exist in modularized - * functions package, need to convert to "host" and "port" args that - * `useFunctionsEmulatorExp` takes. - * @deprecated - */ - useFunctionsEmulator(origin: string): void { - const match = origin.match('[a-zA-Z]+://([a-zA-Z0-9.-]+)(?::([0-9]+))?'); - if (match == null) { - throw new FirebaseError( - 'functions', - 'No origin provided to useFunctionsEmulator()' - ); - } - if (match[2] == null) { - throw new FirebaseError( - 'functions', - 'Port missing in origin provided to useFunctionsEmulator()' - ); - } - return useFunctionsEmulatorExp( - this._functionsInstance, - match[1], - Number(match[2]) - ); - } - useEmulator(host: string, port: number): void { - return useFunctionsEmulatorExp(this._functionsInstance, host, port); - } -} diff --git a/packages-exp/functions-compat/test/utils.ts b/packages-exp/functions-compat/test/utils.ts deleted file mode 100644 index b824c8eb32e..00000000000 --- a/packages-exp/functions-compat/test/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types'; -import { FunctionsService } from '../src/service'; -import { getFunctions } from '@firebase/functions-exp'; - -export function createTestService( - app: FirebaseApp, - regionOrCustomDomain?: string -): FunctionsService { - const functions = new FunctionsService( - app, - getFunctions(app, regionOrCustomDomain) - ); - const useEmulator = !!process.env.FIREBASE_FUNCTIONS_EMULATOR_HOST; - if (useEmulator) { - functions.useEmulator( - process.env.FIREBASE_FUNCTIONS_EMULATOR_HOST!, - Number(process.env.FIREBASE_FUNCTIONS_EMULATOR_PORT!) - ); - } - return functions; -} diff --git a/packages-exp/functions-compat/tsconfig.json b/packages-exp/functions-compat/tsconfig.json deleted file mode 100644 index a06ed9a374c..00000000000 --- a/packages-exp/functions-compat/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist" - }, - "exclude": [ - "dist/**/*" - ] -} \ No newline at end of file diff --git a/packages-exp/functions-exp/.eslintrc.js b/packages-exp/functions-exp/.eslintrc.js deleted file mode 100644 index 11fa60d3e6a..00000000000 --- a/packages-exp/functions-exp/.eslintrc.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -const path = require('path'); - -module.exports = { - extends: '../../config/.eslintrc.js', - parserOptions: { - project: 'tsconfig.json', - // to make vscode-eslint work with monorepo - // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 - tsconfigRootDir: __dirname - }, - rules: { - 'import/no-extraneous-dependencies': [ - 'error', - { - 'packageDir': [path.resolve(__dirname, '../../'), __dirname], - devDependencies: true - } - ] - } -}; diff --git a/packages-exp/functions-exp/README.md b/packages-exp/functions-exp/README.md deleted file mode 100644 index 6008bd1df1a..00000000000 --- a/packages-exp/functions-exp/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# `@firebase/functions` - -This is the Firebase Functions component of the Firebase JS SDK. - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/functions-exp/karma.conf.js b/packages-exp/functions-exp/karma.conf.js deleted file mode 100644 index d180371aeba..00000000000 --- a/packages-exp/functions-exp/karma.conf.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const karmaBase = require('../../config/karma.base'); - -const files = [`src/**/*.test.ts`]; - -module.exports = function (config) { - const karmaConfig = Object.assign({}, karmaBase, { - // files to load into karma - files, - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'] - }); - - config.set(karmaConfig); -}; - -module.exports.files = files; diff --git a/packages-exp/functions-exp/package.json b/packages-exp/functions-exp/package.json deleted file mode 100644 index 60dc296c658..00000000000 --- a/packages-exp/functions-exp/package.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "name": "@firebase/functions-exp", - "version": "0.0.900", - "description": "", - "private": true, - "author": "Firebase (https://firebase.google.com/)", - "main": "dist/index.node.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "esm2017": "dist/index.esm2017.js", - "files": ["dist"], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c && yarn api-report", - "build:deps": "lerna run --scope @firebase/functions-exp --include-dependencies build", - "build:release": "rollup -c rollup.config.release.js && yarn api-report", - "dev": "rollup -c -w", - "test": "run-p lint test:all", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", - "test:all": "run-p test:browser test:node", - "test:browser": "karma start --single-run", - "test:browser:debug": "karma start --browsers=Chrome --auto-watch", - "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'src/{,!(browser)/**/}*.test.ts' --file src/index.node.ts --config ../../config/mocharc.node.js", - "test:emulator": "env FIREBASE_FUNCTIONS_EMULATOR_ORIGIN=http://localhost:5005 run-p test:node", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn build && yarn doc" - }, - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-exp": "0.x", - "@firebase/app-types-exp": "0.x" - }, - "devDependencies": { - "@firebase/app-exp": "0.0.900", - "rollup": "2.35.1", - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-typescript2": "0.29.0", - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages/functions", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "dist/functions-exp-public.d.ts", - "dependencies": { - "@firebase/component": "0.1.21", - "@firebase/functions-types-exp": "0.0.900", - "@firebase/messaging-types": "0.5.0", - "@firebase/util": "0.3.4", - "node-fetch": "2.6.1", - "tslib": "^1.11.1" - }, - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/functions-exp/rollup.config.js b/packages-exp/functions-exp/rollup.config.js deleted file mode 100644 index 63db899d25f..00000000000 --- a/packages-exp/functions-exp/rollup.config.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - } - }), - json({ preferConst: true }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/functions-exp/rollup.config.release.js b/packages-exp/functions-exp/rollup.config.release.js deleted file mode 100644 index 1e3b338e4b5..00000000000 --- a/packages-exp/functions-exp/rollup.config.release.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import json from '@rollup/plugin-json'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - clean: true, - abortOnError: false, - transformers: [importPathTransformer] - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins, - treeshake: { - moduleSideEffects: false - } -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - abortOnError: false, - clean: true, - transformers: [importPathTransformer] - }), - json({ - preferConst: true - }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins, - treeshake: { - moduleSideEffects: false - } -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/functions-exp/rollup.shared.js b/packages-exp/functions-exp/rollup.shared.js deleted file mode 100644 index 222b923d075..00000000000 --- a/packages-exp/functions-exp/rollup.shared.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import pkg from './package.json'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -export const es5BuildsNoPlugin = [ - /** - * Browser Builds - */ - { - input: 'src/index.ts', - output: [{ file: pkg.module, format: 'es', sourcemap: true }], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - /** - * Node.js Build - */ - { - input: 'src/index.node.ts', - output: [{ file: pkg.main, format: 'cjs', sourcemap: true }], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -/** - * ES2017 Builds - */ -export const es2017BuildsNoPlugin = [ - { - /** - * Browser Build - */ - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; diff --git a/packages-exp/functions-exp/src/api.ts b/packages-exp/functions-exp/src/api.ts deleted file mode 100644 index c6dc36073b4..00000000000 --- a/packages-exp/functions-exp/src/api.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _getProvider } from '@firebase/app-exp'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import { FUNCTIONS_TYPE } from './constants'; - -import { Provider } from '@firebase/component'; -import { - Functions, - HttpsCallableOptions, - HttpsCallable -} from '@firebase/functions-types-exp'; -import { - FunctionsService, - DEFAULT_REGION, - useFunctionsEmulator as _useFunctionsEmulator, - httpsCallable as _httpsCallable -} from './service'; - -/** - * Returns a Functions instance for the given app. - * @param app - The FirebaseApp to use. - * @param regionOrCustomDomain - one of: - * a) The region the callable functions are located in (ex: us-central1) - * b) A custom domain hosting the callable functions (ex: https://mydomain.com) - * @public - */ -export function getFunctions( - app: FirebaseApp, - regionOrCustomDomain: string = DEFAULT_REGION -): Functions { - // Dependencies - const functionsProvider: Provider<'functions'> = _getProvider( - app, - FUNCTIONS_TYPE - ); - const functionsInstance = functionsProvider.getImmediate({ - identifier: regionOrCustomDomain - }); - return functionsInstance; -} - -/** - * Modify this instance to communicate with the Cloud Functions emulator. - * - * Note: this must be called before this instance has been used to do any operations. - * - * @param host The emulator host (ex: localhost) - * @param port The emulator port (ex: 5001) - * @public - */ -export function useFunctionsEmulator( - functionsInstance: Functions, - host: string, - port: number -): void { - _useFunctionsEmulator(functionsInstance as FunctionsService, host, port); -} - -/** - * Returns a reference to the callable https trigger with the given name. - * @param name - The name of the trigger. - * @public - */ -export function httpsCallable( - functionsInstance: Functions, - name: string, - options?: HttpsCallableOptions -): HttpsCallable { - return _httpsCallable(functionsInstance as FunctionsService, name, options); -} diff --git a/packages-exp/functions-exp/src/callable.test.ts b/packages-exp/functions-exp/src/callable.test.ts deleted file mode 100644 index f2d2ca72be3..00000000000 --- a/packages-exp/functions-exp/src/callable.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import { FunctionsErrorCode } from '@firebase/functions-types-exp'; -import { - Provider, - ComponentContainer, - Component, - ComponentType -} from '@firebase/component'; -import { - FirebaseMessaging, - FirebaseMessagingName -} from '@firebase/messaging-types'; -import { - FirebaseAuthInternal, - FirebaseAuthInternalName -} from '@firebase/auth-interop-types'; -import { makeFakeApp, createTestService } from '../test/utils'; -import { httpsCallable } from './service'; -import { FUNCTIONS_TYPE } from './constants'; - -// eslint-disable-next-line @typescript-eslint/no-require-imports -export const TEST_PROJECT = require('../../../config/project.json'); - -// Chai doesn't handle Error comparisons in a useful way. -// https://github.com/chaijs/chai/issues/608 -async function expectError( - promise: Promise, - code: FunctionsErrorCode, - message: string, - details?: any -): Promise { - let failed = false; - try { - await promise; - } catch (e) { - failed = true; - expect(e.code).to.equal(`${FUNCTIONS_TYPE}/${code}`); - expect(e.message).to.equal(message); - expect(e.details).to.deep.equal(details); - } - if (!failed) { - expect(false, 'Promise should have failed.').to.be.true; - } -} - -describe('Firebase Functions > Call', () => { - let app: FirebaseApp; - const region = 'us-central1'; - - before(() => { - const useEmulator = !!process.env.FIREBASE_FUNCTIONS_EMULATOR_ORIGIN; - const projectId = useEmulator - ? 'functions-integration-test' - : TEST_PROJECT.projectId; - const messagingSenderId = 'messaging-sender-id'; - - app = makeFakeApp({ projectId, messagingSenderId }); - }); - - it('simple data', async () => { - const functions = createTestService(app, region); - // TODO(klimt): Should we add an API to create a "long" in JS? - const data = { - bool: true, - int: 2, - str: 'four', - array: [5, 6], - null: null - }; - - const func = httpsCallable(functions, 'dataTest'); - const result = await func(data); - - expect(result.data).to.deep.equal({ - message: 'stub response', - code: 42, - long: 420 - }); - }); - - it('scalars', async () => { - const functions = createTestService(app, region); - const func = httpsCallable(functions, 'scalarTest'); - const result = await func(17); - expect(result.data).to.equal(76); - }); - - it('token', async () => { - // mock auth-internal service - const authMock: FirebaseAuthInternal = ({ - getToken: async () => ({ accessToken: 'token' }) - } as unknown) as FirebaseAuthInternal; - const authProvider = new Provider( - 'auth-internal', - new ComponentContainer('test') - ); - authProvider.setComponent( - new Component('auth-internal', () => authMock, ComponentType.PRIVATE) - ); - - const functions = createTestService(app, region, authProvider); - - // Stub out the internals to get an auth token. - const stub = sinon.stub(authMock, 'getToken').callThrough(); - const func = httpsCallable(functions, 'tokenTest'); - const result = await func({}); - expect(result.data).to.deep.equal({}); - - expect(stub.callCount).to.equal(1); - stub.restore(); - }); - - it('instance id', async () => { - // Should effectively skip this test in environments where messaging doesn't work. - // (Node, IE) - if (process || !('Notification' in self)) { - console.log('No Notification API: skipping instance id test.'); - return; - } - // mock firebase messaging - const messagingMock: FirebaseMessaging = ({ - getToken: async () => 'iid' - } as unknown) as FirebaseMessaging; - const messagingProvider = new Provider( - 'messaging', - new ComponentContainer('test') - ); - messagingProvider.setComponent( - new Component('messaging', () => messagingMock, ComponentType.PRIVATE) - ); - - const functions = createTestService( - app, - region, - undefined, - messagingProvider - ); - - // Stub out the messaging method get an instance id token. - const stub = sinon.stub(messagingMock, 'getToken').callThrough(); - sinon.stub(Notification, 'permission').value('granted'); - - const func = httpsCallable(functions, 'instanceIdTest'); - const result = await func({}); - expect(result.data).to.deep.equal({}); - - expect(stub.callCount).to.equal(1); - stub.restore(); - }); - - it('null', async () => { - const functions = createTestService(app, region); - const func = httpsCallable(functions, 'nullTest'); - let result = await func(null); - expect(result.data).to.be.null; - - // Test with void arguments version. - result = await func(); - expect(result.data).to.be.null; - }); - - it('missing result', async () => { - const functions = createTestService(app, region); - const func = httpsCallable(functions, 'missingResultTest'); - await expectError(func(), 'internal', 'Response is missing data field.'); - }); - - it('unhandled error', async () => { - const functions = createTestService(app, region); - const func = httpsCallable(functions, 'unhandledErrorTest'); - await expectError(func(), 'internal', 'internal'); - }); - - it('unknown error', async () => { - const functions = createTestService(app, region); - const func = httpsCallable(functions, 'unknownErrorTest'); - await expectError(func(), 'internal', 'internal'); - }); - - it('explicit error', async () => { - const functions = createTestService(app, region); - const func = httpsCallable(functions, 'explicitErrorTest'); - await expectError(func(), 'out-of-range', 'explicit nope', { - start: 10, - end: 20, - long: 30 - }); - }); - - it('http error', async () => { - const functions = createTestService(app, region); - const func = httpsCallable(functions, 'httpErrorTest'); - await expectError(func(), 'invalid-argument', 'invalid-argument'); - }); - - it('timeout', async () => { - const functions = createTestService(app, region); - const func = httpsCallable(functions, 'timeoutTest', { timeout: 10 }); - await expectError(func(), 'deadline-exceeded', 'deadline-exceeded'); - }); -}); diff --git a/packages-exp/functions-exp/src/config.ts b/packages-exp/functions-exp/src/config.ts deleted file mode 100644 index 9279835b2c7..00000000000 --- a/packages-exp/functions-exp/src/config.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _registerComponent } from '@firebase/app-exp'; -import { FunctionsService } from './service'; -import { - Component, - ComponentType, - ComponentContainer, - InstanceFactory -} from '@firebase/component'; -import { FUNCTIONS_TYPE } from './constants'; - -export const DEFAULT_REGION = 'us-central1'; - -export function registerFunctions(fetchImpl: typeof fetch): void { - const factory: InstanceFactory<'functions'> = ( - container: ComponentContainer, - regionOrCustomDomain?: string - ) => { - // Dependencies - const app = container.getProvider('app-exp').getImmediate(); - const authProvider = container.getProvider('auth-internal'); - const messagingProvider = container.getProvider('messaging'); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return new FunctionsService( - app, - authProvider, - messagingProvider, - regionOrCustomDomain, - fetchImpl - ); - }; - - _registerComponent( - new Component( - FUNCTIONS_TYPE, - factory, - ComponentType.PUBLIC - ).setMultipleInstances(true) - ); -} diff --git a/packages-exp/functions-exp/src/context.ts b/packages-exp/functions-exp/src/context.ts deleted file mode 100644 index 7ccd5f8823a..00000000000 --- a/packages-exp/functions-exp/src/context.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - FirebaseMessaging, - FirebaseMessagingName -} from '@firebase/messaging-types'; -import { - FirebaseAuthInternal, - FirebaseAuthInternalName -} from '@firebase/auth-interop-types'; -import { Provider } from '@firebase/component'; - -/** - * The metadata that should be supplied with function calls. - * @internal - */ -export interface Context { - authToken?: string; - messagingToken?: string; -} - -/** - * Helper class to get metadata that should be included with a function call. - * @internal - */ -export class ContextProvider { - private auth: FirebaseAuthInternal | null = null; - private messaging: FirebaseMessaging | null = null; - constructor( - authProvider: Provider, - messagingProvider: Provider - ) { - this.auth = authProvider.getImmediate({ optional: true }); - this.messaging = messagingProvider.getImmediate({ - optional: true - }); - - if (!this.auth) { - authProvider.get().then( - auth => (this.auth = auth), - () => { - /* get() never rejects */ - } - ); - } - - if (!this.messaging) { - messagingProvider.get().then( - messaging => (this.messaging = messaging), - () => { - /* get() never rejects */ - } - ); - } - } - - async getAuthToken(): Promise { - if (!this.auth) { - return undefined; - } - - try { - const token = await this.auth.getToken(); - return token?.accessToken; - } catch (e) { - // If there's any error when trying to get the auth token, leave it off. - return undefined; - } - } - - async getMessagingToken(): Promise { - if ( - !this.messaging || - !('Notification' in self) || - Notification.permission !== 'granted' - ) { - return undefined; - } - - try { - return this.messaging.getToken(); - } catch (e) { - // We don't warn on this, because it usually means messaging isn't set up. - // console.warn('Failed to retrieve instance id token.', e); - - // If there's any error when trying to get the token, leave it off. - return undefined; - } - } - - async getContext(): Promise { - const authToken = await this.getAuthToken(); - const messagingToken = await this.getMessagingToken(); - return { authToken, messagingToken }; - } -} diff --git a/packages-exp/functions-exp/src/error.ts b/packages-exp/functions-exp/src/error.ts deleted file mode 100644 index 16d454e025c..00000000000 --- a/packages-exp/functions-exp/src/error.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FunctionsErrorCode } from '@firebase/functions-types-exp'; -import { decode } from './serializer'; -import { HttpResponseBody } from './service'; -import { FirebaseError } from '@firebase/util'; -import { FUNCTIONS_TYPE } from './constants'; - -/** - * Standard error codes for different ways a request can fail, as defined by: - * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto - * - * This map is used primarily to convert from a backend error code string to - * a client SDK error code string, and make sure it's in the supported set. - */ -const errorCodeMap: { [name: string]: FunctionsErrorCode } = { - OK: 'ok', - CANCELLED: 'cancelled', - UNKNOWN: 'unknown', - INVALID_ARGUMENT: 'invalid-argument', - DEADLINE_EXCEEDED: 'deadline-exceeded', - NOT_FOUND: 'not-found', - ALREADY_EXISTS: 'already-exists', - PERMISSION_DENIED: 'permission-denied', - UNAUTHENTICATED: 'unauthenticated', - RESOURCE_EXHAUSTED: 'resource-exhausted', - FAILED_PRECONDITION: 'failed-precondition', - ABORTED: 'aborted', - OUT_OF_RANGE: 'out-of-range', - UNIMPLEMENTED: 'unimplemented', - INTERNAL: 'internal', - UNAVAILABLE: 'unavailable', - DATA_LOSS: 'data-loss' -}; - -/** - * An explicit error that can be thrown from a handler to send an error to the - * client that called the function. - */ -export class FunctionsError extends FirebaseError { - constructor( - /** - * A standard error code that will be returned to the client. This also - * determines the HTTP status code of the response, as defined in code.proto. - */ - code: FunctionsErrorCode, - message?: string, - /** - * Extra data to be converted to JSON and included in the error response. - */ - readonly details?: unknown - ) { - super(`${FUNCTIONS_TYPE}/${code}`, message || ''); - } -} - -/** - * Takes an HTTP status code and returns the corresponding ErrorCode. - * This is the standard HTTP status code -> error mapping defined in: - * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto - * - * @param status An HTTP status code. - * @return The corresponding ErrorCode, or ErrorCode.UNKNOWN if none. - */ -function codeForHTTPStatus(status: number): FunctionsErrorCode { - // Make sure any successful status is OK. - if (status >= 200 && status < 300) { - return 'ok'; - } - switch (status) { - case 0: - // This can happen if the server returns 500. - return 'internal'; - case 400: - return 'invalid-argument'; - case 401: - return 'unauthenticated'; - case 403: - return 'permission-denied'; - case 404: - return 'not-found'; - case 409: - return 'aborted'; - case 429: - return 'resource-exhausted'; - case 499: - return 'cancelled'; - case 500: - return 'internal'; - case 501: - return 'unimplemented'; - case 503: - return 'unavailable'; - case 504: - return 'deadline-exceeded'; - default: // ignore - } - return 'unknown'; -} - -/** - * Takes an HTTP response and returns the corresponding Error, if any. - */ -export function _errorForResponse( - status: number, - bodyJSON: HttpResponseBody | null -): Error | null { - let code = codeForHTTPStatus(status); - - // Start with reasonable defaults from the status code. - let description: string = code; - - let details: unknown = undefined; - - // Then look through the body for explicit details. - try { - const errorJSON = bodyJSON && bodyJSON.error; - if (errorJSON) { - const status = errorJSON.status; - if (typeof status === 'string') { - if (!errorCodeMap[status]) { - // They must've included an unknown error code in the body. - return new FunctionsError('internal', 'internal'); - } - code = errorCodeMap[status]; - - // TODO(klimt): Add better default descriptions for error enums. - // The default description needs to be updated for the new code. - description = status; - } - - const message = errorJSON.message; - if (typeof message === 'string') { - description = message; - } - - details = errorJSON.details; - if (details !== undefined) { - details = decode(details); - } - } - } catch (e) { - // If we couldn't parse explicit error data, that's fine. - } - - if (code === 'ok') { - // Technically, there's an edge case where a developer could explicitly - // return an error code of OK, and we will treat it as success, but that - // seems reasonable. - return null; - } - - return new FunctionsError(code, description, details); -} diff --git a/packages-exp/functions-exp/src/index.node.ts b/packages-exp/functions-exp/src/index.node.ts deleted file mode 100644 index 5955172f350..00000000000 --- a/packages-exp/functions-exp/src/index.node.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { registerVersion } from '@firebase/app-exp'; -import { registerFunctions } from './config'; -import nodeFetch from 'node-fetch'; - -import { name, version } from '../package.json'; - -export * from './api'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -registerFunctions(nodeFetch as any); -registerVersion(name, version, 'node'); diff --git a/packages-exp/functions-exp/src/index.ts b/packages-exp/functions-exp/src/index.ts deleted file mode 100644 index ad8b299b634..00000000000 --- a/packages-exp/functions-exp/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { registerVersion } from '@firebase/app-exp'; -import { registerFunctions } from './config'; - -import { name, version } from '../package.json'; - -export * from './api'; - -registerFunctions(fetch.bind(self)); -registerVersion(name, version); diff --git a/packages-exp/functions-exp/src/serializer.test.ts b/packages-exp/functions-exp/src/serializer.test.ts deleted file mode 100644 index 0a675cd01f3..00000000000 --- a/packages-exp/functions-exp/src/serializer.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { expect } from 'chai'; -import { encode, decode } from './serializer'; - -describe('Serializer', () => { - it('encodes null', () => { - expect(encode(null)).to.be.null; - expect(encode(undefined)).to.be.null; - }); - - it('decodes null', () => { - expect(decode(null)).to.be.null; - }); - - it('encodes int', () => { - expect(encode(1)).to.equal(1); - // Number isn't allowed in our own codebase, but we need to test it, in case - // a user passes one in. There's no reason not to support it, and we don't - // want to unintentionally encode them as {}. - // eslint-disable-next-line no-new-wrappers - expect(encode(new Number(1))).to.equal(1); - }); - - it('decodes int', () => { - expect(decode(1)).to.equal(1); - }); - - it('encodes long', () => { - expect(encode(-9223372036854775000)).to.equal(-9223372036854775000); - }); - - it('decodes long', () => { - expect( - decode({ - '@type': 'type.googleapis.com/google.protobuf.Int64Value', - value: '-9223372036854775000' - }) - ).to.equal(-9223372036854775000); - }); - - it('encodes unsigned long', () => { - expect(encode(9223372036854800000)).to.equal(9223372036854800000); - }); - - it('decodes unsigned long', () => { - expect( - decode({ - '@type': 'type.googleapis.com/google.protobuf.UInt64Value', - value: '9223372036854800000' - }) - ).to.equal(9223372036854800000); - }); - - it('encodes double', () => { - expect(encode(1.2)).to.equal(1.2); - }); - - it('decodes double', () => { - expect(decode(1.2)).to.equal(1.2); - }); - - it('encodes string', () => { - expect(encode('hello')).to.equal('hello'); - }); - - it('decodes string', () => { - expect(decode('hello')).to.equal('hello'); - }); - - // TODO(klimt): Make this test more interesting once we have a complex type - // that can be created in JavaScript. - it('encodes array', () => { - expect(encode([1, '2', [3, 4]])).to.deep.equal([1, '2', [3, 4]]); - }); - - it('decodes array', () => { - expect( - decode([ - 1, - '2', - [ - 3, - { - value: '1099511627776', - '@type': 'type.googleapis.com/google.protobuf.Int64Value' - } - ] - ]) - ).to.deep.equal([1, '2', [3, 1099511627776]]); - }); - - // TODO(klimt): Make this test more interesting once we have a complex type - // that can be created in JavaScript. - it('encodes object', () => { - expect( - encode({ - foo: 1, - bar: 'hello', - baz: [1, 2, 3] - }) - ).to.deep.equal({ - foo: 1, - bar: 'hello', - baz: [1, 2, 3] - }); - }); - - it('decodes object', () => { - expect( - decode({ - foo: 1, - bar: 'hello', - baz: [ - 1, - 2, - { - value: '1099511627776', - '@type': 'type.googleapis.com/google.protobuf.Int64Value' - } - ] - }) - ).to.deep.equal({ - foo: 1, - bar: 'hello', - baz: [1, 2, 1099511627776] - }); - }); - - it('fails to encode NaN', () => { - expect(() => encode(NaN)).to.throw(); - }); - - it('fails to decode unknown type', () => { - expect(() => - decode({ - '@type': 'unknown', - value: 'should be ignored' - }) - ).to.throw(); - }); -}); diff --git a/packages-exp/functions-exp/src/serializer.ts b/packages-exp/functions-exp/src/serializer.ts deleted file mode 100644 index bbe2208b025..00000000000 --- a/packages-exp/functions-exp/src/serializer.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -const LONG_TYPE = 'type.googleapis.com/google.protobuf.Int64Value'; -const UNSIGNED_LONG_TYPE = 'type.googleapis.com/google.protobuf.UInt64Value'; - -function mapValues( - // { [k: string]: unknown } is no longer a wildcard assignment target after typescript 3.5 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - o: { [key: string]: any }, - f: (arg0: unknown) => unknown -): object { - const result: { [key: string]: unknown } = {}; - for (const key in o) { - if (o.hasOwnProperty(key)) { - result[key] = f(o[key]); - } - } - return result; -} - -/** - * Takes data and encodes it in a JSON-friendly way, such that types such as - * Date are preserved. - * @internal - * @param data - Data to encode. - */ -export function encode(data: unknown): unknown { - if (data == null) { - return null; - } - if (data instanceof Number) { - data = data.valueOf(); - } - if (typeof data === 'number' && isFinite(data)) { - // Any number in JS is safe to put directly in JSON and parse as a double - // without any loss of precision. - return data; - } - if (data === true || data === false) { - return data; - } - if (Object.prototype.toString.call(data) === '[object String]') { - return data; - } - if (Array.isArray(data)) { - return data.map(x => encode(x)); - } - if (typeof data === 'function' || typeof data === 'object') { - return mapValues(data!, x => encode(x)); - } - // If we got this far, the data is not encodable. - throw new Error('Data cannot be encoded in JSON: ' + data); -} - -/** - * Takes data that's been encoded in a JSON-friendly form and returns a form - * with richer datatypes, such as Dates, etc. - * @internal - * @param json - JSON to convert. - */ -export function decode(json: unknown): unknown { - if (json == null) { - return json; - } - if ((json as { [key: string]: unknown })['@type']) { - switch ((json as { [key: string]: unknown })['@type']) { - case LONG_TYPE: - // Fall through and handle this the same as unsigned. - case UNSIGNED_LONG_TYPE: { - // Technically, this could work return a valid number for malformed - // data if there was a number followed by garbage. But it's just not - // worth all the extra code to detect that case. - const value = Number((json as { [key: string]: unknown })['value']); - if (isNaN(value)) { - throw new Error('Data cannot be decoded from JSON: ' + json); - } - return value; - } - default: { - throw new Error('Data cannot be decoded from JSON: ' + json); - } - } - } - if (Array.isArray(json)) { - return json.map(x => decode(x)); - } - if (typeof json === 'function' || typeof json === 'object') { - return mapValues(json!, x => decode(x)); - } - // Anything else is safe to return. - return json; -} diff --git a/packages-exp/functions-exp/src/service.test.ts b/packages-exp/functions-exp/src/service.test.ts deleted file mode 100644 index 764e323ed03..00000000000 --- a/packages-exp/functions-exp/src/service.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { assert } from 'chai'; -import { createTestService } from '../test/utils'; -import { FunctionsService, useFunctionsEmulator } from './service'; - -describe('Firebase Functions > Service', () => { - describe('simple constructor', () => { - let app: any; - let service: FunctionsService; - - beforeEach(() => { - app = { - options: { - projectId: 'my-project' - } - }; - }); - - it('has valid urls', () => { - service = createTestService(app); - assert.equal( - service._url('foo'), - 'https://us-central1-my-project.cloudfunctions.net/foo' - ); - }); - - it('can use emulator', () => { - service = createTestService(app); - useFunctionsEmulator(service, 'localhost', 5005); - assert.equal( - service._url('foo'), - 'http://localhost:5005/my-project/us-central1/foo' - ); - }); - - it('correctly sets region', () => { - service = createTestService(app, 'my-region'); - assert.equal( - service._url('foo'), - 'https://my-region-my-project.cloudfunctions.net/foo' - ); - }); - - it('correctly sets region with emulator', () => { - service = createTestService(app, 'my-region'); - useFunctionsEmulator(service, 'localhost', 5005); - assert.equal( - service._url('foo'), - 'http://localhost:5005/my-project/my-region/foo' - ); - }); - - it('correctly sets custom domain', () => { - service = createTestService(app, 'https://mydomain.com'); - assert.equal(service._url('foo'), 'https://mydomain.com/foo'); - }); - - it('prefers emulator to custom domain', () => { - const service = createTestService(app, 'https://mydomain.com'); - useFunctionsEmulator(service, 'localhost', 5005); - assert.equal( - service._url('foo'), - 'http://localhost:5005/my-project/us-central1/foo' - ); - }); - }); -}); diff --git a/packages-exp/functions-exp/src/service.ts b/packages-exp/functions-exp/src/service.ts deleted file mode 100644 index 53cc07ade4d..00000000000 --- a/packages-exp/functions-exp/src/service.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp, _FirebaseService } from '@firebase/app-types-exp'; -import { - HttpsCallable, - HttpsCallableResult, - HttpsCallableOptions -} from '@firebase/functions-types-exp'; -import { _errorForResponse, FunctionsError } from './error'; -import { ContextProvider } from './context'; -import { encode, decode } from './serializer'; -import { Provider } from '@firebase/component'; -import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; -import { FirebaseMessagingName } from '@firebase/messaging-types'; - -export const DEFAULT_REGION = 'us-central1'; - -/** - * The response to an http request. - */ -interface HttpResponse { - status: number; - json: HttpResponseBody | null; -} -/** - * Describes the shape of the HttpResponse body. - * It makes functions that would otherwise take {} able to access the - * possible elements in the body more easily - */ -export interface HttpResponseBody { - data?: unknown; - result?: unknown; - error?: { - message?: unknown; - status?: unknown; - details?: unknown; - }; -} - -/** - * Returns a Promise that will be rejected after the given duration. - * The error will be of type FunctionsError. - * - * @param millis Number of milliseconds to wait before rejecting. - */ -function failAfter(millis: number): Promise { - return new Promise((_, reject) => { - setTimeout(() => { - reject(new FunctionsError('deadline-exceeded', 'deadline-exceeded')); - }, millis); - }); -} - -/** - * The main class for the Firebase Functions SDK. - * @internal - */ -export class FunctionsService implements _FirebaseService { - readonly contextProvider: ContextProvider; - emulatorOrigin: string | null = null; - cancelAllRequests: Promise; - deleteService!: () => Promise; - region: string; - customDomain: string | null; - - /** - * Creates a new Functions service for the given app. - * @param app - The FirebaseApp to use. - */ - constructor( - readonly app: FirebaseApp, - authProvider: Provider, - messagingProvider: Provider, - regionOrCustomDomain: string = DEFAULT_REGION, - readonly fetchImpl: typeof fetch - ) { - this.contextProvider = new ContextProvider(authProvider, messagingProvider); - // Cancels all ongoing requests when resolved. - this.cancelAllRequests = new Promise(resolve => { - this.deleteService = () => { - return Promise.resolve(resolve()); - }; - }); - - // Resolve the region or custom domain overload by attempting to parse it. - try { - const url = new URL(regionOrCustomDomain); - this.customDomain = url.origin; - this.region = DEFAULT_REGION; - } catch (e) { - this.customDomain = null; - this.region = regionOrCustomDomain; - } - } - - _delete(): Promise { - return this.deleteService(); - } - - /** - * Returns the URL for a callable with the given name. - * @param name - The name of the callable. - * @internal - */ - _url(name: string): string { - const projectId = this.app.options.projectId; - if (this.emulatorOrigin !== null) { - const origin = this.emulatorOrigin; - return `${origin}/${projectId}/${this.region}/${name}`; - } - - if (this.customDomain !== null) { - return `${this.customDomain}/${name}`; - } - - return `https://${this.region}-${projectId}.cloudfunctions.net/${name}`; - } -} - -/** - * Modify this instance to communicate with the Cloud Functions emulator. - * - * Note: this must be called before this instance has been used to do any operations. - * - * @param host The emulator host (ex: localhost) - * @param port The emulator port (ex: 5001) - * @public - */ -export function useFunctionsEmulator( - functionsInstance: FunctionsService, - host: string, - port: number -): void { - functionsInstance.emulatorOrigin = `http://${host}:${port}`; -} - -/** - * Returns a reference to the callable https trigger with the given name. - * @param name - The name of the trigger. - * @public - */ -export function httpsCallable( - functionsInstance: FunctionsService, - name: string, - options?: HttpsCallableOptions -): HttpsCallable { - return data => { - return call(functionsInstance, name, data, options || {}); - }; -} - -/** - * Does an HTTP POST and returns the completed response. - * @param url The url to post to. - * @param body The JSON body of the post. - * @param headers The HTTP headers to include in the request. - * @return A Promise that will succeed when the request finishes. - */ -async function postJSON( - url: string, - body: unknown, - headers: { [key: string]: string }, - fetchImpl: typeof fetch -): Promise { - headers['Content-Type'] = 'application/json'; - - let response: Response; - try { - response = await fetchImpl(url, { - method: 'POST', - body: JSON.stringify(body), - headers - }); - } catch (e) { - // This could be an unhandled error on the backend, or it could be a - // network error. There's no way to know, since an unhandled error on the - // backend will fail to set the proper CORS header, and thus will be - // treated as a network error by fetch. - return { - status: 0, - json: null - }; - } - let json: HttpResponseBody | null = null; - try { - json = await response.json(); - } catch (e) { - // If we fail to parse JSON, it will fail the same as an empty body. - } - return { - status: response.status, - json - }; -} - -/** - * Calls a callable function asynchronously and returns the result. - * @param name The name of the callable trigger. - * @param data The data to pass as params to the function.s - */ -async function call( - functionsInstance: FunctionsService, - name: string, - data: unknown, - options: HttpsCallableOptions -): Promise { - const url = functionsInstance._url(name); - - // Encode any special types, such as dates, in the input data. - data = encode(data); - const body = { data }; - - // Add a header for the authToken. - const headers: { [key: string]: string } = {}; - const context = await functionsInstance.contextProvider.getContext(); - if (context.authToken) { - headers['Authorization'] = 'Bearer ' + context.authToken; - } - if (context.messagingToken) { - headers['Firebase-Instance-ID-Token'] = context.messagingToken; - } - - // Default timeout to 70s, but let the options override it. - const timeout = options.timeout || 70000; - - const response = await Promise.race([ - postJSON(url, body, headers, functionsInstance.fetchImpl), - failAfter(timeout), - functionsInstance.cancelAllRequests - ]); - - // If service was deleted, interrupted response throws an error. - if (!response) { - throw new FunctionsError( - 'cancelled', - 'Firebase Functions instance was deleted.' - ); - } - - // Check for an error status, regardless of http status. - const error = _errorForResponse(response.status, response.json); - if (error) { - throw error; - } - - if (!response.json) { - throw new FunctionsError('internal', 'Response is not valid JSON object.'); - } - - let responseData = response.json.data; - // TODO(klimt): For right now, allow "result" instead of "data", for - // backwards compatibility. - if (typeof responseData === 'undefined') { - responseData = response.json.result; - } - if (typeof responseData === 'undefined') { - // Consider the response malformed. - throw new FunctionsError('internal', 'Response is missing data field.'); - } - - // Decode any special types, such as dates, in the returned data. - const decodedData = decode(responseData); - - return { data: decodedData }; -} diff --git a/packages-exp/functions-exp/test/utils.ts b/packages-exp/functions-exp/test/utils.ts deleted file mode 100644 index a36dae606c6..00000000000 --- a/packages-exp/functions-exp/test/utils.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseOptions, FirebaseApp } from '@firebase/app-types-exp'; -import { Provider, ComponentContainer } from '@firebase/component'; -import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; -import { FirebaseMessagingName } from '@firebase/messaging-types'; -import { FunctionsService } from '../src/service'; -import { useFunctionsEmulator } from '../src/api'; -import nodeFetch from 'node-fetch'; - -export function makeFakeApp(options: FirebaseOptions = {}): FirebaseApp { - options = { - apiKey: 'apiKey', - projectId: 'projectId', - authDomain: 'authDomain', - messagingSenderId: '1234567890', - databaseURL: 'databaseUrl', - storageBucket: 'storageBucket', - appId: '1:777777777777:web:d93b5ca1475efe57', - ...options - }; - return { - name: 'appName', - options, - automaticDataCollectionEnabled: true - }; -} - -export function createTestService( - app: FirebaseApp, - region?: string, - authProvider = new Provider( - 'auth-internal', - new ComponentContainer('test') - ), - messagingProvider = new Provider( - 'messaging', - new ComponentContainer('test') - ) -): FunctionsService { - const fetchImpl: typeof fetch = - typeof window !== 'undefined' ? fetch.bind(window) : (nodeFetch as any); - const functions = new FunctionsService( - app, - authProvider, - messagingProvider, - region, - fetchImpl - ); - const useEmulator = !!process.env.FIREBASE_FUNCTIONS_EMULATOR_ORIGIN; - if (useEmulator) { - const url = new URL(process.env.FIREBASE_FUNCTIONS_EMULATOR_ORIGIN!); - useFunctionsEmulator( - functions, - url.hostname, - Number.parseInt(url.port, 10) - ); - } - return functions; -} diff --git a/packages-exp/functions-exp/tsconfig.json b/packages-exp/functions-exp/tsconfig.json deleted file mode 100644 index a06ed9a374c..00000000000 --- a/packages-exp/functions-exp/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist" - }, - "exclude": [ - "dist/**/*" - ] -} \ No newline at end of file diff --git a/packages-exp/functions-types-exp/README.md b/packages-exp/functions-types-exp/README.md deleted file mode 100644 index 59e884b28ea..00000000000 --- a/packages-exp/functions-types-exp/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @firebase/functions-types - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/functions-types-exp/api-extractor.json b/packages-exp/functions-types-exp/api-extractor.json deleted file mode 100644 index 42f37a88c4b..00000000000 --- a/packages-exp/functions-types-exp/api-extractor.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/index.d.ts" -} \ No newline at end of file diff --git a/packages-exp/functions-types-exp/index.d.ts b/packages-exp/functions-types-exp/index.d.ts deleted file mode 100644 index 9b529870825..00000000000 --- a/packages-exp/functions-types-exp/index.d.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { FirebaseApp } from '@firebase/app-types-exp'; -import { FirebaseError } from '@firebase/util'; - -/** - * An HttpsCallableResult wraps a single result from a function call. - */ -export interface HttpsCallableResult { - readonly data: any; -} - -/** - * An HttpsCallable is a reference to a "callable" http trigger in - * Google Cloud Functions. - */ -export interface HttpsCallable { - (data?: {} | null): Promise; -} - -/** - * HttpsCallableOptions specify metadata about how calls should be executed. - */ -export interface HttpsCallableOptions { - timeout?: number; // in millis -} - -/** - * `Functions` represents a Functions instance, and is a required argument for - * all Functions operations. - */ -export interface Functions { - /** - * The FirebaseApp this Functions instance is associated with. - */ - app: FirebaseApp; - - /** - * The region the callable Cloud Functions are located in. - * Default is `us-central-1`. - */ - region: string; - - /** - * A custom domain hosting the callable Cloud Functions. - * ex: https://mydomain.com - */ - customDomain: string | null; -} - -/** - * The set of Firebase Functions status codes. The codes are the same at the - * ones exposed by gRPC here: - * https://github.com/grpc/grpc/blob/master/doc/statuscodes.md - * - * Possible values: - * - 'cancelled': The operation was cancelled (typically by the caller). - * - 'unknown': Unknown error or an error from a different error domain. - * - 'invalid-argument': Client specified an invalid argument. Note that this - * differs from 'failed-precondition'. 'invalid-argument' indicates - * arguments that are problematic regardless of the state of the system - * (e.g. an invalid field name). - * - 'deadline-exceeded': Deadline expired before operation could complete. - * For operations that change the state of the system, this error may be - * returned even if the operation has completed successfully. For example, - * a successful response from a server could have been delayed long enough - * for the deadline to expire. - * - 'not-found': Some requested document was not found. - * - 'already-exists': Some document that we attempted to create already - * exists. - * - 'permission-denied': The caller does not have permission to execute the - * specified operation. - * - 'resource-exhausted': Some resource has been exhausted, perhaps a - * per-user quota, or perhaps the entire file system is out of space. - * - 'failed-precondition': Operation was rejected because the system is not - * in a state required for the operation's execution. - * - 'aborted': The operation was aborted, typically due to a concurrency - * issue like transaction aborts, etc. - * - 'out-of-range': Operation was attempted past the valid range. - * - 'unimplemented': Operation is not implemented or not supported/enabled. - * - 'internal': Internal errors. Means some invariants expected by - * underlying system has been broken. If you see one of these errors, - * something is very broken. - * - 'unavailable': The service is currently unavailable. This is most likely - * a transient condition and may be corrected by retrying with a backoff. - * - 'data-loss': Unrecoverable data loss or corruption. - * - 'unauthenticated': The request does not have valid authentication - * credentials for the operation. - */ -export type FunctionsErrorCode = - | 'ok' - | 'cancelled' - | 'unknown' - | 'invalid-argument' - | 'deadline-exceeded' - | 'not-found' - | 'already-exists' - | 'permission-denied' - | 'resource-exhausted' - | 'failed-precondition' - | 'aborted' - | 'out-of-range' - | 'unimplemented' - | 'internal' - | 'unavailable' - | 'data-loss' - | 'unauthenticated'; - -export interface FunctionsError extends FirebaseError { - /** - * A standard error code that will be returned to the client. This also - * determines the HTTP status code of the response, as defined in code.proto. - */ - readonly code: FunctionsErrorCode; - - /** - * Extra data to be converted to JSON and included in the error response. - */ - readonly details?: any; -} - -declare module '@firebase/component' { - interface NameServiceMapping { - 'functions': Functions; - } -} diff --git a/packages-exp/functions-types-exp/package.json b/packages-exp/functions-types-exp/package.json deleted file mode 100644 index f9b3cb02418..00000000000 --- a/packages-exp/functions-types-exp/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@firebase/functions-types-exp", - "version": "0.0.900", - "description": "@firebase/functions Types", - "private": true, - "author": "Firebase (https://firebase.google.com/)", - "license": "Apache-2.0", - "scripts": { - "test": "tsc", - "test:ci": "node ../../scripts/run_tests_in_ci.js", - "build:release": "node ../../scripts/exp/remove-exp.js ./index.d.ts", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn api-report && yarn doc" - }, - "files": [ - "index.d.ts" - ], - "repository": { - "directory": "packages/functions-types", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "devDependencies": { - "typescript": "4.0.5" - } -} diff --git a/packages-exp/functions-types-exp/tsconfig.json b/packages-exp/functions-types-exp/tsconfig.json deleted file mode 100644 index 9a785433d90..00000000000 --- a/packages-exp/functions-types-exp/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "noEmit": true - }, - "exclude": [ - "dist/**/*" - ] -} diff --git a/packages-exp/installations-compat/package.json b/packages-exp/installations-compat/package.json deleted file mode 100644 index 61adef7a637..00000000000 --- a/packages-exp/installations-compat/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "@firebase/installations-compat", - "version": "0.0.900", - "private": true, - "author": "Firebase (https://firebase.google.com/)", - "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", - "browser": "dist/index.esm.js", - "esm2017": "dist/index.esm2017.js", - "typings": "dist/installations-compat.d.ts", - "license": "Apache-2.0", - "files": ["dist"], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c", - "build:deps": "lerna run --scope @firebase/installations-compat --include-dependencies build", - "build:release": "rollup -c rollup.config.release.js", - "dev": "rollup -c -w", - "test": "yarn type-check && yarn test:karma && yarn lint", - "test:ci": "node ../../scripts/run_tests_in_ci.js", - "test:karma": "karma start --single-run", - "test:debug": "karma start --browsers=Chrome --auto-watch", - "type-check": "tsc -p . --noEmit", - "serve": "yarn serve:build && yarn serve:host", - "serve:build": "rollup -c test-app/rollup.config.js", - "serve:host": "http-server -c-1 test-app", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn build && yarn doc" - }, - "repository": { - "directory": "packages-exp/installations-compat", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "devDependencies": { - "@firebase/app-compat": "0.0.900", - "rollup": "2.33.2", - "@rollup/plugin-commonjs": "15.1.0", - "@rollup/plugin-json": "4.1.0", - "@rollup/plugin-node-resolve": "9.0.0", - "rollup-plugin-typescript2": "0.29.0", - "rollup-plugin-uglify": "6.0.4", - "typescript": "4.0.5" - }, - "peerDependencies": { - "@firebase/app-compat": "0.x" - }, - "dependencies": { - "@firebase/installations-exp": "0.0.900", - "@firebase/installations-types-exp": "0.0.900", - "@firebase/util": "0.3.4", - "@firebase/component": "0.1.21", - "idb": "3.0.2", - "tslib": "^1.11.1" - } -} diff --git a/packages-exp/installations-compat/rollup.config.js b/packages-exp/installations-compat/rollup.config.js deleted file mode 100644 index d66e464f546..00000000000 --- a/packages-exp/installations-compat/rollup.config.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [typescriptPlugin({ typescript }), json()]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - } - }), - json({ preferConst: true }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/installations-compat/rollup.config.release.js b/packages-exp/installations-compat/rollup.config.release.js deleted file mode 100644 index 5050932f4a4..00000000000 --- a/packages-exp/installations-compat/rollup.config.release.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - clean: true, - abortOnError: false, - transformers: [importPathTransformer] - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins, - treeshake: { - moduleSideEffects: false - } -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - abortOnError: false, - clean: true, - transformers: [importPathTransformer] - }), - json({ preferConst: true }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins, - treeshake: { - moduleSideEffects: false - } -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/installations-compat/rollup.shared.js b/packages-exp/installations-compat/rollup.shared.js deleted file mode 100644 index ca79f888314..00000000000 --- a/packages-exp/installations-compat/rollup.shared.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import pkg from './package.json'; - -const deps = Object.keys({ ...pkg.peerDependencies, ...pkg.dependencies }); - -/** - * ES5 Builds - */ -export const es5BuildsNoPlugin = [ - { - input: 'src/index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } - ], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -/** - * ES2017 Builds - */ -export const es2017BuildsNoPlugin = [ - { - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; diff --git a/packages-exp/installations-compat/src/index.ts b/packages-exp/installations-compat/src/index.ts deleted file mode 100644 index dab8ad4607d..00000000000 --- a/packages-exp/installations-compat/src/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { firebase } from '@firebase/app-compat'; -import { name, version } from '../package.json'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { Component, ComponentType } from '@firebase/component'; -import { FirebaseInstallations as FirebaseInstallationsCompat } from '@firebase/installations-types'; -import { FirebaseApp } from '@firebase/app-types'; -import { InstallationsCompat } from './installationsCompat'; - -declare module '@firebase/component' { - interface NameServiceMapping { - 'app-compat': FirebaseApp; - 'installations-compat': FirebaseInstallationsCompat; - } -} - -function registerInstallations(instance: _FirebaseNamespace): void { - instance.INTERNAL.registerComponent( - new Component( - 'installations-compat', - container => { - const app = container.getProvider('app-compat').getImmediate()!; - const installations = container - .getProvider('installations-exp') - .getImmediate()!; - return new InstallationsCompat(app, installations); - }, - ComponentType.PUBLIC - ) - ); - - instance.registerVersion(name, version); -} - -registerInstallations(firebase as _FirebaseNamespace); diff --git a/packages-exp/installations-compat/src/installationsCompat.ts b/packages-exp/installations-compat/src/installationsCompat.ts deleted file mode 100644 index 467a9cae4f2..00000000000 --- a/packages-exp/installations-compat/src/installationsCompat.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseInstallations } from '@firebase/installations-types-exp'; -import { FirebaseInstallations as FirebaseInstallationsCompat } from '@firebase/installations-types'; -import { FirebaseApp } from '@firebase/app-types'; -import { FirebaseService } from '@firebase/app-types/private'; -import { - deleteInstallations, - getId, - getToken, - IdChangeCallbackFn, - IdChangeUnsubscribeFn, - onIdChange -} from '@firebase/installations-exp'; - -export class InstallationsCompat - implements FirebaseInstallationsCompat, FirebaseService { - constructor( - public app: FirebaseApp, - private _installations: FirebaseInstallations - ) {} - - getId(): Promise { - return getId(this._installations); - } - getToken(forceRefresh?: boolean): Promise { - return getToken(this._installations, forceRefresh); - } - delete(): Promise { - return deleteInstallations(this._installations); - } - onIdChange(callback: IdChangeCallbackFn): IdChangeUnsubscribeFn { - return onIdChange(this._installations, callback); - } -} diff --git a/packages-exp/installations-compat/src/testing/setup.ts b/packages-exp/installations-compat/src/testing/setup.ts deleted file mode 100644 index 463089fa69b..00000000000 --- a/packages-exp/installations-compat/src/testing/setup.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { use } from 'chai'; -import { restore } from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -use(sinonChai); - -afterEach(async () => { - restore(); -}); diff --git a/packages-exp/installations-compat/src/testing/util.ts b/packages-exp/installations-compat/src/testing/util.ts deleted file mode 100644 index ad124e13236..00000000000 --- a/packages-exp/installations-compat/src/testing/util.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; - -const appName = 'testApp'; -const apiKey = 'AIzaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaA'; -const projectId = 'fis-test-app'; -const appId = '1:777777777777:web:aaaaaaaaaaaaaaaa'; - -export function getFakeApp(): FirebaseApp { - return { - name: appName, - options: { - apiKey, - projectId, - authDomain: 'authDomain', - messagingSenderId: 'messagingSenderId', - databaseURL: 'databaseUrl', - storageBucket: 'storageBucket', - appId - }, - automaticDataCollectionEnabled: true, - delete: async () => {} - }; -} - -export function getFakeInstallations(): FirebaseInstallations { - return { - app: getFakeApp(), - appConfig: { - appName, - projectId, - apiKey, - appId - }, - platformLoggerProvider: null, - _delete: () => Promise.resolve() - }; -} diff --git a/packages-exp/installations-exp/.eslintrc.js b/packages-exp/installations-exp/.eslintrc.js deleted file mode 100644 index ca80aa0f69a..00000000000 --- a/packages-exp/installations-exp/.eslintrc.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module.exports = { - extends: '../../config/.eslintrc.js', - parserOptions: { - project: 'tsconfig.json', - // to make vscode-eslint work with monorepo - // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 - tsconfigRootDir: __dirname - } -}; diff --git a/packages-exp/installations-exp/api-extractor.json b/packages-exp/installations-exp/api-extractor.json deleted file mode 100644 index f291311f711..00000000000 --- a/packages-exp/installations-exp/api-extractor.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/dist/src/index.d.ts", - "dtsRollup": { - "enabled": true - } -} \ No newline at end of file diff --git a/packages-exp/installations-exp/karma.conf.js b/packages-exp/installations-exp/karma.conf.js deleted file mode 100644 index 1699a0681ec..00000000000 --- a/packages-exp/installations-exp/karma.conf.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const karmaBase = require('../../config/karma.base'); - -const files = [`src/**/*.test.ts`]; - -module.exports = function (config) { - const karmaConfig = { - ...karmaBase, - // files to load into karma - files, - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'] - }; - - config.set(karmaConfig); -}; - -module.exports.files = files; diff --git a/packages-exp/installations-exp/package.json b/packages-exp/installations-exp/package.json deleted file mode 100644 index 7ea6761c711..00000000000 --- a/packages-exp/installations-exp/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "@firebase/installations-exp", - "version": "0.0.900", - "private": true, - "author": "Firebase (https://firebase.google.com/)", - "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", - "browser": "dist/index.esm.js", - "esm2017": "dist/index.esm2017.js", - "typings": "dist/installations-exp.d.ts", - "license": "Apache-2.0", - "files": ["dist"], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c && yarn api-report", - "build:deps": "lerna run --scope @firebase/installations-exp --include-dependencies build", - "build:release": "rollup -c rollup.config.release.js && yarn api-report", - "dev": "rollup -c -w", - "test": "yarn type-check && yarn test:karma && yarn lint", - "test:ci": "node ../../scripts/run_tests_in_ci.js", - "test:karma": "karma start --single-run", - "test:debug": "karma start --browsers=Chrome --auto-watch", - "type-check": "tsc -p . --noEmit", - "serve": "yarn serve:build && yarn serve:host", - "serve:build": "rollup -c test-app/rollup.config.js", - "serve:host": "http-server -c-1 test-app", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn build && yarn doc" - }, - "repository": { - "directory": "packages-exp/installations-exp", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "devDependencies": { - "@firebase/app-exp": "0.0.900", - "rollup": "2.35.1", - "@rollup/plugin-commonjs": "15.1.0", - "@rollup/plugin-json": "4.1.0", - "@rollup/plugin-node-resolve": "9.0.0", - "rollup-plugin-typescript2": "0.29.0", - "rollup-plugin-uglify": "6.0.4", - "typescript": "4.0.5" - }, - "peerDependencies": { - "@firebase/app-exp": "0.x", - "@firebase/app-types-exp": "0.x" - }, - "dependencies": { - "@firebase/installations-types-exp": "0.0.900", - "@firebase/util": "0.3.4", - "@firebase/component": "0.1.21", - "idb": "3.0.2", - "tslib": "^1.11.1" - } -} diff --git a/packages-exp/installations-exp/rollup.config.js b/packages-exp/installations-exp/rollup.config.js deleted file mode 100644 index d66e464f546..00000000000 --- a/packages-exp/installations-exp/rollup.config.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [typescriptPlugin({ typescript }), json()]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - } - }), - json({ preferConst: true }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/installations-exp/rollup.config.release.js b/packages-exp/installations-exp/rollup.config.release.js deleted file mode 100644 index 5050932f4a4..00000000000 --- a/packages-exp/installations-exp/rollup.config.release.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - clean: true, - abortOnError: false, - transformers: [importPathTransformer] - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins, - treeshake: { - moduleSideEffects: false - } -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - abortOnError: false, - clean: true, - transformers: [importPathTransformer] - }), - json({ preferConst: true }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins, - treeshake: { - moduleSideEffects: false - } -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/installations-exp/rollup.shared.js b/packages-exp/installations-exp/rollup.shared.js deleted file mode 100644 index ca79f888314..00000000000 --- a/packages-exp/installations-exp/rollup.shared.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import pkg from './package.json'; - -const deps = Object.keys({ ...pkg.peerDependencies, ...pkg.dependencies }); - -/** - * ES5 Builds - */ -export const es5BuildsNoPlugin = [ - { - input: 'src/index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } - ], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -/** - * ES2017 Builds - */ -export const es2017BuildsNoPlugin = [ - { - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; diff --git a/packages-exp/installations-exp/src/api/get-id.test.ts b/packages-exp/installations-exp/src/api/get-id.test.ts deleted file mode 100644 index 77f14a03cb8..00000000000 --- a/packages-exp/installations-exp/src/api/get-id.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { SinonStub, stub } from 'sinon'; -import * as getInstallationEntryModule from '../helpers/get-installation-entry'; -import * as refreshAuthTokenModule from '../helpers/refresh-auth-token'; -import { - RegisteredInstallationEntry, - RequestStatus -} from '../interfaces/installation-entry'; -import { getFakeInstallations } from '../testing/fake-generators'; -import '../testing/setup'; -import { getId } from './get-id'; -import { - FirebaseInstallationsImpl, - AppConfig -} from '../interfaces/installation-impl'; - -const FID = 'disciples-of-the-watch'; - -describe('getId', () => { - let installations: FirebaseInstallationsImpl; - let getInstallationEntrySpy: SinonStub< - [AppConfig], - Promise - >; - - beforeEach(() => { - installations = getFakeInstallations(); - - getInstallationEntrySpy = stub( - getInstallationEntryModule, - 'getInstallationEntry' - ); - }); - - it('returns the FID in InstallationEntry returned by getInstallationEntry', async () => { - getInstallationEntrySpy.resolves({ - installationEntry: { - fid: FID, - registrationStatus: RequestStatus.NOT_STARTED - }, - registrationPromise: Promise.resolve({} as RegisteredInstallationEntry) - }); - - const fid = await getId(installations); - expect(fid).to.equal(FID); - expect(getInstallationEntrySpy).to.be.calledOnce; - }); - - it('calls refreshAuthToken if the installation is registered', async () => { - getInstallationEntrySpy.resolves({ - installationEntry: { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - requestStatus: RequestStatus.NOT_STARTED - } - } - }); - - const refreshAuthTokenSpy = stub( - refreshAuthTokenModule, - 'refreshAuthToken' - ).resolves({ - token: 'authToken', - expiresIn: 123456, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - }); - - await getId(installations); - expect(refreshAuthTokenSpy).to.be.calledOnce; - }); -}); diff --git a/packages-exp/installations-exp/src/api/get-id.ts b/packages-exp/installations-exp/src/api/get-id.ts deleted file mode 100644 index 83a3871d78d..00000000000 --- a/packages-exp/installations-exp/src/api/get-id.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getInstallationEntry } from '../helpers/get-installation-entry'; -import { refreshAuthToken } from '../helpers/refresh-auth-token'; -import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; - -/** - * Creates a Firebase Installation if there isn't one for the app and - * returns the Installation ID. - * - * @public - */ -export async function getId( - installations: FirebaseInstallations -): Promise { - const installationsImpl = installations as FirebaseInstallationsImpl; - const { installationEntry, registrationPromise } = await getInstallationEntry( - installationsImpl.appConfig - ); - - if (registrationPromise) { - registrationPromise.catch(console.error); - } else { - // If the installation is already registered, update the authentication - // token if needed. - refreshAuthToken(installationsImpl).catch(console.error); - } - - return installationEntry.fid; -} diff --git a/packages-exp/installations-exp/src/api/get-installations.ts b/packages-exp/installations-exp/src/api/get-installations.ts deleted file mode 100644 index d2032710ab8..00000000000 --- a/packages-exp/installations-exp/src/api/get-installations.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; -import { _getProvider } from '@firebase/app-exp'; - -/** - * Returns an instance of FirebaseInstallations associated with the given FirebaseApp instance. - * - * @public - */ -export function getInstallations(app: FirebaseApp): FirebaseInstallations { - const installationsImpl = _getProvider( - app, - 'installations-exp' - ).getImmediate(); - return installationsImpl; -} diff --git a/packages-exp/installations-exp/src/api/get-token.test.ts b/packages-exp/installations-exp/src/api/get-token.test.ts deleted file mode 100644 index 430d341b3c2..00000000000 --- a/packages-exp/installations-exp/src/api/get-token.test.ts +++ /dev/null @@ -1,467 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; -import * as createInstallationRequestModule from '../functions/create-installation-request'; -import * as generateAuthTokenRequestModule from '../functions/generate-auth-token-request'; -import { get, set } from '../helpers/idb-manager'; -import { - CompletedAuthToken, - InProgressInstallationEntry, - RegisteredInstallationEntry, - RequestStatus, - UnregisteredInstallationEntry -} from '../interfaces/installation-entry'; -import { getFakeInstallations } from '../testing/fake-generators'; -import '../testing/setup'; -import { TOKEN_EXPIRATION_BUFFER } from '../util/constants'; -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; -import { sleep } from '../util/sleep'; -import { getToken } from './get-token'; -import { - AppConfig, - FirebaseInstallationsImpl -} from '../interfaces/installation-impl'; - -const FID = 'dont-talk-to-strangers'; -const AUTH_TOKEN = 'authToken'; -const NEW_AUTH_TOKEN = 'newAuthToken'; -const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; - -/** - * A map of different states of the database and a function that creates the - * said state. - */ -const setupInstallationEntryMap: Map< - string, - (appConfig: AppConfig) => Promise -> = new Map([ - [ - 'existing and valid auth token', - async (appConfig: AppConfig) => { - const entry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - token: AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - } - }; - await set(appConfig, entry); - } - ], - [ - 'expired auth token', - async (appConfig: AppConfig) => { - const entry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - token: AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - 2 * ONE_WEEK_MS - } - }; - await set(appConfig, entry); - } - ], - [ - 'pending auth token', - async (appConfig: AppConfig) => { - const entry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - requestStatus: RequestStatus.IN_PROGRESS, - requestTime: Date.now() - 3 * 1000 - } - }; - - await set(appConfig, entry); - - // Finish pending request after 500 ms - // eslint-disable-next-line @typescript-eslint/no-floating-promises - sleep(500).then(async () => { - const updatedEntry: RegisteredInstallationEntry = { - ...entry, - authToken: { - token: NEW_AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - } - }; - await set(appConfig, updatedEntry); - }); - } - ], - [ - 'no auth token', - async (appConfig: AppConfig) => { - const entry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - requestStatus: RequestStatus.NOT_STARTED - } - }; - await set(appConfig, entry); - } - ], - [ - 'pending fid registration', - async (appConfig: AppConfig) => { - const entry: InProgressInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.IN_PROGRESS, - registrationTime: Date.now() - 3 * 1000 - }; - - await set(appConfig, entry); - - // Finish pending request after 500 ms - // eslint-disable-next-line @typescript-eslint/no-floating-promises - sleep(500).then(async () => { - const updatedEntry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - token: AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - } - }; - await set(appConfig, updatedEntry); - }); - } - ], - [ - 'unregistered fid', - async (appConfig: AppConfig) => { - const entry: UnregisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.NOT_STARTED - }; - - await set(appConfig, entry); - } - ] -]); - -describe('getToken', () => { - let installations: FirebaseInstallationsImpl; - let createInstallationRequestSpy: SinonStub< - [AppConfig, InProgressInstallationEntry], - Promise - >; - let generateAuthTokenRequestSpy: SinonStub< - [FirebaseInstallationsImpl, RegisteredInstallationEntry], - Promise - >; - - beforeEach(() => { - installations = getFakeInstallations(); - - createInstallationRequestSpy = stub( - createInstallationRequestModule, - 'createInstallationRequest' - ).callsFake(async (_, installationEntry) => { - await sleep(100); // Request would take some time - const result: RegisteredInstallationEntry = { - fid: installationEntry.fid, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - token: NEW_AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - } - }; - return result; - }); - generateAuthTokenRequestSpy = stub( - generateAuthTokenRequestModule, - 'generateAuthTokenRequest' - ).callsFake(async () => { - await sleep(100); // Request would take some time - const result: CompletedAuthToken = { - token: NEW_AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - }; - return result; - }); - }); - - describe('basic functionality', () => { - for (const [title, setup] of setupInstallationEntryMap.entries()) { - describe(`when ${title} in the DB`, () => { - beforeEach(() => setup(installations.appConfig)); - - it('resolves with an auth token', async () => { - const token = await getToken(installations); - expect(token).to.be.oneOf([AUTH_TOKEN, NEW_AUTH_TOKEN]); - }); - - it('saves the token in the DB', async () => { - const token = await getToken(installations); - const installationEntry = (await get( - installations.appConfig - )) as RegisteredInstallationEntry; - expect(installationEntry).not.to.be.undefined; - expect(installationEntry.registrationStatus).to.equal( - RequestStatus.COMPLETED - ); - expect(installationEntry.authToken.requestStatus).to.equal( - RequestStatus.COMPLETED - ); - expect( - (installationEntry.authToken as CompletedAuthToken).token - ).to.equal(token); - }); - - it('returns the same token on subsequent calls', async () => { - const token1 = await getToken(installations); - const token2 = await getToken(installations); - expect(token1).to.equal(token2); - }); - }); - } - }); - - describe('when there is no FID in the DB', () => { - it('gets the token by registering a new FID', async () => { - await getToken(installations); - expect(createInstallationRequestSpy).to.be.called; - expect(generateAuthTokenRequestSpy).not.to.be.called; - }); - - it('does not register a new FID on subsequent calls', async () => { - await getToken(installations); - await getToken(installations); - expect(createInstallationRequestSpy).to.be.calledOnce; - }); - - it('throws if the app is offline', async () => { - stub(navigator, 'onLine').value(false); - - await expect(getToken(installations)).to.be.rejected; - }); - }); - - describe('when there is a FID in the DB, but no auth token', () => { - let installationEntry: RegisteredInstallationEntry; - - beforeEach(async () => { - installationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - requestStatus: RequestStatus.NOT_STARTED - } - }; - await set(installations.appConfig, installationEntry); - }); - - it('gets the token by calling generateAuthToken', async () => { - await getToken(installations); - expect(generateAuthTokenRequestSpy).to.be.called; - expect(createInstallationRequestSpy).not.to.be.called; - }); - - it('does not call generateAuthToken twice on subsequent calls', async () => { - await getToken(installations); - await getToken(installations); - expect(generateAuthTokenRequestSpy).to.be.calledOnce; - }); - - it('does not call generateAuthToken twice on simultaneous calls', async () => { - await Promise.all([getToken(installations), getToken(installations)]); - expect(generateAuthTokenRequestSpy).to.be.calledOnce; - }); - - it('throws if the app is offline', async () => { - stub(navigator, 'onLine').value(false); - - await expect(getToken(installations)).to.be.rejected; - }); - - describe('and the server returns an error', () => { - it('removes the FID from the DB if the server returns a 401 response', async () => { - generateAuthTokenRequestSpy.callsFake(async () => { - throw ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { - requestName: 'Generate Auth Token', - serverCode: 401, - serverStatus: 'UNAUTHENTICATED', - serverMessage: 'Invalid Authentication.' - }); - }); - - await expect(getToken(installations)).to.be.rejected; - await expect(get(installations.appConfig)).to.eventually.be.undefined; - }); - - it('removes the FID from the DB if the server returns a 404 response', async () => { - generateAuthTokenRequestSpy.callsFake(async () => { - throw ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { - requestName: 'Generate Auth Token', - serverCode: 404, - serverStatus: 'NOT_FOUND', - serverMessage: 'FID not found.' - }); - }); - - await expect(getToken(installations)).to.be.rejected; - await expect(get(installations.appConfig)).to.eventually.be.undefined; - }); - - it('does not remove the FID from the DB if the server returns any other response', async () => { - generateAuthTokenRequestSpy.callsFake(async () => { - throw ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { - requestName: 'Generate Auth Token', - serverCode: 500, - serverStatus: 'INTERNAL', - serverMessage: 'Internal server error.' - }); - }); - - await expect(getToken(installations)).to.be.rejected; - await expect(get(installations.appConfig)).to.eventually.deep.equal( - installationEntry - ); - }); - }); - }); - - describe('when there is a registered auth token in the DB', () => { - beforeEach(async () => { - const installationEntry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - token: AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - } - }; - await set(installations.appConfig, installationEntry); - }); - - it('does not call any server APIs', async () => { - await getToken(installations); - expect(createInstallationRequestSpy).not.to.be.called; - expect(generateAuthTokenRequestSpy).not.to.be.called; - }); - - it('refreshes the token if forceRefresh is true', async () => { - const token = await getToken(installations, true); - expect(token).to.equal(NEW_AUTH_TOKEN); - expect(generateAuthTokenRequestSpy).to.be.called; - }); - - it('works even if the app is offline', async () => { - stub(navigator, 'onLine').value(false); - - const token = await getToken(installations); - expect(token).to.equal(AUTH_TOKEN); - }); - - it('throws if the app is offline and forceRefresh is true', async () => { - stub(navigator, 'onLine').value(false); - - await expect(getToken(installations, true)).to.be.rejected; - }); - }); - - describe('when there is an auth token that is about to expire in the DB', () => { - let clock: SinonFakeTimers; - - beforeEach(async () => { - clock = useFakeTimers({ shouldAdvanceTime: true }); - const installationEntry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - token: AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: - // Expires in ten minutes - Date.now() - ONE_WEEK_MS + TOKEN_EXPIRATION_BUFFER + 10 * 60 * 1000 - } - }; - await set(installations.appConfig, installationEntry); - }); - - it('returns a different token after expiration', async () => { - const token1 = await getToken(installations); - expect(token1).to.equal(AUTH_TOKEN); - - // Wait 30 minutes. - clock.tick('30:00'); - - const token2 = await getToken(installations); - await expect(token2).to.equal(NEW_AUTH_TOKEN); - await expect(token2).not.to.equal(token1); - }); - }); - - describe('when there is an expired auth token in the DB', () => { - beforeEach(async () => { - const installationEntry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - token: AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - 2 * ONE_WEEK_MS - } - }; - await set(installations.appConfig, installationEntry); - }); - - it('returns a different token', async () => { - const token = await getToken(installations); - expect(token).to.equal(NEW_AUTH_TOKEN); - expect(generateAuthTokenRequestSpy).to.be.called; - }); - - it('throws if the app is offline', async () => { - stub(navigator, 'onLine').value(false); - - await expect(getToken(installations)).to.be.rejected; - }); - }); -}); diff --git a/packages-exp/installations-exp/src/api/get-token.ts b/packages-exp/installations-exp/src/api/get-token.ts deleted file mode 100644 index 53cb89899dd..00000000000 --- a/packages-exp/installations-exp/src/api/get-token.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getInstallationEntry } from '../helpers/get-installation-entry'; -import { refreshAuthToken } from '../helpers/refresh-auth-token'; -import { - FirebaseInstallationsImpl, - AppConfig -} from '../interfaces/installation-impl'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; - -/** - * Returns an Installation auth token, identifying the current Firebase Installation. - * - * @public - */ -export async function getToken( - installations: FirebaseInstallations, - forceRefresh = false -): Promise { - const installationsImpl = installations as FirebaseInstallationsImpl; - await completeInstallationRegistration(installationsImpl.appConfig); - - // At this point we either have a Registered Installation in the DB, or we've - // already thrown an error. - const authToken = await refreshAuthToken(installationsImpl, forceRefresh); - return authToken.token; -} - -async function completeInstallationRegistration( - appConfig: AppConfig -): Promise { - const { registrationPromise } = await getInstallationEntry(appConfig); - - if (registrationPromise) { - // A createInstallation request is in progress. Wait until it finishes. - await registrationPromise; - } -} diff --git a/packages-exp/installations-exp/src/api/on-id-change.ts b/packages-exp/installations-exp/src/api/on-id-change.ts deleted file mode 100644 index a9e4360dfd4..00000000000 --- a/packages-exp/installations-exp/src/api/on-id-change.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { addCallback, removeCallback } from '../helpers/fid-changed'; -import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; - -/** - * An user defined callback function that gets called when Installations ID changes. - * - * @public - */ -export type IdChangeCallbackFn = (installationId: string) => void; -/** - * Unsubscribe a callback function previously added via {@link #IdChangeCallbackFn}. - * - * @public - */ -export type IdChangeUnsubscribeFn = () => void; - -/** - * Sets a new callback that will get called when Installation ID changes. - * Returns an unsubscribe function that will remove the callback when called. - * - * @public - */ -export function onIdChange( - installations: FirebaseInstallations, - callback: IdChangeCallbackFn -): IdChangeUnsubscribeFn { - const { appConfig } = installations as FirebaseInstallationsImpl; - - addCallback(appConfig, callback); - return () => { - removeCallback(appConfig, callback); - }; -} diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts deleted file mode 100644 index 267ad40a293..00000000000 --- a/packages-exp/installations-exp/src/functions/config.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _registerComponent, _getProvider } from '@firebase/app-exp'; -import { - Component, - ComponentType, - InstanceFactory, - ComponentContainer -} from '@firebase/component'; -import { getId, getToken } from '../api/index'; -import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; -import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; -import { extractAppConfig } from '../helpers/extract-app-config'; - -const INSTALLATIONS_NAME = 'installations-exp'; -const INSTALLATIONS_NAME_INTERNAL = 'installations-exp-internal'; - -const publicFactory: InstanceFactory<'installations-exp'> = ( - container: ComponentContainer -) => { - const app = container.getProvider('app-exp').getImmediate(); - // Throws if app isn't configured properly. - const appConfig = extractAppConfig(app); - const platformLoggerProvider = _getProvider(app, 'platform-logger'); - - const installationsImpl: FirebaseInstallationsImpl = { - app, - appConfig, - platformLoggerProvider, - _delete: () => Promise.resolve() - }; - return installationsImpl; -}; - -const internalFactory: InstanceFactory<'installations-exp-internal'> = ( - container: ComponentContainer -) => { - const app = container.getProvider('app-exp').getImmediate(); - // Internal FIS instance relies on public FIS instance. - const installations = _getProvider(app, INSTALLATIONS_NAME).getImmediate(); - - const installationsInternal: _FirebaseInstallationsInternal = { - getId: () => getId(installations), - getToken: (forceRefresh?: boolean) => getToken(installations, forceRefresh) - }; - return installationsInternal; -}; - -export function registerInstallations(): void { - _registerComponent( - new Component(INSTALLATIONS_NAME, publicFactory, ComponentType.PUBLIC) - ); - _registerComponent( - new Component( - INSTALLATIONS_NAME_INTERNAL, - internalFactory, - ComponentType.PRIVATE - ) - ); -} diff --git a/packages-exp/installations-exp/src/functions/create-installation-request.test.ts b/packages-exp/installations-exp/src/functions/create-installation-request.test.ts deleted file mode 100644 index 67f00585595..00000000000 --- a/packages-exp/installations-exp/src/functions/create-installation-request.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseError } from '@firebase/util'; -import { expect } from 'chai'; -import { SinonStub, stub } from 'sinon'; -import { CreateInstallationResponse } from '../interfaces/api-response'; -import { AppConfig } from '../interfaces/installation-impl'; -import { - InProgressInstallationEntry, - RequestStatus -} from '../interfaces/installation-entry'; -import { compareHeaders } from '../testing/compare-headers'; -import { getFakeAppConfig } from '../testing/fake-generators'; -import '../testing/setup'; -import { - INSTALLATIONS_API_URL, - INTERNAL_AUTH_VERSION, - PACKAGE_VERSION -} from '../util/constants'; -import { ErrorResponse } from './common'; -import { createInstallationRequest } from './create-installation-request'; - -const FID = 'defenders-of-the-faith'; - -describe('createInstallationRequest', () => { - let appConfig: AppConfig; - let fetchSpy: SinonStub<[RequestInfo, RequestInit?], Promise>; - let inProgressInstallationEntry: InProgressInstallationEntry; - let response: CreateInstallationResponse; - - beforeEach(() => { - appConfig = getFakeAppConfig(); - - inProgressInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.IN_PROGRESS, - registrationTime: Date.now() - }; - - response = { - refreshToken: 'refreshToken', - authToken: { - token: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', - expiresIn: '604800s' - }, - fid: FID - }; - fetchSpy = stub(self, 'fetch'); - }); - - describe('successful request', () => { - beforeEach(() => { - fetchSpy.resolves(new Response(JSON.stringify(response))); - }); - - it('registers a pending InstallationEntry', async () => { - const registeredInstallationEntry = await createInstallationRequest( - appConfig, - inProgressInstallationEntry - ); - expect(registeredInstallationEntry.registrationStatus).to.equal( - RequestStatus.COMPLETED - ); - }); - - it('calls the createInstallation server API with correct parameters', async () => { - const expectedHeaders = new Headers({ - 'Content-Type': 'application/json', - Accept: 'application/json', - 'x-goog-api-key': 'apiKey' - }); - const expectedBody = { - fid: FID, - authVersion: INTERNAL_AUTH_VERSION, - appId: appConfig.appId, - sdkVersion: PACKAGE_VERSION - }; - const expectedRequest: RequestInit = { - method: 'POST', - headers: expectedHeaders, - body: JSON.stringify(expectedBody) - }; - const expectedEndpoint = `${INSTALLATIONS_API_URL}/projects/projectId/installations`; - - await createInstallationRequest(appConfig, inProgressInstallationEntry); - expect(fetchSpy).to.be.calledOnceWith(expectedEndpoint, expectedRequest); - const actualHeaders = fetchSpy.lastCall.lastArg.headers; - compareHeaders(expectedHeaders, actualHeaders); - }); - }); - - it('returns the FID from the request if the response does not contain one', async () => { - response = { - refreshToken: 'refreshToken', - authToken: { - token: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', - expiresIn: '604800s' - } - }; - fetchSpy.resolves(new Response(JSON.stringify(response))); - - const registeredInstallationEntry = await createInstallationRequest( - appConfig, - inProgressInstallationEntry - ); - expect(registeredInstallationEntry.fid).to.equal(FID); - }); - - describe('failed request', () => { - it('throws a FirebaseError with the error information from the server', async () => { - const errorResponse: ErrorResponse = { - error: { - code: 409, - message: 'Requested entity already exists', - status: 'ALREADY_EXISTS' - } - }; - - fetchSpy.resolves( - new Response(JSON.stringify(errorResponse), { status: 409 }) - ); - - await expect( - createInstallationRequest(appConfig, inProgressInstallationEntry) - ).to.be.rejectedWith(FirebaseError); - }); - - it('retries once if the server returns a 5xx error', async () => { - const errorResponse: ErrorResponse = { - error: { - code: 500, - message: 'Internal server error', - status: 'SERVER_ERROR' - } - }; - - fetchSpy - .onCall(0) - .resolves(new Response(JSON.stringify(errorResponse), { status: 500 })); - fetchSpy.onCall(1).resolves(new Response(JSON.stringify(response))); - - await expect( - createInstallationRequest(appConfig, inProgressInstallationEntry) - ).to.be.fulfilled; - expect(fetchSpy).to.be.calledTwice; - }); - }); -}); diff --git a/packages-exp/installations-exp/src/functions/create-installation-request.ts b/packages-exp/installations-exp/src/functions/create-installation-request.ts deleted file mode 100644 index fe8242613f6..00000000000 --- a/packages-exp/installations-exp/src/functions/create-installation-request.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CreateInstallationResponse } from '../interfaces/api-response'; -import { - InProgressInstallationEntry, - RegisteredInstallationEntry, - RequestStatus -} from '../interfaces/installation-entry'; -import { INTERNAL_AUTH_VERSION, PACKAGE_VERSION } from '../util/constants'; -import { - extractAuthTokenInfoFromResponse, - getErrorFromResponse, - getHeaders, - getInstallationsEndpoint, - retryIfServerError -} from './common'; -import { AppConfig } from '../interfaces/installation-impl'; - -export async function createInstallationRequest( - appConfig: AppConfig, - { fid }: InProgressInstallationEntry -): Promise { - const endpoint = getInstallationsEndpoint(appConfig); - - const headers = getHeaders(appConfig); - const body = { - fid, - authVersion: INTERNAL_AUTH_VERSION, - appId: appConfig.appId, - sdkVersion: PACKAGE_VERSION - }; - - const request: RequestInit = { - method: 'POST', - headers, - body: JSON.stringify(body) - }; - - const response = await retryIfServerError(() => fetch(endpoint, request)); - if (response.ok) { - const responseValue: CreateInstallationResponse = await response.json(); - const registeredInstallationEntry: RegisteredInstallationEntry = { - fid: responseValue.fid || fid, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: responseValue.refreshToken, - authToken: extractAuthTokenInfoFromResponse(responseValue.authToken) - }; - return registeredInstallationEntry; - } else { - throw await getErrorFromResponse('Create Installation', response); - } -} diff --git a/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts b/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts deleted file mode 100644 index 68c069e6bf4..00000000000 --- a/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseError } from '@firebase/util'; -import { expect } from 'chai'; -import { SinonStub, stub } from 'sinon'; -import { AppConfig } from '../interfaces/installation-impl'; -import { - RegisteredInstallationEntry, - RequestStatus -} from '../interfaces/installation-entry'; -import { compareHeaders } from '../testing/compare-headers'; -import { getFakeAppConfig } from '../testing/fake-generators'; -import '../testing/setup'; -import { - INSTALLATIONS_API_URL, - INTERNAL_AUTH_VERSION -} from '../util/constants'; -import { ErrorResponse } from './common'; -import { deleteInstallationRequest } from './delete-installation-request'; - -const FID = 'foreclosure-of-a-dream'; - -describe('deleteInstallationRequest', () => { - let appConfig: AppConfig; - let fetchSpy: SinonStub<[RequestInfo, RequestInit?], Promise>; - let registeredInstallationEntry: RegisteredInstallationEntry; - - beforeEach(() => { - appConfig = getFakeAppConfig(); - - registeredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - requestStatus: RequestStatus.NOT_STARTED - } - }; - - fetchSpy = stub(self, 'fetch'); - }); - - describe('successful request', () => { - beforeEach(() => { - fetchSpy.resolves(new Response()); - }); - - it('calls the deleteInstallation server API with correct parameters', async () => { - const expectedHeaders = new Headers({ - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: `${INTERNAL_AUTH_VERSION} refreshToken`, - 'x-goog-api-key': 'apiKey' - }); - const expectedRequest: RequestInit = { - method: 'DELETE', - headers: expectedHeaders - }; - const expectedEndpoint = `${INSTALLATIONS_API_URL}/projects/projectId/installations/${FID}`; - - await deleteInstallationRequest(appConfig, registeredInstallationEntry); - - expect(fetchSpy).to.be.calledOnceWith(expectedEndpoint, expectedRequest); - const actualHeaders = fetchSpy.lastCall.lastArg.headers; - compareHeaders(expectedHeaders, actualHeaders); - }); - }); - - describe('failed request', () => { - it('throws a FirebaseError with the error information from the server', async () => { - const errorResponse: ErrorResponse = { - error: { - code: 409, - message: 'Requested entity already exists', - status: 'ALREADY_EXISTS' - } - }; - - fetchSpy.resolves( - new Response(JSON.stringify(errorResponse), { status: 409 }) - ); - - await expect( - deleteInstallationRequest(appConfig, registeredInstallationEntry) - ).to.be.rejectedWith(FirebaseError); - }); - - it('retries once if the server returns a 5xx error', async () => { - const errorResponse: ErrorResponse = { - error: { - code: 500, - message: 'Internal server error', - status: 'SERVER_ERROR' - } - }; - - fetchSpy - .onCall(0) - .resolves(new Response(JSON.stringify(errorResponse), { status: 500 })); - fetchSpy.onCall(1).resolves(new Response()); - - await expect( - deleteInstallationRequest(appConfig, registeredInstallationEntry) - ).to.be.fulfilled; - expect(fetchSpy).to.be.calledTwice; - }); - }); -}); diff --git a/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts b/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts deleted file mode 100644 index cf1d4231ef4..00000000000 --- a/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseError } from '@firebase/util'; -import { expect } from 'chai'; -import { SinonStub, stub } from 'sinon'; -import { GenerateAuthTokenResponse } from '../interfaces/api-response'; -import { - CompletedAuthToken, - RegisteredInstallationEntry, - RequestStatus -} from '../interfaces/installation-entry'; -import { compareHeaders } from '../testing/compare-headers'; -import { getFakeInstallations } from '../testing/fake-generators'; -import '../testing/setup'; -import { - INSTALLATIONS_API_URL, - INTERNAL_AUTH_VERSION, - PACKAGE_VERSION -} from '../util/constants'; -import { ErrorResponse } from './common'; -import { generateAuthTokenRequest } from './generate-auth-token-request'; -import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; - -const FID = 'evil-has-no-boundaries'; - -describe('generateAuthTokenRequest', () => { - let installations: FirebaseInstallationsImpl; - let fetchSpy: SinonStub<[RequestInfo, RequestInit?], Promise>; - let registeredInstallationEntry: RegisteredInstallationEntry; - let response: GenerateAuthTokenResponse; - - beforeEach(() => { - installations = getFakeInstallations(); - - registeredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - requestStatus: RequestStatus.NOT_STARTED - } - }; - - response = { - token: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', - expiresIn: '604800s' - }; - - fetchSpy = stub(self, 'fetch'); - }); - - describe('successful request', () => { - beforeEach(() => { - fetchSpy.resolves(new Response(JSON.stringify(response))); - }); - - it('fetches a new Authentication Token', async () => { - const completedAuthToken: CompletedAuthToken = await generateAuthTokenRequest( - installations, - registeredInstallationEntry - ); - expect(completedAuthToken.requestStatus).to.equal( - RequestStatus.COMPLETED - ); - }); - - it('calls the generateAuthToken server API with correct parameters', async () => { - const expectedHeaders = new Headers({ - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: `${INTERNAL_AUTH_VERSION} refreshToken`, - 'x-goog-api-key': 'apiKey', - 'x-firebase-client': 'a/1.2.3 b/2.3.4' - }); - const expectedBody = { - installation: { - sdkVersion: PACKAGE_VERSION - } - }; - const expectedRequest: RequestInit = { - method: 'POST', - headers: expectedHeaders, - body: JSON.stringify(expectedBody) - }; - const expectedEndpoint = `${INSTALLATIONS_API_URL}/projects/projectId/installations/${FID}/authTokens:generate`; - - await generateAuthTokenRequest( - installations, - registeredInstallationEntry - ); - - expect(fetchSpy).to.be.calledOnceWith(expectedEndpoint, expectedRequest); - const actualHeaders = fetchSpy.lastCall.lastArg.headers; - compareHeaders(expectedHeaders, actualHeaders); - }); - }); - - describe('failed request', () => { - it('throws a FirebaseError with the error information from the server', async () => { - const errorResponse: ErrorResponse = { - error: { - code: 409, - message: 'Requested entity already exists', - status: 'ALREADY_EXISTS' - } - }; - - fetchSpy.resolves( - new Response(JSON.stringify(errorResponse), { status: 409 }) - ); - - await expect( - generateAuthTokenRequest(installations, registeredInstallationEntry) - ).to.be.rejectedWith(FirebaseError); - }); - - it('retries once if the server returns a 5xx error', async () => { - const errorResponse: ErrorResponse = { - error: { - code: 500, - message: 'Internal server error', - status: 'SERVER_ERROR' - } - }; - - fetchSpy - .onCall(0) - .resolves(new Response(JSON.stringify(errorResponse), { status: 500 })); - fetchSpy.onCall(1).resolves(new Response(JSON.stringify(response))); - - await expect( - generateAuthTokenRequest(installations, registeredInstallationEntry) - ).to.be.fulfilled; - expect(fetchSpy).to.be.calledTwice; - }); - }); -}); diff --git a/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts b/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts deleted file mode 100644 index d662745d300..00000000000 --- a/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { GenerateAuthTokenResponse } from '../interfaces/api-response'; -import { - CompletedAuthToken, - RegisteredInstallationEntry -} from '../interfaces/installation-entry'; -import { PACKAGE_VERSION } from '../util/constants'; -import { - extractAuthTokenInfoFromResponse, - getErrorFromResponse, - getHeadersWithAuth, - getInstallationsEndpoint, - retryIfServerError -} from './common'; -import { - FirebaseInstallationsImpl, - AppConfig -} from '../interfaces/installation-impl'; - -export async function generateAuthTokenRequest( - { appConfig, platformLoggerProvider }: FirebaseInstallationsImpl, - installationEntry: RegisteredInstallationEntry -): Promise { - const endpoint = getGenerateAuthTokenEndpoint(appConfig, installationEntry); - - const headers = getHeadersWithAuth(appConfig, installationEntry); - - // If platform logger exists, add the platform info string to the header. - const platformLogger = platformLoggerProvider.getImmediate({ - optional: true - }); - if (platformLogger) { - headers.append('x-firebase-client', platformLogger.getPlatformInfoString()); - } - - const body = { - installation: { - sdkVersion: PACKAGE_VERSION - } - }; - - const request: RequestInit = { - method: 'POST', - headers, - body: JSON.stringify(body) - }; - - const response = await retryIfServerError(() => fetch(endpoint, request)); - if (response.ok) { - const responseValue: GenerateAuthTokenResponse = await response.json(); - const completedAuthToken: CompletedAuthToken = extractAuthTokenInfoFromResponse( - responseValue - ); - return completedAuthToken; - } else { - throw await getErrorFromResponse('Generate Auth Token', response); - } -} - -function getGenerateAuthTokenEndpoint( - appConfig: AppConfig, - { fid }: RegisteredInstallationEntry -): string { - return `${getInstallationsEndpoint(appConfig)}/${fid}/authTokens:generate`; -} diff --git a/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.test.ts b/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.test.ts deleted file mode 100644 index 18e56f436f2..00000000000 --- a/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import '../testing/setup'; -import { bufferToBase64UrlSafe } from './buffer-to-base64-url-safe'; - -const str = 'hello world'; -const TYPED_ARRAY_REPRESENTATION = new Uint8Array(str.length); -for (let i = 0; i < str.length; i++) { - TYPED_ARRAY_REPRESENTATION[i] = str.charCodeAt(i); -} - -const BASE_64_REPRESENTATION = btoa(str); - -describe('bufferToBase64', () => { - it('returns a base64 representation of a Uint8Array', () => { - expect(bufferToBase64UrlSafe(TYPED_ARRAY_REPRESENTATION)).to.equal( - BASE_64_REPRESENTATION - ); - }); -}); diff --git a/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.ts b/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.ts deleted file mode 100644 index c336ce63528..00000000000 --- a/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export function bufferToBase64UrlSafe(array: Uint8Array): string { - const b64 = btoa(String.fromCharCode(...array)); - return b64.replace(/\+/g, '-').replace(/\//g, '_'); -} diff --git a/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts b/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts deleted file mode 100644 index 06a7ec2dcb6..00000000000 --- a/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseError } from '@firebase/util'; -import { expect } from 'chai'; -import { AppConfig } from '../interfaces/installation-impl'; -import { getFakeApp } from '../testing/fake-generators'; -import '../testing/setup'; -import { extractAppConfig } from './extract-app-config'; - -describe('extractAppConfig', () => { - it('returns AppConfig if the argument is a FirebaseApp object that includes an appId', () => { - const firebaseApp = getFakeApp(); - const expected: AppConfig = { - appName: 'appName', - apiKey: 'apiKey', - projectId: 'projectId', - appId: '1:777777777777:web:d93b5ca1475efe57' - }; - expect(extractAppConfig(firebaseApp)).to.deep.equal(expected); - }); - - it('throws if a necessary value is missing', () => { - expect(() => extractAppConfig(undefined as any)).to.throw(FirebaseError); - - let firebaseApp = getFakeApp(); - delete (firebaseApp as any).name; - expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); - - firebaseApp = getFakeApp(); - delete (firebaseApp as any).options; - expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); - - firebaseApp = getFakeApp(); - delete firebaseApp.options.projectId; - expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); - - firebaseApp = getFakeApp(); - delete firebaseApp.options.apiKey; - expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); - - firebaseApp = getFakeApp(); - delete firebaseApp.options.appId; - expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); - }); -}); diff --git a/packages-exp/installations-exp/src/helpers/extract-app-config.ts b/packages-exp/installations-exp/src/helpers/extract-app-config.ts deleted file mode 100644 index e1390d4ab83..00000000000 --- a/packages-exp/installations-exp/src/helpers/extract-app-config.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp, FirebaseOptions } from '@firebase/app-types-exp'; -import { FirebaseError } from '@firebase/util'; -import { AppConfig } from '../interfaces/installation-impl'; -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; - -export function extractAppConfig(app: FirebaseApp): AppConfig { - if (!app || !app.options) { - throw getMissingValueError('App Configuration'); - } - - if (!app.name) { - throw getMissingValueError('App Name'); - } - - // Required app config keys - const configKeys: Array = [ - 'projectId', - 'apiKey', - 'appId' - ]; - - for (const keyName of configKeys) { - if (!app.options[keyName]) { - throw getMissingValueError(keyName); - } - } - - return { - appName: app.name, - projectId: app.options.projectId!, - apiKey: app.options.apiKey!, - appId: app.options.appId! - }; -} - -function getMissingValueError(valueName: string): FirebaseError { - return ERROR_FACTORY.create(ErrorCode.MISSING_APP_CONFIG_VALUES, { - valueName - }); -} diff --git a/packages-exp/installations-exp/src/helpers/fid-changed.test.ts b/packages-exp/installations-exp/src/helpers/fid-changed.test.ts deleted file mode 100644 index 458bc1d097d..00000000000 --- a/packages-exp/installations-exp/src/helpers/fid-changed.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { stub } from 'sinon'; -import '../testing/setup'; -import { AppConfig } from '../interfaces/installation-impl'; -import { - fidChanged, - addCallback, - removeCallback -} from '../helpers/fid-changed'; -import { getFakeAppConfig } from '../testing/fake-generators'; - -const FID = 'evil-lies-in-every-man'; - -describe('onIdChange', () => { - describe('with single app', () => { - let appConfig: AppConfig; - - beforeEach(() => { - appConfig = getFakeAppConfig(); - }); - - it('calls the provided callback when FID changes', () => { - const stubFn = stub(); - addCallback(appConfig, stubFn); - - fidChanged(appConfig, FID); - - expect(stubFn).to.have.been.calledOnceWith(FID); - }); - - it('calls multiple callbacks', () => { - const stubA = stub(); - addCallback(appConfig, stubA); - const stubB = stub(); - addCallback(appConfig, stubB); - - fidChanged(appConfig, FID); - - expect(stubA).to.have.been.calledOnceWith(FID); - expect(stubB).to.have.been.calledOnceWith(FID); - }); - - it('does not call removed callbacks', () => { - const stubFn = stub(); - addCallback(appConfig, stubFn); - - removeCallback(appConfig, stubFn); - fidChanged(appConfig, FID); - - expect(stubFn).not.to.have.been.called; - }); - - it('does not throw when removeCallback is called multiple times', () => { - const stubFn = stub(); - addCallback(appConfig, stubFn); - - removeCallback(appConfig, stubFn); - removeCallback(appConfig, stubFn); - fidChanged(appConfig, FID); - - expect(stubFn).not.to.have.been.called; - }); - }); - - describe('with multiple apps', () => { - let appConfigA: AppConfig; - let appConfigB: AppConfig; - - beforeEach(() => { - appConfigA = getFakeAppConfig(); - appConfigB = getFakeAppConfig({ appName: 'differentAppName' }); - }); - - it('calls the correct callback when FID changes', () => { - const stubA = stub(); - addCallback(appConfigA, stubA); - const stubB = stub(); - addCallback(appConfigB, stubB); - - fidChanged(appConfigA, FID); - - expect(stubA).to.have.been.calledOnceWith(FID); - expect(stubB).not.to.have.been.called; - }); - }); -}); diff --git a/packages-exp/installations-exp/src/helpers/fid-changed.ts b/packages-exp/installations-exp/src/helpers/fid-changed.ts deleted file mode 100644 index 63f04d75cea..00000000000 --- a/packages-exp/installations-exp/src/helpers/fid-changed.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getKey } from '../util/get-key'; -import { AppConfig } from '../interfaces/installation-impl'; -import { IdChangeCallbackFn } from '../api'; - -const fidChangeCallbacks: Map> = new Map(); - -/** - * Calls the onIdChange callbacks with the new FID value, and broadcasts the - * change to other tabs. - */ -export function fidChanged(appConfig: AppConfig, fid: string): void { - const key = getKey(appConfig); - - callFidChangeCallbacks(key, fid); - broadcastFidChange(key, fid); -} - -export function addCallback( - appConfig: AppConfig, - callback: IdChangeCallbackFn -): void { - // Open the broadcast channel if it's not already open, - // to be able to listen to change events from other tabs. - getBroadcastChannel(); - - const key = getKey(appConfig); - - let callbackSet = fidChangeCallbacks.get(key); - if (!callbackSet) { - callbackSet = new Set(); - fidChangeCallbacks.set(key, callbackSet); - } - callbackSet.add(callback); -} - -export function removeCallback( - appConfig: AppConfig, - callback: IdChangeCallbackFn -): void { - const key = getKey(appConfig); - - const callbackSet = fidChangeCallbacks.get(key); - - if (!callbackSet) { - return; - } - - callbackSet.delete(callback); - if (callbackSet.size === 0) { - fidChangeCallbacks.delete(key); - } - - // Close broadcast channel if there are no more callbacks. - closeBroadcastChannel(); -} - -function callFidChangeCallbacks(key: string, fid: string): void { - const callbacks = fidChangeCallbacks.get(key); - if (!callbacks) { - return; - } - - for (const callback of callbacks) { - callback(fid); - } -} - -function broadcastFidChange(key: string, fid: string): void { - const channel = getBroadcastChannel(); - if (channel) { - channel.postMessage({ key, fid }); - } - closeBroadcastChannel(); -} - -let broadcastChannel: BroadcastChannel | null = null; -/** Opens and returns a BroadcastChannel if it is supported by the browser. */ -function getBroadcastChannel(): BroadcastChannel | null { - if (!broadcastChannel && 'BroadcastChannel' in self) { - broadcastChannel = new BroadcastChannel('[Firebase] FID Change'); - broadcastChannel.onmessage = e => { - callFidChangeCallbacks(e.data.key, e.data.fid); - }; - } - return broadcastChannel; -} - -function closeBroadcastChannel(): void { - if (fidChangeCallbacks.size === 0 && broadcastChannel) { - broadcastChannel.close(); - broadcastChannel = null; - } -} diff --git a/packages-exp/installations-exp/src/helpers/generate-fid.test.ts b/packages-exp/installations-exp/src/helpers/generate-fid.test.ts deleted file mode 100644 index 5d5e3414d10..00000000000 --- a/packages-exp/installations-exp/src/helpers/generate-fid.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { stub } from 'sinon'; -import '../testing/setup'; -import { generateFid, VALID_FID_PATTERN } from './generate-fid'; - -/** A few random values to generate a FID from. */ -// prettier-ignore -const MOCK_RANDOM_VALUES = [ - [14, 107, 44, 183, 190, 84, 253, 45, 219, 233, 43, 190, 240, 152, 195, 222, 237], - [184, 251, 91, 157, 125, 225, 209, 15, 116, 66, 46, 113, 194, 126, 16, 13, 226], - [197, 123, 13, 142, 239, 129, 252, 139, 156, 36, 219, 192, 153, 52, 182, 231, 177], - [69, 154, 197, 91, 156, 196, 125, 111, 3, 67, 212, 132, 169, 11, 14, 254, 125], - [193, 102, 58, 19, 244, 69, 36, 135, 170, 106, 98, 216, 246, 209, 24, 155, 149], - [252, 59, 222, 160, 82, 160, 82, 186, 14, 172, 196, 114, 146, 191, 196, 194, 146], - [64, 147, 153, 236, 225, 142, 235, 109, 184, 249, 174, 127, 33, 238, 227, 172, 111], - [129, 137, 136, 120, 248, 206, 253, 78, 159, 201, 216, 15, 246, 80, 118, 185, 211], - [117, 150, 2, 180, 116, 230, 45, 188, 183, 43, 152, 100, 50, 255, 101, 175, 190], - [156, 129, 30, 101, 58, 137, 217, 249, 12, 227, 235, 80, 248, 81, 191, 2, 5], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], -]; - -/** The FIDs that should be generated based on MOCK_RANDOM_VALUES. */ -const EXPECTED_FIDS = [ - 'fmsst75U_S3b6Su-8JjD3u', - 'ePtbnX3h0Q90Qi5xwn4QDe', - 'dXsNju-B_IucJNvAmTS257', - 'dZrFW5zEfW8DQ9SEqQsO_n', - 'cWY6E_RFJIeqamLY9tEYm5', - 'fDveoFKgUroOrMRykr_Ewp', - 'cJOZ7OGO6224-a5_Ie7jrG', - 'cYmIePjO_U6fydgP9lB2ud', - 'dZYCtHTmLby3K5hkMv9lr7', - 'fIEeZTqJ2fkM4-tQ-FG_Ag', - 'cAAAAAAAAAAAAAAAAAAAAA', - 'f_____________________' -]; - -describe('generateFid', () => { - it('deterministically generates FIDs based on crypto.getRandomValues', () => { - let randomValueIndex = 0; - stub(crypto, 'getRandomValues').callsFake(array => { - if (!(array instanceof Uint8Array)) { - throw new Error('what'); - } - const values = MOCK_RANDOM_VALUES[randomValueIndex++]; - for (let i = 0; i < array.length; i++) { - array[i] = values[i]; - } - return array; - }); - - for (const expectedFid of EXPECTED_FIDS) { - expect(generateFid()).to.deep.equal(expectedFid); - } - }); - - it('generates valid FIDs', () => { - for (let i = 0; i < 1000; i++) { - const fid = generateFid(); - expect(VALID_FID_PATTERN.test(fid)).to.equal( - true, - `${fid} is not a valid FID` - ); - } - }); - - it('generates FIDs where each character is equally likely to appear in each location', () => { - const numTries = 200000; - - const charOccurrencesMapList: Array> = new Array(22); - for (let i = 0; i < charOccurrencesMapList.length; i++) { - charOccurrencesMapList[i] = new Map(); - } - - for (let i = 0; i < numTries; i++) { - const fid = generateFid(); - - Array.from(fid).forEach((char, location) => { - const map = charOccurrencesMapList[location]; - map.set(char, (map.get(char) || 0) + 1); - }); - } - - for (let i = 0; i < charOccurrencesMapList.length; i++) { - const map = charOccurrencesMapList[i]; - if (i === 0) { - // In the first location only 4 characters (c, d, e, f) are valid. - expect(map.size).to.equal(4); - } else { - // In locations other than the first, all 64 characters are valid. - expect(map.size).to.equal(64); - } - - Array.from(map.entries()).forEach(([_, occurrence]) => { - const expectedOccurrence = numTries / map.size; - - // 10% margin of error - expect(occurrence).to.be.above(expectedOccurrence * 0.9); - expect(occurrence).to.be.below(expectedOccurrence * 1.1); - }); - } - }).timeout(30000); - - it('returns an empty string if FID generation fails', () => { - stub(crypto, 'getRandomValues').throws(); - - const fid = generateFid(); - expect(fid).to.equal(''); - }); -}); diff --git a/packages-exp/installations-exp/src/helpers/generate-fid.ts b/packages-exp/installations-exp/src/helpers/generate-fid.ts deleted file mode 100644 index 5d87df04628..00000000000 --- a/packages-exp/installations-exp/src/helpers/generate-fid.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { bufferToBase64UrlSafe } from './buffer-to-base64-url-safe'; - -export const VALID_FID_PATTERN = /^[cdef][\w-]{21}$/; -export const INVALID_FID = ''; - -/** - * Generates a new FID using random values from Web Crypto API. - * Returns an empty string if FID generation fails for any reason. - */ -export function generateFid(): string { - try { - // A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5 - // bytes. our implementation generates a 17 byte array instead. - const fidByteArray = new Uint8Array(17); - const crypto = - self.crypto || ((self as unknown) as { msCrypto: Crypto }).msCrypto; - crypto.getRandomValues(fidByteArray); - - // Replace the first 4 random bits with the constant FID header of 0b0111. - fidByteArray[0] = 0b01110000 + (fidByteArray[0] % 0b00010000); - - const fid = encode(fidByteArray); - - return VALID_FID_PATTERN.test(fid) ? fid : INVALID_FID; - } catch { - // FID generation errored - return INVALID_FID; - } -} - -/** Converts a FID Uint8Array to a base64 string representation. */ -function encode(fidByteArray: Uint8Array): string { - const b64String = bufferToBase64UrlSafe(fidByteArray); - - // Remove the 23rd character that was added because of the extra 4 bits at the - // end of our 17 byte array, and the '=' padding. - return b64String.substr(0, 22); -} diff --git a/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts b/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts deleted file mode 100644 index b19ec6ea037..00000000000 --- a/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts +++ /dev/null @@ -1,477 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AssertionError, expect } from 'chai'; -import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; -import * as createInstallationRequestModule from '../functions/create-installation-request'; -import { AppConfig } from '../interfaces/installation-impl'; -import { - InProgressInstallationEntry, - RegisteredInstallationEntry, - RequestStatus, - UnregisteredInstallationEntry -} from '../interfaces/installation-entry'; -import { getFakeAppConfig } from '../testing/fake-generators'; -import '../testing/setup'; -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; -import { sleep } from '../util/sleep'; -import * as generateFidModule from './generate-fid'; -import { getInstallationEntry } from './get-installation-entry'; -import { get, set } from './idb-manager'; - -const FID = 'cry-of-the-black-birds'; - -describe('getInstallationEntry', () => { - let clock: SinonFakeTimers; - let appConfig: AppConfig; - let createInstallationRequestSpy: SinonStub< - [AppConfig, InProgressInstallationEntry], - Promise - >; - - beforeEach(() => { - clock = useFakeTimers({ now: 1_000_000 }); - appConfig = getFakeAppConfig(); - createInstallationRequestSpy = stub( - createInstallationRequestModule, - 'createInstallationRequest' - ).callsFake( - async (_, installationEntry): Promise => { - await sleep(500); // Request would take some time - const registeredInstallationEntry: RegisteredInstallationEntry = { - // Returns new FID if client FID is invalid. - fid: installationEntry.fid || FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now(), - token: 'token', - expiresIn: 1_000_000_000 - } - }; - return registeredInstallationEntry; - } - ); - }); - - afterEach(() => { - // Clean up all pending requests. - clock.runAll(); - }); - - it('saves the InstallationEntry in the database before returning it', async () => { - const oldDbEntry = await get(appConfig); - expect(oldDbEntry).to.be.undefined; - - const { installationEntry } = await getInstallationEntry(appConfig); - - const newDbEntry = await get(appConfig); - expect(newDbEntry).to.deep.equal(installationEntry); - }); - - it('saves the InstallationEntry in the database if app is offline', async () => { - stub(navigator, 'onLine').value(false); - - const oldDbEntry = await get(appConfig); - expect(oldDbEntry).to.be.undefined; - - const { installationEntry } = await getInstallationEntry(appConfig); - - const newDbEntry = await get(appConfig); - expect(newDbEntry).to.deep.equal(installationEntry); - }); - - it('saves the InstallationEntry in the database when registration completes', async () => { - const { - installationEntry, - registrationPromise - } = await getInstallationEntry(appConfig); - expect(installationEntry.registrationStatus).to.equal( - RequestStatus.IN_PROGRESS - ); - expect(registrationPromise).to.be.an.instanceOf(Promise); - - const oldDbEntry = await get(appConfig); - expect(oldDbEntry).to.deep.equal(installationEntry); - - clock.next(); // Finish registration request. - await expect(registrationPromise).to.be.fulfilled; - - const newDbEntry = await get(appConfig); - expect(newDbEntry!.registrationStatus).to.equal(RequestStatus.COMPLETED); - }); - - it('saves the InstallationEntry in the database when registration fails', async () => { - createInstallationRequestSpy.callsFake(async () => { - await sleep(500); // Request would take some time - throw ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { - requestName: 'Create Installation', - serverCode: 500, - serverStatus: 'INTERNAL', - serverMessage: 'Internal server error.' - }); - }); - - const { - installationEntry, - registrationPromise - } = await getInstallationEntry(appConfig); - expect(installationEntry.registrationStatus).to.equal( - RequestStatus.IN_PROGRESS - ); - expect(registrationPromise).to.be.an.instanceOf(Promise); - - const oldDbEntry = await get(appConfig); - expect(oldDbEntry).to.deep.equal(installationEntry); - - clock.next(); // Finish registration request. - await expect(registrationPromise).to.be.rejected; - - const newDbEntry = await get(appConfig); - expect(newDbEntry!.registrationStatus).to.equal(RequestStatus.NOT_STARTED); - }); - - it('removes the InstallationEntry from the database when registration fails with 409', async () => { - createInstallationRequestSpy.callsFake(async () => { - await sleep(500); // Request would take some time - throw ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { - requestName: 'Create Installation', - serverCode: 409, - serverStatus: 'INVALID_ARGUMENT', - serverMessage: 'FID can not be used.' - }); - }); - - const { - installationEntry, - registrationPromise - } = await getInstallationEntry(appConfig); - expect(installationEntry.registrationStatus).to.equal( - RequestStatus.IN_PROGRESS - ); - - const oldDbEntry = await get(appConfig); - expect(oldDbEntry).to.deep.equal(installationEntry); - - clock.next(); // Finish registration request. - await expect(registrationPromise).to.be.rejected; - - const newDbEntry = await get(appConfig); - expect(newDbEntry).to.be.undefined; - }); - - it('returns the same FID on subsequent calls', async () => { - const { installationEntry: entry1 } = await getInstallationEntry(appConfig); - const { installationEntry: entry2 } = await getInstallationEntry(appConfig); - expect(entry1.fid).to.equal(entry2.fid); - }); - - describe('when there is no InstallationEntry in database', () => { - let generateInstallationEntrySpy: SinonStub<[], string>; - - beforeEach(() => { - generateInstallationEntrySpy = stub( - generateFidModule, - 'generateFid' - ).returns(FID); - }); - - it('returns a new pending InstallationEntry and triggers createInstallation', async () => { - const { - installationEntry, - registrationPromise - } = await getInstallationEntry(appConfig); - - if (installationEntry.registrationStatus !== RequestStatus.IN_PROGRESS) { - throw new AssertionError('InstallationEntry is not IN_PROGRESS.'); - } - - expect(registrationPromise).to.be.an.instanceOf(Promise); - expect(installationEntry).to.deep.equal({ - fid: FID, - registrationStatus: RequestStatus.IN_PROGRESS, - - // https://github.com/chaijs/chai/issues/644 - registrationTime: installationEntry.registrationTime - }); - expect(generateInstallationEntrySpy).to.be.called; - expect(createInstallationRequestSpy).to.be.called; - }); - - it('returns a new unregistered InstallationEntry if app is offline', async () => { - stub(navigator, 'onLine').value(false); - - const { installationEntry } = await getInstallationEntry(appConfig); - - expect(installationEntry).to.deep.equal({ - fid: FID, - registrationStatus: RequestStatus.NOT_STARTED - }); - expect(generateInstallationEntrySpy).to.be.called; - expect(createInstallationRequestSpy).not.to.be.called; - }); - - it('does not trigger createInstallation REST call on subsequent calls', async () => { - await getInstallationEntry(appConfig); - await getInstallationEntry(appConfig); - - expect(createInstallationRequestSpy).to.be.calledOnce; - }); - - it('returns a registrationPromise on subsequent calls before initial promise resolves', async () => { - const { registrationPromise: promise1 } = await getInstallationEntry( - appConfig - ); - const { registrationPromise: promise2 } = await getInstallationEntry( - appConfig - ); - - expect(createInstallationRequestSpy).to.be.calledOnce; - expect(promise1).to.be.an.instanceOf(Promise); - expect(promise2).to.be.an.instanceOf(Promise); - }); - - it('does not return a registrationPromise on subsequent calls after initial promise resolves', async () => { - const { registrationPromise: promise1 } = await getInstallationEntry( - appConfig - ); - expect(promise1).to.be.an.instanceOf(Promise); - - clock.next(); // Finish registration request. - await expect(promise1).to.be.fulfilled; - - const { registrationPromise: promise2 } = await getInstallationEntry( - appConfig - ); - expect(promise2).to.be.undefined; - - expect(createInstallationRequestSpy).to.be.calledOnce; - }); - - it('waits for the FID from the server if FID generation fails', async () => { - clock.restore(); - clock = useFakeTimers({ - now: 1_000_000, - shouldAdvanceTime: true /* Needed to allow the createInstallation request to complete. */ - }); - - // FID generation fails. - generateInstallationEntrySpy.returns(generateFidModule.INVALID_FID); - - const getInstallationEntryPromise = getInstallationEntry(appConfig); - - const { - installationEntry, - registrationPromise - } = await getInstallationEntryPromise; - - expect(installationEntry.fid).to.equal(FID); - expect(registrationPromise).to.be.undefined; - }); - }); - - describe('when there is an unregistered InstallationEntry in the database', () => { - beforeEach(async () => { - const unregisteredInstallationEntry: UnregisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.NOT_STARTED - }; - await set(appConfig, unregisteredInstallationEntry); - }); - - it('returns a pending InstallationEntry and triggers createInstallation', async () => { - const { - installationEntry, - registrationPromise - } = await getInstallationEntry(appConfig); - - if (installationEntry.registrationStatus !== RequestStatus.IN_PROGRESS) { - throw new AssertionError('InstallationEntry is not IN_PROGRESS.'); - } - - expect(registrationPromise).to.be.an.instanceOf(Promise); - expect(installationEntry).to.deep.equal({ - fid: FID, - registrationStatus: RequestStatus.IN_PROGRESS, - // https://github.com/chaijs/chai/issues/644 - registrationTime: installationEntry.registrationTime - }); - expect(createInstallationRequestSpy).to.be.calledOnce; - }); - - it('returns the same InstallationEntry if the app is offline', async () => { - stub(navigator, 'onLine').value(false); - - const { installationEntry } = await getInstallationEntry(appConfig); - - expect(installationEntry).to.deep.equal({ - fid: FID, - registrationStatus: RequestStatus.NOT_STARTED - }); - expect(createInstallationRequestSpy).not.to.be.called; - }); - }); - - describe('when there is a pending InstallationEntry in the database', () => { - beforeEach(async () => { - const inProgressInstallationEntry: InProgressInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.IN_PROGRESS, - registrationTime: 1_000_000 - }; - await set(appConfig, inProgressInstallationEntry); - }); - - it("returns the same InstallationEntry if the request hasn't timed out", async () => { - clock.now = 1_001_000; // One second after the request was initiated. - - const { installationEntry } = await getInstallationEntry(appConfig); - - expect(installationEntry).to.deep.equal({ - fid: FID, - registrationStatus: RequestStatus.IN_PROGRESS, - registrationTime: 1_000_000 - }); - expect(createInstallationRequestSpy).not.to.be.called; - }); - - it('updates the InstallationEntry and triggers createInstallation if the request fails', async () => { - clock.restore(); - clock = useFakeTimers({ - now: 1_001_000 /* One second after the request was initiated. */, - shouldAdvanceTime: true /* Needed to allow the createInstallation request to complete. */ - }); - - const installationEntryPromise = getInstallationEntry(appConfig); - - // The pending request fails after a while. - clock.tick(3000); - await set(appConfig, { - fid: FID, - registrationStatus: RequestStatus.NOT_STARTED - }); - - const { registrationPromise } = await installationEntryPromise; - - // Let the new getInstallationEntry process start. - await sleep(250); - - const tokenDetails = (await get( - appConfig - )) as InProgressInstallationEntry; - expect(tokenDetails.registrationTime).to.be.at.least( - /* When the first pending request failed. */ 1_004_000 - ); - expect(tokenDetails).to.deep.equal({ - fid: FID, - registrationStatus: RequestStatus.IN_PROGRESS, - // Ignore registrationTime as we already checked it. - registrationTime: tokenDetails.registrationTime - }); - - expect(registrationPromise).to.be.an.instanceOf(Promise); - await registrationPromise; - expect(createInstallationRequestSpy).to.be.calledOnce; - }); - - it('updates the InstallationEntry if the request fails and the app is offline', async () => { - stub(navigator, 'onLine').value(false); - - clock.restore(); - clock = useFakeTimers({ - now: 1_001_000 /* One second after the request was initiated. */, - shouldAdvanceTime: true /* Needed to allow the createInstallation request to complete. */ - }); - - const installationEntryPromise = getInstallationEntry(appConfig); - - // The pending request fails after a while. - clock.tick(3000); - await set(appConfig, { - fid: FID, - registrationStatus: RequestStatus.NOT_STARTED - }); - - const { registrationPromise } = await installationEntryPromise; - - // Let the new getInstallationEntry process start. - await sleep(250); - - expect(await get(appConfig)).to.deep.equal({ - fid: FID, - registrationStatus: RequestStatus.NOT_STARTED - }); - - expect(registrationPromise).to.be.an.instanceOf(Promise); - await expect(registrationPromise).to.be.rejectedWith( - 'Application offline' - ); - expect(createInstallationRequestSpy).not.to.be.called; - }); - - it('returns a new pending InstallationEntry and triggers createInstallation if the request had already timed out', async () => { - clock.now = 1_015_000; // Fifteen seconds after the request was initiated. - - const { installationEntry } = await getInstallationEntry(appConfig); - - expect(installationEntry).to.deep.equal({ - fid: FID, - registrationStatus: RequestStatus.IN_PROGRESS, - registrationTime: 1_015_000 - }); - expect(createInstallationRequestSpy).to.be.calledOnce; - }); - - it('returns a new unregistered InstallationEntry if the request had already timed out and the app is offline', async () => { - stub(navigator, 'onLine').value(false); - clock.now = 1_015_000; // Fifteen seconds after the request was initiated. - - const { installationEntry } = await getInstallationEntry(appConfig); - - expect(installationEntry).to.deep.equal({ - fid: FID, - registrationStatus: RequestStatus.NOT_STARTED - }); - expect(createInstallationRequestSpy).not.to.be.called; - }); - }); - - describe('when there is a registered InstallationEntry in the database', () => { - beforeEach(async () => { - const registeredInstallationEntry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { requestStatus: RequestStatus.NOT_STARTED } - }; - await set(appConfig, registeredInstallationEntry); - }); - - it('returns the InstallationEntry from the database', async () => { - const { installationEntry } = await getInstallationEntry(appConfig); - - expect(installationEntry).to.deep.equal({ - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { requestStatus: RequestStatus.NOT_STARTED } - }); - expect(createInstallationRequestSpy).not.to.be.called; - }); - }); -}); diff --git a/packages-exp/installations-exp/src/helpers/get-installation-entry.ts b/packages-exp/installations-exp/src/helpers/get-installation-entry.ts deleted file mode 100644 index 6b57563eb80..00000000000 --- a/packages-exp/installations-exp/src/helpers/get-installation-entry.ts +++ /dev/null @@ -1,227 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createInstallationRequest } from '../functions/create-installation-request'; -import { AppConfig } from '../interfaces/installation-impl'; -import { - InProgressInstallationEntry, - InstallationEntry, - RegisteredInstallationEntry, - RequestStatus -} from '../interfaces/installation-entry'; -import { PENDING_TIMEOUT_MS } from '../util/constants'; -import { ERROR_FACTORY, ErrorCode, isServerError } from '../util/errors'; -import { sleep } from '../util/sleep'; -import { generateFid, INVALID_FID } from './generate-fid'; -import { remove, set, update } from './idb-manager'; - -export interface InstallationEntryWithRegistrationPromise { - installationEntry: InstallationEntry; - /** Exist iff the installationEntry is not registered. */ - registrationPromise?: Promise; -} - -/** - * Updates and returns the InstallationEntry from the database. - * Also triggers a registration request if it is necessary and possible. - */ -export async function getInstallationEntry( - appConfig: AppConfig -): Promise { - let registrationPromise: Promise | undefined; - - const installationEntry = await update(appConfig, oldEntry => { - const installationEntry = updateOrCreateInstallationEntry(oldEntry); - const entryWithPromise = triggerRegistrationIfNecessary( - appConfig, - installationEntry - ); - registrationPromise = entryWithPromise.registrationPromise; - return entryWithPromise.installationEntry; - }); - - if (installationEntry.fid === INVALID_FID) { - // FID generation failed. Waiting for the FID from the server. - return { installationEntry: await registrationPromise! }; - } - - return { - installationEntry, - registrationPromise - }; -} - -/** - * Creates a new Installation Entry if one does not exist. - * Also clears timed out pending requests. - */ -function updateOrCreateInstallationEntry( - oldEntry: InstallationEntry | undefined -): InstallationEntry { - const entry: InstallationEntry = oldEntry || { - fid: generateFid(), - registrationStatus: RequestStatus.NOT_STARTED - }; - - return clearTimedOutRequest(entry); -} - -/** - * If the Firebase Installation is not registered yet, this will trigger the - * registration and return an InProgressInstallationEntry. - * - * If registrationPromise does not exist, the installationEntry is guaranteed - * to be registered. - */ -function triggerRegistrationIfNecessary( - appConfig: AppConfig, - installationEntry: InstallationEntry -): InstallationEntryWithRegistrationPromise { - if (installationEntry.registrationStatus === RequestStatus.NOT_STARTED) { - if (!navigator.onLine) { - // Registration required but app is offline. - const registrationPromiseWithError = Promise.reject( - ERROR_FACTORY.create(ErrorCode.APP_OFFLINE) - ); - return { - installationEntry, - registrationPromise: registrationPromiseWithError - }; - } - - // Try registering. Change status to IN_PROGRESS. - const inProgressEntry: InProgressInstallationEntry = { - fid: installationEntry.fid, - registrationStatus: RequestStatus.IN_PROGRESS, - registrationTime: Date.now() - }; - const registrationPromise = registerInstallation( - appConfig, - inProgressEntry - ); - return { installationEntry: inProgressEntry, registrationPromise }; - } else if ( - installationEntry.registrationStatus === RequestStatus.IN_PROGRESS - ) { - return { - installationEntry, - registrationPromise: waitUntilFidRegistration(appConfig) - }; - } else { - return { installationEntry }; - } -} - -/** This will be executed only once for each new Firebase Installation. */ -async function registerInstallation( - appConfig: AppConfig, - installationEntry: InProgressInstallationEntry -): Promise { - try { - const registeredInstallationEntry = await createInstallationRequest( - appConfig, - installationEntry - ); - return set(appConfig, registeredInstallationEntry); - } catch (e) { - if (isServerError(e) && e.customData.serverCode === 409) { - // Server returned a "FID can not be used" error. - // Generate a new ID next time. - await remove(appConfig); - } else { - // Registration failed. Set FID as not registered. - await set(appConfig, { - fid: installationEntry.fid, - registrationStatus: RequestStatus.NOT_STARTED - }); - } - throw e; - } -} - -/** Call if FID registration is pending in another request. */ -async function waitUntilFidRegistration( - appConfig: AppConfig -): Promise { - // Unfortunately, there is no way of reliably observing when a value in - // IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers), - // so we need to poll. - - let entry: InstallationEntry = await updateInstallationRequest(appConfig); - while (entry.registrationStatus === RequestStatus.IN_PROGRESS) { - // createInstallation request still in progress. - await sleep(100); - - entry = await updateInstallationRequest(appConfig); - } - - if (entry.registrationStatus === RequestStatus.NOT_STARTED) { - // The request timed out or failed in a different call. Try again. - const { - installationEntry, - registrationPromise - } = await getInstallationEntry(appConfig); - - if (registrationPromise) { - return registrationPromise; - } else { - // if there is no registrationPromise, entry is registered. - return installationEntry as RegisteredInstallationEntry; - } - } - - return entry; -} - -/** - * Called only if there is a CreateInstallation request in progress. - * - * Updates the InstallationEntry in the DB based on the status of the - * CreateInstallation request. - * - * Returns the updated InstallationEntry. - */ -function updateInstallationRequest( - appConfig: AppConfig -): Promise { - return update(appConfig, oldEntry => { - if (!oldEntry) { - throw ERROR_FACTORY.create(ErrorCode.INSTALLATION_NOT_FOUND); - } - return clearTimedOutRequest(oldEntry); - }); -} - -function clearTimedOutRequest(entry: InstallationEntry): InstallationEntry { - if (hasInstallationRequestTimedOut(entry)) { - return { - fid: entry.fid, - registrationStatus: RequestStatus.NOT_STARTED - }; - } - - return entry; -} - -function hasInstallationRequestTimedOut( - installationEntry: InstallationEntry -): boolean { - return ( - installationEntry.registrationStatus === RequestStatus.IN_PROGRESS && - installationEntry.registrationTime + PENDING_TIMEOUT_MS < Date.now() - ); -} diff --git a/packages-exp/installations-exp/src/helpers/idb-manager.test.ts b/packages-exp/installations-exp/src/helpers/idb-manager.test.ts deleted file mode 100644 index db7eaca58f9..00000000000 --- a/packages-exp/installations-exp/src/helpers/idb-manager.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { stub } from 'sinon'; -import { AppConfig } from '../interfaces/installation-impl'; -import { - InstallationEntry, - RequestStatus -} from '../interfaces/installation-entry'; -import { getFakeAppConfig } from '../testing/fake-generators'; -import '../testing/setup'; -import { clear, get, remove, set, update } from './idb-manager'; -import * as fidChangedModule from './fid-changed'; - -const VALUE_A: InstallationEntry = { - fid: 'VALUE_A', - registrationStatus: RequestStatus.NOT_STARTED -}; -const VALUE_B: InstallationEntry = { - fid: 'VALUE_B', - registrationStatus: RequestStatus.NOT_STARTED -}; - -describe('idb manager', () => { - let appConfig: AppConfig; - - beforeEach(() => { - appConfig = { ...getFakeAppConfig(), appName: 'appName1' }; - }); - - describe('get / set', () => { - it('sets a value and then gets the same value back', async () => { - await set(appConfig, VALUE_A); - const value = await get(appConfig); - expect(value).to.deep.equal(VALUE_A); - }); - - it('gets undefined for a key that does not exist', async () => { - const value = await get(appConfig); - expect(value).to.be.undefined; - }); - - it('sets and gets multiple values with different keys', async () => { - const appConfig2: AppConfig = { - ...getFakeAppConfig(), - appName: 'appName2' - }; - - await set(appConfig, VALUE_A); - await set(appConfig2, VALUE_B); - expect(await get(appConfig)).to.deep.equal(VALUE_A); - expect(await get(appConfig2)).to.deep.equal(VALUE_B); - }); - - it('overwrites a value', async () => { - await set(appConfig, VALUE_A); - await set(appConfig, VALUE_B); - expect(await get(appConfig)).to.deep.equal(VALUE_B); - }); - - it('calls fidChanged when a new FID is generated', async () => { - const fidChangedStub = stub(fidChangedModule, 'fidChanged'); - await set(appConfig, VALUE_A); - - expect(fidChangedStub).to.have.been.calledOnceWith( - appConfig, - VALUE_A.fid - ); - }); - - it('calls fidChanged when the FID changes', async () => { - await set(appConfig, VALUE_A); - - const fidChangedStub = stub(fidChangedModule, 'fidChanged'); - await set(appConfig, VALUE_B); - - expect(fidChangedStub).to.have.been.calledOnceWith( - appConfig, - VALUE_B.fid - ); - }); - - it('does not call fidChanged when the FID is the same', async () => { - await set(appConfig, VALUE_A); - - const fidChangedStub = stub(fidChangedModule, 'fidChanged'); - await set(appConfig, /* Same value */ VALUE_A); - - expect(fidChangedStub).not.to.have.been.called; - }); - }); - - describe('remove', () => { - it('deletes a key', async () => { - await set(appConfig, VALUE_A); - await remove(appConfig); - expect(await get(appConfig)).to.be.undefined; - }); - - it('does not throw if key does not exist', async () => { - await remove(appConfig); - expect(await get(appConfig)).to.be.undefined; - }); - }); - - describe('clear', () => { - it('deletes all keys', async () => { - const appConfig2: AppConfig = { - ...getFakeAppConfig(), - appName: 'appName2' - }; - - await set(appConfig, VALUE_A); - await set(appConfig2, VALUE_B); - await clear(); - expect(await get(appConfig)).to.be.undefined; - expect(await get(appConfig2)).to.be.undefined; - }); - }); - - describe('update', () => { - it('gets and sets a value atomically, returns the new value', async () => { - let isGetCalled = false; - - await set(appConfig, VALUE_A); - - const resultPromise = update(appConfig, oldValue => { - // get is already called for the same key, but it will only complete - // after update transaction finishes, at which point it will return the - // new value. - expect(isGetCalled).to.be.true; - - expect(oldValue).to.deep.equal(VALUE_A); - return VALUE_B; - }); - - // Called immediately after update, but before update completed. - const getPromise = get(appConfig); - isGetCalled = true; - - // Update returns the new value - expect(await resultPromise).to.deep.equal(VALUE_B); - - // If update weren't atomic, this would return the old value. - expect(await getPromise).to.deep.equal(VALUE_B); - }); - - it('calls fidChanged when a new FID is generated', async () => { - const fidChangedStub = stub(fidChangedModule, 'fidChanged'); - await update(appConfig, () => VALUE_A); - - expect(fidChangedStub).to.have.been.calledOnceWith( - appConfig, - VALUE_A.fid - ); - }); - - it('calls fidChanged when the FID changes', async () => { - await set(appConfig, VALUE_A); - - const fidChangedStub = stub(fidChangedModule, 'fidChanged'); - await update(appConfig, () => VALUE_B); - - expect(fidChangedStub).to.have.been.calledOnceWith( - appConfig, - VALUE_B.fid - ); - }); - - it('does not call fidChanged when the FID is the same', async () => { - await set(appConfig, VALUE_A); - - const fidChangedStub = stub(fidChangedModule, 'fidChanged'); - await update(appConfig, () => /* Same value */ VALUE_A); - - expect(fidChangedStub).not.to.have.been.called; - }); - }); -}); diff --git a/packages-exp/installations-exp/src/helpers/idb-manager.ts b/packages-exp/installations-exp/src/helpers/idb-manager.ts deleted file mode 100644 index bc30563fa06..00000000000 --- a/packages-exp/installations-exp/src/helpers/idb-manager.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DB, openDb } from 'idb'; -import { AppConfig } from '../interfaces/installation-impl'; -import { InstallationEntry } from '../interfaces/installation-entry'; -import { getKey } from '../util/get-key'; -import { fidChanged } from './fid-changed'; - -const DATABASE_NAME = 'firebase-installations-database'; -const DATABASE_VERSION = 1; -const OBJECT_STORE_NAME = 'firebase-installations-store'; - -let dbPromise: Promise | null = null; -function getDbPromise(): Promise { - if (!dbPromise) { - dbPromise = openDb(DATABASE_NAME, DATABASE_VERSION, upgradeDB => { - // We don't use 'break' in this switch statement, the fall-through - // behavior is what we want, because if there are multiple versions between - // the old version and the current version, we want ALL the migrations - // that correspond to those versions to run, not only the last one. - // eslint-disable-next-line default-case - switch (upgradeDB.oldVersion) { - case 0: - upgradeDB.createObjectStore(OBJECT_STORE_NAME); - } - }); - } - return dbPromise; -} - -/** Gets record(s) from the objectStore that match the given key. */ -export async function get( - appConfig: AppConfig -): Promise { - const key = getKey(appConfig); - const db = await getDbPromise(); - return db - .transaction(OBJECT_STORE_NAME) - .objectStore(OBJECT_STORE_NAME) - .get(key); -} - -/** Assigns or overwrites the record for the given key with the given value. */ -export async function set( - appConfig: AppConfig, - value: ValueType -): Promise { - const key = getKey(appConfig); - const db = await getDbPromise(); - const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); - const objectStore = tx.objectStore(OBJECT_STORE_NAME); - const oldValue = await objectStore.get(key); - await objectStore.put(value, key); - await tx.complete; - - if (!oldValue || oldValue.fid !== value.fid) { - fidChanged(appConfig, value.fid); - } - - return value; -} - -/** Removes record(s) from the objectStore that match the given key. */ -export async function remove(appConfig: AppConfig): Promise { - const key = getKey(appConfig); - const db = await getDbPromise(); - const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); - await tx.objectStore(OBJECT_STORE_NAME).delete(key); - await tx.complete; -} - -/** - * Atomically updates a record with the result of updateFn, which gets - * called with the current value. If newValue is undefined, the record is - * deleted instead. - * @return Updated value - */ -export async function update( - appConfig: AppConfig, - updateFn: (previousValue: InstallationEntry | undefined) => ValueType -): Promise { - const key = getKey(appConfig); - const db = await getDbPromise(); - const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); - const store = tx.objectStore(OBJECT_STORE_NAME); - const oldValue: InstallationEntry | undefined = await store.get(key); - const newValue = updateFn(oldValue); - - if (newValue === undefined) { - await store.delete(key); - } else { - await store.put(newValue, key); - } - await tx.complete; - - if (newValue && (!oldValue || oldValue.fid !== newValue.fid)) { - fidChanged(appConfig, newValue.fid); - } - - return newValue; -} - -export async function clear(): Promise { - const db = await getDbPromise(); - const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); - await tx.objectStore(OBJECT_STORE_NAME).clear(); - await tx.complete; -} diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts deleted file mode 100644 index 3bdd859a6b0..00000000000 --- a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; -import * as generateAuthTokenRequestModule from '../functions/generate-auth-token-request'; -import { - CompletedAuthToken, - RegisteredInstallationEntry, - RequestStatus, - UnregisteredInstallationEntry -} from '../interfaces/installation-entry'; -import { getFakeInstallations } from '../testing/fake-generators'; -import '../testing/setup'; -import { TOKEN_EXPIRATION_BUFFER } from '../util/constants'; -import { sleep } from '../util/sleep'; -import { get, set } from './idb-manager'; -import { refreshAuthToken } from './refresh-auth-token'; -import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; - -const FID = 'carry-the-blessed-home'; -const AUTH_TOKEN = 'authTokenFromServer'; -const DB_AUTH_TOKEN = 'authTokenFromDB'; -const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; - -describe('refreshAuthToken', () => { - let installations: FirebaseInstallationsImpl; - let generateAuthTokenRequestSpy: SinonStub< - [FirebaseInstallationsImpl, RegisteredInstallationEntry], - Promise - >; - - beforeEach(() => { - installations = getFakeInstallations(); - - generateAuthTokenRequestSpy = stub( - generateAuthTokenRequestModule, - 'generateAuthTokenRequest' - ).callsFake(async () => { - await sleep(100); // Request would take some time - const result: CompletedAuthToken = { - token: AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - }; - return result; - }); - }); - - it('throws when there is no installation in the DB', async () => { - await expect(refreshAuthToken(installations)).to.be.rejected; - }); - - it('throws when there is an unregistered installation in the db', async () => { - const installationEntry: UnregisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.NOT_STARTED - }; - await set(installations.appConfig, installationEntry); - - await expect(refreshAuthToken(installations)).to.be.rejected; - }); - - describe('when there is a valid auth token in the DB', () => { - beforeEach(async () => { - const installationEntry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - token: AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - } - }; - await set(installations.appConfig, installationEntry); - }); - - it('returns the token from the DB', async () => { - const { token } = await refreshAuthToken(installations); - expect(token).to.equal(AUTH_TOKEN); - }); - - it('does not call any server APIs', async () => { - await refreshAuthToken(installations); - expect(generateAuthTokenRequestSpy).not.to.be.called; - }); - - it('works even if the app is offline', async () => { - stub(navigator, 'onLine').value(false); - - const { token } = await refreshAuthToken(installations); - expect(token).to.equal(AUTH_TOKEN); - }); - }); - - describe('when there is an auth token that is about to expire in the DB', () => { - let clock: SinonFakeTimers; - - beforeEach(async () => { - clock = useFakeTimers({ shouldAdvanceTime: true }); - - const installationEntry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - token: DB_AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: - // Expires in ten minutes - Date.now() - ONE_WEEK_MS + TOKEN_EXPIRATION_BUFFER + 10 * 60 * 1000 - } - }; - await set(installations.appConfig, installationEntry); - }); - - it('returns a different token after expiration', async () => { - const token1 = await refreshAuthToken(installations); - expect(token1.token).to.equal(DB_AUTH_TOKEN); - - // Wait 30 minutes. - clock.tick('30:00'); - - const token2 = await refreshAuthToken(installations); - await expect(token2.token).to.equal(AUTH_TOKEN); - await expect(token2.token).not.to.equal(DB_AUTH_TOKEN); - expect(generateAuthTokenRequestSpy).to.be.calledOnce; - }); - }); - - describe('when there is an expired auth token in the DB', () => { - beforeEach(async () => { - const installationEntry: RegisteredInstallationEntry = { - fid: FID, - registrationStatus: RequestStatus.COMPLETED, - refreshToken: 'refreshToken', - authToken: { - token: DB_AUTH_TOKEN, - expiresIn: ONE_WEEK_MS, - requestStatus: RequestStatus.COMPLETED, - creationTime: Date.now() - 2 * ONE_WEEK_MS - } - }; - await set(installations.appConfig, installationEntry); - }); - - it('does not call generateAuthToken twice on subsequent calls', async () => { - await refreshAuthToken(installations); - await refreshAuthToken(installations); - expect(generateAuthTokenRequestSpy).to.be.calledOnce; - }); - - it('does not call generateAuthToken twice on simultaneous calls', async () => { - await Promise.all([ - refreshAuthToken(installations), - refreshAuthToken(installations) - ]); - expect(generateAuthTokenRequestSpy).to.be.calledOnce; - }); - - it('returns a new token', async () => { - const { token } = await refreshAuthToken(installations); - await expect(token).to.equal(AUTH_TOKEN); - await expect(token).not.to.equal(DB_AUTH_TOKEN); - expect(generateAuthTokenRequestSpy).to.be.calledOnce; - }); - - it('throws if the app is offline', async () => { - stub(navigator, 'onLine').value(false); - - await expect(refreshAuthToken(installations)).to.be.rejected; - }); - - it('saves the new token in the DB', async () => { - const { token } = await refreshAuthToken(installations); - - const installationEntry = (await get( - installations.appConfig - )) as RegisteredInstallationEntry; - expect(installationEntry).not.to.be.undefined; - expect(installationEntry.registrationStatus).to.equal( - RequestStatus.COMPLETED - ); - - const authToken = installationEntry.authToken as CompletedAuthToken; - expect(authToken.requestStatus).to.equal(RequestStatus.COMPLETED); - expect(authToken.token).to.equal(token); - }); - }); -}); diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts deleted file mode 100644 index ac2a0ffc02d..00000000000 --- a/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts +++ /dev/null @@ -1,214 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { generateAuthTokenRequest } from '../functions/generate-auth-token-request'; -import { - AppConfig, - FirebaseInstallationsImpl -} from '../interfaces/installation-impl'; -import { - AuthToken, - CompletedAuthToken, - InProgressAuthToken, - InstallationEntry, - RegisteredInstallationEntry, - RequestStatus -} from '../interfaces/installation-entry'; -import { PENDING_TIMEOUT_MS, TOKEN_EXPIRATION_BUFFER } from '../util/constants'; -import { ERROR_FACTORY, ErrorCode, isServerError } from '../util/errors'; -import { sleep } from '../util/sleep'; -import { remove, set, update } from './idb-manager'; - -/** - * Returns a valid authentication token for the installation. Generates a new - * token if one doesn't exist, is expired or about to expire. - * - * Should only be called if the Firebase Installation is registered. - */ -export async function refreshAuthToken( - installations: FirebaseInstallationsImpl, - forceRefresh = false -): Promise { - let tokenPromise: Promise | undefined; - const entry = await update(installations.appConfig, oldEntry => { - if (!isEntryRegistered(oldEntry)) { - throw ERROR_FACTORY.create(ErrorCode.NOT_REGISTERED); - } - - const oldAuthToken = oldEntry.authToken; - if (!forceRefresh && isAuthTokenValid(oldAuthToken)) { - // There is a valid token in the DB. - return oldEntry; - } else if (oldAuthToken.requestStatus === RequestStatus.IN_PROGRESS) { - // There already is a token request in progress. - tokenPromise = waitUntilAuthTokenRequest(installations, forceRefresh); - return oldEntry; - } else { - // No token or token expired. - if (!navigator.onLine) { - throw ERROR_FACTORY.create(ErrorCode.APP_OFFLINE); - } - - const inProgressEntry = makeAuthTokenRequestInProgressEntry(oldEntry); - tokenPromise = fetchAuthTokenFromServer(installations, inProgressEntry); - return inProgressEntry; - } - }); - - const authToken = tokenPromise - ? await tokenPromise - : (entry.authToken as CompletedAuthToken); - return authToken; -} - -/** - * Call only if FID is registered and Auth Token request is in progress. - * - * Waits until the current pending request finishes. If the request times out, - * tries once in this thread as well. - */ -async function waitUntilAuthTokenRequest( - installations: FirebaseInstallationsImpl, - forceRefresh: boolean -): Promise { - // Unfortunately, there is no way of reliably observing when a value in - // IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers), - // so we need to poll. - - let entry = await updateAuthTokenRequest(installations.appConfig); - while (entry.authToken.requestStatus === RequestStatus.IN_PROGRESS) { - // generateAuthToken still in progress. - await sleep(100); - - entry = await updateAuthTokenRequest(installations.appConfig); - } - - const authToken = entry.authToken; - if (authToken.requestStatus === RequestStatus.NOT_STARTED) { - // The request timed out or failed in a different call. Try again. - return refreshAuthToken(installations, forceRefresh); - } else { - return authToken; - } -} - -/** - * Called only if there is a GenerateAuthToken request in progress. - * - * Updates the InstallationEntry in the DB based on the status of the - * GenerateAuthToken request. - * - * Returns the updated InstallationEntry. - */ -function updateAuthTokenRequest( - appConfig: AppConfig -): Promise { - return update(appConfig, oldEntry => { - if (!isEntryRegistered(oldEntry)) { - throw ERROR_FACTORY.create(ErrorCode.NOT_REGISTERED); - } - - const oldAuthToken = oldEntry.authToken; - if (hasAuthTokenRequestTimedOut(oldAuthToken)) { - return { - ...oldEntry, - authToken: { requestStatus: RequestStatus.NOT_STARTED } - }; - } - - return oldEntry; - }); -} - -async function fetchAuthTokenFromServer( - installations: FirebaseInstallationsImpl, - installationEntry: RegisteredInstallationEntry -): Promise { - try { - const authToken = await generateAuthTokenRequest( - installations, - installationEntry - ); - const updatedInstallationEntry: RegisteredInstallationEntry = { - ...installationEntry, - authToken - }; - await set(installations.appConfig, updatedInstallationEntry); - return authToken; - } catch (e) { - if ( - isServerError(e) && - (e.customData.serverCode === 401 || e.customData.serverCode === 404) - ) { - // Server returned a "FID not found" or a "Invalid authentication" error. - // Generate a new ID next time. - await remove(installations.appConfig); - } else { - const updatedInstallationEntry: RegisteredInstallationEntry = { - ...installationEntry, - authToken: { requestStatus: RequestStatus.NOT_STARTED } - }; - await set(installations.appConfig, updatedInstallationEntry); - } - throw e; - } -} - -function isEntryRegistered( - installationEntry: InstallationEntry | undefined -): installationEntry is RegisteredInstallationEntry { - return ( - installationEntry !== undefined && - installationEntry.registrationStatus === RequestStatus.COMPLETED - ); -} - -function isAuthTokenValid(authToken: AuthToken): boolean { - return ( - authToken.requestStatus === RequestStatus.COMPLETED && - !isAuthTokenExpired(authToken) - ); -} - -function isAuthTokenExpired(authToken: CompletedAuthToken): boolean { - const now = Date.now(); - return ( - now < authToken.creationTime || - authToken.creationTime + authToken.expiresIn < now + TOKEN_EXPIRATION_BUFFER - ); -} - -/** Returns an updated InstallationEntry with an InProgressAuthToken. */ -function makeAuthTokenRequestInProgressEntry( - oldEntry: RegisteredInstallationEntry -): RegisteredInstallationEntry { - const inProgressAuthToken: InProgressAuthToken = { - requestStatus: RequestStatus.IN_PROGRESS, - requestTime: Date.now() - }; - return { - ...oldEntry, - authToken: inProgressAuthToken - }; -} - -function hasAuthTokenRequestTimedOut(authToken: AuthToken): boolean { - return ( - authToken.requestStatus === RequestStatus.IN_PROGRESS && - authToken.requestTime + PENDING_TIMEOUT_MS < Date.now() - ); -} diff --git a/packages-exp/installations-exp/src/index.ts b/packages-exp/installations-exp/src/index.ts deleted file mode 100644 index 98351b54056..00000000000 --- a/packages-exp/installations-exp/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { registerInstallations } from './functions/config'; -import { registerVersion } from '@firebase/app-exp'; -import { name, version } from '../package.json'; - -export * from './api'; - -registerInstallations(); -registerVersion(name, version); diff --git a/packages-exp/installations-exp/src/interfaces/api-response.ts b/packages-exp/installations-exp/src/interfaces/api-response.ts deleted file mode 100644 index f7560a6925c..00000000000 --- a/packages-exp/installations-exp/src/interfaces/api-response.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface CreateInstallationResponse { - readonly refreshToken: string; - readonly authToken: GenerateAuthTokenResponse; - readonly fid?: string; -} - -export interface GenerateAuthTokenResponse { - readonly token: string; - - /** - * Encoded as a string with the suffix 's' (indicating seconds), preceded by - * the number of seconds. - * - * Example: "604800s". - */ - readonly expiresIn: string; -} diff --git a/packages-exp/installations-exp/src/interfaces/installation-entry.ts b/packages-exp/installations-exp/src/interfaces/installation-entry.ts deleted file mode 100644 index 4b30aa2486f..00000000000 --- a/packages-exp/installations-exp/src/interfaces/installation-entry.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Status of a server request. */ -export const enum RequestStatus { - NOT_STARTED, - IN_PROGRESS, - COMPLETED -} - -export interface NotStartedAuthToken { - readonly requestStatus: RequestStatus.NOT_STARTED; -} - -export interface InProgressAuthToken { - readonly requestStatus: RequestStatus.IN_PROGRESS; - - /** - * Unix timestamp when the current generateAuthRequest was initiated. - * Used for figuring out how long the request status has been IN_PROGRESS. - */ - readonly requestTime: number; -} - -export interface CompletedAuthToken { - readonly requestStatus: RequestStatus.COMPLETED; - - /** - * Firebase Installations Authentication Token. - * Only exists if requestStatus is COMPLETED. - */ - readonly token: string; - - /** - * Unix timestamp when Authentication Token was created. - * Only exists if requestStatus is COMPLETED. - */ - readonly creationTime: number; - - /** - * Authentication Token time to live duration in milliseconds. - * Only exists if requestStatus is COMPLETED. - */ - readonly expiresIn: number; -} - -export type AuthToken = - | NotStartedAuthToken - | InProgressAuthToken - | CompletedAuthToken; - -export interface UnregisteredInstallationEntry { - /** Status of the Firebase Installation registration on the server. */ - readonly registrationStatus: RequestStatus.NOT_STARTED; - - /** Firebase Installation ID */ - readonly fid: string; -} - -export interface InProgressInstallationEntry { - /** Status of the Firebase Installation registration on the server. */ - readonly registrationStatus: RequestStatus.IN_PROGRESS; - - /** - * Unix timestamp that shows the time when the current createInstallation - * request was initiated. - * Used for figuring out how long the registration status has been PENDING. - */ - readonly registrationTime: number; - - /** Firebase Installation ID */ - readonly fid: string; -} - -export interface RegisteredInstallationEntry { - /** Status of the Firebase Installation registration on the server. */ - readonly registrationStatus: RequestStatus.COMPLETED; - - /** Firebase Installation ID */ - readonly fid: string; - - /** - * Refresh Token returned from the server. - * Used for authenticating generateAuthToken requests. - */ - readonly refreshToken: string; - - /** Firebase Installation Authentication Token. */ - readonly authToken: AuthToken; -} - -/** Firebase Installation ID and related data in the database. */ -export type InstallationEntry = - | UnregisteredInstallationEntry - | InProgressInstallationEntry - | RegisteredInstallationEntry; diff --git a/packages-exp/installations-exp/src/testing/compare-headers.test.ts b/packages-exp/installations-exp/src/testing/compare-headers.test.ts deleted file mode 100644 index 8bd6fb81203..00000000000 --- a/packages-exp/installations-exp/src/testing/compare-headers.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AssertionError, expect } from 'chai'; -import '../testing/setup'; -import { compareHeaders } from './compare-headers'; - -describe('compareHeaders', () => { - it("doesn't fail if headers contain the same entries", () => { - const headers1 = new Headers({ a: '123', b: '456' }); - const headers2 = new Headers({ a: '123', b: '456' }); - compareHeaders(headers1, headers2); - }); - - it('fails if headers contain different keys', () => { - const headers1 = new Headers({ a: '123', b: '456', extraKey: '789' }); - const headers2 = new Headers({ a: '123', b: '456' }); - expect(() => { - compareHeaders(headers1, headers2); - }).to.throw(AssertionError); - }); - - it('fails if headers contain different values', () => { - const headers1 = new Headers({ a: '123', b: '456' }); - const headers2 = new Headers({ a: '123', b: 'differentValue' }); - expect(() => { - compareHeaders(headers1, headers2); - }).to.throw(AssertionError); - }); -}); diff --git a/packages-exp/installations-exp/src/testing/compare-headers.ts b/packages-exp/installations-exp/src/testing/compare-headers.ts deleted file mode 100644 index 5b93c14933e..00000000000 --- a/packages-exp/installations-exp/src/testing/compare-headers.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AssertionError, expect } from 'chai'; - -// Trick TS since it's set to target ES5. -declare class HeadersWithEntries extends Headers { - entries?(): Iterable<[string, string]>; -} - -// Chai doesn't check if Headers objects contain the same entries, -// so we need to do that manually. -export function compareHeaders( - expectedHeaders: HeadersWithEntries, - actualHeaders: HeadersWithEntries -): void { - if ( - expectedHeaders.entries === undefined || - actualHeaders.entries === undefined - ) { - throw new AssertionError('Headers object does not have entries method'); - } - - const expected = new Map(Array.from(expectedHeaders.entries())); - const actual = new Map(Array.from(actualHeaders.entries())); - expect(actual).to.deep.equal(expected); -} diff --git a/packages-exp/installations-exp/src/testing/fake-generators.ts b/packages-exp/installations-exp/src/testing/fake-generators.ts deleted file mode 100644 index 5d8a59a6fa6..00000000000 --- a/packages-exp/installations-exp/src/testing/fake-generators.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { - Component, - ComponentContainer, - ComponentType -} from '@firebase/component'; -import { extractAppConfig } from '../helpers/extract-app-config'; -import { - FirebaseInstallationsImpl, - AppConfig -} from '../interfaces/installation-impl'; - -export function getFakeApp(): FirebaseApp { - return { - name: 'appName', - options: { - apiKey: 'apiKey', - projectId: 'projectId', - authDomain: 'authDomain', - messagingSenderId: 'messagingSenderId', - databaseURL: 'databaseUrl', - storageBucket: 'storageBucket', - appId: '1:777777777777:web:d93b5ca1475efe57' - }, - automaticDataCollectionEnabled: true - }; -} - -export function getFakeAppConfig( - customValues: Partial = {} -): AppConfig { - return { ...extractAppConfig(getFakeApp()), ...customValues }; -} - -export function getFakeInstallations(): FirebaseInstallationsImpl { - const container = new ComponentContainer('test'); - container.addComponent( - new Component( - 'platform-logger', - () => ({ getPlatformInfoString: () => 'a/1.2.3 b/2.3.4' }), - ComponentType.PRIVATE - ) - ); - - return { - app: getFakeApp(), - appConfig: getFakeAppConfig(), - platformLoggerProvider: container.getProvider('platform-logger'), - _delete: () => { - return Promise.resolve(); - } - }; -} diff --git a/packages-exp/installations-exp/src/testing/setup.ts b/packages-exp/installations-exp/src/testing/setup.ts deleted file mode 100644 index 3db746533e0..00000000000 --- a/packages-exp/installations-exp/src/testing/setup.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import { restore } from 'sinon'; -import * as sinonChai from 'sinon-chai'; -import { clear } from '../helpers/idb-manager'; - -use(chaiAsPromised); -use(sinonChai); - -afterEach(async () => { - restore(); - await clear(); -}); diff --git a/packages-exp/installations-exp/src/util/constants.ts b/packages-exp/installations-exp/src/util/constants.ts deleted file mode 100644 index c20fa260274..00000000000 --- a/packages-exp/installations-exp/src/util/constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { version } from '../../package.json'; - -export const PENDING_TIMEOUT_MS = 10000; - -export const PACKAGE_VERSION = `w:${version}`; -export const INTERNAL_AUTH_VERSION = 'FIS_v2'; - -export const INSTALLATIONS_API_URL = - 'https://firebaseinstallations.googleapis.com/v1'; - -export const TOKEN_EXPIRATION_BUFFER = 60 * 60 * 1000; // One hour - -export const SERVICE = 'installations'; -export const SERVICE_NAME = 'Installations'; diff --git a/packages-exp/installations-exp/src/util/errors.ts b/packages-exp/installations-exp/src/util/errors.ts deleted file mode 100644 index 25cc69b1ff0..00000000000 --- a/packages-exp/installations-exp/src/util/errors.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ErrorFactory, FirebaseError } from '@firebase/util'; -import { SERVICE, SERVICE_NAME } from './constants'; - -export const enum ErrorCode { - MISSING_APP_CONFIG_VALUES = 'missing-app-config-values', - NOT_REGISTERED = 'not-registered', - INSTALLATION_NOT_FOUND = 'installation-not-found', - REQUEST_FAILED = 'request-failed', - APP_OFFLINE = 'app-offline', - DELETE_PENDING_REGISTRATION = 'delete-pending-registration' -} - -const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { - [ErrorCode.MISSING_APP_CONFIG_VALUES]: - 'Missing App configuration value: "{$valueName}"', - [ErrorCode.NOT_REGISTERED]: 'Firebase Installation is not registered.', - [ErrorCode.INSTALLATION_NOT_FOUND]: 'Firebase Installation not found.', - [ErrorCode.REQUEST_FAILED]: - '{$requestName} request failed with error "{$serverCode} {$serverStatus}: {$serverMessage}"', - [ErrorCode.APP_OFFLINE]: 'Could not process request. Application offline.', - [ErrorCode.DELETE_PENDING_REGISTRATION]: - "Can't delete installation while there is a pending registration request." -}; - -interface ErrorParams { - [ErrorCode.MISSING_APP_CONFIG_VALUES]: { - valueName: string; - }; - [ErrorCode.REQUEST_FAILED]: { - requestName: string; - [index: string]: string | number; // to make Typescript 3.8 happy - } & ServerErrorData; -} - -export const ERROR_FACTORY = new ErrorFactory( - SERVICE, - SERVICE_NAME, - ERROR_DESCRIPTION_MAP -); - -export interface ServerErrorData { - serverCode: number; - serverMessage: string; - serverStatus: string; -} - -export type ServerError = FirebaseError & { customData: ServerErrorData }; - -/** Returns true if error is a FirebaseError that is based on an error from the server. */ -export function isServerError(error: unknown): error is ServerError { - return ( - error instanceof FirebaseError && - error.code.includes(ErrorCode.REQUEST_FAILED) - ); -} diff --git a/packages-exp/installations-exp/src/util/get-key.ts b/packages-exp/installations-exp/src/util/get-key.ts deleted file mode 100644 index 272d342d366..00000000000 --- a/packages-exp/installations-exp/src/util/get-key.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AppConfig } from '../interfaces/installation-impl'; - -/** Returns a string key that can be used to identify the app. */ -export function getKey(appConfig: AppConfig): string { - return `${appConfig.appName}!${appConfig.appId}`; -} diff --git a/packages-exp/installations-exp/src/util/sleep.test.ts b/packages-exp/installations-exp/src/util/sleep.test.ts deleted file mode 100644 index 6dfc4b328ee..00000000000 --- a/packages-exp/installations-exp/src/util/sleep.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { SinonFakeTimers, useFakeTimers } from 'sinon'; -import '../testing/setup'; -import { sleep } from './sleep'; - -describe('sleep', () => { - let clock: SinonFakeTimers; - - beforeEach(() => { - clock = useFakeTimers({ shouldAdvanceTime: true }); - }); - - it('returns a promise that resolves after a given amount of time', async () => { - const t0 = clock.now; - await sleep(100); - const t1 = clock.now; - - expect(t1 - t0).to.equal(100); - }); -}); diff --git a/packages-exp/installations-exp/src/util/sleep.ts b/packages-exp/installations-exp/src/util/sleep.ts deleted file mode 100644 index 2bd1eb9283b..00000000000 --- a/packages-exp/installations-exp/src/util/sleep.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Returns a promise that resolves after given time passes. */ -export function sleep(ms: number): Promise { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -} diff --git a/packages-exp/installations-exp/test-app/.gitignore b/packages-exp/installations-exp/test-app/.gitignore deleted file mode 100644 index e706d63f780..00000000000 --- a/packages-exp/installations-exp/test-app/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -sdk.js -sdk.js.map diff --git a/packages-exp/installations-exp/test-app/index.html b/packages-exp/installations-exp/test-app/index.html deleted file mode 100644 index f5e2958cea0..00000000000 --- a/packages-exp/installations-exp/test-app/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - Test App - - - - -

- - -

-

- - -

-

- - -

-

- - -

-

- - - - -

-

Requests

-
-

Database Contents

-
- - - diff --git a/packages-exp/installations-exp/test-app/index.js b/packages-exp/installations-exp/test-app/index.js deleted file mode 100644 index d0101e3114e..00000000000 --- a/packages-exp/installations-exp/test-app/index.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const DATABASE_NAME = 'firebase-installations-database'; -const DATABASE_VERSION = 1; -const OBJECT_STORE_NAME = 'firebase-installations-store'; - -const requestLogs = []; -let db; - -window.indexedDB.open(DATABASE_NAME, DATABASE_VERSION).onsuccess = event => { - db = event.target.result; - setInterval(refreshDatabase, 1000); -}; - -function refreshDatabase() { - const request = db - .transaction(OBJECT_STORE_NAME, 'readwrite') - .objectStore(OBJECT_STORE_NAME) - .getAll(); - - request.onsuccess = () => { - const dbElement = getElement('database'); - dbElement.innerHTML = request.result - .map(v => `

${format(v)}

`) - .join(''); - }; -} - -function clearDb() { - const request = db - .transaction(OBJECT_STORE_NAME, 'readwrite') - .objectStore(OBJECT_STORE_NAME) - .clear(); - request.onsuccess = refreshDatabase; -} - -function getElement(id) { - const element = document.getElementById(id); - if (!element) { - throw new Error(`Element not found: ${id}`); - } - return element; -} - -function getInputValue(elementId) { - const element = getElement(elementId); - return element.value; -} - -function getId() { - printRequest('Get ID', FirebaseInstallations.getId(getApp())); -} - -function getToken() { - printRequest('Get Token', FirebaseInstallations.getToken(getApp())); -} - -function deleteInstallation() { - printRequest( - 'Delete Installation', - FirebaseInstallations.deleteInstallation(getApp()) - ); -} - -async function printRequest(requestInfo, promise) { - const requestsElement = getElement('requests'); - requestsElement.innerHTML = '

Loading...

' + requestLogs.join(''); - let result; - try { - const request = await promise; - result = request ? format(request) : 'Completed successfully'; - } catch (e) { - result = e.toString(); - } - requestLogs.unshift(`

${requestInfo}:
${result}

`); - requestsElement.innerHTML = requestLogs.join(''); -} - -function format(o) { - const escapedString = JSON.stringify(o, null, 2); - return `${escapedString}`; -} - -function getApp() { - const appName = getInputValue('appName'); - const projectId = getInputValue('projectId'); - const apiKey = getInputValue('apiKey'); - const appId = getInputValue('appId'); - return { - name: appName, - appConfig: { appName, projectId, apiKey, appId } - }; -} - -getElement('getId').onclick = getId; -getElement('getToken').onclick = getToken; -getElement('deleteInstallation').onclick = deleteInstallation; -getElement('clearDb').onclick = clearDb; diff --git a/packages-exp/installations-exp/test-app/rollup.config.js b/packages-exp/installations-exp/test-app/rollup.config.js deleted file mode 100644 index 6f6d3a3eae7..00000000000 --- a/packages-exp/installations-exp/test-app/rollup.config.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import typescriptPlugin from 'rollup-plugin-typescript2'; -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -import { uglify } from 'rollup-plugin-uglify'; -import typescript from 'typescript'; - -/** - * Creates an iife build to run with the Test App. - */ -export default [ - { - input: 'src/api/index.ts', - output: { - name: 'FirebaseInstallations', - file: 'test-app/sdk.js', - format: 'iife', - sourcemap: true - }, - plugins: [ - typescriptPlugin({ - typescript, - tsconfigOverride: { compilerOptions: { declaration: false } } - }), - json(), - resolve(), - commonjs(), - uglify() - ] - } -]; diff --git a/packages-exp/installations-exp/tsconfig.json b/packages-exp/installations-exp/tsconfig.json deleted file mode 100644 index 420eda97a1d..00000000000 --- a/packages-exp/installations-exp/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "downlevelIteration": true, - "resolveJsonModule": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "exclude": ["dist/**/*"] -} diff --git a/packages-exp/installations-types-exp/api-extractor.json b/packages-exp/installations-types-exp/api-extractor.json deleted file mode 100644 index 42f37a88c4b..00000000000 --- a/packages-exp/installations-types-exp/api-extractor.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/index.d.ts" -} \ No newline at end of file diff --git a/packages-exp/installations-types-exp/index.d.ts b/packages-exp/installations-types-exp/index.d.ts deleted file mode 100644 index 1803c45d422..00000000000 --- a/packages-exp/installations-types-exp/index.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Public interface of the FirebaseInstallations SDK. - * - * @public - */ -export interface FirebaseInstallations {} - -/** - * An interface for Firebase internal SDKs use only. - * - * @internal - */ -export interface _FirebaseInstallationsInternal { - /** - * Creates a Firebase Installation if there isn't one for the app and - * returns the Installation ID. - */ - getId(): Promise; - - /** - * Returns an Authentication Token for the current Firebase Installation. - */ - getToken(forceRefresh?: boolean): Promise; -} - -declare module '@firebase/component' { - interface NameServiceMapping { - 'installations-exp': FirebaseInstallations; - 'installations-exp-internal': _FirebaseInstallationsInternal; - } -} diff --git a/packages-exp/installations-types-exp/package.json b/packages-exp/installations-types-exp/package.json deleted file mode 100644 index 4ada3a065a2..00000000000 --- a/packages-exp/installations-types-exp/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@firebase/installations-types-exp", - "private": true, - "version": "0.0.900", - "description": "@firebase/installations-exp Types", - "author": "Firebase (https://firebase.google.com/)", - "license": "Apache-2.0", - "scripts": { - "test": "tsc", - "test:ci": "node ../../scripts/run_tests_in_ci.js", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn api-report && yarn doc" - }, - "files": [ - "index.d.ts" - ], - "peerDependencies": { - "@firebase/app-types": "0.x" - }, - "repository": { - "directory": "packages/installations-types-exp", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "devDependencies": { - "typescript": "4.0.5" - } -} diff --git a/packages-exp/installations-types-exp/tsconfig.json b/packages-exp/installations-types-exp/tsconfig.json deleted file mode 100644 index 9ec79aa816b..00000000000 --- a/packages-exp/installations-types-exp/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../installations-exp/tsconfig.json", - "compilerOptions": { - "noEmit": true - }, - "exclude": ["dist/**/*"] -} diff --git a/packages-exp/messaging-exp/.eslintrc.js b/packages-exp/messaging-exp/.eslintrc.js deleted file mode 100644 index ca80aa0f69a..00000000000 --- a/packages-exp/messaging-exp/.eslintrc.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module.exports = { - extends: '../../config/.eslintrc.js', - parserOptions: { - project: 'tsconfig.json', - // to make vscode-eslint work with monorepo - // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 - tsconfigRootDir: __dirname - } -}; diff --git a/packages-exp/messaging-exp/README.md b/packages-exp/messaging-exp/README.md deleted file mode 100644 index 8f3fd52738a..00000000000 --- a/packages-exp/messaging-exp/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @firebase/messaging - -This is the Firebase Cloud Messaging component of the Firebase JS SDK. - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/messaging-exp/api-extractor.json b/packages-exp/messaging-exp/api-extractor.json deleted file mode 100644 index 44b475ca490..00000000000 --- a/packages-exp/messaging-exp/api-extractor.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/dist/index.d.ts", - "dtsRollup": { - "enabled": true - } -} diff --git a/packages-exp/messaging-exp/karma.conf.js b/packages-exp/messaging-exp/karma.conf.js deleted file mode 100644 index c9bc6b770c9..00000000000 --- a/packages-exp/messaging-exp/karma.conf.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const karmaBase = require('../../config/karma.base'); - -const files = [`src/**/*.test.ts`]; - -module.exports = function (config) { - const karmaConfig = { - ...karmaBase, - files, - frameworks: ['mocha'] - }; - - config.set(karmaConfig); -}; - -module.exports.files = files; diff --git a/packages-exp/messaging-exp/package.json b/packages-exp/messaging-exp/package.json deleted file mode 100644 index 388f4cd4b6f..00000000000 --- a/packages-exp/messaging-exp/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@firebase/messaging-exp", - "private": true, - "version": "0.0.900", - "description": "", - "author": "Firebase (https://firebase.google.com/)", - "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", - "esm2017": "dist/index.esm2017.js", - "sw": "dist/index.sw.esm5.js", - "files": [ - "dist" - ], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c && yarn api-report", - "build:deps": "lerna run --scope @firebase/'{app-exp,messaging-exp}' --include-dependencies build", - "build:release": "rollup -c rollup.config.release.js && yarn api-report", - "dev": "rollup -c -w", - "test": "run-p test:karma type-check lint ", - "test:integration": "run-p test:karma type-check lint && cd ../../integration/messaging && npm run-script test", - "test:ci": "node ../../scripts/run_tests_in_ci.js", - "test:karma": "karma start --single-run", - "test:debug": "karma start --browsers=Chrome --auto-watch", - "api-report": "api-extractor run --local --verbose", - "type-check": "tsc --noEmit" - }, - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-exp": "0.x", - "@firebase/app-types-exp": "0.x" - }, - "dependencies": { - "@firebase/component": "0.1.21", - "@firebase/installations-exp": "0.0.900", - "@firebase/messaging-types-exp": "0.0.900", - "@firebase/util": "0.3.4", - "idb": "3.0.2", - "tslib": "^1.11.1" - }, - "devDependencies": { - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-typescript2": "0.29.0", - "ts-essentials": "7.0.1", - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages/messaging", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "dist/index.d.ts" -} \ No newline at end of file diff --git a/packages-exp/messaging-exp/rollup.config.js b/packages-exp/messaging-exp/rollup.config.js deleted file mode 100644 index 2c81fd25d51..00000000000 --- a/packages-exp/messaging-exp/rollup.config.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; -import pkg from './package.json'; -import typescript from 'typescript'; -import typescriptPlugin from 'rollup-plugin-typescript2'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript - }), - json() -]; - -const es5Builds = [ - // window builds - { - input: 'src/index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } - ], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - - // sw builds - { - input: 'src/index.sw.ts', - output: [{ file: pkg.sw, format: 'es', sourcemap: true }], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - } - }), - json({ preferConst: true }) -]; - -const es2017Builds = [ - { - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - plugins: es2017BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/messaging-exp/rollup.config.release.js b/packages-exp/messaging-exp/rollup.config.release.js deleted file mode 100644 index 0ec683bc272..00000000000 --- a/packages-exp/messaging-exp/rollup.config.release.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import json from '@rollup/plugin-json'; -import pkg from './package.json'; -import typescript from 'typescript'; -import typescriptPlugin from 'rollup-plugin-typescript2'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - clean: true, - abortOnError: false, - transformers: [importPathTransformer] - }), - json() -]; - -const es5Builds = [ - // window builds - { - input: 'src/index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } - ], - plugins: es5BuildPlugins, - treeshake: { - moduleSideEffects: false - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - }, - - // sw builds - { - input: 'src/index.sw.ts', - output: [{ file: pkg.sw, format: 'es', sourcemap: true }], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - abortOnError: false, - clean: true, - transformers: [importPathTransformer], - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - } - }), - json({ preferConst: true }) -]; - -const es2017Builds = [ - { - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - plugins: es2017BuildPlugins, - treeshake: { - moduleSideEffects: false - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/messaging-exp/src/api.ts b/packages-exp/messaging-exp/src/api.ts deleted file mode 100644 index 5f23959802d..00000000000 --- a/packages-exp/messaging-exp/src/api.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - FirebaseMessaging, - MessagePayload -} from '@firebase/messaging-types-exp'; -import { NextFn, Observer, Unsubscribe } from '@firebase/util'; - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { MessagingService } from './messaging-service'; -import { Provider } from '@firebase/component'; -import { deleteToken as _deleteToken } from './api/deleteToken'; -import { _getProvider } from '@firebase/app-exp'; -import { getToken as _getToken } from './api/getToken'; -import { onBackgroundMessage as _onBackgroundMessage } from './api/onBackgroundMessage'; -import { onMessage as _onMessage } from './api/onMessage'; - -/** - * Retrieves a firebase messaging instance. - * - * @return the firebase messaging instance associated with the provided firebase app. - */ -export function getMessaging(app: FirebaseApp): FirebaseMessaging { - const messagingProvider: Provider<'messaging-exp'> = _getProvider( - app, - 'messaging-exp' - ); - - return messagingProvider.getImmediate(); -} - -/** - * Subscribes the messaging instance to push notifications. Returns an FCM registration token - * that can be used to send push messages to that messaging instance. - * - * If a notification permission isn't already granted, this method asks the user for permission. - * The returned promise rejects if the user does not allow the app to show notifications. - * - * @param messaging: the messaging instance. - * @param options.vapidKey The public server key provided to push services. It is used to - * authenticate the push subscribers to receive push messages only from sending servers that - * hold the corresponding private key. If it is not provided, a default VAPID key is used. Note - * that some push services (Chrome Push Service) require a non-default VAPID key. Therefore, it - * is recommended to generate and import a VAPID key for your project with - * {@link https://firebase.google.com/docs/cloud-messaging/js/client#configure_web_credentials_with_fcm Configure Web Credentials with FCM}. - * See - * {@link https://developers.google.com/web/fundamentals/push-notifications/web-push-protocol The Web Push Protocol} - * for details on web push services.} - * - * @param options.serviceWorkerRegistration The service worker registration for receiving push - * messaging. If the registration is not provided explicitly, you need to have a - * `firebase-messaging-sw.js` at your root location. See - * {@link https://firebase.google.com/docs/cloud-messaging/js/client#retrieve-the-current-registration-token Retrieve the current registration token} - * for more details. - * - * @return The promise resolves with an FCM registration token. - * - */ -export async function getToken( - messaging: FirebaseMessaging, - options?: { vapidKey?: string; swReg?: ServiceWorkerRegistration } -): Promise { - return _getToken(messaging as MessagingService, options); -} - -/** - * Deletes the registration token associated with this messaging instance and unsubscribes the - * messaging instance from the push subscription. - * - * @param messaging: the messaging instance. - * - * @return The promise resolves when the token has been successfully deleted. - */ -export function deleteToken(messaging: FirebaseMessaging): Promise { - return _deleteToken(messaging as MessagingService); -} - -/** - * When a push message is received and the user is currently on a page for your origin, the - * message is passed to the page and an `onMessage()` event is dispatched with the payload of - * the push message. - * - * - * @param messaging: the messaging instance. - * @param - * nextOrObserver This function, or observer object with `next` defined, - * is called when a message is received and the user is currently viewing your page. - * @return To stop listening for messages execute this returned function. - */ -export function onMessage( - messaging: FirebaseMessaging, - nextOrObserver: NextFn | Observer -): Unsubscribe { - return _onMessage(messaging as MessagingService, nextOrObserver); -} - -/** - * Called when a message is received while the app is in the background. An app is considered to - * be in the background if no active window is displayed. - * - * @param messaging: the messaging instance. - * @param - * nextOrObserver This function, or observer object with `next` defined, - * is called when a message is received and the app is currently in the background. - * - * @return To stop listening for messages execute this returned function - */ -export function onBackgroundMessage( - messaging: FirebaseMessaging, - nextOrObserver: NextFn | Observer -): Unsubscribe { - return _onBackgroundMessage(messaging as MessagingService, nextOrObserver); -} diff --git a/packages-exp/messaging-exp/src/helpers/array-base64-translator.test.ts b/packages-exp/messaging-exp/src/helpers/array-base64-translator.test.ts deleted file mode 100644 index c161b365dbc..00000000000 --- a/packages-exp/messaging-exp/src/helpers/array-base64-translator.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '../testing/setup'; - -import { arrayToBase64, base64ToArray } from './array-base64-translator'; - -import { expect } from 'chai'; - -// prettier-ignore -const TEST_P256_ARRAY = new Uint8Array([ - 4, 181, 98, 240, 48, 62, 75, 119, 193, 227, 154, 69, 250, 216, 53, 110, - 157, 120, 62, 76, 213, 249, 11, 62, 12, 19, 149, 36, 5, 82, 140, 37, 141, - 134, 132, 98, 87, 152, 175, 98, 53, 83, 196, 242, 202, 155, 19, 173, 157, - 216, 45, 147, 20, 12, 151, 160, 147, 159, 205, 219, 75, 133, 156, 129, 152 -]); -const TEST_P256_BASE64 = - 'BLVi8DA-S3fB45pF-tg1bp14PkzV-Qs-DBOVJAVSjCWNhoRi' + - 'V5ivYjVTxPLKmxOtndgtkxQMl6CTn83bS4WcgZg'; - -// prettier-ignore -const TEST_AUTH_ARRAY = new Uint8Array([ - 255, 237, 107, 177, 171, 78, 84, 131, 221, 231, 87, 188, 22, 232, 71, 15 -]); -const TEST_AUTH_BASE64 = '_-1rsatOVIPd51e8FuhHDw'; - -// prettier-ignore -const TEST_VAPID_ARRAY = new Uint8Array([4, 48, 191, 217, 11, 218, 74, 124, 103, 143, 63, 182, 203, - 91, 0, 68, 221, 68, 172, 74, 89, 133, 198, 252, 145, 164, 136, 243, 186, 75, 198, 32, 45, 64, 240, - 120, 141, 173, 240, 131, 253, 83, 209, 193, 129, 50, 155, 126, 189, 23, 127, 232, 109, 75, 101, - 229, 92, 85, 137, 80, 121, 35, 229, 118, 207]); -const TEST_VAPID_BASE64 = - 'BDC_2QvaSnxnjz-2y1sARN1ErEpZhcb8kaSI87pLxiAtQPB4ja3wg_1T0cGBMpt' + - '-vRd_6G1LZeVcVYlQeSPlds8'; - -describe('arrayToBase64', () => { - it('array to base64 translation succeed', () => { - expect(arrayToBase64(TEST_P256_ARRAY)).to.equal(TEST_P256_BASE64); - expect(arrayToBase64(TEST_AUTH_ARRAY)).to.equal(TEST_AUTH_BASE64); - expect(arrayToBase64(TEST_VAPID_ARRAY)).to.equal(TEST_VAPID_BASE64); - }); -}); - -describe('base64ToArray', () => { - it('base64 to array translation succeed', () => { - expect(isEqual(base64ToArray(TEST_P256_BASE64), TEST_P256_ARRAY)).to.equal( - true - ); - expect(isEqual(base64ToArray(TEST_AUTH_BASE64), TEST_AUTH_ARRAY)).to.equal( - true - ); - expect( - isEqual(base64ToArray(TEST_VAPID_BASE64), TEST_VAPID_ARRAY) - ).to.equal(true); - }); -}); - -function isEqual(a: Uint8Array, b: Uint8Array): boolean { - if (a.length !== b.length) { - return false; - } - - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } - } - - return true; -} diff --git a/packages-exp/messaging-exp/src/helpers/array-base64-translator.ts b/packages-exp/messaging-exp/src/helpers/array-base64-translator.ts deleted file mode 100644 index bbade845ae4..00000000000 --- a/packages-exp/messaging-exp/src/helpers/array-base64-translator.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export function arrayToBase64(array: Uint8Array | ArrayBuffer): string { - const uint8Array = new Uint8Array(array); - const base64String = btoa(String.fromCharCode(...uint8Array)); - return base64String.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); -} - -export function base64ToArray(base64String: string): Uint8Array { - const padding = '='.repeat((4 - (base64String.length % 4)) % 4); - const base64 = (base64String + padding) - .replace(/\-/g, '+') - .replace(/_/g, '/'); - - const rawData = atob(base64); - const outputArray = new Uint8Array(rawData.length); - - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i); - } - return outputArray; -} diff --git a/packages-exp/messaging-exp/src/helpers/externalizePayload.test.ts b/packages-exp/messaging-exp/src/helpers/externalizePayload.test.ts deleted file mode 100644 index fdf782107cc..00000000000 --- a/packages-exp/messaging-exp/src/helpers/externalizePayload.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { MessagePayload } from '@firebase/messaging-types-exp'; -import { MessagePayloadInternal } from '../interfaces/internal-message-payload'; -import { expect } from 'chai'; -import { externalizePayload } from './externalizePayload'; - -describe('externalizePayload', () => { - it('externalizes internalMessage with only notification payload', () => { - const internalPayload: MessagePayloadInternal = { - notification: { - title: 'title', - body: 'body', - image: 'image' - }, - from: 'from', - // eslint-disable-next-line camelcase - collapse_key: 'collapse' - }; - - const payload: MessagePayload = { - notification: { title: 'title', body: 'body', image: 'image' }, - from: 'from', - collapseKey: 'collapse' - }; - expect(externalizePayload(internalPayload)).to.deep.equal(payload); - }); - - it('externalizes internalMessage with only data payload', () => { - const internalPayload: MessagePayloadInternal = { - data: { - foo: 'foo', - bar: 'bar', - baz: 'baz' - }, - from: 'from', - // eslint-disable-next-line camelcase - collapse_key: 'collapse' - }; - - const payload: MessagePayload = { - data: { foo: 'foo', bar: 'bar', baz: 'baz' }, - from: 'from', - collapseKey: 'collapse' - }; - expect(externalizePayload(internalPayload)).to.deep.equal(payload); - }); - - it('externalizes internalMessage with all three payloads', () => { - const internalPayload: MessagePayloadInternal = { - notification: { - title: 'title', - body: 'body', - image: 'image' - }, - data: { - foo: 'foo', - bar: 'bar', - baz: 'baz' - }, - fcmOptions: { - link: 'link', - // eslint-disable-next-line camelcase - analytics_label: 'label' - }, - from: 'from', - // eslint-disable-next-line camelcase - collapse_key: 'collapse' - }; - - const payload: MessagePayload = { - notification: { - title: 'title', - body: 'body', - image: 'image' - }, - data: { - foo: 'foo', - bar: 'bar', - baz: 'baz' - }, - fcmOptions: { - link: 'link', - analyticsLabel: 'label' - }, - from: 'from', - collapseKey: 'collapse' - }; - expect(externalizePayload(internalPayload)).to.deep.equal(payload); - }); -}); diff --git a/packages-exp/messaging-exp/src/helpers/externalizePayload.ts b/packages-exp/messaging-exp/src/helpers/externalizePayload.ts deleted file mode 100644 index 57ca6b26e59..00000000000 --- a/packages-exp/messaging-exp/src/helpers/externalizePayload.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { MessagePayload } from '@firebase/messaging-types-exp'; -import { MessagePayloadInternal } from '../interfaces/internal-message-payload'; - -export function externalizePayload( - internalPayload: MessagePayloadInternal -): MessagePayload { - const payload: MessagePayload = { - from: internalPayload.from, - // eslint-disable-next-line camelcase - collapseKey: internalPayload.collapse_key - } as MessagePayload; - - propagateNotificationPayload(payload, internalPayload); - propagateDataPayload(payload, internalPayload); - propagateFcmOptions(payload, internalPayload); - - return payload; -} - -function propagateNotificationPayload( - payload: MessagePayload, - messagePayloadInternal: MessagePayloadInternal -): void { - if (!messagePayloadInternal.notification) { - return; - } - - payload.notification = {}; - - const title = messagePayloadInternal.notification!.title; - if (!!title) { - payload.notification!.title = title; - } - - const body = messagePayloadInternal.notification!.body; - if (!!body) { - payload.notification!.body = body; - } - - const image = messagePayloadInternal.notification!.image; - if (!!image) { - payload.notification!.image = image; - } -} - -function propagateDataPayload( - payload: MessagePayload, - messagePayloadInternal: MessagePayloadInternal -): void { - if (!messagePayloadInternal.data) { - return; - } - - payload.data = messagePayloadInternal.data as { [key: string]: string }; -} - -function propagateFcmOptions( - payload: MessagePayload, - messagePayloadInternal: MessagePayloadInternal -): void { - if (!messagePayloadInternal.fcmOptions) { - return; - } - - payload.fcmOptions = {}; - - const link = messagePayloadInternal.fcmOptions!.link; - if (!!link) { - payload.fcmOptions!.link = link; - } - - // eslint-disable-next-line camelcase - const analyticsLabel = messagePayloadInternal.fcmOptions!.analytics_label; - if (!!analyticsLabel) { - payload.fcmOptions!.analyticsLabel = analyticsLabel; - } -} diff --git a/packages-exp/messaging-exp/src/helpers/extract-app-config.test.ts b/packages-exp/messaging-exp/src/helpers/extract-app-config.test.ts deleted file mode 100644 index 796717e2b33..00000000000 --- a/packages-exp/messaging-exp/src/helpers/extract-app-config.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '../testing/setup'; - -import { AppConfig } from '../interfaces/app-config'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import { expect } from 'chai'; -import { extractAppConfig } from './extract-app-config'; -import { getFakeApp } from '../testing/fakes/firebase-dependencies'; - -describe('extractAppConfig', () => { - it('returns AppConfig if the argument is a FirebaseApp object that includes an appId', () => { - const firebaseApp = getFakeApp(); - const expected: AppConfig = { - appName: 'appName', - apiKey: 'apiKey', - projectId: 'projectId', - appId: '1:777777777777:web:d93b5ca1475efe57', - senderId: '1234567890' - }; - expect(extractAppConfig(firebaseApp)).to.deep.equal(expected); - }); - - it('throws if a necessary value is missing', () => { - expect(() => - extractAppConfig((undefined as unknown) as FirebaseApp) - ).to.throw('Missing App configuration value: "App Configuration Object"'); - - let firebaseApp = getFakeApp(); - delete firebaseApp.options; - expect(() => extractAppConfig(firebaseApp)).to.throw( - 'Missing App configuration value: "App Configuration Object"' - ); - - firebaseApp = getFakeApp(); - delete firebaseApp.name; - expect(() => extractAppConfig(firebaseApp)).to.throw( - 'Missing App configuration value: "App Name"' - ); - - firebaseApp = getFakeApp(); - delete firebaseApp.options.projectId; - expect(() => extractAppConfig(firebaseApp)).to.throw( - 'Missing App configuration value: "projectId"' - ); - - firebaseApp = getFakeApp(); - delete firebaseApp.options.apiKey; - expect(() => extractAppConfig(firebaseApp)).to.throw( - 'Missing App configuration value: "apiKey"' - ); - - firebaseApp = getFakeApp(); - delete firebaseApp.options.appId; - expect(() => extractAppConfig(firebaseApp)).to.throw( - 'Missing App configuration value: "appId"' - ); - - firebaseApp = getFakeApp(); - delete firebaseApp.options.messagingSenderId; - expect(() => extractAppConfig(firebaseApp)).to.throw( - 'Missing App configuration value: "messagingSenderId"' - ); - }); -}); diff --git a/packages-exp/messaging-exp/src/helpers/extract-app-config.ts b/packages-exp/messaging-exp/src/helpers/extract-app-config.ts deleted file mode 100644 index 8007f35e2c3..00000000000 --- a/packages-exp/messaging-exp/src/helpers/extract-app-config.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; -import { FirebaseApp, FirebaseOptions } from '@firebase/app-types-exp'; - -import { AppConfig } from '../interfaces/app-config'; -import { FirebaseError } from '@firebase/util'; - -export function extractAppConfig(app: FirebaseApp): AppConfig { - if (!app || !app.options) { - throw getMissingValueError('App Configuration Object'); - } - - if (!app.name) { - throw getMissingValueError('App Name'); - } - - // Required app config keys - const configKeys: ReadonlyArray = [ - 'projectId', - 'apiKey', - 'appId', - 'messagingSenderId' - ]; - - const { options } = app; - for (const keyName of configKeys) { - if (!options[keyName]) { - throw getMissingValueError(keyName); - } - } - - return { - appName: app.name, - projectId: options.projectId!, - apiKey: options.apiKey!, - appId: options.appId!, - senderId: options.messagingSenderId! - }; -} - -function getMissingValueError(valueName: string): FirebaseError { - return ERROR_FACTORY.create(ErrorCode.MISSING_APP_CONFIG_VALUES, { - valueName - }); -} diff --git a/packages-exp/messaging-exp/src/helpers/is-console-message.ts b/packages-exp/messaging-exp/src/helpers/is-console-message.ts deleted file mode 100644 index 151713be132..00000000000 --- a/packages-exp/messaging-exp/src/helpers/is-console-message.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CONSOLE_CAMPAIGN_ID } from '../util/constants'; -import { ConsoleMessageData } from '../interfaces/internal-message-payload'; - -export function isConsoleMessage(data: unknown): data is ConsoleMessageData { - // This message has a campaign ID, meaning it was sent using the Firebase Console. - return typeof data === 'object' && !!data && CONSOLE_CAMPAIGN_ID in data; -} diff --git a/packages-exp/messaging-exp/src/helpers/isSupported.ts b/packages-exp/messaging-exp/src/helpers/isSupported.ts deleted file mode 100644 index c73122eaad1..00000000000 --- a/packages-exp/messaging-exp/src/helpers/isSupported.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export function isSupported(): boolean { - if (self && 'ServiceWorkerGlobalScope' in self) { - // Running in ServiceWorker context - return isSWControllerSupported(); - } else { - // Assume we are in the window context. - return isWindowControllerSupported(); - } -} - -/** - * Checks to see if the required APIs exist. - */ -function isWindowControllerSupported(): boolean { - return ( - 'indexedDB' in window && - indexedDB !== null && - navigator.cookieEnabled && - 'serviceWorker' in navigator && - 'PushManager' in window && - 'Notification' in window && - 'fetch' in window && - ServiceWorkerRegistration.prototype.hasOwnProperty('showNotification') && - PushSubscription.prototype.hasOwnProperty('getKey') - ); -} - -/** - * Checks to see if the required APIs exist within SW Context. - */ -function isSWControllerSupported(): boolean { - return ( - 'indexedDB' in self && - indexedDB !== null && - 'PushManager' in self && - 'Notification' in self && - ServiceWorkerRegistration.prototype.hasOwnProperty('showNotification') && - PushSubscription.prototype.hasOwnProperty('getKey') - ); -} diff --git a/packages-exp/messaging-exp/src/helpers/migrate-old-database.test.ts b/packages-exp/messaging-exp/src/helpers/migrate-old-database.test.ts deleted file mode 100644 index 020295ca2fd..00000000000 --- a/packages-exp/messaging-exp/src/helpers/migrate-old-database.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '../testing/setup'; - -import { - V2TokenDetails, - V3TokenDetails, - V4TokenDetails, - migrateOldDatabase -} from './migrate-old-database'; - -import { FakePushSubscription } from '../testing/fakes/service-worker'; -import { base64ToArray } from './array-base64-translator'; -import { expect } from 'chai'; -import { getFakeTokenDetails } from '../testing/fakes/token-details'; -import { openDb } from 'idb'; - -describe('migrateOldDb', () => { - it("does nothing if old DB didn't exist", async () => { - const tokenDetails = await migrateOldDatabase('1234567890'); - expect(tokenDetails).to.be.null; - }); - - it('does nothing if old DB was too old', async () => { - await put(1, { - swScope: '/scope-value', - fcmSenderId: '1234567890', - fcmToken: 'token-value' - }); - - const tokenDetails = await migrateOldDatabase('1234567890'); - expect(tokenDetails).to.be.null; - }); - - describe('version 2', () => { - beforeEach(async () => { - const v2TokenDetails: V2TokenDetails = { - fcmToken: 'token-value', - swScope: '/scope-value', - vapidKey: base64ToArray('dmFwaWQta2V5LXZhbHVl'), - fcmSenderId: '1234567890', - fcmPushSet: '7654321', - auth: 'YXV0aC12YWx1ZQ', - p256dh: 'cDI1Ni12YWx1ZQ', - endpoint: 'https://example.org', - subscription: new FakePushSubscription() - }; - - await put(2, v2TokenDetails); - }); - - it('can get a value from old DB', async () => { - const tokenDetails = await migrateOldDatabase('1234567890'); - - const expectedTokenDetails = getFakeTokenDetails(); - // Ignore createTime difference. - expectedTokenDetails.createTime = tokenDetails!.createTime; - - expect(tokenDetails).to.deep.equal(expectedTokenDetails); - }); - - it('only migrates once', async () => { - await migrateOldDatabase('1234567890'); - const tokenDetails = await migrateOldDatabase('1234567890'); - - expect(tokenDetails).to.be.null; - }); - - it('does not get a value that has a different sender ID', async () => { - const tokenDetails = await migrateOldDatabase('321321321'); - expect(tokenDetails).to.be.null; - }); - - it('does not migrate an entry with missing optional values', async () => { - const v2TokenDetails: V2TokenDetails = { - fcmToken: 'token-value', - swScope: '/scope-value', - vapidKey: base64ToArray('dmFwaWQta2V5LXZhbHVl'), - fcmSenderId: '1234567890', - fcmPushSet: '7654321', - subscription: new FakePushSubscription() - }; - await put(2, v2TokenDetails); - - const tokenDetails = await migrateOldDatabase('1234567890'); - expect(tokenDetails).to.be.null; - }); - }); - - describe('version 3', () => { - beforeEach(async () => { - const v3TokenDetails: V3TokenDetails = { - createTime: 1234567890, - fcmToken: 'token-value', - swScope: '/scope-value', - vapidKey: base64ToArray('dmFwaWQta2V5LXZhbHVl'), - fcmSenderId: '1234567890', - fcmPushSet: '7654321', - auth: base64ToArray('YXV0aC12YWx1ZQ'), - p256dh: base64ToArray('cDI1Ni12YWx1ZQ'), - endpoint: 'https://example.org' - }; - - await put(3, v3TokenDetails); - }); - - it('can get a value from old DB', async () => { - const tokenDetails = await migrateOldDatabase('1234567890'); - - const expectedTokenDetails = getFakeTokenDetails(); - - expect(tokenDetails).to.deep.equal(expectedTokenDetails); - }); - - it('only migrates once', async () => { - await migrateOldDatabase('1234567890'); - const tokenDetails = await migrateOldDatabase('1234567890'); - - expect(tokenDetails).to.be.null; - }); - - it('does not get a value that has a different sender ID', async () => { - const tokenDetails = await migrateOldDatabase('321321321'); - expect(tokenDetails).to.be.null; - }); - }); - - describe('version 4', () => { - beforeEach(async () => { - const v4TokenDetails: V4TokenDetails = { - createTime: 1234567890, - fcmToken: 'token-value', - swScope: '/scope-value', - vapidKey: base64ToArray('dmFwaWQta2V5LXZhbHVl'), - fcmSenderId: '1234567890', - auth: base64ToArray('YXV0aC12YWx1ZQ'), - p256dh: base64ToArray('cDI1Ni12YWx1ZQ'), - endpoint: 'https://example.org' - }; - - await put(4, v4TokenDetails); - }); - - it('can get a value from old DB', async () => { - const tokenDetails = await migrateOldDatabase('1234567890'); - - const expectedTokenDetails = getFakeTokenDetails(); - - expect(tokenDetails).to.deep.equal(expectedTokenDetails); - }); - - it('only migrates once', async () => { - await migrateOldDatabase('1234567890'); - const tokenDetails = await migrateOldDatabase('1234567890'); - - expect(tokenDetails).to.be.null; - }); - - it('does not get a value that has a different sender ID', async () => { - const tokenDetails = await migrateOldDatabase('321321321'); - expect(tokenDetails).to.be.null; - }); - }); -}); - -async function put(version: number, value: object): Promise { - const db = await openDb('fcm_token_details_db', version, upgradeDb => { - if (upgradeDb.oldVersion === 0) { - const objectStore = upgradeDb.createObjectStore( - 'fcm_token_object_Store', - { - keyPath: 'swScope' - } - ); - objectStore.createIndex('fcmSenderId', 'fcmSenderId', { - unique: false - }); - objectStore.createIndex('fcmToken', 'fcmToken', { unique: true }); - } - }); - - try { - const tx = db.transaction('fcm_token_object_Store', 'readwrite'); - await tx.objectStore('fcm_token_object_Store').put(value); - await tx.complete; - } finally { - db.close(); - } -} diff --git a/packages-exp/messaging-exp/src/helpers/migrate-old-database.ts b/packages-exp/messaging-exp/src/helpers/migrate-old-database.ts deleted file mode 100644 index 40fdece6171..00000000000 --- a/packages-exp/messaging-exp/src/helpers/migrate-old-database.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { deleteDb, openDb } from 'idb'; - -import { TokenDetails } from '../interfaces/token-details'; -import { arrayToBase64 } from './array-base64-translator'; - -// https://github.com/firebase/firebase-js-sdk/blob/7857c212f944a2a9eb421fd4cb7370181bc034b5/packages/messaging/src/interfaces/token-details.ts -export interface V2TokenDetails { - fcmToken: string; - swScope: string; - vapidKey: string | Uint8Array; - subscription: PushSubscription; - fcmSenderId: string; - fcmPushSet: string; - createTime?: number; - endpoint?: string; - auth?: string; - p256dh?: string; -} - -// https://github.com/firebase/firebase-js-sdk/blob/6b5b15ce4ea3df5df5df8a8b33a4e41e249c7715/packages/messaging/src/interfaces/token-details.ts -export interface V3TokenDetails { - fcmToken: string; - swScope: string; - vapidKey: Uint8Array; - fcmSenderId: string; - fcmPushSet: string; - endpoint: string; - auth: ArrayBuffer; - p256dh: ArrayBuffer; - createTime: number; -} - -// https://github.com/firebase/firebase-js-sdk/blob/9567dba664732f681fa7fe60f5b7032bb1daf4c9/packages/messaging/src/interfaces/token-details.ts -export interface V4TokenDetails { - fcmToken: string; - swScope: string; - vapidKey: Uint8Array; - fcmSenderId: string; - endpoint: string; - auth: ArrayBufferLike; - p256dh: ArrayBufferLike; - createTime: number; -} - -const OLD_DB_NAME = 'fcm_token_details_db'; -/** - * The last DB version of 'fcm_token_details_db' was 4. This is one higher, so that the upgrade - * callback is called for all versions of the old DB. - */ -const OLD_DB_VERSION = 5; -const OLD_OBJECT_STORE_NAME = 'fcm_token_object_Store'; - -export async function migrateOldDatabase( - senderId: string -): Promise { - if ('databases' in indexedDB) { - // indexedDb.databases() is an IndexedDB v3 API and does not exist in all browsers. TODO: Remove - // typecast when it lands in TS types. - const databases = await (indexedDB as { - databases(): Promise>; - }).databases(); - const dbNames = databases.map(db => db.name); - - if (!dbNames.includes(OLD_DB_NAME)) { - // old DB didn't exist, no need to open. - return null; - } - } - - let tokenDetails: TokenDetails | null = null; - - const db = await openDb(OLD_DB_NAME, OLD_DB_VERSION, async db => { - if (db.oldVersion < 2) { - // Database too old, skip migration. - return; - } - - if (!db.objectStoreNames.contains(OLD_OBJECT_STORE_NAME)) { - // Database did not exist. Nothing to do. - return; - } - - const objectStore = db.transaction.objectStore(OLD_OBJECT_STORE_NAME); - const value = await objectStore.index('fcmSenderId').get(senderId); - await objectStore.clear(); - - if (!value) { - // No entry in the database, nothing to migrate. - return; - } - - if (db.oldVersion === 2) { - const oldDetails = value as V2TokenDetails; - - if (!oldDetails.auth || !oldDetails.p256dh || !oldDetails.endpoint) { - return; - } - - tokenDetails = { - token: oldDetails.fcmToken, - createTime: oldDetails.createTime ?? Date.now(), - subscriptionOptions: { - auth: oldDetails.auth, - p256dh: oldDetails.p256dh, - endpoint: oldDetails.endpoint, - swScope: oldDetails.swScope, - vapidKey: - typeof oldDetails.vapidKey === 'string' - ? oldDetails.vapidKey - : arrayToBase64(oldDetails.vapidKey) - } - }; - } else if (db.oldVersion === 3) { - const oldDetails = value as V3TokenDetails; - - tokenDetails = { - token: oldDetails.fcmToken, - createTime: oldDetails.createTime, - subscriptionOptions: { - auth: arrayToBase64(oldDetails.auth), - p256dh: arrayToBase64(oldDetails.p256dh), - endpoint: oldDetails.endpoint, - swScope: oldDetails.swScope, - vapidKey: arrayToBase64(oldDetails.vapidKey) - } - }; - } else if (db.oldVersion === 4) { - const oldDetails = value as V4TokenDetails; - - tokenDetails = { - token: oldDetails.fcmToken, - createTime: oldDetails.createTime, - subscriptionOptions: { - auth: arrayToBase64(oldDetails.auth), - p256dh: arrayToBase64(oldDetails.p256dh), - endpoint: oldDetails.endpoint, - swScope: oldDetails.swScope, - vapidKey: arrayToBase64(oldDetails.vapidKey) - } - }; - } - }); - db.close(); - - // Delete all old databases. - await deleteDb(OLD_DB_NAME); - await deleteDb('fcm_vapid_details_db'); - await deleteDb('undefined'); - - return checkTokenDetails(tokenDetails) ? tokenDetails : null; -} - -function checkTokenDetails( - tokenDetails: TokenDetails | null -): tokenDetails is TokenDetails { - if (!tokenDetails || !tokenDetails.subscriptionOptions) { - return false; - } - const { subscriptionOptions } = tokenDetails; - return ( - typeof tokenDetails.createTime === 'number' && - tokenDetails.createTime > 0 && - typeof tokenDetails.token === 'string' && - tokenDetails.token.length > 0 && - typeof subscriptionOptions.auth === 'string' && - subscriptionOptions.auth.length > 0 && - typeof subscriptionOptions.p256dh === 'string' && - subscriptionOptions.p256dh.length > 0 && - typeof subscriptionOptions.endpoint === 'string' && - subscriptionOptions.endpoint.length > 0 && - typeof subscriptionOptions.swScope === 'string' && - subscriptionOptions.swScope.length > 0 && - typeof subscriptionOptions.vapidKey === 'string' && - subscriptionOptions.vapidKey.length > 0 - ); -} diff --git a/packages-exp/messaging-exp/src/helpers/register.ts b/packages-exp/messaging-exp/src/helpers/register.ts deleted file mode 100644 index cb6b99259bb..00000000000 --- a/packages-exp/messaging-exp/src/helpers/register.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Component, - ComponentContainer, - ComponentType, - InstanceFactory -} from '@firebase/component'; -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; - -import { MessagingService } from '../messaging-service'; -import { _registerComponent } from '@firebase/app-exp'; -import { isSupported } from './isSupported'; - -const messagingFactory: InstanceFactory<'messaging-exp'> = ( - container: ComponentContainer -) => { - if (!isSupported()) { - throw ERROR_FACTORY.create(ErrorCode.UNSUPPORTED_BROWSER); - } - - return new MessagingService( - container.getProvider('app-exp').getImmediate(), - container.getProvider('installations-exp-internal').getImmediate(), - container.getProvider('analytics-internal') - ); -}; - -export function registerMessaging(): void { - _registerComponent( - new Component('messaging-exp', messagingFactory, ComponentType.PUBLIC) - ); -} diff --git a/packages-exp/messaging-exp/src/helpers/registerDefaultSw.ts b/packages-exp/messaging-exp/src/helpers/registerDefaultSw.ts deleted file mode 100644 index 2d929596c68..00000000000 --- a/packages-exp/messaging-exp/src/helpers/registerDefaultSw.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DEFAULT_SW_PATH, DEFAULT_SW_SCOPE } from '../util/constants'; -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; - -import { MessagingService } from '../messaging-service'; - -export async function registerDefaultSw( - messaging: MessagingService -): Promise { - try { - messaging.swRegistration = await navigator.serviceWorker.register( - DEFAULT_SW_PATH, - { - scope: DEFAULT_SW_SCOPE - } - ); - - // The timing when browser updates sw when sw has an update is unreliable from experiment. It - // leads to version conflict when the SDK upgrades to a newer version in the main page, but sw - // is stuck with the old version. For example, - // https://github.com/firebase/firebase-js-sdk/issues/2590 The following line reliably updates - // sw if there was an update. - messaging.swRegistration.update().catch(() => { - /* it is non blocking and we don't care if it failed */ - }); - } catch (e) { - throw ERROR_FACTORY.create(ErrorCode.FAILED_DEFAULT_REGISTRATION, { - browserErrorMessage: e.message - }); - } -} diff --git a/packages-exp/messaging-exp/src/helpers/sleep.test.ts b/packages-exp/messaging-exp/src/helpers/sleep.test.ts deleted file mode 100644 index b7c4e228f10..00000000000 --- a/packages-exp/messaging-exp/src/helpers/sleep.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '../testing/setup'; - -import { SinonFakeTimers, useFakeTimers } from 'sinon'; - -import { expect } from 'chai'; -import { sleep } from './sleep'; - -describe('sleep', () => { - let clock: SinonFakeTimers; - - beforeEach(() => { - clock = useFakeTimers({ shouldAdvanceTime: true }); - }); - - it('returns a promise that resolves after a given amount of time', async () => { - const t0 = clock.now; - await sleep(100); - const t1 = clock.now; - - expect(t1 - t0).to.equal(100); - }); -}); diff --git a/packages-exp/messaging-exp/src/helpers/sleep.ts b/packages-exp/messaging-exp/src/helpers/sleep.ts deleted file mode 100644 index 2bd1eb9283b..00000000000 --- a/packages-exp/messaging-exp/src/helpers/sleep.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** Returns a promise that resolves after given time passes. */ -export function sleep(ms: number): Promise { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -} diff --git a/packages-exp/messaging-exp/src/index.sw.ts b/packages-exp/messaging-exp/src/index.sw.ts deleted file mode 100644 index 363bcc1301b..00000000000 --- a/packages-exp/messaging-exp/src/index.sw.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseMessaging } from '@firebase/messaging-types-exp'; -import { registerMessaging } from './helpers/register'; - -export { onBackgroundMessage, getMessaging } from './api'; - -declare module '@firebase/component' { - interface NameServiceMapping { - 'messaging-exp': FirebaseMessaging; - } -} - -registerMessaging(); diff --git a/packages-exp/messaging-exp/src/index.ts b/packages-exp/messaging-exp/src/index.ts deleted file mode 100644 index 9fba2c0b93b..00000000000 --- a/packages-exp/messaging-exp/src/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseMessaging } from '@firebase/messaging-types-exp'; -import { registerMessaging } from './helpers/register'; - -export { getToken, deleteToken, onMessage, getMessaging } from './api'; - -declare module '@firebase/component' { - interface NameServiceMapping { - 'messaging-exp': FirebaseMessaging; - } -} - -registerMessaging(); diff --git a/packages-exp/messaging-exp/src/interfaces/app-config.ts b/packages-exp/messaging-exp/src/interfaces/app-config.ts deleted file mode 100644 index 4a887eeb3cc..00000000000 --- a/packages-exp/messaging-exp/src/interfaces/app-config.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface AppConfig { - readonly appName: string; - readonly projectId: string; - readonly apiKey: string; - readonly appId: string; - /** Only used for old DB migration. */ - readonly senderId: string; -} diff --git a/packages-exp/messaging-exp/src/interfaces/internal-dependencies.ts b/packages-exp/messaging-exp/src/interfaces/internal-dependencies.ts deleted file mode 100644 index c8adfabf225..00000000000 --- a/packages-exp/messaging-exp/src/interfaces/internal-dependencies.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AppConfig } from './app-config'; -import { FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import { Provider } from '@firebase/component'; -import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; - -export interface FirebaseInternalDependencies { - app: FirebaseApp; - appConfig: AppConfig; - installations: _FirebaseInstallationsInternal; - analyticsProvider: Provider; -} diff --git a/packages-exp/messaging-exp/src/interfaces/internal-message-payload.ts b/packages-exp/messaging-exp/src/interfaces/internal-message-payload.ts deleted file mode 100644 index 92f7b19fa02..00000000000 --- a/packages-exp/messaging-exp/src/interfaces/internal-message-payload.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -import { - CONSOLE_CAMPAIGN_ANALYTICS_ENABLED, - CONSOLE_CAMPAIGN_ID, - CONSOLE_CAMPAIGN_NAME, - CONSOLE_CAMPAIGN_TIME -} from '../util/constants'; - -export interface MessagePayloadInternal { - notification?: NotificationPayloadInternal; - data?: unknown; - fcmOptions?: FcmOptionsInternal; - messageType?: MessageType; - isFirebaseMessaging?: boolean; - from: string; - // eslint-disable-next-line camelcase - collapse_key: string; -} - -export interface NotificationPayloadInternal extends NotificationOptions { - title: string; - // Supported in the Legacy Send API. - // See:https://firebase.google.com/docs/cloud-messaging/xmpp-server-ref. - // eslint-disable-next-line camelcase - click_action?: string; -} - -// Defined in -// https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushfcmoptions. Note -// that the keys are sent to the clients in snake cases which we need to convert to camel so it can -// be exposed as a type to match the Firebase API convention. -export interface FcmOptionsInternal { - link?: string; - - // eslint-disable-next-line camelcase - analytics_label?: string; -} - -export enum MessageType { - PUSH_RECEIVED = 'push-received', - NOTIFICATION_CLICKED = 'notification-clicked' -} - -/** Additional data of a message sent from the FN Console. */ -export interface ConsoleMessageData { - [CONSOLE_CAMPAIGN_ID]: string; - [CONSOLE_CAMPAIGN_TIME]: string; - [CONSOLE_CAMPAIGN_NAME]?: string; - [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]?: '1'; -} diff --git a/packages-exp/messaging-exp/src/interfaces/token-details.ts b/packages-exp/messaging-exp/src/interfaces/token-details.ts deleted file mode 100644 index 791c94d267b..00000000000 --- a/packages-exp/messaging-exp/src/interfaces/token-details.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface TokenDetails { - token: string; - createTime: number; - /** Does not exist in Safari since it's not using Push API. */ - subscriptionOptions?: SubscriptionOptions; -} - -/** - * Additional options and values required by a Push API subscription. - */ -export interface SubscriptionOptions { - vapidKey: string; - swScope: string; - endpoint: string; - auth: string; - p256dh: string; -} diff --git a/packages-exp/messaging-exp/src/internals/idb-manager.test.ts b/packages-exp/messaging-exp/src/internals/idb-manager.test.ts deleted file mode 100644 index c66f11ad440..00000000000 --- a/packages-exp/messaging-exp/src/internals/idb-manager.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '../testing/setup'; - -import * as migrateOldDatabaseModule from '../helpers/migrate-old-database'; - -import { dbGet, dbRemove, dbSet } from '../internals/idb-manager'; - -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; -import { Stub } from '../testing/sinon-types'; -import { TokenDetails } from '../interfaces/token-details'; -import { expect } from 'chai'; -import { getFakeFirebaseDependencies } from '../testing/fakes/firebase-dependencies'; -import { getFakeTokenDetails } from '../testing/fakes/token-details'; -import { stub } from 'sinon'; - -describe('idb manager', () => { - let firebaseDependencies: FirebaseInternalDependencies; - let tokenDetailsA: TokenDetails; - let tokenDetailsB: TokenDetails; - - beforeEach(() => { - firebaseDependencies = getFakeFirebaseDependencies(); - tokenDetailsA = getFakeTokenDetails(); - tokenDetailsB = getFakeTokenDetails(); - tokenDetailsA.token = 'TOKEN_A'; - tokenDetailsB.token = 'TOKEN_B'; - }); - - describe('get / set', () => { - it('sets a value and then gets the same value back', async () => { - await dbSet(firebaseDependencies, tokenDetailsA); - const value = await dbGet(firebaseDependencies); - expect(value).to.deep.equal(tokenDetailsA); - }); - - it('gets undefined for a key that does not exist', async () => { - const value = await dbGet(firebaseDependencies); - expect(value).to.be.undefined; - }); - - it('sets and gets multiple values with different keys', async () => { - const firebaseDependenciesB = getFakeFirebaseDependencies({ - appId: 'different-app-id' - }); - await dbSet(firebaseDependencies, tokenDetailsA); - await dbSet(firebaseDependenciesB, tokenDetailsB); - expect(await dbGet(firebaseDependencies)).to.deep.equal(tokenDetailsA); - expect(await dbGet(firebaseDependenciesB)).to.deep.equal(tokenDetailsB); - }); - - it('overwrites a value', async () => { - await dbSet(firebaseDependencies, tokenDetailsA); - await dbSet(firebaseDependencies, tokenDetailsB); - expect(await dbGet(firebaseDependencies)).to.deep.equal(tokenDetailsB); - }); - - describe('old DB migration', () => { - let migrateOldDatabaseStub: Stub< - typeof migrateOldDatabaseModule['migrateOldDatabase'] - >; - - beforeEach(() => { - migrateOldDatabaseStub = stub( - migrateOldDatabaseModule, - 'migrateOldDatabase' - ).resolves(tokenDetailsA); - }); - - it('gets value from old DB if there is one', async () => { - await dbGet(firebaseDependencies); - - expect(migrateOldDatabaseStub).to.have.been.calledOnceWith( - firebaseDependencies.appConfig.senderId - ); - }); - - it('does not call migrateOldDatabase a second time', async () => { - await dbGet(firebaseDependencies); - await dbGet(firebaseDependencies); - - expect(migrateOldDatabaseStub).to.have.been.calledOnceWith( - firebaseDependencies.appConfig.senderId - ); - }); - - it('does not call migrateOldDatabase if there is already a value in the DB', async () => { - await dbSet(firebaseDependencies, tokenDetailsA); - - await dbGet(firebaseDependencies); - - expect(migrateOldDatabaseStub).not.to.have.been.called; - }); - }); - }); - - describe('remove', () => { - it('deletes a key', async () => { - await dbSet(firebaseDependencies, tokenDetailsA); - await dbRemove(firebaseDependencies); - expect(await dbGet(firebaseDependencies)).to.be.undefined; - }); - - it('does not throw if key does not exist', async () => { - await dbRemove(firebaseDependencies); - expect(await dbGet(firebaseDependencies)).to.be.undefined; - }); - }); -}); diff --git a/packages-exp/messaging-exp/src/internals/idb-manager.ts b/packages-exp/messaging-exp/src/internals/idb-manager.ts deleted file mode 100644 index 4ddebf5ae96..00000000000 --- a/packages-exp/messaging-exp/src/internals/idb-manager.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DB, deleteDb, openDb } from 'idb'; - -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; -import { TokenDetails } from '../interfaces/token-details'; -import { migrateOldDatabase } from '../helpers/migrate-old-database'; - -// Exported for tests. -export const DATABASE_NAME = 'firebase-messaging-database'; -const DATABASE_VERSION = 1; -const OBJECT_STORE_NAME = 'firebase-messaging-store'; - -let dbPromise: Promise | null = null; -function getDbPromise(): Promise { - if (!dbPromise) { - dbPromise = openDb(DATABASE_NAME, DATABASE_VERSION, upgradeDb => { - // We don't use 'break' in this switch statement, the fall-through behavior is what we want, - // because if there are multiple versions between the old version and the current version, we - // want ALL the migrations that correspond to those versions to run, not only the last one. - // eslint-disable-next-line default-case - switch (upgradeDb.oldVersion) { - case 0: - upgradeDb.createObjectStore(OBJECT_STORE_NAME); - } - }); - } - return dbPromise; -} - -/** Gets record(s) from the objectStore that match the given key. */ -export async function dbGet( - firebaseDependencies: FirebaseInternalDependencies -): Promise { - const key = getKey(firebaseDependencies); - const db = await getDbPromise(); - const tokenDetails = await db - .transaction(OBJECT_STORE_NAME) - .objectStore(OBJECT_STORE_NAME) - .get(key); - - if (tokenDetails) { - return tokenDetails; - } else { - // Check if there is a tokenDetails object in the old DB. - const oldTokenDetails = await migrateOldDatabase( - firebaseDependencies.appConfig.senderId - ); - if (oldTokenDetails) { - await dbSet(firebaseDependencies, oldTokenDetails); - return oldTokenDetails; - } - } -} - -/** Assigns or overwrites the record for the given key with the given value. */ -export async function dbSet( - firebaseDependencies: FirebaseInternalDependencies, - tokenDetails: TokenDetails -): Promise { - const key = getKey(firebaseDependencies); - const db = await getDbPromise(); - const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); - await tx.objectStore(OBJECT_STORE_NAME).put(tokenDetails, key); - await tx.complete; - return tokenDetails; -} - -/** Removes record(s) from the objectStore that match the given key. */ -export async function dbRemove( - firebaseDependencies: FirebaseInternalDependencies -): Promise { - const key = getKey(firebaseDependencies); - const db = await getDbPromise(); - const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); - await tx.objectStore(OBJECT_STORE_NAME).delete(key); - await tx.complete; -} - -/** Deletes the DB. Useful for tests. */ -export async function dbDelete(): Promise { - if (dbPromise) { - (await dbPromise).close(); - await deleteDb(DATABASE_NAME); - dbPromise = null; - } -} - -function getKey({ appConfig }: FirebaseInternalDependencies): string { - return appConfig.appId; -} diff --git a/packages-exp/messaging-exp/src/internals/requests.ts b/packages-exp/messaging-exp/src/internals/requests.ts deleted file mode 100644 index 21fc6f2acf1..00000000000 --- a/packages-exp/messaging-exp/src/internals/requests.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DEFAULT_VAPID_KEY, ENDPOINT } from '../util/constants'; -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; -import { SubscriptionOptions, TokenDetails } from '../interfaces/token-details'; - -import { AppConfig } from '../interfaces/app-config'; -import { FirebaseInternalDependencies } from '../interfaces/internal-dependencies'; -import { getToken } from '@firebase/installations-exp'; - -export interface ApiResponse { - token?: string; - error?: { message: string }; -} - -export interface ApiRequestBody { - web: { - endpoint: string; - p256dh: string; - auth: string; - applicationPubKey?: string; - }; -} - -export async function requestGetToken( - firebaseDependencies: FirebaseInternalDependencies, - subscriptionOptions: SubscriptionOptions -): Promise { - const headers = await getHeaders(firebaseDependencies); - const body = getBody(subscriptionOptions); - - const subscribeOptions = { - method: 'POST', - headers, - body: JSON.stringify(body) - }; - - let responseData: ApiResponse; - try { - const response = await fetch( - getEndpoint(firebaseDependencies.appConfig), - subscribeOptions - ); - responseData = await response.json(); - } catch (err) { - throw ERROR_FACTORY.create(ErrorCode.TOKEN_SUBSCRIBE_FAILED, { - errorInfo: err - }); - } - - if (responseData.error) { - const message = responseData.error.message; - throw ERROR_FACTORY.create(ErrorCode.TOKEN_SUBSCRIBE_FAILED, { - errorInfo: message - }); - } - - if (!responseData.token) { - throw ERROR_FACTORY.create(ErrorCode.TOKEN_SUBSCRIBE_NO_TOKEN); - } - - return responseData.token; -} - -export async function requestUpdateToken( - firebaseDependencies: FirebaseInternalDependencies, - tokenDetails: TokenDetails -): Promise { - const headers = await getHeaders(firebaseDependencies); - const body = getBody(tokenDetails.subscriptionOptions!); - - const updateOptions = { - method: 'PATCH', - headers, - body: JSON.stringify(body) - }; - - let responseData: ApiResponse; - try { - const response = await fetch( - `${getEndpoint(firebaseDependencies.appConfig)}/${tokenDetails.token}`, - updateOptions - ); - responseData = await response.json(); - } catch (err) { - throw ERROR_FACTORY.create(ErrorCode.TOKEN_UPDATE_FAILED, { - errorInfo: err - }); - } - - if (responseData.error) { - const message = responseData.error.message; - throw ERROR_FACTORY.create(ErrorCode.TOKEN_UPDATE_FAILED, { - errorInfo: message - }); - } - - if (!responseData.token) { - throw ERROR_FACTORY.create(ErrorCode.TOKEN_UPDATE_NO_TOKEN); - } - - return responseData.token; -} - -export async function requestDeleteToken( - firebaseDependencies: FirebaseInternalDependencies, - token: string -): Promise { - const headers = await getHeaders(firebaseDependencies); - - const unsubscribeOptions = { - method: 'DELETE', - headers - }; - - try { - const response = await fetch( - `${getEndpoint(firebaseDependencies.appConfig)}/${token}`, - unsubscribeOptions - ); - const responseData: ApiResponse = await response.json(); - if (responseData.error) { - const message = responseData.error.message; - throw ERROR_FACTORY.create(ErrorCode.TOKEN_UNSUBSCRIBE_FAILED, { - errorInfo: message - }); - } - } catch (err) { - throw ERROR_FACTORY.create(ErrorCode.TOKEN_UNSUBSCRIBE_FAILED, { - errorInfo: err - }); - } -} - -function getEndpoint({ projectId }: AppConfig): string { - return `${ENDPOINT}/projects/${projectId!}/registrations`; -} - -async function getHeaders({ - appConfig, - installations -}: FirebaseInternalDependencies): Promise { - const authToken = await getToken(installations); - - return new Headers({ - 'Content-Type': 'application/json', - Accept: 'application/json', - 'x-goog-api-key': appConfig.apiKey!, - 'x-goog-firebase-installations-auth': `FIS ${authToken}` - }); -} - -function getBody({ - p256dh, - auth, - endpoint, - vapidKey -}: SubscriptionOptions): ApiRequestBody { - const body: ApiRequestBody = { - web: { - endpoint, - auth, - p256dh - } - }; - - if (vapidKey !== DEFAULT_VAPID_KEY) { - body.web.applicationPubKey = vapidKey; - } - - return body; -} diff --git a/packages-exp/messaging-exp/src/listeners/sw-controller.test.ts b/packages-exp/messaging-exp/src/listeners/sw-controller.test.ts deleted file mode 100644 index 8a7a901dafb..00000000000 --- a/packages-exp/messaging-exp/src/listeners/sw-controller.test.ts +++ /dev/null @@ -1,465 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '../testing/setup'; - -import * as tokenManagementModule from '../internals/token-manager'; - -import { - CONSOLE_CAMPAIGN_ANALYTICS_ENABLED, - CONSOLE_CAMPAIGN_ID, - CONSOLE_CAMPAIGN_NAME, - CONSOLE_CAMPAIGN_TIME, - FCM_MSG -} from '../util/constants'; -import { DeepPartial, ValueOf, Writable } from 'ts-essentials'; -import { - FakeEvent, - FakePushSubscription, - mockServiceWorker, - restoreServiceWorker -} from '../testing/fakes/service-worker'; -import { - MessagePayloadInternal, - MessageType -} from '../interfaces/internal-message-payload'; -import { - NotificationEvent, - ServiceWorkerGlobalScope, - ServiceWorkerGlobalScopeEventMap, - WindowClient -} from '../util/sw-types'; -import { - getFakeAnalyticsProvider, - getFakeApp, - getFakeInstallations -} from '../testing/fakes/firebase-dependencies'; -import { spy, stub } from 'sinon'; - -import { MessagingService } from '../messaging-service'; -import { Stub } from '../testing/sinon-types'; -import { SwController } from './sw-controller'; -import { expect } from 'chai'; - -const LOCAL_HOST = self.location.host; -const TEST_LINK = 'https://' + LOCAL_HOST + '/test-link.org'; -const TEST_CLICK_ACTION = 'https://' + LOCAL_HOST + '/test-click-action.org'; - -// Add fake SW types. -declare const self: Window & Writable; - -// internal message payload (parsed directly from the push event) that contains and only contains -// notification payload. -const DISPLAY_MESSAGE: MessagePayloadInternal = { - notification: { - title: 'title', - body: 'body' - }, - fcmOptions: { - link: TEST_LINK - }, - from: 'from', - // eslint-disable-next-line camelcase - collapse_key: 'collapse' -}; - -describe('SwController', () => { - let addEventListenerStub: Stub; - // eslint-disable-next-line @typescript-eslint/ban-types - let eventListenerMap: Map; - let messaging: MessagingService; - let getTokenStub: Stub; - let deleteTokenStub: Stub< - typeof tokenManagementModule['deleteTokenInternal'] - >; - - beforeEach(() => { - mockServiceWorker(); - - stub(Notification, 'permission').value('granted'); - - // Instead of calling actual addEventListener, add the event to the eventListeners list. Actual - // event listeners can't be used as the tests are not running in a Service Worker, which means - // Push events do not exist. - addEventListenerStub = stub(self, 'addEventListener').callsFake( - (type, listener) => { - eventListenerMap.set(type, listener); - } - ); - eventListenerMap = new Map(); - - getTokenStub = stub(tokenManagementModule, 'getTokenInternal').resolves( - 'token-value' - ); - deleteTokenStub = stub( - tokenManagementModule, - 'deleteTokenInternal' - ).resolves(true); - - messaging = new MessagingService( - getFakeApp(), - getFakeInstallations(), - getFakeAnalyticsProvider() - ); - new SwController(messaging); - }); - - afterEach(() => { - restoreServiceWorker(); - }); - - it('sets event listeners on initialization', () => { - expect(addEventListenerStub).to.have.been.calledThrice; - expect(addEventListenerStub).to.have.been.calledWith('push'); - expect(addEventListenerStub).to.have.been.calledWith( - 'pushsubscriptionchange' - ); - expect(addEventListenerStub).to.have.been.calledWith('notificationclick'); - }); - - describe('onPush', () => { - it('does nothing if push is not from FCM', async () => { - const showNotificationSpy = spy(self.registration, 'showNotification'); - const matchAllSpy = spy(self.clients, 'matchAll'); - - await callEventListener(makeEvent('push', {})); - - await callEventListener( - makeEvent('push', { - data: {} - }) - ); - - expect(showNotificationSpy).not.to.have.been.called; - expect(matchAllSpy).not.to.have.been.called; - }); - - it('sends a message to window clients if a window client is visible', async () => { - const client: Writable = (await self.clients.openWindow( - 'https://example.org' - ))!; - client.visibilityState = 'visible'; - const postMessageSpy = spy(client, 'postMessage'); - - await callEventListener( - makeEvent('push', { - data: { - json: () => DISPLAY_MESSAGE - } - }) - ); - - const expectedMessage: MessagePayloadInternal = { - ...DISPLAY_MESSAGE, - messageType: MessageType.PUSH_RECEIVED - }; - expect(postMessageSpy).to.have.been.calledOnceWith(expectedMessage); - }); - - it('does not send a message to window clients if window clients are hidden', async () => { - const client = (await self.clients.openWindow('https://example.org'))!; - const postMessageSpy = spy(client, 'postMessage'); - const showNotificationSpy = spy(self.registration, 'showNotification'); - - await callEventListener( - makeEvent('push', { - data: { - json: () => DISPLAY_MESSAGE - } - }) - ); - - expect(postMessageSpy).not.to.have.been.called; - expect(showNotificationSpy).to.have.been.calledWith('title', { - ...DISPLAY_MESSAGE.notification, - data: { - [FCM_MSG]: DISPLAY_MESSAGE - } - }); - }); - - it('displays a notification if a window client does not exist', async () => { - const showNotificationSpy = spy(self.registration, 'showNotification'); - - await callEventListener( - makeEvent('push', { - data: { - json: () => DISPLAY_MESSAGE - } - }) - ); - - expect(showNotificationSpy).to.have.been.calledWith('title', { - ...DISPLAY_MESSAGE.notification, - data: { - ...DISPLAY_MESSAGE.notification!.data, - [FCM_MSG]: DISPLAY_MESSAGE - } - }); - }); - - it('warns if there are more action buttons than the browser limit', async () => { - // This doesn't exist on Firefox: - // https://developer.mozilla.org/en-US/docs/Web/API/notification/maxActions - if (!Notification.maxActions) { - return; - } - stub(Notification, 'maxActions').value(1); - - const warnStub = stub(console, 'warn'); - - await callEventListener( - makeEvent('push', { - data: { - json: () => ({ - notification: { - ...DISPLAY_MESSAGE, - actions: [ - { action: 'like', title: 'Like' }, - { action: 'favorite', title: 'Favorite' } - ] - } - }) - } - }) - ); - - expect(warnStub).to.have.been.calledOnceWith( - 'This browser only supports 1 actions. The remaining actions will not be displayed.' - ); - }); - }); - - describe('onNotificationClick', () => { - let NOTIFICATION_CLICK_PAYLOAD: DeepPartial; - - beforeEach(() => { - NOTIFICATION_CLICK_PAYLOAD = { - notification: new Notification('title', { - ...DISPLAY_MESSAGE.notification, - data: { - ...DISPLAY_MESSAGE.notification!.data, - [FCM_MSG]: DISPLAY_MESSAGE - } - }) - }; - }); - - it('does nothing if notification is not from FCM', async () => { - delete NOTIFICATION_CLICK_PAYLOAD.notification!.data![FCM_MSG]; - - const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD); - const stopImmediatePropagationSpy = spy( - event, - 'stopImmediatePropagation' - ); - - await callEventListener(event); - - expect(stopImmediatePropagationSpy).not.to.have.been.called; - }); - - it('does nothing if an action button was clicked', async () => { - const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD); - event.action = 'actionName'; - const stopImmediatePropagationSpy = spy( - event, - 'stopImmediatePropagation' - ); - - await callEventListener(event); - - expect(stopImmediatePropagationSpy).not.to.have.been.called; - }); - - it('calls stopImmediatePropagation and notification.close', async () => { - const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD); - const stopImmediatePropagationSpy = spy( - event, - 'stopImmediatePropagation' - ); - const notificationCloseSpy = spy(event.notification, 'close'); - - await callEventListener(event); - - expect(stopImmediatePropagationSpy).to.have.been.called; - expect(notificationCloseSpy).to.have.been.called; - }); - - it('does not redirect if there is no link', async () => { - // Remove link. - delete NOTIFICATION_CLICK_PAYLOAD.notification!.data![FCM_MSG].fcmOptions; - - const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD); - const stopImmediatePropagationSpy = spy( - event, - 'stopImmediatePropagation' - ); - const notificationCloseSpy = spy(event.notification, 'close'); - const matchAllSpy = spy(self.clients, 'matchAll'); - - await callEventListener(event); - - expect(stopImmediatePropagationSpy).to.have.been.called; - expect(notificationCloseSpy).to.have.been.called; - expect(matchAllSpy).not.to.have.been.called; - }); - - it('does not redirect if link is not from origin', async () => { - // Remove link. - NOTIFICATION_CLICK_PAYLOAD.notification!.data![FCM_MSG].fcmOptions.link = - 'https://www.youtube.com'; - - const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD); - const stopImmediatePropagationSpy = spy( - event, - 'stopImmediatePropagation' - ); - const notificationCloseSpy = spy(event.notification, 'close'); - const matchAllSpy = spy(self.clients, 'matchAll'); - - await callEventListener(event); - - expect(stopImmediatePropagationSpy).to.have.been.called; - expect(notificationCloseSpy).to.have.been.called; - expect(matchAllSpy).not.to.have.been.called; - }); - - it('focuses on and sends the message to an open WindowClient', async () => { - const client: Writable = (await self.clients.openWindow( - TEST_LINK - ))!; - const focusSpy = spy(client, 'focus'); - const matchAllSpy = spy(self.clients, 'matchAll'); - const openWindowSpy = spy(self.clients, 'openWindow'); - const postMessageSpy = spy(client, 'postMessage'); - - const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD); - - await callEventListener(event); - - expect(matchAllSpy).to.have.been.called; - expect(openWindowSpy).not.to.have.been.called; - expect(focusSpy).to.have.been.called; - expect(postMessageSpy).to.have.been.calledWith({ - ...DISPLAY_MESSAGE, - messageType: MessageType.NOTIFICATION_CLICKED - }); - }); - - it("opens a new client if there isn't one already open", async () => { - const matchAllSpy = spy(self.clients, 'matchAll'); - const openWindowSpy = spy(self.clients, 'openWindow'); - - const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD); - - await callEventListener(event); - - expect(matchAllSpy).to.have.been.called; - expect(openWindowSpy).to.have.been.calledWith(TEST_LINK); - }); - - it('works with click_action', async () => { - // Replace link with the deprecated click_action. - delete NOTIFICATION_CLICK_PAYLOAD.notification!.data![FCM_MSG].fcmOptions; - NOTIFICATION_CLICK_PAYLOAD.notification!.data![ - FCM_MSG - ].notification.click_action = TEST_CLICK_ACTION; // eslint-disable-line camelcase - - const matchAllSpy = spy(self.clients, 'matchAll'); - const openWindowSpy = spy(self.clients, 'openWindow'); - - const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD); - - await callEventListener(event); - - expect(matchAllSpy).to.have.been.called; - expect(openWindowSpy).to.have.been.calledWith(TEST_CLICK_ACTION); - }); - - it('redirects to origin if message was sent from the FN Console', async () => { - // Remove link. - delete NOTIFICATION_CLICK_PAYLOAD.notification!.data![FCM_MSG].fcmOptions; - // Add FN data. - NOTIFICATION_CLICK_PAYLOAD.notification!.data![FCM_MSG].data = { - [CONSOLE_CAMPAIGN_ID]: '123456', - [CONSOLE_CAMPAIGN_NAME]: 'Campaign Name', - [CONSOLE_CAMPAIGN_TIME]: '1234567890', - [CONSOLE_CAMPAIGN_ANALYTICS_ENABLED]: '1' - }; - - const matchAllSpy = spy(self.clients, 'matchAll'); - const openWindowSpy = spy(self.clients, 'openWindow'); - - const event = makeEvent('notificationclick', NOTIFICATION_CLICK_PAYLOAD); - - await callEventListener(event); - - expect(matchAllSpy).to.have.been.called; - expect(openWindowSpy).to.have.been.calledWith(self.location.origin); - }); - }); - - describe('onSubChange', () => { - it('calls deleteToken if there is no new subscription', async () => { - const event = makeEvent('pushsubscriptionchange', { - oldSubscription: new FakePushSubscription(), - newSubscription: undefined - }); - - await callEventListener(event); - - expect(deleteTokenStub).to.have.been.called; - expect(getTokenStub).not.to.have.been.called; - }); - - it('calls deleteToken and getToken if subscription changed', async () => { - const event = makeEvent('pushsubscriptionchange', { - oldSubscription: new FakePushSubscription(), - newSubscription: new FakePushSubscription() - }); - - await callEventListener(event); - - expect(deleteTokenStub).to.have.been.called; - expect(getTokenStub).to.have.been.called; - }); - }); - - async function callEventListener( - event: ValueOf - ): Promise { - const listener = eventListenerMap.get(event.type); - if (!listener) { - throw new Error(`Event listener for ${event.type} was not defined.`); - } - - const waitUntil = spy(event, 'waitUntil'); - listener(event); - await waitUntil.getCall(0).args[0]; - } -}); - -/** Makes fake push events. */ -function makeEvent( - type: K, - data: DeepPartial -): Writable { - const event = new FakeEvent(type); - Object.assign(event, data); - return (event as unknown) as ServiceWorkerGlobalScopeEventMap[K]; -} diff --git a/packages-exp/messaging-exp/src/listeners/sw-controller.ts b/packages-exp/messaging-exp/src/listeners/sw-controller.ts deleted file mode 100644 index 2b6209c292d..00000000000 --- a/packages-exp/messaging-exp/src/listeners/sw-controller.ts +++ /dev/null @@ -1,282 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { DEFAULT_VAPID_KEY, FCM_MSG } from '../util/constants'; -import { - MessagePayloadInternal, - MessageType, - NotificationPayloadInternal -} from '../interfaces/internal-message-payload'; -import { - NotificationEvent, - PushEvent, - PushSubscriptionChangeEvent, - ServiceWorkerGlobalScope, - WindowClient -} from '../util/sw-types'; -import { - deleteTokenInternal, - getTokenInternal -} from '../internals/token-manager'; - -import { MessagingService } from '../messaging-service'; -import { dbGet } from '../internals/idb-manager'; -import { externalizePayload } from '../helpers/externalizePayload'; -import { isConsoleMessage } from '../helpers/is-console-message'; -import { sleep } from '../helpers/sleep'; - -// Let TS know that this is a service worker -declare const self: ServiceWorkerGlobalScope; - -export class SwController { - constructor(private readonly messaging: MessagingService) { - self.addEventListener('push', e => { - e.waitUntil(this.onPush(e)); - }); - self.addEventListener('pushsubscriptionchange', e => { - e.waitUntil(this.onSubChange(e)); - }); - self.addEventListener('notificationclick', e => { - e.waitUntil(this.onNotificationClick(e)); - }); - } - - /** - * A handler for push events that shows notifications based on the content of the payload. - * - * The payload must be a JSON-encoded Object with a `notification` key. The value of the - * `notification` property will be used as the NotificationOptions object passed to - * showNotification. Additionally, the `title` property of the notification object will be used as - * the title. - * - * If there is no notification data in the payload then no notification will be shown. - */ - async onPush(event: PushEvent): Promise { - const internalPayload = getMessagePayloadInternal(event); - if (!internalPayload) { - // Failed to get parsed MessagePayload from the PushEvent. Skip handling the push. - return; - } - - // foreground handling: eventually passed to onMessage hook - const clientList = await getClientList(); - if (hasVisibleClients(clientList)) { - return sendMessagePayloadInternalToWindows(clientList, internalPayload); - } - - // background handling: display if possible and pass to onBackgroundMessage hook - if (!!internalPayload.notification) { - await showNotification(wrapInternalPayload(internalPayload)); - } - - if (!!this.messaging.onBackgroundMessageHandler) { - const payload = externalizePayload(internalPayload); - - if (typeof this.messaging.onBackgroundMessageHandler === 'function') { - this.messaging.onBackgroundMessageHandler(payload); - } else { - this.messaging.onBackgroundMessageHandler.next(payload); - } - } - } - - async onSubChange(event: PushSubscriptionChangeEvent): Promise { - const { newSubscription } = event; - if (!newSubscription) { - // Subscription revoked, delete token - await deleteTokenInternal(this.messaging); - return; - } - - const tokenDetails = await dbGet(this.messaging.firebaseDependencies); - await deleteTokenInternal(this.messaging); - - this.messaging.vapidKey = - tokenDetails?.subscriptionOptions?.vapidKey ?? DEFAULT_VAPID_KEY; - await getTokenInternal(this.messaging); - } - - async onNotificationClick(event: NotificationEvent): Promise { - const internalPayload: MessagePayloadInternal = - event.notification?.data?.[FCM_MSG]; - - if (!internalPayload) { - return; - } else if (event.action) { - // User clicked on an action button. This will allow developers to act on action button clicks - // by using a custom onNotificationClick listener that they define. - return; - } - - // Prevent other listeners from receiving the event - event.stopImmediatePropagation(); - event.notification.close(); - - // Note clicking on a notification with no link set will focus the Chrome's current tab. - const link = getLink(internalPayload); - if (!link) { - return; - } - - // FM should only open/focus links from app's origin. - const url = new URL(link, self.location.href); - const originUrl = new URL(self.location.origin); - - if (url.host !== originUrl.host) { - return; - } - - let client = await getWindowClient(url); - - if (!client) { - client = await self.clients.openWindow(link); - - // Wait three seconds for the client to initialize and set up the message handler so that it - // can receive the message. - await sleep(3000); - } else { - client = await client.focus(); - } - - if (!client) { - // Window Client will not be returned if it's for a third party origin. - return; - } - - internalPayload.messageType = MessageType.NOTIFICATION_CLICKED; - internalPayload.isFirebaseMessaging = true; - return client.postMessage(internalPayload); - } -} - -function wrapInternalPayload( - internalPayload: MessagePayloadInternal -): NotificationPayloadInternal { - const wrappedInternalPayload: NotificationPayloadInternal = { - ...((internalPayload.notification as unknown) as NotificationPayloadInternal) - }; - - // Put the message payload under FCM_MSG name so we can identify the notification as being an FCM - // notification vs a notification from somewhere else (i.e. normal web push or developer generated - // notification). - wrappedInternalPayload.data = { - [FCM_MSG]: internalPayload - }; - - return wrappedInternalPayload; -} - -function getMessagePayloadInternal({ - data -}: PushEvent): MessagePayloadInternal | null { - if (!data) { - return null; - } - - try { - return data.json(); - } catch (err) { - // Not JSON so not an FCM message. - return null; - } -} - -/** - * @param url The URL to look for when focusing a client. - * @return Returns an existing window client or a newly opened WindowClient. - */ -async function getWindowClient(url: URL): Promise { - const clientList = await getClientList(); - - for (const client of clientList) { - const clientUrl = new URL(client.url, self.location.href); - - if (url.host === clientUrl.host) { - return client; - } - } - - return null; -} - -/** - * @returns If there is currently a visible WindowClient, this method will resolve to true, - * otherwise false. - */ -function hasVisibleClients(clientList: WindowClient[]): boolean { - return clientList.some( - client => - client.visibilityState === 'visible' && - // Ignore chrome-extension clients as that matches the background pages of extensions, which - // are always considered visible for some reason. - !client.url.startsWith('chrome-extension://') - ); -} - -function sendMessagePayloadInternalToWindows( - clientList: WindowClient[], - internalPayload: MessagePayloadInternal -): void { - internalPayload.isFirebaseMessaging = true; - internalPayload.messageType = MessageType.PUSH_RECEIVED; - - for (const client of clientList) { - client.postMessage(internalPayload); - } -} - -function getClientList(): Promise { - return self.clients.matchAll({ - type: 'window', - includeUncontrolled: true - // TS doesn't know that "type: 'window'" means it'll return WindowClient[] - }) as Promise; -} - -function showNotification( - notificationPayloadInternal: NotificationPayloadInternal -): Promise { - // Note: Firefox does not support the maxActions property. - // https://developer.mozilla.org/en-US/docs/Web/API/notification/maxActions - const { actions } = notificationPayloadInternal; - const { maxActions } = Notification; - if (actions && maxActions && actions.length > maxActions) { - console.warn( - `This browser only supports ${maxActions} actions. The remaining actions will not be displayed.` - ); - } - - return self.registration.showNotification( - /* title= */ notificationPayloadInternal.title ?? '', - notificationPayloadInternal - ); -} - -function getLink(payload: MessagePayloadInternal): string | null { - // eslint-disable-next-line camelcase - const link = payload.fcmOptions?.link ?? payload.notification?.click_action; - if (link) { - return link; - } - - if (isConsoleMessage(payload.data)) { - // Notification created in the Firebase Console. Redirect to origin. - return self.location.origin; - } else { - return null; - } -} diff --git a/packages-exp/messaging-exp/src/messaging-service.ts b/packages-exp/messaging-exp/src/messaging-service.ts deleted file mode 100644 index 96cac006c40..00000000000 --- a/packages-exp/messaging-exp/src/messaging-service.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp, _FirebaseService } from '@firebase/app-types-exp'; -import { NextFn, Observer } from '@firebase/util'; - -import { FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types'; -import { FirebaseInternalDependencies } from './interfaces/internal-dependencies'; -import { MessagePayload } from '@firebase/messaging-types-exp'; -import { Provider } from '@firebase/component'; -import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; -import { extractAppConfig } from './helpers/extract-app-config'; - -export class MessagingService implements _FirebaseService { - readonly app!: FirebaseApp; - readonly firebaseDependencies!: FirebaseInternalDependencies; - deleteService!: () => Promise; - - swRegistration?: ServiceWorkerRegistration; - vapidKey?: string; - - onBackgroundMessageHandler: - | NextFn - | Observer - | null = null; - - onMessageHandler: - | NextFn - | Observer - | null = null; - - constructor( - app: FirebaseApp, - installations: _FirebaseInstallationsInternal, - analyticsProvider: Provider - ) { - const appConfig = extractAppConfig(app); - - this.firebaseDependencies = { - app, - appConfig, - installations, - analyticsProvider - }; - } - - _delete(): Promise { - return this.deleteService(); - } -} diff --git a/packages-exp/messaging-exp/src/testing/compare-headers.test.ts b/packages-exp/messaging-exp/src/testing/compare-headers.test.ts deleted file mode 100644 index ad171740ace..00000000000 --- a/packages-exp/messaging-exp/src/testing/compare-headers.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import './setup'; - -import { AssertionError, expect } from 'chai'; - -import { compareHeaders } from './compare-headers'; - -describe('compareHeaders', () => { - it("doesn't fail if headers contain the same entries", () => { - const headers1 = new Headers({ a: '123', b: '456' }); - const headers2 = new Headers({ a: '123', b: '456' }); - compareHeaders(headers1, headers2); - }); - - it('fails if headers contain different keys', () => { - const headers1 = new Headers({ a: '123', b: '456', extraKey: '789' }); - const headers2 = new Headers({ a: '123', b: '456' }); - expect(() => { - compareHeaders(headers1, headers2); - }).to.throw(AssertionError); - }); - - it('fails if headers contain different values', () => { - const headers1 = new Headers({ a: '123', b: '456' }); - const headers2 = new Headers({ a: '123', b: 'differentValue' }); - expect(() => { - compareHeaders(headers1, headers2); - }).to.throw(AssertionError); - }); -}); diff --git a/packages-exp/messaging-exp/src/testing/compare-headers.ts b/packages-exp/messaging-exp/src/testing/compare-headers.ts deleted file mode 100644 index 6f760caf32d..00000000000 --- a/packages-exp/messaging-exp/src/testing/compare-headers.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import './setup'; - -import { expect } from 'chai'; - -// Trick TS since it's set to target ES5. -declare class HeadersWithEntries extends Headers { - entries?(): Iterable<[string, string]>; -} - -// Chai doesn't check if Headers objects contain the same entries, so we need to do that manually. -export function compareHeaders( - expectedHeaders: HeadersWithEntries, - actualHeaders: HeadersWithEntries -): void { - const expected = makeMap(expectedHeaders); - const actual = makeMap(actualHeaders); - expect(actual).to.deep.equal(expected); -} - -function makeMap(headers: HeadersWithEntries): Map { - expect(headers.entries).not.to.be.undefined; - return new Map(headers.entries!()); -} diff --git a/packages-exp/messaging-exp/src/testing/fakes/firebase-dependencies.ts b/packages-exp/messaging-exp/src/testing/fakes/firebase-dependencies.ts deleted file mode 100644 index b72ec417e56..00000000000 --- a/packages-exp/messaging-exp/src/testing/fakes/firebase-dependencies.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - FirebaseAnalyticsInternal, - FirebaseAnalyticsInternalName -} from '@firebase/analytics-interop-types'; - -import { FirebaseInternalDependencies } from '../../interfaces/internal-dependencies'; -import { FirebaseOptions } from '@firebase/app-types-exp'; -import { Provider } from '@firebase/component'; -import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; -import { extractAppConfig } from '../../helpers/extract-app-config'; - -export function getFakeFirebaseDependencies( - options: FirebaseOptions = {} -): FirebaseInternalDependencies { - const app = getFakeApp(options); - return { - app, - appConfig: extractAppConfig(app), - installations: getFakeInstallations(), - analyticsProvider: getFakeAnalyticsProvider() - }; -} - -export function getFakeApp(options: FirebaseOptions = {}): any { - options = { - apiKey: 'apiKey', - projectId: 'projectId', - authDomain: 'authDomain', - messagingSenderId: '1234567890', - databaseURL: 'databaseUrl', - storageBucket: 'storageBucket', - appId: '1:777777777777:web:d93b5ca1475efe57', - ...options - }; - return { - name: 'appName', - options, - automaticDataCollectionEnabled: true, - delete: async () => {}, - messaging: (() => null as unknown) as any, - installations: () => getFakeInstallations() - }; -} - -export function getFakeInstallations(): _FirebaseInstallationsInternal { - return { - getId: async () => 'FID', - getToken: async () => 'authToken' - }; -} - -export function getFakeAnalyticsProvider(): Provider< - FirebaseAnalyticsInternalName -> { - const analytics: FirebaseAnalyticsInternal = { - logEvent() {} - }; - - return ({ - get: async () => analytics, - getImmediate: () => analytics - } as unknown) as Provider; -} diff --git a/packages-exp/messaging-exp/src/testing/fakes/service-worker.ts b/packages-exp/messaging-exp/src/testing/fakes/service-worker.ts deleted file mode 100644 index 8cc09811e0e..00000000000 --- a/packages-exp/messaging-exp/src/testing/fakes/service-worker.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Client, - Clients, - ExtendableEvent, - ServiceWorkerGlobalScope, - WindowClient -} from '../../util/sw-types'; - -import { Writable } from 'ts-essentials'; - -// Add fake SW types. -declare const self: Window & Writable; - -// When trying to stub self.clients self.registration, Sinon complains that these properties do not -// exist. This is because we're not actually running these tests in a service worker context. - -// Here we're adding placeholders for Sinon to overwrite, which prevents the "Cannot stub -// non-existent own property" errors. - -// Casting to any is needed because TS also thinks that we're in a SW context and considers these -// properties readonly. - -// Missing function types are implemented from interfaces, so types are actually defined. -/* eslint-disable @typescript-eslint/explicit-function-return-type */ - -// const originalSwRegistration = ServiceWorkerRegistration; -export function mockServiceWorker(): void { - self.clients = new FakeClients(); - self.registration = new FakeServiceWorkerRegistration(); -} - -export function restoreServiceWorker(): void { - self.clients = new FakeClients(); - self.registration = new FakeServiceWorkerRegistration(); -} - -class FakeClients implements Clients { - private readonly clients: Client[] = []; - - async get(id: string) { - return this.clients.find(c => id === c.id) ?? null; - } - - async matchAll({ type = 'all' } = {}) { - if (type === 'all') { - return this.clients; - } - return this.clients.filter(c => c.type === type); - } - - async openWindow(url: string) { - const windowClient = new FakeWindowClient(); - windowClient.url = url; - this.clients.push(windowClient); - return windowClient; - } - - async claim() {} -} - -let currentId = 0; -class FakeWindowClient implements WindowClient { - readonly id: string; - readonly type = 'window'; - focused = false; - visibilityState: VisibilityState = 'hidden'; - url = 'https://example.org'; - - constructor() { - this.id = (currentId++).toString(); - } - - async focus() { - this.focused = true; - return this; - } - - async navigate(url: string) { - this.url = url; - return this; - } - - postMessage() {} -} - -export class FakeServiceWorkerRegistration - implements ServiceWorkerRegistration { - active = null; - installing = null; - waiting = null; - onupdatefound = null; - pushManager = new FakePushManager(); - scope = '/scope-value'; - - // Unused in FCM Web SDK, no need to mock these. - navigationPreload = (null as unknown) as NavigationPreloadManager; - sync = (null as unknown) as SyncManager; - updateViaCache = (null as unknown) as ServiceWorkerUpdateViaCache; - - async getNotifications() { - return []; - } - - async showNotification() {} - - async update() {} - - async unregister() { - return true; - } - - addEventListener() {} - removeEventListener() {} - dispatchEvent() { - return true; - } -} - -class FakePushManager implements PushManager { - private subscription: FakePushSubscription | null = null; - - async permissionState() { - return 'granted' as const; - } - - async getSubscription() { - return this.subscription; - } - - async subscribe() { - if (!this.subscription) { - this.subscription = new FakePushSubscription(); - } - return this.subscription!; - } -} - -export class FakePushSubscription implements PushSubscription { - endpoint = 'https://example.org'; - expirationTime = 1234567890; - auth = 'auth-value'; // Encoded: 'YXV0aC12YWx1ZQ' - p256 = 'p256-value'; // Encoded: 'cDI1Ni12YWx1ZQ' - - getKey(name: PushEncryptionKeyName) { - const encoder = new TextEncoder(); - return encoder.encode(name === 'auth' ? this.auth : this.p256); - } - - async unsubscribe() { - return true; - } - - // Unused in FCM - toJSON = (null as unknown) as () => PushSubscriptionJSON; - options = (null as unknown) as PushSubscriptionOptions; -} - -/** - * Most of the fields in here are unused / deprecated. They are only added here to match the TS - * Event interface. - */ -export class FakeEvent implements ExtendableEvent { - NONE = Event.NONE; - AT_TARGET = Event.AT_TARGET; - BUBBLING_PHASE = Event.BUBBLING_PHASE; - CAPTURING_PHASE = Event.CAPTURING_PHASE; - bubbles: boolean; - cancelable: boolean; - composed: boolean; - timeStamp = 123456; - isTrusted = true; - eventPhase = Event.NONE; - target = null; - currentTarget = null; - srcElement = null; - cancelBubble = false; - defaultPrevented = false; - returnValue = false; - - preventDefault() { - this.defaultPrevented = true; - this.returnValue = true; - } - - stopPropagation() {} - - stopImmediatePropagation() {} - - initEvent() {} - - waitUntil() {} - - composedPath() { - return []; - } - - constructor(public type: string, options: EventInit = {}) { - this.bubbles = options.bubbles ?? false; - this.cancelable = options.cancelable ?? false; - this.composed = options.composed ?? false; - } -} diff --git a/packages-exp/messaging-exp/src/testing/fakes/token-details.ts b/packages-exp/messaging-exp/src/testing/fakes/token-details.ts deleted file mode 100644 index 73eea06b2e5..00000000000 --- a/packages-exp/messaging-exp/src/testing/fakes/token-details.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FakePushSubscription } from './service-worker'; -import { TokenDetails } from '../../interfaces/token-details'; -import { arrayToBase64 } from '../../helpers/array-base64-translator'; - -export function getFakeTokenDetails(): TokenDetails { - const subscription = new FakePushSubscription(); - - return { - token: 'token-value', - createTime: 1234567890, - subscriptionOptions: { - swScope: '/scope-value', - vapidKey: arrayToBase64(new TextEncoder().encode('vapid-key-value')), // 'dmFwaWQta2V5LXZhbHVl', - endpoint: subscription.endpoint, - auth: arrayToBase64(subscription.getKey('auth')), // 'YXV0aC12YWx1ZQ' - p256dh: arrayToBase64(subscription.getKey('p256dh')) // 'cDI1Ni12YWx1ZQ' - } - }; -} diff --git a/packages-exp/messaging-exp/src/testing/setup.ts b/packages-exp/messaging-exp/src/testing/setup.ts deleted file mode 100644 index 61b3524ca74..00000000000 --- a/packages-exp/messaging-exp/src/testing/setup.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinonChai from 'sinon-chai'; - -import { dbDelete } from '../internals/idb-manager'; -import { deleteDb } from 'idb'; -import { restore } from 'sinon'; -import { use } from 'chai'; - -use(chaiAsPromised); -use(sinonChai); - -afterEach(async () => { - restore(); - await dbDelete(); - await deleteDb('fcm_token_details_db'); -}); diff --git a/packages-exp/messaging-exp/src/testing/sinon-types.ts b/packages-exp/messaging-exp/src/testing/sinon-types.ts deleted file mode 100644 index 13eafbac969..00000000000 --- a/packages-exp/messaging-exp/src/testing/sinon-types.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SinonSpy, SinonStub } from 'sinon'; - -// Helper types for Sinon stubs and spies. - -export type Stub any> = SinonStub< - Parameters, - ReturnType ->; - -export type Spy any> = SinonSpy< - Parameters, - ReturnType ->; diff --git a/packages-exp/messaging-exp/src/util/constants.ts b/packages-exp/messaging-exp/src/util/constants.ts deleted file mode 100644 index 4bafce33b0a..00000000000 --- a/packages-exp/messaging-exp/src/util/constants.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const DEFAULT_SW_PATH = '/firebase-messaging-sw.js'; -export const DEFAULT_SW_SCOPE = '/firebase-cloud-messaging-push-scope'; - -export const DEFAULT_VAPID_KEY = - 'BDOU99-h67HcA6JeFXHbSNMu7e2yNNu3RzoMj8TM4W88jITfq7ZmPvIM1Iv-4_l2LxQcYwhqby2xGpWwzjfAnG4'; - -export const ENDPOINT = 'https://fcmregistrations.googleapis.com/v1'; - -/** Key of FCM Payload in Notification's data field. */ -export const FCM_MSG = 'FCM_MSG'; - -export const CONSOLE_CAMPAIGN_ID = 'google.c.a.c_id'; -export const CONSOLE_CAMPAIGN_NAME = 'google.c.a.c_l'; -export const CONSOLE_CAMPAIGN_TIME = 'google.c.a.ts'; -/** Set to '1' if Analytics is enabled for the campaign */ -export const CONSOLE_CAMPAIGN_ANALYTICS_ENABLED = 'google.c.a.e'; -export const TAG = 'FirebaseMessaging: '; diff --git a/packages-exp/messaging-exp/src/util/errors.ts b/packages-exp/messaging-exp/src/util/errors.ts deleted file mode 100644 index 6a8fca2fdef..00000000000 --- a/packages-exp/messaging-exp/src/util/errors.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ErrorFactory, ErrorMap } from '@firebase/util'; - -export const enum ErrorCode { - MISSING_APP_CONFIG_VALUES = 'missing-app-config-values', - AVAILABLE_IN_WINDOW = 'only-available-in-window', - AVAILABLE_IN_SW = 'only-available-in-sw', - PERMISSION_DEFAULT = 'permission-default', - PERMISSION_BLOCKED = 'permission-blocked', - UNSUPPORTED_BROWSER = 'unsupported-browser', - FAILED_DEFAULT_REGISTRATION = 'failed-service-worker-registration', - TOKEN_SUBSCRIBE_FAILED = 'token-subscribe-failed', - TOKEN_SUBSCRIBE_NO_TOKEN = 'token-subscribe-no-token', - TOKEN_UNSUBSCRIBE_FAILED = 'token-unsubscribe-failed', - TOKEN_UPDATE_FAILED = 'token-update-failed', - TOKEN_UPDATE_NO_TOKEN = 'token-update-no-token', - INVALID_BG_HANDLER = 'invalid-bg-handler', - USE_SW_AFTER_GET_TOKEN = 'use-sw-after-get-token', - INVALID_SW_REGISTRATION = 'invalid-sw-registration', - USE_VAPID_KEY_AFTER_GET_TOKEN = 'use-vapid-key-after-get-token', - INVALID_VAPID_KEY = 'invalid-vapid-key' -} - -export const ERROR_MAP: ErrorMap = { - [ErrorCode.MISSING_APP_CONFIG_VALUES]: - 'Missing App configuration value: "{$valueName}"', - [ErrorCode.AVAILABLE_IN_WINDOW]: - 'This method is available in a Window context.', - [ErrorCode.AVAILABLE_IN_SW]: - 'This method is available in a service worker context.', - [ErrorCode.PERMISSION_DEFAULT]: - 'The notification permission was not granted and dismissed instead.', - [ErrorCode.PERMISSION_BLOCKED]: - 'The notification permission was not granted and blocked instead.', - [ErrorCode.UNSUPPORTED_BROWSER]: - "This browser doesn't support the API's required to use the firebase SDK.", - [ErrorCode.FAILED_DEFAULT_REGISTRATION]: - 'We are unable to register the default service worker. {$browserErrorMessage}', - [ErrorCode.TOKEN_SUBSCRIBE_FAILED]: - 'A problem occurred while subscribing the user to FCM: {$errorInfo}', - [ErrorCode.TOKEN_SUBSCRIBE_NO_TOKEN]: - 'FCM returned no token when subscribing the user to push.', - [ErrorCode.TOKEN_UNSUBSCRIBE_FAILED]: - 'A problem occurred while unsubscribing the ' + - 'user from FCM: {$errorInfo}', - [ErrorCode.TOKEN_UPDATE_FAILED]: - 'A problem occurred while updating the user from FCM: {$errorInfo}', - [ErrorCode.TOKEN_UPDATE_NO_TOKEN]: - 'FCM returned no token when updating the user to push.', - [ErrorCode.USE_SW_AFTER_GET_TOKEN]: - 'The useServiceWorker() method may only be called once and must be ' + - 'called before calling getToken() to ensure your service worker is used.', - [ErrorCode.INVALID_SW_REGISTRATION]: - 'The input to useServiceWorker() must be a ServiceWorkerRegistration.', - [ErrorCode.INVALID_BG_HANDLER]: - 'The input to setBackgroundMessageHandler() must be a function.', - [ErrorCode.INVALID_VAPID_KEY]: 'The public VAPID key must be a string.', - [ErrorCode.USE_VAPID_KEY_AFTER_GET_TOKEN]: - 'The usePublicVapidKey() method may only be called once and must be ' + - 'called before calling getToken() to ensure your VAPID key is used.' -}; - -interface ErrorParams { - [ErrorCode.MISSING_APP_CONFIG_VALUES]: { - valueName: string; - }; - [ErrorCode.FAILED_DEFAULT_REGISTRATION]: { browserErrorMessage: string }; - [ErrorCode.TOKEN_SUBSCRIBE_FAILED]: { errorInfo: string }; - [ErrorCode.TOKEN_UNSUBSCRIBE_FAILED]: { errorInfo: string }; - [ErrorCode.TOKEN_UPDATE_FAILED]: { errorInfo: string }; -} - -export const ERROR_FACTORY = new ErrorFactory( - 'messaging', - 'Messaging', - ERROR_MAP -); diff --git a/packages-exp/messaging-exp/src/util/sw-types.ts b/packages-exp/messaging-exp/src/util/sw-types.ts deleted file mode 100644 index 587cc248f50..00000000000 --- a/packages-exp/messaging-exp/src/util/sw-types.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Subset of Web Worker types from lib.webworker.d.ts - * https://github.com/Microsoft/TypeScript/blob/master/lib/lib.webworker.d.ts - * - * Since it's not possible to have both "dom" and "webworker" libs in a single project, we have to - * manually declare the web worker types we need. - */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ // These types are from TS - -// Not the whole interface, just the parts we're currently using. If TS claims that something does -// not exist on this, feel free to add it. -export interface ServiceWorkerGlobalScope { - readonly location: WorkerLocation; - readonly clients: Clients; - readonly registration: ServiceWorkerRegistration; - addEventListener( - type: K, - listener: ( - this: ServiceWorkerGlobalScope, - ev: ServiceWorkerGlobalScopeEventMap[K] - ) => any, - options?: boolean | AddEventListenerOptions - ): void; -} - -// Same as the previous interface -export interface ServiceWorkerGlobalScopeEventMap { - notificationclick: NotificationEvent; - push: PushEvent; - pushsubscriptionchange: PushSubscriptionChangeEvent; -} - -export interface Client { - readonly id: string; - readonly type: ClientTypes; - readonly url: string; - postMessage(message: any, transfer?: Transferable[]): void; -} - -export interface ClientQueryOptions { - includeReserved?: boolean; - includeUncontrolled?: boolean; - type?: ClientTypes; -} - -export interface WindowClient extends Client { - readonly focused: boolean; - readonly visibilityState: VisibilityState; - focus(): Promise; - navigate(url: string): Promise; -} - -export interface Clients { - claim(): Promise; - get(id: string): Promise; - matchAll(options?: ClientQueryOptions): Promise; - openWindow(url: string): Promise; -} - -export interface ExtendableEvent extends Event { - waitUntil(f: Promise): void; -} - -export interface NotificationEvent extends ExtendableEvent { - readonly action: string; - readonly notification: Notification; -} - -interface PushMessageData { - arrayBuffer(): ArrayBuffer; - blob(): Blob; - json(): any; - text(): string; -} - -export interface PushEvent extends ExtendableEvent { - readonly data: PushMessageData | null; -} - -export interface PushSubscriptionChangeEvent extends ExtendableEvent { - readonly newSubscription: PushSubscription | null; - readonly oldSubscription: PushSubscription | null; -} - -interface WorkerLocation { - readonly hash: string; - readonly host: string; - readonly hostname: string; - readonly href: string; - readonly origin: string; - readonly pathname: string; - readonly port: string; - readonly protocol: string; - readonly search: string; - toString(): string; -} diff --git a/packages-exp/messaging-exp/tsconfig.json b/packages-exp/messaging-exp/tsconfig.json deleted file mode 100644 index 4b63b47c5b5..00000000000 --- a/packages-exp/messaging-exp/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "noUnusedLocals": true, - "lib": [ - "dom", - "es2017" - ], - "downlevelIteration": true - }, - "exclude": [ - "dist/**/*" - ] -} diff --git a/packages-exp/messaging-types-exp/README.md b/packages-exp/messaging-types-exp/README.md deleted file mode 100644 index c10e1551f13..00000000000 --- a/packages-exp/messaging-types-exp/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @firebase/messaging-types-exp - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/messaging-types-exp/index.d.ts b/packages-exp/messaging-types-exp/index.d.ts deleted file mode 100644 index 7bd08407fd4..00000000000 --- a/packages-exp/messaging-types-exp/index.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// Currently supported fcm notification display parameters. Note that -// {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/notifications/NotificationOptions} -// defines a full list of display notification parameters. This interface we only include what the -// SEND API support for clarity. -export interface NotificationPayload { - title?: string; - body?: string; - image?: string; -} - -export interface FcmOptions { - link?: string; - analyticsLabel?: string; -} - -export interface MessagePayload { - notification?: NotificationPayload; - data?: { [key: string]: string }; - fcmOptions?: FcmOptions; - from: string; - collapseKey: string; -} - -export interface FirebaseMessaging {} - -export type FirebaseMessagingName = 'messaging'; - -declare module '@firebase/component' { - interface NameServiceMapping { - 'messaging-exp': FirebaseMessaging; - } -} diff --git a/packages-exp/messaging-types-exp/package.json b/packages-exp/messaging-types-exp/package.json deleted file mode 100644 index 4030037a7e7..00000000000 --- a/packages-exp/messaging-types-exp/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@firebase/messaging-types-exp", - "private": true, - "version": "0.0.900", - "description": "@firebase/messaging Types", - "author": "Firebase (https://firebase.google.com/)", - "license": "Apache-2.0", - "scripts": { - "test": "tsc", - "test:ci": "node ../../scripts/run_tests_in_ci.js" - }, - "files": [ - "index.d.ts", - "index.sw.d.ts" - ], - "peerDependencies": { - "@firebase/app-types": "0.x" - }, - "repository": { - "directory": "packages/messaging-types", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "devDependencies": { - "typescript": "4.0.5" - } -} diff --git a/packages-exp/messaging-types-exp/tsconfig.json b/packages-exp/messaging-types-exp/tsconfig.json deleted file mode 100644 index 9a785433d90..00000000000 --- a/packages-exp/messaging-types-exp/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "noEmit": true - }, - "exclude": [ - "dist/**/*" - ] -} diff --git a/packages-exp/performance-exp/.eslintrc.js b/packages-exp/performance-exp/.eslintrc.js deleted file mode 100644 index ca80aa0f69a..00000000000 --- a/packages-exp/performance-exp/.eslintrc.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module.exports = { - extends: '../../config/.eslintrc.js', - parserOptions: { - project: 'tsconfig.json', - // to make vscode-eslint work with monorepo - // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 - tsconfigRootDir: __dirname - } -}; diff --git a/packages-exp/performance-exp/README.md b/packages-exp/performance-exp/README.md deleted file mode 100644 index 6193537a9a5..00000000000 --- a/packages-exp/performance-exp/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @firebase/performance-exp - -This is the Firebase Performance component of the Firebase JS SDK. - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/performance-exp/karma.conf.js b/packages-exp/performance-exp/karma.conf.js deleted file mode 100644 index 7f333fdb2fd..00000000000 --- a/packages-exp/performance-exp/karma.conf.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const karmaBase = require('../../config/karma.base'); - -const files = [`test/**/*`, 'src/**/*.test.ts']; - -module.exports = function (config) { - config.set({ - ...karmaBase, - // files to load into karma - files, - preprocessors: { '**/*.ts': ['webpack', 'sourcemap'] }, - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'] - }); -}; - -module.exports.files = files; diff --git a/packages-exp/performance-exp/package.json b/packages-exp/performance-exp/package.json deleted file mode 100644 index 591f9f8ec0f..00000000000 --- a/packages-exp/performance-exp/package.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "@firebase/performance-exp", - "version": "0.0.900", - "description": "Firebase performance for web", - "author": "Firebase (https://firebase.google.com/)", - "private": true, - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "esm2017": "dist/index.esm2017.js", - "files": ["dist"], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c && yarn api-report", - "build:deps": "lerna run --scope @firebase/performance-exp --include-dependencies build", - "build:release": "rollup -c rollup.config.release.js", - "dev": "rollup -c -w", - "test": "run-p lint test:browser", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:browser", - "test:browser": "karma start --single-run", - "test:debug": "karma start --browsers=Chrome --auto-watch", - "prettier": "prettier --write '{src,test}/**/*.{js,ts}'", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn build && yarn doc" - }, - "peerDependencies": { - "@firebase/app-exp": "0.x", - "@firebase/app-types-exp": "0.x" - }, - "dependencies": { - "@firebase/logger": "0.2.6", - "@firebase/installations-exp": "0.0.900", - "@firebase/util": "0.3.4", - "@firebase/performance-types-exp": "0.0.900", - "@firebase/component": "0.1.21", - "tslib": "^1.11.1" - }, - "license": "Apache-2.0", - "devDependencies": { - "@firebase/app-exp": "0.0.900", - "rollup": "2.35.1", - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-typescript2": "0.29.0", - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages/performance-exp", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "dist/src/index.d.ts", - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/performance-exp/rollup.config.js b/packages-exp/performance-exp/rollup.config.js deleted file mode 100644 index b5221c26179..00000000000 --- a/packages-exp/performance-exp/rollup.config.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import pkg from './package.json'; -import { es5BuildsNoPlugin, es2017BuildsNoPlugin } from './rollup.shared'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * ES5 Builds - */ -const es5BuildPlugins = [typescriptPlugin({ typescript }), json()]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - } - }), - json({ preferConst: true }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/performance-exp/rollup.config.release.js b/packages-exp/performance-exp/rollup.config.release.js deleted file mode 100644 index a217c6990b0..00000000000 --- a/packages-exp/performance-exp/rollup.config.release.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - clean: true, - abortOnError: false, - transformers: [importPathTransformer] - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins, - treeshake: { - moduleSideEffects: ['@firebase/installations'] - } -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - abortOnError: false, - clean: true, - transformers: [importPathTransformer] - }), - json({ preferConst: true }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins, - treeshake: { - moduleSideEffects: ['@firebase/installations'] - } -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/performance-exp/rollup.shared.js b/packages-exp/performance-exp/rollup.shared.js deleted file mode 100644 index cbb57224a81..00000000000 --- a/packages-exp/performance-exp/rollup.shared.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import pkg from './package.json'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * ES5 Builds - */ -export const es5BuildsNoPlugin = [ - { - input: 'src/index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } - ], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -/** - * ES2017 Builds - */ -export const es2017BuildsNoPlugin = [ - { - input: 'src/index.ts', - output: [{ file: pkg.esm2017, format: 'es', sourcemap: true }], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; diff --git a/packages-exp/performance-exp/src/constants.ts b/packages-exp/performance-exp/src/constants.ts deleted file mode 100644 index 2cac126da97..00000000000 --- a/packages-exp/performance-exp/src/constants.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { version } from '../package.json'; - -export const SDK_VERSION = version; -/** The prefix for start User Timing marks used for creating Traces. */ -export const TRACE_START_MARK_PREFIX = 'FB-PERF-TRACE-START'; -/** The prefix for stop User Timing marks used for creating Traces. */ -export const TRACE_STOP_MARK_PREFIX = 'FB-PERF-TRACE-STOP'; -/** The prefix for User Timing measure used for creating Traces. */ -export const TRACE_MEASURE_PREFIX = 'FB-PERF-TRACE-MEASURE'; -/** The prefix for out of the box page load Trace name. */ -export const OOB_TRACE_PAGE_LOAD_PREFIX = '_wt_'; - -export const FIRST_PAINT_COUNTER_NAME = '_fp'; - -export const FIRST_CONTENTFUL_PAINT_COUNTER_NAME = '_fcp'; - -export const FIRST_INPUT_DELAY_COUNTER_NAME = '_fid'; - -export const CONFIG_LOCAL_STORAGE_KEY = '@firebase/performance/config'; - -export const CONFIG_EXPIRY_LOCAL_STORAGE_KEY = - '@firebase/performance/configexpire'; - -export const SERVICE = 'performance'; -export const SERVICE_NAME = 'Performance'; diff --git a/packages-exp/performance-exp/src/controllers/perf.test.ts b/packages-exp/performance-exp/src/controllers/perf.test.ts deleted file mode 100644 index 6efc944870f..00000000000 --- a/packages-exp/performance-exp/src/controllers/perf.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { stub } from 'sinon'; -import { PerformanceController } from '../controllers/perf'; -import { Api, setupApi } from '../services/api_service'; -import * as initializationService from '../services/initialization_service'; -import { SettingsService } from '../services/settings_service'; -import { consoleLogger } from '../utils/console_logger'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; -import '../../test/setup'; - -describe('Firebase Performance Test', () => { - setupApi(window); - - const fakeFirebaseConfig = { - apiKey: 'api-key', - authDomain: 'project-id.firebaseapp.com', - databaseURL: 'https://project-id.firebaseio.com', - projectId: 'project-id', - storageBucket: 'project-id.appspot.com', - messagingSenderId: 'sender-id', - appId: '1:111:web:a1234' - }; - - const fakeFirebaseApp = ({ - options: fakeFirebaseConfig - } as unknown) as FirebaseApp; - - const fakeInstallations = ({} as unknown) as _FirebaseInstallationsInternal; - - describe('#constructor', () => { - it('does not initialize performance if the required apis are not available', () => { - stub(Api.prototype, 'requiredApisAvailable').returns(false); - stub(initializationService, 'getInitializationPromise'); - stub(consoleLogger, 'info'); - const performanceController = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - performanceController._init(); - - expect(initializationService.getInitializationPromise).not.be.called; - expect(consoleLogger.info).to.be.calledWithMatch( - /.*Fetch.*Promise.*cookies.*/ - ); - }); - }); - - describe('#settings', () => { - it('applies the settings if provided', async () => { - const settings = { - instrumentationEnabled: false, - dataCollectionEnabled: false - }; - - const performance = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - performance._init(settings); - - expect(performance.instrumentationEnabled).is.equal(false); - expect(performance.dataCollectionEnabled).is.equal(false); - }); - - it('uses defaults when settings are not provided', async () => { - const expectedInstrumentationEnabled = SettingsService.getInstance() - .instrumentationEnabled; - const expectedDataCollectionEnabled = SettingsService.getInstance() - .dataCollectionEnabled; - - const performance = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - performance._init(); - - expect(performance.instrumentationEnabled).is.equal( - expectedInstrumentationEnabled - ); - expect(performance.dataCollectionEnabled).is.equal( - expectedDataCollectionEnabled - ); - }); - - describe('#instrumentationEnabled', () => { - it('sets instrumentationEnabled to enabled', async () => { - const performance = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - performance._init(); - - performance.instrumentationEnabled = true; - expect(performance.instrumentationEnabled).is.equal(true); - }); - - it('sets instrumentationEnabled to disabled', async () => { - const performance = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - performance._init(); - - performance.instrumentationEnabled = false; - expect(performance.instrumentationEnabled).is.equal(false); - }); - }); - - describe('#dataCollectionEnabled', () => { - it('sets dataCollectionEnabled to enabled', async () => { - const performance = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - performance._init(); - - performance.dataCollectionEnabled = true; - expect(performance.dataCollectionEnabled).is.equal(true); - }); - - it('sets dataCollectionEnabled to disabled', () => { - const performance = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - performance._init(); - - performance.dataCollectionEnabled = false; - expect(performance.dataCollectionEnabled).is.equal(false); - }); - }); - }); -}); diff --git a/packages-exp/performance-exp/src/controllers/perf.ts b/packages-exp/performance-exp/src/controllers/perf.ts deleted file mode 100644 index 9434c0f2a94..00000000000 --- a/packages-exp/performance-exp/src/controllers/perf.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { setupOobResources } from '../services/oob_resources_service'; -import { SettingsService } from '../services/settings_service'; -import { getInitializationPromise } from '../services/initialization_service'; -import { Api } from '../services/api_service'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; -import { - PerformanceSettings, - FirebasePerformance -} from '@firebase/performance-types-exp'; -import { validateIndexedDBOpenable } from '@firebase/util'; -import { setupTransportService } from '../services/transport_service'; -import { consoleLogger } from '../utils/console_logger'; - -export class PerformanceController implements FirebasePerformance { - private initialized: boolean = false; - - constructor( - readonly app: FirebaseApp, - readonly installations: _FirebaseInstallationsInternal - ) {} - - /** - * This method *must* be called internally as part of creating a - * PerformanceController instance. - * - * Currently it's not possible to pass the settings object through the - * constructor using Components, so this method exists to be called with the - * desired settings, to ensure nothing is collected without the user's - * consent. - */ - _init(settings?: PerformanceSettings): void { - if (this.initialized) { - return; - } - - if (settings?.dataCollectionEnabled !== undefined) { - this.dataCollectionEnabled = settings.dataCollectionEnabled; - } - if (settings?.instrumentationEnabled !== undefined) { - this.instrumentationEnabled = settings.instrumentationEnabled; - } - - if (Api.getInstance().requiredApisAvailable()) { - validateIndexedDBOpenable() - .then(isAvailable => { - if (isAvailable) { - setupTransportService(); - getInitializationPromise(this).then( - () => setupOobResources(this), - () => setupOobResources(this) - ); - this.initialized = true; - } - }) - .catch(error => { - consoleLogger.info(`Environment doesn't support IndexedDB: ${error}`); - }); - } else { - consoleLogger.info( - 'Firebase Performance cannot start if the browser does not support ' + - '"Fetch" and "Promise", or cookies are disabled.' - ); - } - } - - set instrumentationEnabled(val: boolean) { - SettingsService.getInstance().instrumentationEnabled = val; - } - get instrumentationEnabled(): boolean { - return SettingsService.getInstance().instrumentationEnabled; - } - - set dataCollectionEnabled(val: boolean) { - SettingsService.getInstance().dataCollectionEnabled = val; - } - get dataCollectionEnabled(): boolean { - return SettingsService.getInstance().dataCollectionEnabled; - } -} diff --git a/packages-exp/performance-exp/src/index.ts b/packages-exp/performance-exp/src/index.ts deleted file mode 100644 index b976ac9d398..00000000000 --- a/packages-exp/performance-exp/src/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { - FirebasePerformance, - PerformanceSettings, - PerformanceTrace -} from '@firebase/performance-types-exp'; -import { ERROR_FACTORY, ErrorCode } from './utils/errors'; -import { setupApi } from './services/api_service'; -import { PerformanceController } from './controllers/perf'; -import { - _registerComponent, - _getProvider, - registerVersion -} from '@firebase/app-exp'; -import { - InstanceFactory, - ComponentContainer, - Component, - ComponentType -} from '@firebase/component'; -import { name, version } from '../package.json'; -import { Trace } from './resources/trace'; -import '@firebase/installations-exp'; - -const DEFAULT_ENTRY_NAME = '[DEFAULT]'; - -/** - * Returns a FirebasePerformance instance for the given app. - * @param app - The FirebaseApp to use. - * @param settings - Optional settings for the Performance instance. - * @public - */ -export function getPerformance( - app: FirebaseApp, - settings?: PerformanceSettings -): FirebasePerformance { - const provider = _getProvider(app, 'performance-exp'); - const perfInstance = provider.getImmediate() as PerformanceController; - perfInstance._init(settings); - return perfInstance; -} - -/** - * Returns a new PerformanceTrace instance. - * @param performance - The FirebasePerformance instance to use. - * @param name - The name of the trace. - * @public - */ -export function trace( - performance: FirebasePerformance, - name: string -): PerformanceTrace { - return new Trace(performance as PerformanceController, name); -} - -const factory: InstanceFactory<'performance-exp'> = ( - container: ComponentContainer -) => { - // Dependencies - const app = container.getProvider('app-exp').getImmediate(); - const installations = container - .getProvider('installations-exp-internal') - .getImmediate(); - - if (app.name !== DEFAULT_ENTRY_NAME) { - throw ERROR_FACTORY.create(ErrorCode.FB_NOT_DEFAULT); - } - if (typeof window === 'undefined') { - throw ERROR_FACTORY.create(ErrorCode.NO_WINDOW); - } - setupApi(window); - return new PerformanceController(app, installations); -}; - -function registerPerformance(): void { - _registerComponent( - new Component('performance-exp', factory, ComponentType.PUBLIC) - ); -} - -registerPerformance(); -registerVersion(name, version); diff --git a/packages-exp/performance-exp/src/resources/network_request.test.ts b/packages-exp/performance-exp/src/resources/network_request.test.ts deleted file mode 100644 index 068fbb58b92..00000000000 --- a/packages-exp/performance-exp/src/resources/network_request.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { stub, restore } from 'sinon'; -import { createNetworkRequestEntry } from '../../src/resources/network_request'; -import { expect } from 'chai'; -import { Api, setupApi } from '../services/api_service'; -import * as perfLogger from '../services/perf_logger'; - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { PerformanceController } from '../controllers/perf'; -import { FirebaseInstallations } from '@firebase/installations-types'; -import '../../test/setup'; - -describe('Firebase Performance > network_request', () => { - setupApi(window); - - const fakeFirebaseApp = ({ - options: {} - } as unknown) as FirebaseApp; - - const fakeInstallations = ({} as unknown) as FirebaseInstallations; - const performanceController = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - - beforeEach(() => { - stub(Api.prototype, 'getTimeOrigin').returns(1528521843799.5032); - stub(perfLogger, 'logNetworkRequest'); - }); - - afterEach(() => { - restore(); - }); - - describe('#createNetworkRequestEntry', () => { - it('logs network request when all required fields present', () => { - const PERFORMANCE_ENTRY = ({ - name: 'http://some.test.website.com', - transferSize: 500, - startTime: 1645352.632345, - responseStart: 1645360.244323, - responseEnd: 1645360.832443 - } as unknown) as PerformanceResourceTiming; - - const EXPECTED_NETWORK_REQUEST = { - performanceController, - url: 'http://some.test.website.com', - responsePayloadBytes: 500, - startTimeUs: 1528523489152135, - timeToResponseInitiatedUs: 7611, - timeToResponseCompletedUs: 8200 - }; - - createNetworkRequestEntry(performanceController, PERFORMANCE_ENTRY); - - expect( - (perfLogger.logNetworkRequest as any).calledWith( - EXPECTED_NETWORK_REQUEST - ) - ).to.be.true; - }); - - it('doesnt log network request when responseStart is absent', () => { - const PERFORMANCE_ENTRY = ({ - name: 'http://some.test.website.com', - transferSize: 500, - startTime: 1645352.632345, - responseEnd: 1645360.832443 - } as unknown) as PerformanceResourceTiming; - - createNetworkRequestEntry(performanceController, PERFORMANCE_ENTRY); - - expect(perfLogger.logNetworkRequest).to.not.have.been.called; - }); - }); -}); diff --git a/packages-exp/performance-exp/src/resources/network_request.ts b/packages-exp/performance-exp/src/resources/network_request.ts deleted file mode 100644 index c5a8103eb03..00000000000 --- a/packages-exp/performance-exp/src/resources/network_request.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Api } from '../services/api_service'; -import { logNetworkRequest } from '../services/perf_logger'; -import { PerformanceController } from '../controllers/perf'; - -// The order of values of this enum should not be changed. -export const enum HttpMethod { - HTTP_METHOD_UNKNOWN = 0, - GET = 1, - PUT = 2, - POST = 3, - DELETE = 4, - HEAD = 5, - PATCH = 6, - OPTIONS = 7, - TRACE = 8, - CONNECT = 9 -} - -// Durations are in microseconds. -export interface NetworkRequest { - performanceController: PerformanceController; - url: string; - httpMethod?: HttpMethod; - requestPayloadBytes?: number; - responsePayloadBytes?: number; - httpResponseCode?: number; - responseContentType?: string; - startTimeUs?: number; - timeToRequestCompletedUs?: number; - timeToResponseInitiatedUs?: number; - timeToResponseCompletedUs?: number; -} - -export function createNetworkRequestEntry( - performanceController: PerformanceController, - entry: PerformanceEntry -): void { - const performanceEntry = entry as PerformanceResourceTiming; - if (!performanceEntry || performanceEntry.responseStart === undefined) { - return; - } - const timeOrigin = Api.getInstance().getTimeOrigin(); - const startTimeUs = Math.floor( - (performanceEntry.startTime + timeOrigin) * 1000 - ); - const timeToResponseInitiatedUs = performanceEntry.responseStart - ? Math.floor( - (performanceEntry.responseStart - performanceEntry.startTime) * 1000 - ) - : undefined; - const timeToResponseCompletedUs = Math.floor( - (performanceEntry.responseEnd - performanceEntry.startTime) * 1000 - ); - // Remove the query params from logged network request url. - const url = performanceEntry.name && performanceEntry.name.split('?')[0]; - const networkRequest: NetworkRequest = { - performanceController, - url, - responsePayloadBytes: performanceEntry.transferSize, - startTimeUs, - timeToResponseInitiatedUs, - timeToResponseCompletedUs - }; - - logNetworkRequest(networkRequest); -} diff --git a/packages-exp/performance-exp/src/resources/trace.test.ts b/packages-exp/performance-exp/src/resources/trace.test.ts deleted file mode 100644 index a4104cf4723..00000000000 --- a/packages-exp/performance-exp/src/resources/trace.test.ts +++ /dev/null @@ -1,293 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { spy, stub, restore } from 'sinon'; -import { Trace } from '../resources/trace'; -import { expect } from 'chai'; -import { Api, setupApi } from '../services/api_service'; -import * as perfLogger from '../services/perf_logger'; -import { PerformanceController } from '../controllers/perf'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import { FirebaseInstallations } from '@firebase/installations-types'; - -import '../../test/setup'; - -describe('Firebase Performance > trace', () => { - setupApi(window); - const fakeFirebaseConfig = { - apiKey: 'api-key', - authDomain: 'project-id.firebaseapp.com', - databaseURL: 'https://project-id.firebaseio.com', - projectId: 'project-id', - storageBucket: 'project-id.appspot.com', - messagingSenderId: 'sender-id', - appId: '1:111:web:a1234' - }; - - const fakeFirebaseApp = ({ - options: fakeFirebaseConfig - } as unknown) as FirebaseApp; - - const fakeInstallations = ({} as unknown) as FirebaseInstallations; - const performanceController = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - - let trace: Trace; - const createTrace = (): Trace => { - return new Trace(performanceController, 'test'); - }; - - beforeEach(() => { - spy(Api.prototype, 'mark'); - stub(perfLogger, 'logTrace'); - trace = createTrace(); - }); - - afterEach(() => { - restore(); - }); - - describe('#start', () => { - beforeEach(() => { - trace.start(); - }); - - it('uses the underlying api method', () => { - expect(Api.getInstance().mark).to.be.calledOnce; - }); - - it('throws if a trace is started twice', () => { - expect(() => trace.start()).to.throw(); - }); - }); - - describe('#stop', () => { - it('adds a mark to the performance timeline', () => { - trace.start(); - trace.stop(); - - expect(Api.getInstance().mark).to.be.calledTwice; - }); - - it('logs the trace', () => { - trace.start(); - trace.stop(); - - expect((perfLogger.logTrace as any).calledOnceWith(trace)).to.be.true; - }); - }); - - describe('#record', () => { - it('logs a custom trace with non-positive start time value', () => { - expect(() => trace.record(0, 20)).to.throw(); - expect(() => trace.record(-100, 20)).to.throw(); - }); - - it('logs a custom trace with non-positive duration value', () => { - expect(() => trace.record(1000, 0)).to.throw(); - expect(() => trace.record(1000, -200)).to.throw(); - }); - - it('logs a trace without metrics or custom attributes', () => { - trace.record(1, 20); - - expect((perfLogger.logTrace as any).calledOnceWith(trace)).to.be.true; - }); - - it('logs a trace with metrics', () => { - trace.record(1, 20, { metrics: { cacheHits: 1 } }); - - expect((perfLogger.logTrace as any).calledOnceWith(trace)).to.be.true; - expect(trace.getMetric('cacheHits')).to.eql(1); - }); - - it('logs a trace with custom attributes', () => { - trace.record(1, 20, { attributes: { level: '1' } }); - - expect((perfLogger.logTrace as any).calledOnceWith(trace)).to.be.true; - expect(trace.getAttributes()).to.eql({ level: '1' }); - }); - - it('logs a trace with custom attributes and metrics', () => { - trace.record(1, 20, { - attributes: { level: '1' }, - metrics: { cacheHits: 1 } - }); - - expect((perfLogger.logTrace as any).calledOnceWith(trace)).to.be.true; - expect(trace.getAttributes()).to.eql({ level: '1' }); - expect(trace.getMetric('cacheHits')).to.eql(1); - }); - }); - - describe('#incrementMetric', () => { - it('creates new metric if one doesnt exist.', () => { - trace.incrementMetric('cacheHits', 200); - - expect(trace.getMetric('cacheHits')).to.eql(200); - }); - - it('increments metric if it already exists.', () => { - trace.incrementMetric('cacheHits', 200); - trace.incrementMetric('cacheHits', 400); - - expect(trace.getMetric('cacheHits')).to.eql(600); - }); - - it('increments metric value as an integer even if the value is provided in float.', () => { - trace.incrementMetric('cacheHits', 200); - trace.incrementMetric('cacheHits', 400.38); - - expect(trace.getMetric('cacheHits')).to.eql(600); - }); - - it('increments metric value with a negative float.', () => { - trace.incrementMetric('cacheHits', 200); - trace.incrementMetric('cacheHits', -230.38); - - expect(trace.getMetric('cacheHits')).to.eql(-31); - }); - - it('throws error if metric doesnt exist and has invalid name', () => { - expect(() => trace.incrementMetric('_invalidMetric', 1)).to.throw(); - }); - }); - - describe('#putMetric', () => { - it('creates new metric if one doesnt exist and has valid name.', () => { - trace.putMetric('cacheHits', 200); - - expect(trace.getMetric('cacheHits')).to.eql(200); - }); - - it('sets the metric value as an integer even if the value is provided in float.', () => { - trace.putMetric('timelapse', 200.48); - - expect(trace.getMetric('timelapse')).to.eql(200); - }); - - it('replaces metric if it already exists.', () => { - trace.putMetric('cacheHits', 200); - trace.putMetric('cacheHits', 400); - - expect(trace.getMetric('cacheHits')).to.eql(400); - }); - - it('throws error if metric doesnt exist and has invalid name', () => { - expect(() => trace.putMetric('_invalidMetric', 1)).to.throw(); - expect(() => trace.putMetric('_fid', 1)).to.throw(); - }); - }); - - describe('#getMetric', () => { - it('returns 0 if metric doesnt exist', () => { - expect(trace.getMetric('doesThisExist')).to.equal(0); - }); - - it('returns 0 if it exists and equals 0', () => { - trace.putMetric('cacheHits', 0); - - expect(trace.getMetric('cacheHits')).to.equal(0); - }); - - it('returns metric if it exists', () => { - trace.putMetric('cacheHits', 200); - - expect(trace.getMetric('cacheHits')).to.equal(200); - }); - - it('returns multiple metrics if they exist', () => { - trace.putMetric('cacheHits', 200); - trace.putMetric('bytesDownloaded', 25); - - expect(trace.getMetric('cacheHits')).to.equal(200); - expect(trace.getMetric('bytesDownloaded')).to.equal(25); - }); - }); - - describe('#putAttribute', () => { - it('creates new attribute if it doesnt exist', () => { - trace.putAttribute('level', '4'); - - expect(trace.getAttributes()).to.eql({ level: '4' }); - }); - - it('replaces attribute if it exists', () => { - trace.putAttribute('level', '4'); - trace.putAttribute('level', '7'); - - expect(trace.getAttributes()).to.eql({ level: '7' }); - }); - - it('throws error if attribute name is invalid', () => { - expect(() => trace.putAttribute('_invalidAttribute', '1')).to.throw(); - }); - - it('throws error if attribute value is invalid', () => { - const longAttributeValue = - 'too-long-attribute-value-over-one-hundred-characters-too-long-attribute-value-over-one-' + - 'hundred-charac'; - expect(() => - trace.putAttribute('validName', longAttributeValue) - ).to.throw(); - }); - }); - - describe('#getAttribute', () => { - it('returns undefined for attribute that doesnt exist', () => { - expect(trace.getAttribute('level')).to.be.undefined; - }); - - it('returns attribute if it exists', () => { - trace.putAttribute('level', '4'); - expect(trace.getAttribute('level')).to.equal('4'); - }); - - it('returns separate attributes if they exist', () => { - trace.putAttribute('level', '4'); - trace.putAttribute('stage', 'beginning'); - - expect(trace.getAttribute('level')).to.equal('4'); - expect(trace.getAttribute('stage')).to.equal('beginning'); - }); - }); - - describe('#removeAttribute', () => { - it('does not throw if removing attribute that doesnt exist', () => { - expect(() => trace.removeAttribute('doesNotExist')).to.not.throw; - }); - - it('removes attribute if it exists', () => { - trace.putAttribute('level', '4'); - expect(trace.getAttribute('level')).to.equal('4'); - - trace.removeAttribute('level'); - expect(trace.getAttribute('level')).to.be.undefined; - }); - - it('retains other attributes', () => { - trace.putAttribute('level', '4'); - trace.putAttribute('stage', 'beginning'); - - trace.removeAttribute('level'); - expect(trace.getAttribute('level')).to.be.undefined; - expect(trace.getAttribute('stage')).to.equal('beginning'); - }); - }); -}); diff --git a/packages-exp/performance-exp/src/resources/trace.ts b/packages-exp/performance-exp/src/resources/trace.ts deleted file mode 100644 index 2da51f56a6b..00000000000 --- a/packages-exp/performance-exp/src/resources/trace.ts +++ /dev/null @@ -1,356 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - TRACE_START_MARK_PREFIX, - TRACE_STOP_MARK_PREFIX, - TRACE_MEASURE_PREFIX, - OOB_TRACE_PAGE_LOAD_PREFIX, - FIRST_PAINT_COUNTER_NAME, - FIRST_CONTENTFUL_PAINT_COUNTER_NAME, - FIRST_INPUT_DELAY_COUNTER_NAME -} from '../constants'; -import { Api } from '../services/api_service'; -import { logTrace } from '../services/perf_logger'; -import { ERROR_FACTORY, ErrorCode } from '../utils/errors'; -import { - isValidCustomAttributeName, - isValidCustomAttributeValue -} from '../utils/attributes_utils'; -import { - isValidMetricName, - convertMetricValueToInteger -} from '../utils/metric_utils'; -import { PerformanceTrace } from '@firebase/performance-types-exp'; -import { PerformanceController } from '../controllers/perf'; - -const enum TraceState { - UNINITIALIZED = 1, - RUNNING, - TERMINATED -} - -export class Trace implements PerformanceTrace { - private state: TraceState = TraceState.UNINITIALIZED; - startTimeUs!: number; - durationUs!: number; - private customAttributes: { [key: string]: string } = {}; - counters: { [counterName: string]: number } = {}; - private api = Api.getInstance(); - private randomId = Math.floor(Math.random() * 1000000); - private traceStartMark!: string; - private traceStopMark!: string; - private traceMeasure!: string; - - /** - * @param performanceController The performance controller running. - * @param name The name of the trace. - * @param isAuto If the trace is auto-instrumented. - * @param traceMeasureName The name of the measure marker in user timing specification. This field - * is only set when the trace is built for logging when the user directly uses the user timing - * api (performance.mark and performance.measure). - */ - constructor( - readonly performanceController: PerformanceController, - readonly name: string, - readonly isAuto = false, - traceMeasureName?: string - ) { - if (!this.isAuto) { - this.traceStartMark = `${TRACE_START_MARK_PREFIX}-${this.randomId}-${this.name}`; - this.traceStopMark = `${TRACE_STOP_MARK_PREFIX}-${this.randomId}-${this.name}`; - this.traceMeasure = - traceMeasureName || - `${TRACE_MEASURE_PREFIX}-${this.randomId}-${this.name}`; - - if (traceMeasureName) { - // For the case of direct user timing traces, no start stop will happen. The measure object - // is already available. - this.calculateTraceMetrics(); - } - } - } - - /** - * Starts a trace. The measurement of the duration starts at this point. - */ - start(): void { - if (this.state !== TraceState.UNINITIALIZED) { - throw ERROR_FACTORY.create(ErrorCode.TRACE_STARTED_BEFORE, { - traceName: this.name - }); - } - this.api.mark(this.traceStartMark); - this.state = TraceState.RUNNING; - } - - /** - * Stops the trace. The measurement of the duration of the trace stops at this point and trace - * is logged. - */ - stop(): void { - if (this.state !== TraceState.RUNNING) { - throw ERROR_FACTORY.create(ErrorCode.TRACE_STOPPED_BEFORE, { - traceName: this.name - }); - } - this.state = TraceState.TERMINATED; - this.api.mark(this.traceStopMark); - this.api.measure( - this.traceMeasure, - this.traceStartMark, - this.traceStopMark - ); - this.calculateTraceMetrics(); - logTrace(this); - } - - /** - * Records a trace with predetermined values. If this method is used a trace is created and logged - * directly. No need to use start and stop methods. - * @param startTime Trace start time since epoch in millisec - * @param duration The duraction of the trace in millisec - * @param options An object which can optionally hold maps of custom metrics and custom attributes - */ - record( - startTime: number, - duration: number, - options?: { - metrics?: { [key: string]: number }; - attributes?: { [key: string]: string }; - } - ): void { - if (startTime <= 0) { - throw ERROR_FACTORY.create(ErrorCode.NONPOSITIVE_TRACE_START_TIME, { - traceName: this.name - }); - } - if (duration <= 0) { - throw ERROR_FACTORY.create(ErrorCode.NONPOSITIVE_TRACE_DURATION, { - traceName: this.name - }); - } - - this.durationUs = Math.floor(duration * 1000); - this.startTimeUs = Math.floor(startTime * 1000); - if (options && options.attributes) { - this.customAttributes = { ...options.attributes }; - } - if (options && options.metrics) { - for (const metric of Object.keys(options.metrics)) { - if (!isNaN(Number(options.metrics[metric]))) { - this.counters[metric] = Number(Math.floor(options.metrics[metric])); - } - } - } - logTrace(this); - } - - /** - * Increments a custom metric by a certain number or 1 if number not specified. Will create a new - * custom metric if one with the given name does not exist. The value will be floored down to an - * integer. - * @param counter Name of the custom metric - * @param numAsInteger Increment by value - */ - incrementMetric(counter: string, numAsInteger = 1): void { - if (this.counters[counter] === undefined) { - this.putMetric(counter, numAsInteger); - } else { - this.putMetric(counter, this.counters[counter] + numAsInteger); - } - } - - /** - * Sets a custom metric to a specified value. Will create a new custom metric if one with the - * given name does not exist. The value will be floored down to an integer. - * @param counter Name of the custom metric - * @param numAsInteger Set custom metric to this value - */ - putMetric(counter: string, numAsInteger: number): void { - if (isValidMetricName(counter, this.name)) { - this.counters[counter] = convertMetricValueToInteger(numAsInteger); - } else { - throw ERROR_FACTORY.create(ErrorCode.INVALID_CUSTOM_METRIC_NAME, { - customMetricName: counter - }); - } - } - - /** - * Returns the value of the custom metric by that name. If a custom metric with that name does - * not exist will return zero. - * @param counter - */ - getMetric(counter: string): number { - return this.counters[counter] || 0; - } - - /** - * Sets a custom attribute of a trace to a certain value. - * @param attr - * @param value - */ - putAttribute(attr: string, value: string): void { - const isValidName = isValidCustomAttributeName(attr); - const isValidValue = isValidCustomAttributeValue(value); - if (isValidName && isValidValue) { - this.customAttributes[attr] = value; - return; - } - // Throw appropriate error when the attribute name or value is invalid. - if (!isValidName) { - throw ERROR_FACTORY.create(ErrorCode.INVALID_ATTRIBUTE_NAME, { - attributeName: attr - }); - } - if (!isValidValue) { - throw ERROR_FACTORY.create(ErrorCode.INVALID_ATTRIBUTE_VALUE, { - attributeValue: value - }); - } - } - - /** - * Retrieves the value a custom attribute of a trace is set to. - * @param attr - */ - getAttribute(attr: string): string | undefined { - return this.customAttributes[attr]; - } - - removeAttribute(attr: string): void { - if (this.customAttributes[attr] === undefined) { - return; - } - delete this.customAttributes[attr]; - } - - getAttributes(): { [key: string]: string } { - return { ...this.customAttributes }; - } - - private setStartTime(startTime: number): void { - this.startTimeUs = startTime; - } - - private setDuration(duration: number): void { - this.durationUs = duration; - } - - /** - * Calculates and assigns the duration and start time of the trace using the measure performance - * entry. - */ - private calculateTraceMetrics(): void { - const perfMeasureEntries = this.api.getEntriesByName(this.traceMeasure); - const perfMeasureEntry = perfMeasureEntries && perfMeasureEntries[0]; - if (perfMeasureEntry) { - this.durationUs = Math.floor(perfMeasureEntry.duration * 1000); - this.startTimeUs = Math.floor( - (perfMeasureEntry.startTime + this.api.getTimeOrigin()) * 1000 - ); - } - } - - /** - * @param navigationTimings A single element array which contains the navigationTIming object of - * the page load - * @param paintTimings A array which contains paintTiming object of the page load - * @param firstInputDelay First input delay in millisec - */ - static createOobTrace( - performanceController: PerformanceController, - navigationTimings: PerformanceNavigationTiming[], - paintTimings: PerformanceEntry[], - firstInputDelay?: number - ): void { - const route = Api.getInstance().getUrl(); - if (!route) { - return; - } - const trace = new Trace( - performanceController, - OOB_TRACE_PAGE_LOAD_PREFIX + route, - true - ); - const timeOriginUs = Math.floor(Api.getInstance().getTimeOrigin() * 1000); - trace.setStartTime(timeOriginUs); - - // navigationTimings includes only one element. - if (navigationTimings && navigationTimings[0]) { - trace.setDuration(Math.floor(navigationTimings[0].duration * 1000)); - trace.putMetric( - 'domInteractive', - Math.floor(navigationTimings[0].domInteractive * 1000) - ); - trace.putMetric( - 'domContentLoadedEventEnd', - Math.floor(navigationTimings[0].domContentLoadedEventEnd * 1000) - ); - trace.putMetric( - 'loadEventEnd', - Math.floor(navigationTimings[0].loadEventEnd * 1000) - ); - } - - const FIRST_PAINT = 'first-paint'; - const FIRST_CONTENTFUL_PAINT = 'first-contentful-paint'; - if (paintTimings) { - const firstPaint = paintTimings.find( - paintObject => paintObject.name === FIRST_PAINT - ); - if (firstPaint && firstPaint.startTime) { - trace.putMetric( - FIRST_PAINT_COUNTER_NAME, - Math.floor(firstPaint.startTime * 1000) - ); - } - const firstContentfulPaint = paintTimings.find( - paintObject => paintObject.name === FIRST_CONTENTFUL_PAINT - ); - if (firstContentfulPaint && firstContentfulPaint.startTime) { - trace.putMetric( - FIRST_CONTENTFUL_PAINT_COUNTER_NAME, - Math.floor(firstContentfulPaint.startTime * 1000) - ); - } - - if (firstInputDelay) { - trace.putMetric( - FIRST_INPUT_DELAY_COUNTER_NAME, - Math.floor(firstInputDelay * 1000) - ); - } - } - - logTrace(trace); - } - - static createUserTimingTrace( - performanceController: PerformanceController, - measureName: string - ): void { - const trace = new Trace( - performanceController, - measureName, - false, - measureName - ); - logTrace(trace); - } -} diff --git a/packages-exp/performance-exp/src/services/api_service.test.ts b/packages-exp/performance-exp/src/services/api_service.test.ts deleted file mode 100644 index c0e9a29b690..00000000000 --- a/packages-exp/performance-exp/src/services/api_service.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { stub } from 'sinon'; -import { expect } from 'chai'; -import { Api, setupApi } from './api_service'; -import '../../test/setup'; - -describe('Firebase Performance > api_service', () => { - const PAGE_URL = 'http://www.test.com/abcd?a=2'; - const PERFORMANCE_ENTRY: PerformanceEntry = { - duration: 0, - entryType: 'paint', - name: 'first-contentful-paint', - startTime: 149.01000005193055, - toJSON: () => {} - }; - - const mockWindow = { ...self }; - // hack for IE11. self.hasOwnProperty('performance') returns false in IE11 - mockWindow.performance = self.performance; - - let api: Api; - - beforeEach(() => { - stub(mockWindow.performance, 'mark'); - stub(mockWindow.performance, 'measure'); - stub(mockWindow.performance, 'getEntriesByType').returns([ - PERFORMANCE_ENTRY - ]); - stub(mockWindow.performance, 'getEntriesByName').returns([ - PERFORMANCE_ENTRY - ]); - // This is to make sure the test page is not changed by changing the href of location object. - mockWindow.location = { ...self.location, href: PAGE_URL }; - - setupApi(mockWindow); - api = Api.getInstance(); - }); - - describe('getUrl', () => { - it('removes the query params', () => { - expect(api.getUrl()).to.equal('http://www.test.com/abcd'); - }); - }); - - describe('mark', () => { - it('creates performance mark', () => { - const MARK_NAME = 'mark1'; - api.mark(MARK_NAME); - - expect(mockWindow.performance.mark).to.be.calledOnceWith(MARK_NAME); - }); - }); - - describe('measure', () => { - it('creates a performance measure', () => { - const MEASURE_NAME = 'measure1'; - const MARK_1_NAME = 'mark1'; - const MARK_2_NAME = 'mark2'; - api.measure(MEASURE_NAME, MARK_1_NAME, MARK_2_NAME); - - expect(mockWindow.performance.measure).to.be.calledOnceWith( - MEASURE_NAME, - MARK_1_NAME, - MARK_2_NAME - ); - }); - }); - - describe('getEntriesByType', () => { - it('calls the underlying performance api', () => { - expect(api.getEntriesByType('paint')).to.deep.equal([PERFORMANCE_ENTRY]); - }); - - it('does not throw if the browser does not include underlying api', () => { - api = new Api(({ performance: undefined } as unknown) as Window); - - expect(() => { - api.getEntriesByType('paint'); - }).to.not.throw(); - expect(api.getEntriesByType('paint')).to.deep.equal([]); - }); - }); - - describe('getEntriesByName', () => { - it('calls the underlying performance api', () => { - expect(api.getEntriesByName('paint')).to.deep.equal([PERFORMANCE_ENTRY]); - }); - - it('does not throw if the browser does not include underlying api', () => { - api = new Api(({ performance: undefined } as any) as Window); - - expect(() => { - api.getEntriesByName('paint'); - }).to.not.throw(); - expect(api.getEntriesByName('paint')).to.deep.equal([]); - }); - }); -}); diff --git a/packages-exp/performance-exp/src/services/api_service.ts b/packages-exp/performance-exp/src/services/api_service.ts deleted file mode 100644 index f500e415e87..00000000000 --- a/packages-exp/performance-exp/src/services/api_service.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ERROR_FACTORY, ErrorCode } from '../utils/errors'; -import { isIndexedDBAvailable } from '@firebase/util'; -import { consoleLogger } from '../utils/console_logger'; - -declare global { - interface Window { - PerformanceObserver: typeof PerformanceObserver; - perfMetrics?: { onFirstInputDelay(fn: (fid: number) => void): void }; - } -} - -let apiInstance: Api | undefined; -let windowInstance: Window | undefined; - -export type EntryType = - | 'mark' - | 'measure' - | 'paint' - | 'resource' - | 'frame' - | 'navigation'; - -/** - * This class holds a reference to various browser related objects injected by - * set methods. - */ -export class Api { - private readonly performance: Performance; - /** PreformanceObserver constructor function. */ - private readonly PerformanceObserver: typeof PerformanceObserver; - private readonly windowLocation: Location; - readonly onFirstInputDelay?: (fn: (fid: number) => void) => void; - readonly localStorage?: Storage; - readonly document: Document; - readonly navigator: Navigator; - - constructor(readonly window?: Window) { - if (!window) { - throw ERROR_FACTORY.create(ErrorCode.NO_WINDOW); - } - this.performance = window.performance; - this.PerformanceObserver = window.PerformanceObserver; - this.windowLocation = window.location; - this.navigator = window.navigator; - this.document = window.document; - if (this.navigator && this.navigator.cookieEnabled) { - // If user blocks cookies on the browser, accessing localStorage will - // throw an exception. - this.localStorage = window.localStorage; - } - if (window.perfMetrics && window.perfMetrics.onFirstInputDelay) { - this.onFirstInputDelay = window.perfMetrics.onFirstInputDelay; - } - } - - getUrl(): string { - // Do not capture the string query part of url. - return this.windowLocation.href.split('?')[0]; - } - - mark(name: string): void { - if (!this.performance || !this.performance.mark) { - return; - } - this.performance.mark(name); - } - - measure(measureName: string, mark1: string, mark2: string): void { - if (!this.performance || !this.performance.measure) { - return; - } - this.performance.measure(measureName, mark1, mark2); - } - - getEntriesByType(type: EntryType): PerformanceEntry[] { - if (!this.performance || !this.performance.getEntriesByType) { - return []; - } - return this.performance.getEntriesByType(type); - } - - getEntriesByName(name: string): PerformanceEntry[] { - if (!this.performance || !this.performance.getEntriesByName) { - return []; - } - return this.performance.getEntriesByName(name); - } - - getTimeOrigin(): number { - // Polyfill the time origin with performance.timing.navigationStart. - return ( - this.performance && - (this.performance.timeOrigin || this.performance.timing.navigationStart) - ); - } - - requiredApisAvailable(): boolean { - if ( - !fetch || - !Promise || - !this.navigator || - !this.navigator.cookieEnabled - ) { - consoleLogger.info( - 'Firebase Performance cannot start if browser does not support fetch and Promise or cookie is disabled.' - ); - return false; - } - - if (!isIndexedDBAvailable()) { - consoleLogger.info('IndexedDB is not supported by current browswer'); - return false; - } - return true; - } - - setupObserver( - entryType: EntryType, - callback: (entry: PerformanceEntry) => void - ): void { - if (!this.PerformanceObserver) { - return; - } - const observer = new this.PerformanceObserver(list => { - for (const entry of list.getEntries()) { - // `entry` is a PerformanceEntry instance. - callback(entry); - } - }); - - // Start observing the entry types you care about. - observer.observe({ entryTypes: [entryType] }); - } - - static getInstance(): Api { - if (apiInstance === undefined) { - apiInstance = new Api(windowInstance); - } - return apiInstance; - } -} - -export function setupApi(window: Window): void { - windowInstance = window; -} diff --git a/packages-exp/performance-exp/src/services/iid_service.test.ts b/packages-exp/performance-exp/src/services/iid_service.test.ts deleted file mode 100644 index 4451b0642b0..00000000000 --- a/packages-exp/performance-exp/src/services/iid_service.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { stub } from 'sinon'; -import { expect } from 'chai'; -import { - getIid, - getIidPromise, - getAuthenticationToken, - getAuthTokenPromise -} from './iid_service'; -import '../../test/setup'; -import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; - -describe('Firebase Perofmrance > iid_service', () => { - const IID = 'fid'; - const AUTH_TOKEN = 'authToken'; - - let fakeInstallations: _FirebaseInstallationsInternal; - before(() => { - const getId = stub().resolves(IID); - const getToken = stub().resolves(AUTH_TOKEN); - fakeInstallations = ({ - getId, - getToken - } as unknown) as _FirebaseInstallationsInternal; - }); - - describe('getIidPromise', () => { - it('provides iid', async () => { - const iid = await getIidPromise(fakeInstallations); - - expect(iid).to.be.equal(IID); - expect(getIid()).to.be.equal(IID); - }); - }); - - describe('getAuthTokenPromise', () => { - it('provides authentication token', async () => { - const token = await getAuthTokenPromise(fakeInstallations); - - expect(token).to.be.equal(AUTH_TOKEN); - expect(getAuthenticationToken()).to.be.equal(AUTH_TOKEN); - }); - }); -}); diff --git a/packages-exp/performance-exp/src/services/iid_service.ts b/packages-exp/performance-exp/src/services/iid_service.ts deleted file mode 100644 index 8899e12d1cb..00000000000 --- a/packages-exp/performance-exp/src/services/iid_service.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; - -let iid: string | undefined; -let authToken: string | undefined; - -export function getIidPromise( - installationsService: _FirebaseInstallationsInternal -): Promise { - const iidPromise = installationsService.getId(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - iidPromise.then((iidVal: string) => { - iid = iidVal; - }); - return iidPromise; -} - -// This method should be used after the iid is retrieved by getIidPromise method. -export function getIid(): string | undefined { - return iid; -} - -export function getAuthTokenPromise( - installationsService: _FirebaseInstallationsInternal -): Promise { - const authTokenPromise = installationsService.getToken(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - authTokenPromise.then((authTokenVal: string) => { - authToken = authTokenVal; - }); - return authTokenPromise; -} - -export function getAuthenticationToken(): string | undefined { - return authToken; -} diff --git a/packages-exp/performance-exp/src/services/initialization_service.test.ts b/packages-exp/performance-exp/src/services/initialization_service.test.ts deleted file mode 100644 index 9efde63ca37..00000000000 --- a/packages-exp/performance-exp/src/services/initialization_service.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { stub } from 'sinon'; -import { expect } from 'chai'; -import { - getInitializationPromise, - isPerfInitialized -} from './initialization_service'; -import { setupApi } from './api_service'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import '../../test/setup'; -import { FirebaseInstallations } from '@firebase/installations-types'; -import { PerformanceController } from '../controllers/perf'; - -describe('Firebase Perofmrance > initialization_service', () => { - const IID = 'fid'; - const AUTH_TOKEN = 'authToken'; - const getId = stub(); - const getToken = stub(); - - const fakeFirebaseConfig = { - apiKey: 'api-key', - authDomain: 'project-id.firebaseapp.com', - databaseURL: 'https://project-id.firebaseio.com', - projectId: 'project-id', - storageBucket: 'project-id.appspot.com', - messagingSenderId: 'sender-id', - appId: '1:111:web:a1234' - }; - - const fakeFirebaseApp = ({ - options: fakeFirebaseConfig - } as unknown) as FirebaseApp; - - const fakeInstallations = ({ - getId, - getToken - } as unknown) as FirebaseInstallations; - const performanceController = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - - const mockWindow = { ...self }; - mockWindow.document = { ...mockWindow.document, readyState: 'complete' }; - - beforeEach(() => { - stub(self, 'fetch').resolves(new Response('{}')); - mockWindow.localStorage = { ...mockWindow.localStorage, setItem: stub() }; - - setupApi(mockWindow); - }); - - it('changes initialization status after initialization is done', async () => { - getId.resolves(IID); - getToken.resolves(AUTH_TOKEN); - await getInitializationPromise(performanceController); - - expect(isPerfInitialized()).to.be.true; - }); - - it('returns initilization as not done before promise is resolved', async () => { - getId.resolves(IID); - getToken.resolves(AUTH_TOKEN); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - getInitializationPromise(performanceController); - - expect(isPerfInitialized()).to.be.false; - }); -}); diff --git a/packages-exp/performance-exp/src/services/initialization_service.ts b/packages-exp/performance-exp/src/services/initialization_service.ts deleted file mode 100644 index 94a1dfab1e5..00000000000 --- a/packages-exp/performance-exp/src/services/initialization_service.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getIidPromise } from './iid_service'; -import { getConfig } from './remote_config_service'; -import { Api } from './api_service'; -import { PerformanceController } from '../controllers/perf'; - -const enum InitializationStatus { - notInitialized = 1, - initializationPending, - initialized -} - -let initializationStatus = InitializationStatus.notInitialized; - -let initializationPromise: Promise | undefined; - -export function getInitializationPromise( - performanceController: PerformanceController -): Promise { - initializationStatus = InitializationStatus.initializationPending; - - initializationPromise = - initializationPromise || initializePerf(performanceController); - - return initializationPromise; -} - -export function isPerfInitialized(): boolean { - return initializationStatus === InitializationStatus.initialized; -} - -function initializePerf( - performanceController: PerformanceController -): Promise { - return getDocumentReadyComplete() - .then(() => getIidPromise(performanceController.installations)) - .then(iid => getConfig(performanceController, iid)) - .then( - () => changeInitializationStatus(), - () => changeInitializationStatus() - ); -} - -/** - * Returns a promise which resolves whenever the document readystate is complete or - * immediately if it is called after page load complete. - */ -function getDocumentReadyComplete(): Promise { - const document = Api.getInstance().document; - return new Promise(resolve => { - if (document && document.readyState !== 'complete') { - const handler = (): void => { - if (document.readyState === 'complete') { - document.removeEventListener('readystatechange', handler); - resolve(); - } - }; - document.addEventListener('readystatechange', handler); - } else { - resolve(); - } - }); -} - -function changeInitializationStatus(): void { - initializationStatus = InitializationStatus.initialized; -} diff --git a/packages-exp/performance-exp/src/services/oob_resources_service.test.ts b/packages-exp/performance-exp/src/services/oob_resources_service.test.ts deleted file mode 100644 index 35e4f0c93a5..00000000000 --- a/packages-exp/performance-exp/src/services/oob_resources_service.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - spy, - stub, - SinonSpy, - SinonStub, - useFakeTimers, - SinonFakeTimers -} from 'sinon'; -import { expect } from 'chai'; -import { Api, setupApi, EntryType } from './api_service'; -import * as iidService from './iid_service'; -import { setupOobResources } from './oob_resources_service'; -import { Trace } from '../resources/trace'; -import '../../test/setup'; -import { PerformanceController } from '../controllers/perf'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import { FirebaseInstallations } from '@firebase/installations-types'; - -describe('Firebase Performance > oob_resources_service', () => { - const MOCK_ID = 'idasdfsffe'; - - const NAVIGATION_PERFORMANCE_ENTRY: PerformanceNavigationTiming = { - connectEnd: 2.9499998781830072, - connectStart: 2.9499998781830072, - decodedBodySize: 1519, - domComplete: 186.48499995470047, - domContentLoadedEventEnd: 64.0499999281019, - domContentLoadedEventStart: 62.440000008791685, - domInteractive: 62.42000008933246, - domainLookupEnd: 2.9499998781830072, - domainLookupStart: 2.9499998781830072, - duration: 187.7349999267608, - encodedBodySize: 732, - entryType: 'navigation', - fetchStart: 2.9499998781830072, - initiatorType: 'navigation', - loadEventEnd: 187.7349999267608, - loadEventStart: 187.72999988868833, - name: 'https://test.firebase.com/', - nextHopProtocol: 'h2', - redirectCount: 0, - redirectEnd: 0, - redirectStart: 0, - requestStart: 5.034999921917915, - responseEnd: 9.305000072345138, - responseStart: 8.940000087022781, - secureConnectionStart: 0, - startTime: 0, - transferSize: 1259, - type: 'reload', - unloadEventEnd: 14.870000071823597, - unloadEventStart: 14.870000071823597, - workerStart: 0, - toJSON: () => {} - }; - - const PAINT_PERFORMANCE_ENTRY: PerformanceEntry = { - duration: 0, - entryType: 'paint', - name: 'first-contentful-paint', - startTime: 122.18499998562038, - toJSON: () => {} - }; - - let getIidStub: SinonStub<[], string | undefined>; - let apiGetInstanceSpy: SinonSpy<[], Api>; - let getEntriesByTypeStub: SinonStub<[EntryType], PerformanceEntry[]>; - let setupObserverStub: SinonStub< - [EntryType, (entry: PerformanceEntry) => void], - void - >; - let createOobTraceStub: SinonStub< - [ - PerformanceController, - PerformanceNavigationTiming[], - PerformanceEntry[], - (number | undefined)? - ], - void - >; - let clock: SinonFakeTimers; - - setupApi(self); - - const fakeFirebaseConfig = { - apiKey: 'api-key', - authDomain: 'project-id.firebaseapp.com', - databaseURL: 'https://project-id.firebaseio.com', - projectId: 'project-id', - storageBucket: 'project-id.appspot.com', - messagingSenderId: 'sender-id', - appId: '1:111:web:a1234' - }; - - const fakeFirebaseApp = ({ - options: fakeFirebaseConfig - } as unknown) as FirebaseApp; - - const fakeInstallations = ({} as unknown) as FirebaseInstallations; - const performanceController = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - - beforeEach(() => { - getIidStub = stub(iidService, 'getIid'); - apiGetInstanceSpy = spy(Api, 'getInstance'); - clock = useFakeTimers(); - getEntriesByTypeStub = stub(Api.prototype, 'getEntriesByType').callsFake( - entry => { - if (entry === 'navigation') { - return [NAVIGATION_PERFORMANCE_ENTRY]; - } - return [PAINT_PERFORMANCE_ENTRY]; - } - ); - setupObserverStub = stub(Api.prototype, 'setupObserver'); - createOobTraceStub = stub(Trace, 'createOobTrace'); - }); - - afterEach(() => { - clock.restore(); - }); - - describe('setupOobResources', () => { - it('does not start if there is no iid', () => { - getIidStub.returns(undefined); - setupOobResources(performanceController); - - expect(apiGetInstanceSpy).not.to.be.called; - }); - - it('sets up network request collection', () => { - getIidStub.returns(MOCK_ID); - setupOobResources(performanceController); - clock.tick(1); - - expect(apiGetInstanceSpy).to.be.called; - expect(getEntriesByTypeStub).to.be.calledWith('resource'); - expect(setupObserverStub).to.be.calledWith('resource'); - }); - - it('sets up page load trace collection', () => { - getIidStub.returns(MOCK_ID); - setupOobResources(performanceController); - clock.tick(1); - - expect(apiGetInstanceSpy).to.be.called; - expect(getEntriesByTypeStub).to.be.calledWith('navigation'); - expect(getEntriesByTypeStub).to.be.calledWith('paint'); - expect(createOobTraceStub).to.be.calledWithExactly( - performanceController, - [NAVIGATION_PERFORMANCE_ENTRY], - [PAINT_PERFORMANCE_ENTRY] - ); - }); - - it('waits for first input delay if polyfill is available', () => { - getIidStub.returns(MOCK_ID); - const api = Api.getInstance(); - //@ts-ignore Assignment to read-only property. - api.onFirstInputDelay = stub(); - setupOobResources(performanceController); - clock.tick(1); - - expect(api.onFirstInputDelay).to.be.called; - expect(createOobTraceStub).not.to.be.called; - clock.tick(5000); - expect(createOobTraceStub).to.be.calledWithExactly( - performanceController, - [NAVIGATION_PERFORMANCE_ENTRY], - [PAINT_PERFORMANCE_ENTRY] - ); - }); - - it('logs first input delay if polyfill is available and callback is called', () => { - getIidStub.returns(MOCK_ID); - const api = Api.getInstance(); - const FIRST_INPUT_DELAY = 123; - // Underscore is to avoid compiler comlaining about variable being declared but not used. - type FirstInputDelayCallback = (firstInputDelay: number) => void; - let firstInputDelayCallback: FirstInputDelayCallback = (): void => {}; - //@ts-ignore Assignment to read-only property. - api.onFirstInputDelay = (cb: FirstInputDelayCallback) => { - firstInputDelayCallback = cb; - }; - setupOobResources(performanceController); - clock.tick(1); - firstInputDelayCallback(FIRST_INPUT_DELAY); - - expect(createOobTraceStub).to.be.calledWithExactly( - performanceController, - [NAVIGATION_PERFORMANCE_ENTRY], - [PAINT_PERFORMANCE_ENTRY], - FIRST_INPUT_DELAY - ); - }); - - it('sets up user timing traces', () => { - getIidStub.returns(MOCK_ID); - setupOobResources(performanceController); - clock.tick(1); - - expect(apiGetInstanceSpy).to.be.called; - expect(getEntriesByTypeStub).to.be.calledWith('measure'); - expect(setupObserverStub).to.be.calledWith('measure'); - }); - }); -}); diff --git a/packages-exp/performance-exp/src/services/oob_resources_service.ts b/packages-exp/performance-exp/src/services/oob_resources_service.ts deleted file mode 100644 index 66891e774fe..00000000000 --- a/packages-exp/performance-exp/src/services/oob_resources_service.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Api } from './api_service'; -import { Trace } from '../resources/trace'; -import { createNetworkRequestEntry } from '../resources/network_request'; -import { TRACE_MEASURE_PREFIX } from '../constants'; -import { getIid } from './iid_service'; -import { PerformanceController } from '../controllers/perf'; - -const FID_WAIT_TIME_MS = 5000; - -export function setupOobResources( - performanceController: PerformanceController -): void { - // Do not initialize unless iid is available. - if (!getIid()) { - return; - } - // The load event might not have fired yet, and that means performance navigation timing - // object has a duration of 0. The setup should run after all current tasks in js queue. - setTimeout(() => setupOobTraces(performanceController), 0); - setTimeout(() => setupNetworkRequests(performanceController), 0); - setTimeout(() => setupUserTimingTraces(performanceController), 0); -} - -function setupNetworkRequests( - performanceController: PerformanceController -): void { - const api = Api.getInstance(); - const resources = api.getEntriesByType('resource'); - for (const resource of resources) { - createNetworkRequestEntry(performanceController, resource); - } - api.setupObserver('resource', entry => - createNetworkRequestEntry(performanceController, entry) - ); -} - -function setupOobTraces(performanceController: PerformanceController): void { - const api = Api.getInstance(); - const navigationTimings = api.getEntriesByType( - 'navigation' - ) as PerformanceNavigationTiming[]; - const paintTimings = api.getEntriesByType('paint'); - // If First Input Desly polyfill is added to the page, report the fid value. - // https://github.com/GoogleChromeLabs/first-input-delay - if (api.onFirstInputDelay) { - // If the fid call back is not called for certain time, continue without it. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let timeoutId: any = setTimeout(() => { - Trace.createOobTrace( - performanceController, - navigationTimings, - paintTimings - ); - timeoutId = undefined; - }, FID_WAIT_TIME_MS); - api.onFirstInputDelay((fid: number) => { - if (timeoutId) { - clearTimeout(timeoutId); - Trace.createOobTrace( - performanceController, - navigationTimings, - paintTimings, - fid - ); - } - }); - } else { - Trace.createOobTrace( - performanceController, - navigationTimings, - paintTimings - ); - } -} - -function setupUserTimingTraces( - performanceController: PerformanceController -): void { - const api = Api.getInstance(); - // Run through the measure performance entries collected up to this point. - const measures = api.getEntriesByType('measure'); - for (const measure of measures) { - createUserTimingTrace(performanceController, measure); - } - // Setup an observer to capture the measures from this point on. - api.setupObserver('measure', entry => - createUserTimingTrace(performanceController, entry) - ); -} - -function createUserTimingTrace( - performanceController: PerformanceController, - measure: PerformanceEntry -): void { - const measureName = measure.name; - // Do not create a trace, if the user timing marks and measures are created by the sdk itself. - if ( - measureName.substring(0, TRACE_MEASURE_PREFIX.length) === - TRACE_MEASURE_PREFIX - ) { - return; - } - Trace.createUserTimingTrace(performanceController, measureName); -} diff --git a/packages-exp/performance-exp/src/services/perf_logger.test.ts b/packages-exp/performance-exp/src/services/perf_logger.test.ts deleted file mode 100644 index 994a72f9d5e..00000000000 --- a/packages-exp/performance-exp/src/services/perf_logger.test.ts +++ /dev/null @@ -1,435 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { stub, SinonStub, useFakeTimers, SinonFakeTimers } from 'sinon'; -import { Trace } from '../resources/trace'; -import * as transportService from './transport_service'; -import * as iidService from './iid_service'; -import { expect } from 'chai'; -import { Api, setupApi } from './api_service'; -import { SettingsService } from './settings_service'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import * as initializationService from './initialization_service'; -import { SDK_VERSION } from '../constants'; -import * as attributeUtils from '../utils/attributes_utils'; -import { createNetworkRequestEntry } from '../resources/network_request'; -import '../../test/setup'; -import { mergeStrings } from '../utils/string_merger'; -import { FirebaseInstallations } from '@firebase/installations-types'; -import { PerformanceController } from '../controllers/perf'; - -describe('Performance Monitoring > perf_logger', () => { - const IID = 'idasdfsffe'; - const PAGE_URL = 'http://mock-page.com'; - const APP_ID = '1:123:web:2er'; - const VISIBILITY_STATE = 3; - const EFFECTIVE_CONNECTION_TYPE = 2; - const SERVICE_WORKER_STATUS = 3; - const TIME_ORIGIN = 1556512199893.9033; - const TRACE_NAME = 'testTrace'; - const START_TIME = 12345; - const DURATION = 321; - // Perf event header which is constant across tests in this file. - const WEBAPP_INFO = `"application_info":{"google_app_id":"${APP_ID}",\ -"app_instance_id":"${IID}","web_app_info":{"sdk_version":"${SDK_VERSION}",\ -"page_url":"${PAGE_URL}","service_worker_status":${SERVICE_WORKER_STATUS},\ -"visibility_state":${VISIBILITY_STATE},"effective_connection_type":${EFFECTIVE_CONNECTION_TYPE}},\ -"application_process_state":0}`; - - let addToQueueStub: SinonStub< - Array<{ message: string; eventTime: number }>, - void - >; - let getIidStub: SinonStub<[], string | undefined>; - let clock: SinonFakeTimers; - - function mockTransportHandler( - serializer: (...args: any[]) => string - ): (...args: any[]) => void { - return (...args) => { - const message = serializer(...args); - addToQueueStub({ - message, - eventTime: Date.now() - }); - }; - } - - setupApi(self); - const fakeFirebaseApp = ({ - options: { appId: APP_ID } - } as unknown) as FirebaseApp; - - const fakeInstallations = ({} as unknown) as FirebaseInstallations; - const performanceController = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - - beforeEach(() => { - getIidStub = stub(iidService, 'getIid'); - addToQueueStub = stub(); - stub(transportService, 'transportHandler').callsFake(mockTransportHandler); - stub(Api.prototype, 'getUrl').returns(PAGE_URL); - stub(Api.prototype, 'getTimeOrigin').returns(TIME_ORIGIN); - stub(attributeUtils, 'getEffectiveConnectionType').returns( - EFFECTIVE_CONNECTION_TYPE - ); - stub(attributeUtils, 'getServiceWorkerStatus').returns( - SERVICE_WORKER_STATUS - ); - clock = useFakeTimers(); - }); - - describe('logTrace', () => { - it('will not drop custom events sent before initialization finishes', async () => { - getIidStub.returns(IID); - stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); - stub(initializationService, 'isPerfInitialized').returns(false); - - // Simulates logging being enabled after initialization completes. - const initializationPromise = Promise.resolve().then(() => { - SettingsService.getInstance().loggingEnabled = true; - SettingsService.getInstance().logTraceAfterSampling = true; - }); - stub(initializationService, 'getInitializationPromise').returns( - initializationPromise - ); - - const trace = new Trace(performanceController, TRACE_NAME); - trace.record(START_TIME, DURATION); - await initializationPromise.then(() => { - clock.tick(1); - }); - - expect(addToQueueStub).to.be.called; - }); - - it('creates, serializes and sends a trace to transport service', () => { - const EXPECTED_TRACE_MESSAGE = - `{` + - WEBAPP_INFO + - `,"trace_metric":{"name":"${TRACE_NAME}","is_auto":false,\ -"client_start_time_us":${START_TIME * 1000},"duration_us":${DURATION * 1000},\ -"counters":{"counter1":3},"custom_attributes":{"attr":"val"}}}`; - getIidStub.returns(IID); - stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); - stub(initializationService, 'isPerfInitialized').returns(true); - SettingsService.getInstance().loggingEnabled = true; - SettingsService.getInstance().logTraceAfterSampling = true; - const trace = new Trace(performanceController, TRACE_NAME); - trace.putAttribute('attr', 'val'); - trace.putMetric('counter1', 3); - trace.record(START_TIME, DURATION); - clock.tick(1); - - expect(addToQueueStub).to.be.called; - expect(addToQueueStub.getCall(0).args[0].message).to.be.equal( - EXPECTED_TRACE_MESSAGE - ); - }); - - it('does not log an event if cookies are disabled in the browser', () => { - stub(Api.prototype, 'requiredApisAvailable').returns(false); - stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); - stub(initializationService, 'isPerfInitialized').returns(true); - const trace = new Trace(performanceController, TRACE_NAME); - trace.record(START_TIME, DURATION); - clock.tick(1); - - expect(addToQueueStub).not.to.be.called; - }); - - it('ascertains that the max number of customMetric allowed is 32', () => { - const EXPECTED_TRACE_MESSAGE = - `{` + - WEBAPP_INFO + - `,"trace_metric":{"name":"${TRACE_NAME}","is_auto":false,\ -"client_start_time_us":${START_TIME * 1000},"duration_us":${DURATION * 1000},\ -"counters":{"counter1":1,"counter2":2,"counter3":3,"counter4":4,"counter5":5,"counter6":6,\ -"counter7":7,"counter8":8,"counter9":9,"counter10":10,"counter11":11,"counter12":12,\ -"counter13":13,"counter14":14,"counter15":15,"counter16":16,"counter17":17,"counter18":18,\ -"counter19":19,"counter20":20,"counter21":21,"counter22":22,"counter23":23,"counter24":24,\ -"counter25":25,"counter26":26,"counter27":27,"counter28":28,"counter29":29,"counter30":30,\ -"counter31":31,"counter32":32}}}`; - getIidStub.returns(IID); - stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); - stub(initializationService, 'isPerfInitialized').returns(true); - SettingsService.getInstance().loggingEnabled = true; - SettingsService.getInstance().logTraceAfterSampling = true; - const trace = new Trace(performanceController, TRACE_NAME); - for (let i = 1; i <= 32; i++) { - trace.putMetric('counter' + i, i); - } - trace.record(START_TIME, DURATION); - clock.tick(1); - - expect(addToQueueStub).to.be.called; - expect(addToQueueStub.getCall(0).args[0].message).to.be.equal( - EXPECTED_TRACE_MESSAGE - ); - }); - - it('ascertains that the max number of custom attributes allowed is 5', () => { - const EXPECTED_TRACE_MESSAGE = - `{` + - WEBAPP_INFO + - `,"trace_metric":{"name":"${TRACE_NAME}","is_auto":false,\ -"client_start_time_us":${START_TIME * 1000},"duration_us":${DURATION * 1000},\ -"custom_attributes":{"attr1":"val1","attr2":"val2","attr3":"val3","attr4":"val4","attr5":"val5"}}}`; - getIidStub.returns(IID); - stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); - stub(initializationService, 'isPerfInitialized').returns(true); - SettingsService.getInstance().loggingEnabled = true; - SettingsService.getInstance().logTraceAfterSampling = true; - const trace = new Trace(performanceController, TRACE_NAME); - for (let i = 1; i <= 5; i++) { - trace.putAttribute('attr' + i, 'val' + i); - } - trace.record(START_TIME, DURATION); - clock.tick(1); - - expect(addToQueueStub).to.be.called; - expect(addToQueueStub.getCall(0).args[0].message).to.be.equal( - EXPECTED_TRACE_MESSAGE - ); - }); - }); - - describe('logPageLoadTrace', () => { - it('creates, serializes and sends a page load trace to cc service', () => { - const flooredStartTime = Math.floor(TIME_ORIGIN * 1000); - const EXPECTED_TRACE_MESSAGE = `{"application_info":{"google_app_id":"${APP_ID}",\ -"app_instance_id":"${IID}","web_app_info":{"sdk_version":"${SDK_VERSION}",\ -"page_url":"${PAGE_URL}","service_worker_status":${SERVICE_WORKER_STATUS},\ -"visibility_state":${ - attributeUtils.VisibilityState.VISIBLE - },"effective_connection_type":${EFFECTIVE_CONNECTION_TYPE}},\ -"application_process_state":0},"trace_metric":{"name":"_wt_${PAGE_URL}","is_auto":true,\ -"client_start_time_us":${flooredStartTime},"duration_us":${DURATION * 1000},\ -"counters":{"domInteractive":10000,"domContentLoadedEventEnd":20000,"loadEventEnd":10000,\ -"_fp":40000,"_fcp":50000,"_fid":90000}}}`; - stub(initializationService, 'isPerfInitialized').returns(true); - getIidStub.returns(IID); - SettingsService.getInstance().loggingEnabled = true; - SettingsService.getInstance().logTraceAfterSampling = true; - - stub(attributeUtils, 'getVisibilityState').returns( - attributeUtils.VisibilityState.VISIBLE - ); - - const navigationTiming: PerformanceNavigationTiming = { - domComplete: 100, - domContentLoadedEventEnd: 20, - domContentLoadedEventStart: 10, - domInteractive: 10, - loadEventEnd: 10, - loadEventStart: 10, - redirectCount: 10, - type: 'navigate', - unloadEventEnd: 10, - unloadEventStart: 10, - duration: DURATION - } as PerformanceNavigationTiming; - - const navigationTimings: PerformanceNavigationTiming[] = [ - navigationTiming - ]; - - const firstPaint: PerformanceEntry = { - name: 'first-paint', - startTime: 40, - duration: 100, - entryType: 'url', - toJSON() {} - }; - - const firstContentfulPaint: PerformanceEntry = { - name: 'first-contentful-paint', - startTime: 50, - duration: 100, - entryType: 'url', - toJSON() {} - }; - - const paintTimings: PerformanceEntry[] = [ - firstPaint, - firstContentfulPaint - ]; - - Trace.createOobTrace( - performanceController, - navigationTimings, - paintTimings, - 90 - ); - clock.tick(1); - - expect(addToQueueStub).to.be.called; - expect(addToQueueStub.getCall(0).args[0].message).to.be.equal( - EXPECTED_TRACE_MESSAGE - ); - }); - }); - - describe('logNetworkRequest', () => { - it('creates, serializes and sends a network request to transport service', () => { - const RESOURCE_PERFORMANCE_ENTRY: PerformanceResourceTiming = { - connectEnd: 0, - connectStart: 0, - decodedBodySize: 0, - domainLookupEnd: 0, - domainLookupStart: 0, - duration: 39.610000094398856, - encodedBodySize: 0, - entryType: 'resource', - fetchStart: 5645.689999917522, - initiatorType: 'fetch', - name: 'https://test.com/abc', - nextHopProtocol: 'http/2+quic/43', - redirectEnd: 0, - redirectStart: 0, - requestStart: 0, - responseEnd: 5685.300000011921, - responseStart: 0, - secureConnectionStart: 0, - startTime: 5645.689999917522, - transferSize: 0, - workerStart: 0, - toJSON: () => {} - }; - const START_TIME = Math.floor( - (TIME_ORIGIN + RESOURCE_PERFORMANCE_ENTRY.startTime) * 1000 - ); - const TIME_TO_RESPONSE_COMPLETED = Math.floor( - (RESOURCE_PERFORMANCE_ENTRY.responseEnd - - RESOURCE_PERFORMANCE_ENTRY.startTime) * - 1000 - ); - const EXPECTED_NETWORK_MESSAGE = - `{` + - WEBAPP_INFO + - `,\ -"network_request_metric":{"url":"${RESOURCE_PERFORMANCE_ENTRY.name}",\ -"http_method":0,"http_response_code":200,\ -"response_payload_bytes":${RESOURCE_PERFORMANCE_ENTRY.transferSize},\ -"client_start_time_us":${START_TIME},\ -"time_to_response_completed_us":${TIME_TO_RESPONSE_COMPLETED}}}`; - stub(initializationService, 'isPerfInitialized').returns(true); - getIidStub.returns(IID); - stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); - SettingsService.getInstance().loggingEnabled = true; - SettingsService.getInstance().logNetworkAfterSampling = true; - // Calls logNetworkRequest under the hood. - createNetworkRequestEntry( - performanceController, - RESOURCE_PERFORMANCE_ENTRY - ); - clock.tick(1); - - expect(addToQueueStub).to.be.called; - expect(addToQueueStub.getCall(0).args[0].message).to.be.equal( - EXPECTED_NETWORK_MESSAGE - ); - }); - - // Performance SDK doesn't instrument requests sent from SDK itself, therefore blacklist - // requests sent to cc endpoint. - it('skips performance collection if domain is cc', () => { - const CC_NETWORK_PERFORMANCE_ENTRY: PerformanceResourceTiming = { - connectEnd: 0, - connectStart: 0, - decodedBodySize: 0, - domainLookupEnd: 0, - domainLookupStart: 0, - duration: 39.610000094398856, - encodedBodySize: 0, - entryType: 'resource', - fetchStart: 5645.689999917522, - initiatorType: 'fetch', - name: 'https://firebaselogging.googleapis.com/v0cc/log?message=a', - nextHopProtocol: 'http/2+quic/43', - redirectEnd: 0, - redirectStart: 0, - requestStart: 0, - responseEnd: 5685.300000011921, - responseStart: 0, - secureConnectionStart: 0, - startTime: 5645.689999917522, - transferSize: 0, - workerStart: 0, - toJSON: () => {} - }; - stub(initializationService, 'isPerfInitialized').returns(true); - getIidStub.returns(IID); - SettingsService.getInstance().loggingEnabled = true; - SettingsService.getInstance().logNetworkAfterSampling = true; - // Calls logNetworkRequest under the hood. - createNetworkRequestEntry( - performanceController, - CC_NETWORK_PERFORMANCE_ENTRY - ); - clock.tick(1); - - expect(addToQueueStub).not.called; - }); - - // Performance SDK doesn't instrument requests sent from SDK itself, therefore blacklist - // requests sent to fl endpoint. - it('skips performance collection if domain is fl', () => { - const FL_NETWORK_PERFORMANCE_ENTRY: PerformanceResourceTiming = { - connectEnd: 0, - connectStart: 0, - decodedBodySize: 0, - domainLookupEnd: 0, - domainLookupStart: 0, - duration: 39.610000094398856, - encodedBodySize: 0, - entryType: 'resource', - fetchStart: 5645.689999917522, - initiatorType: 'fetch', - name: mergeStrings( - 'hts/frbslgigp.ogepscmv/ieo/eaylg', - 'tp:/ieaeogn-agolai.o/1frlglgc/o' - ), - nextHopProtocol: 'http/2+quic/43', - redirectEnd: 0, - redirectStart: 0, - requestStart: 0, - responseEnd: 5685.300000011921, - responseStart: 0, - secureConnectionStart: 0, - startTime: 5645.689999917522, - transferSize: 0, - workerStart: 0, - toJSON: () => {} - }; - stub(initializationService, 'isPerfInitialized').returns(true); - getIidStub.returns(IID); - SettingsService.getInstance().loggingEnabled = true; - SettingsService.getInstance().logNetworkAfterSampling = true; - // Calls logNetworkRequest under the hood. - createNetworkRequestEntry( - performanceController, - FL_NETWORK_PERFORMANCE_ENTRY - ); - clock.tick(1); - - expect(addToQueueStub).not.called; - }); - }); -}); diff --git a/packages-exp/performance-exp/src/services/perf_logger.ts b/packages-exp/performance-exp/src/services/perf_logger.ts deleted file mode 100644 index 3a2a65fdfb0..00000000000 --- a/packages-exp/performance-exp/src/services/perf_logger.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getIid } from './iid_service'; -import { NetworkRequest } from '../resources/network_request'; -import { Trace } from '../resources/trace'; -import { Api } from './api_service'; -import { SettingsService } from './settings_service'; -import { - getServiceWorkerStatus, - getVisibilityState, - VisibilityState, - getEffectiveConnectionType -} from '../utils/attributes_utils'; -import { - isPerfInitialized, - getInitializationPromise -} from './initialization_service'; -import { transportHandler } from './transport_service'; -import { SDK_VERSION } from '../constants'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import { getAppId } from '../utils/app_utils'; - -const enum ResourceType { - NetworkRequest, - Trace -} - -/* eslint-disable camelcase */ -interface ApplicationInfo { - google_app_id: string; - app_instance_id?: string; - web_app_info: WebAppInfo; - application_process_state: number; -} - -interface WebAppInfo { - sdk_version: string; - page_url: string; - service_worker_status: number; - visibility_state: number; - effective_connection_type: number; -} - -interface PerfNetworkLog { - application_info: ApplicationInfo; - network_request_metric: NetworkRequestMetric; -} - -interface PerfTraceLog { - application_info: ApplicationInfo; - trace_metric: TraceMetric; -} - -interface NetworkRequestMetric { - url: string; - http_method: number; - http_response_code: number; - response_payload_bytes?: number; - client_start_time_us?: number; - time_to_response_initiated_us?: number; - time_to_response_completed_us?: number; -} - -interface TraceMetric { - name: string; - is_auto: boolean; - client_start_time_us: number; - duration_us: number; - counters?: { [key: string]: number }; - custom_attributes?: { [key: string]: string }; -} - -/* eslint-enble camelcase */ - -let logger: ( - resource: NetworkRequest | Trace, - resourceType: ResourceType -) => void | undefined; -// This method is not called before initialization. -function sendLog( - resource: NetworkRequest | Trace, - resourceType: ResourceType -): void { - if (!logger) { - logger = transportHandler(serializer); - } - logger(resource, resourceType); -} - -export function logTrace(trace: Trace): void { - const settingsService = SettingsService.getInstance(); - // Do not log if trace is auto generated and instrumentation is disabled. - if (!settingsService.instrumentationEnabled && trace.isAuto) { - return; - } - // Do not log if trace is custom and data collection is disabled. - if (!settingsService.dataCollectionEnabled && !trace.isAuto) { - return; - } - // Do not log if required apis are not available. - if (!Api.getInstance().requiredApisAvailable()) { - return; - } - - // Only log the page load auto traces if page is visible. - if (trace.isAuto && getVisibilityState() !== VisibilityState.VISIBLE) { - return; - } - - if (isPerfInitialized()) { - sendTraceLog(trace); - } else { - // Custom traces can be used before the initialization but logging - // should wait until after. - getInitializationPromise(trace.performanceController).then( - () => sendTraceLog(trace), - () => sendTraceLog(trace) - ); - } -} - -function sendTraceLog(trace: Trace): void { - if (!getIid()) { - return; - } - - const settingsService = SettingsService.getInstance(); - if ( - !settingsService.loggingEnabled || - !settingsService.logTraceAfterSampling - ) { - return; - } - - setTimeout(() => sendLog(trace, ResourceType.Trace), 0); -} - -export function logNetworkRequest(networkRequest: NetworkRequest): void { - const settingsService = SettingsService.getInstance(); - // Do not log network requests if instrumentation is disabled. - if (!settingsService.instrumentationEnabled) { - return; - } - - // Do not log the js sdk's call to transport service domain to avoid unnecessary cycle. - // Need to blacklist both old and new endpoints to avoid migration gap. - const networkRequestUrl = networkRequest.url; - - // Blacklist old log endpoint and new transport endpoint. - // Because Performance SDK doesn't instrument requests sent from SDK itself. - const logEndpointUrl = settingsService.logEndPointUrl.split('?')[0]; - const flEndpointUrl = settingsService.flTransportEndpointUrl.split('?')[0]; - if ( - networkRequestUrl === logEndpointUrl || - networkRequestUrl === flEndpointUrl - ) { - return; - } - - if ( - !settingsService.loggingEnabled || - !settingsService.logNetworkAfterSampling - ) { - return; - } - - setTimeout(() => sendLog(networkRequest, ResourceType.NetworkRequest), 0); -} - -function serializer( - resource: NetworkRequest | Trace, - resourceType: ResourceType -): string { - if (resourceType === ResourceType.NetworkRequest) { - return serializeNetworkRequest(resource as NetworkRequest); - } - return serializeTrace(resource as Trace); -} - -function serializeNetworkRequest(networkRequest: NetworkRequest): string { - const networkRequestMetric: NetworkRequestMetric = { - url: networkRequest.url, - http_method: networkRequest.httpMethod || 0, - http_response_code: 200, - response_payload_bytes: networkRequest.responsePayloadBytes, - client_start_time_us: networkRequest.startTimeUs, - time_to_response_initiated_us: networkRequest.timeToResponseInitiatedUs, - time_to_response_completed_us: networkRequest.timeToResponseCompletedUs - }; - const perfMetric: PerfNetworkLog = { - application_info: getApplicationInfo( - networkRequest.performanceController.app - ), - network_request_metric: networkRequestMetric - }; - return JSON.stringify(perfMetric); -} - -function serializeTrace(trace: Trace): string { - const traceMetric: TraceMetric = { - name: trace.name, - is_auto: trace.isAuto, - client_start_time_us: trace.startTimeUs, - duration_us: trace.durationUs - }; - - if (Object.keys(trace.counters).length !== 0) { - traceMetric.counters = trace.counters; - } - const customAttributes = trace.getAttributes(); - if (Object.keys(customAttributes).length !== 0) { - traceMetric.custom_attributes = customAttributes; - } - - const perfMetric: PerfTraceLog = { - application_info: getApplicationInfo(trace.performanceController.app), - trace_metric: traceMetric - }; - return JSON.stringify(perfMetric); -} - -function getApplicationInfo(firebaseApp: FirebaseApp): ApplicationInfo { - return { - google_app_id: getAppId(firebaseApp), - app_instance_id: getIid(), - web_app_info: { - sdk_version: SDK_VERSION, - page_url: Api.getInstance().getUrl(), - service_worker_status: getServiceWorkerStatus(), - visibility_state: getVisibilityState(), - effective_connection_type: getEffectiveConnectionType() - }, - application_process_state: 0 - }; -} diff --git a/packages-exp/performance-exp/src/services/remote_config_service.test.ts b/packages-exp/performance-exp/src/services/remote_config_service.test.ts deleted file mode 100644 index ddd1e3a3ecb..00000000000 --- a/packages-exp/performance-exp/src/services/remote_config_service.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { stub, useFakeTimers, SinonFakeTimers, SinonStub } from 'sinon'; -import { expect } from 'chai'; -import { SettingsService } from './settings_service'; -import { CONFIG_EXPIRY_LOCAL_STORAGE_KEY } from '../constants'; -import { setupApi, Api } from './api_service'; -import * as iidService from './iid_service'; -import { getConfig } from './remote_config_service'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import '../../test/setup'; -import { FirebaseInstallations } from '@firebase/installations-types'; -import { PerformanceController } from '../controllers/perf'; - -describe('Performance Monitoring > remote_config_service', () => { - const IID = 'asd123'; - const AUTH_TOKEN = 'auth_token'; - const LOG_URL = 'https://firebaselogging.test.com'; - const TRANSPORT_KEY = 'pseudo-transport-key'; - const LOG_SOURCE = 2; - const NETWORK_SAMPLIG_RATE = 0.25; - const TRACE_SAMPLING_RATE = 0.5; - const GLOBAL_CLOCK_NOW = 1556524895326; - const STRINGIFIED_CONFIG = `{"entries":{"fpr_enabled":"true",\ - "fpr_log_endpoint_url":"https://firebaselogging.test.com",\ - "fpr_log_transport_key":"pseudo-transport-key",\ - "fpr_log_source":"2","fpr_vc_network_request_sampling_rate":"0.250000",\ - "fpr_vc_session_sampling_rate":"0.250000","fpr_vc_trace_sampling_rate":"0.500000"},\ - "state":"UPDATE"}`; - const PROJECT_ID = 'project1'; - const APP_ID = '1:23r:web:fewq'; - const API_KEY = 'asdfghjk'; - const NOT_VALID_CONFIG = 'not a valid config and should not be used'; - - let clock: SinonFakeTimers; - - setupApi(self); - const ApiInstance = Api.getInstance(); - - const fakeFirebaseApp = ({ - options: { projectId: PROJECT_ID, appId: APP_ID, apiKey: API_KEY } - } as unknown) as FirebaseApp; - - const fakeInstallations = ({} as unknown) as FirebaseInstallations; - const performanceController = new PerformanceController( - fakeFirebaseApp, - fakeInstallations - ); - - function storageGetItemFakeFactory( - expiry: string, - config: string - ): (key: string) => string { - return (key: string) => { - if (key === CONFIG_EXPIRY_LOCAL_STORAGE_KEY) { - return expiry; - } - return config; - }; - } - - function resetSettingsService(): void { - const settingsService = SettingsService.getInstance(); - settingsService.logSource = 462; - settingsService.loggingEnabled = false; - settingsService.networkRequestsSamplingRate = 1; - settingsService.tracesSamplingRate = 1; - } - - // parameterized beforeEach. Should be called at beginning of each test. - function setup( - storageConfig: { expiry: string; config: string }, - fetchConfig?: { reject: boolean; value?: Response } - ): { - storageGetItemStub: SinonStub<[string], string | null>; - fetchStub: SinonStub<[RequestInfo, RequestInit?], Promise>; - } { - const fetchStub = stub(self, 'fetch'); - - if (fetchConfig) { - fetchConfig.reject - ? fetchStub.rejects() - : fetchStub.resolves(fetchConfig.value); - } - - stub(iidService, 'getAuthTokenPromise').returns( - Promise.resolve(AUTH_TOKEN) - ); - - clock = useFakeTimers(GLOBAL_CLOCK_NOW); - - // we need to stub the entire localStorage, because storage can't be stubbed in Firefox and IE. - // stubbing on self(window) seems to only work the first time (at least in Firefox), the subsequent - // tests will have the same stub. stub.reset() in afterEach doesn't help either. As a result, we stub on ApiInstance. - // https://github.com/sinonjs/sinon/issues/662 - const storageStub = stub(ApiInstance, 'localStorage'); - const getItemStub: SinonStub<[string], string | null> = stub(); - - storageStub.value({ - getItem: getItemStub.callsFake( - storageGetItemFakeFactory(storageConfig.expiry, storageConfig.config) - ), - setItem: () => {} - }); - - return { storageGetItemStub: getItemStub, fetchStub }; - } - - afterEach(() => { - resetSettingsService(); - clock.restore(); - }); - - describe('getConfig', () => { - it('gets the config from the local storage if available and valid', async () => { - // After global clock. Config not expired. - const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895330'; - const { storageGetItemStub: getItemStub } = setup({ - expiry: EXPIRY_LOCAL_STORAGE_VALUE, - config: STRINGIFIED_CONFIG - }); - - await getConfig(performanceController, IID); - - expect(getItemStub).to.be.called; - expect(SettingsService.getInstance().loggingEnabled).to.be.true; - expect(SettingsService.getInstance().logEndPointUrl).to.equal(LOG_URL); - expect(SettingsService.getInstance().transportKey).to.equal( - TRANSPORT_KEY - ); - expect(SettingsService.getInstance().logSource).to.equal(LOG_SOURCE); - expect( - SettingsService.getInstance().networkRequestsSamplingRate - ).to.equal(NETWORK_SAMPLIG_RATE); - expect(SettingsService.getInstance().tracesSamplingRate).to.equal( - TRACE_SAMPLING_RATE - ); - }); - - it('does not call remote config if a valid config is in local storage', async () => { - // After global clock. Config not expired. - const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895330'; - - const { fetchStub } = setup({ - expiry: EXPIRY_LOCAL_STORAGE_VALUE, - config: STRINGIFIED_CONFIG - }); - - await getConfig(performanceController, IID); - - expect(fetchStub).not.to.be.called; - }); - - it('gets the config from RC if local version is not valid', async () => { - // Expired local config. - const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895320'; - - const { storageGetItemStub: getItemStub } = setup( - { expiry: EXPIRY_LOCAL_STORAGE_VALUE, config: STRINGIFIED_CONFIG }, - { reject: false, value: new Response(STRINGIFIED_CONFIG) } - ); - - await getConfig(performanceController, IID); - - expect(getItemStub).to.be.calledOnce; - expect(SettingsService.getInstance().loggingEnabled).to.be.true; - expect(SettingsService.getInstance().logEndPointUrl).to.equal(LOG_URL); - expect(SettingsService.getInstance().transportKey).to.equal( - TRANSPORT_KEY - ); - expect(SettingsService.getInstance().logSource).to.equal(LOG_SOURCE); - expect( - SettingsService.getInstance().networkRequestsSamplingRate - ).to.equal(NETWORK_SAMPLIG_RATE); - expect(SettingsService.getInstance().tracesSamplingRate).to.equal( - TRACE_SAMPLING_RATE - ); - }); - - it('does not change the default config if call to RC fails', async () => { - // Expired local config. - const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895320'; - - setup( - { - expiry: EXPIRY_LOCAL_STORAGE_VALUE, - config: NOT_VALID_CONFIG - }, - { reject: true } - ); - - await getConfig(performanceController, IID); - - expect(SettingsService.getInstance().loggingEnabled).to.equal(false); - }); - - it('uses secondary configs if the response does not have all the fields', async () => { - // Expired local config. - const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895320'; - const STRINGIFIED_PARTIAL_CONFIG = `{"entries":{\ - "fpr_vc_network_request_sampling_rate":"0.250000",\ - "fpr_vc_session_sampling_rate":"0.250000","fpr_vc_trace_sampling_rate":"0.500000"},\ - "state":"UPDATE"}`; - - setup( - { - expiry: EXPIRY_LOCAL_STORAGE_VALUE, - config: NOT_VALID_CONFIG - }, - { reject: false, value: new Response(STRINGIFIED_PARTIAL_CONFIG) } - ); - - await getConfig(performanceController, IID); - - expect(SettingsService.getInstance().loggingEnabled).to.be.true; - }); - - it('uses secondary configs if the response does not have any fields', async () => { - // Expired local config. - const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895320'; - const STRINGIFIED_PARTIAL_CONFIG = '{"state":"NO_TEMPLATE"}'; - - setup( - { - expiry: EXPIRY_LOCAL_STORAGE_VALUE, - config: NOT_VALID_CONFIG - }, - { reject: false, value: new Response(STRINGIFIED_PARTIAL_CONFIG) } - ); - await getConfig(performanceController, IID); - - expect(SettingsService.getInstance().loggingEnabled).to.be.true; - }); - - it('gets the config from RC even with deprecated transport flag', async () => { - // Expired local config. - const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895320'; - const STRINGIFIED_CUSTOM_CONFIG = `{"entries":{\ - "fpr_vc_network_request_sampling_rate":"0.250000",\ - "fpr_log_transport_web_percent":"100.0",\ - "fpr_vc_session_sampling_rate":"0.250000","fpr_vc_trace_sampling_rate":"0.500000"},\ - "state":"UPDATE"}`; - - const { storageGetItemStub: getItemStub } = setup( - { - expiry: EXPIRY_LOCAL_STORAGE_VALUE, - config: STRINGIFIED_CUSTOM_CONFIG - }, - { reject: false, value: new Response(STRINGIFIED_CONFIG) } - ); - - await getConfig(performanceController, IID); - - expect(getItemStub).to.be.calledOnce; - expect(SettingsService.getInstance().loggingEnabled).to.be.true; - expect(SettingsService.getInstance().logEndPointUrl).to.equal(LOG_URL); - expect(SettingsService.getInstance().transportKey).to.equal( - TRANSPORT_KEY - ); - expect(SettingsService.getInstance().logSource).to.equal(LOG_SOURCE); - expect( - SettingsService.getInstance().networkRequestsSamplingRate - ).to.equal(NETWORK_SAMPLIG_RATE); - expect(SettingsService.getInstance().tracesSamplingRate).to.equal( - TRACE_SAMPLING_RATE - ); - }); - }); -}); diff --git a/packages-exp/performance-exp/src/services/remote_config_service.ts b/packages-exp/performance-exp/src/services/remote_config_service.ts deleted file mode 100644 index 558e995e9d6..00000000000 --- a/packages-exp/performance-exp/src/services/remote_config_service.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - CONFIG_EXPIRY_LOCAL_STORAGE_KEY, - CONFIG_LOCAL_STORAGE_KEY, - SDK_VERSION -} from '../constants'; -import { consoleLogger } from '../utils/console_logger'; -import { ERROR_FACTORY, ErrorCode } from '../utils/errors'; - -import { Api } from './api_service'; -import { getAuthTokenPromise } from './iid_service'; -import { SettingsService } from './settings_service'; -import { PerformanceController } from '../controllers/perf'; -import { getProjectId, getApiKey, getAppId } from '../utils/app_utils'; - -const REMOTE_CONFIG_SDK_VERSION = '0.0.1'; - -interface SecondaryConfig { - loggingEnabled?: boolean; - logSource?: number; - logEndPointUrl?: string; - transportKey?: string; - tracesSamplingRate?: number; - networkRequestsSamplingRate?: number; -} - -// These values will be used if the remote config object is successfully -// retrieved, but the template does not have these fields. -const DEFAULT_CONFIGS: SecondaryConfig = { - loggingEnabled: true -}; - -/* eslint-disable camelcase */ -interface RemoteConfigTemplate { - fpr_enabled?: string; - fpr_log_source?: string; - fpr_log_endpoint_url?: string; - fpr_log_transport_key?: string; - fpr_log_transport_web_percent?: string; - fpr_vc_network_request_sampling_rate?: string; - fpr_vc_trace_sampling_rate?: string; - fpr_vc_session_sampling_rate?: string; -} -/* eslint-enable camelcase */ - -interface RemoteConfigResponse { - entries?: RemoteConfigTemplate; - state?: string; -} - -const FIS_AUTH_PREFIX = 'FIREBASE_INSTALLATIONS_AUTH'; - -export function getConfig( - performanceController: PerformanceController, - iid: string -): Promise { - const config = getStoredConfig(); - if (config) { - processConfig(config); - return Promise.resolve(); - } - - return getRemoteConfig(performanceController, iid) - .then(processConfig) - .then( - config => storeConfig(config), - /** Do nothing for error, use defaults set in settings service. */ - () => {} - ); -} - -function getStoredConfig(): RemoteConfigResponse | undefined { - const localStorage = Api.getInstance().localStorage; - if (!localStorage) { - return; - } - const expiryString = localStorage.getItem(CONFIG_EXPIRY_LOCAL_STORAGE_KEY); - if (!expiryString || !configValid(expiryString)) { - return; - } - - const configStringified = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY); - if (!configStringified) { - return; - } - try { - const configResponse: RemoteConfigResponse = JSON.parse(configStringified); - return configResponse; - } catch { - return; - } -} - -function storeConfig(config: RemoteConfigResponse | undefined): void { - const localStorage = Api.getInstance().localStorage; - if (!config || !localStorage) { - return; - } - - localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)); - localStorage.setItem( - CONFIG_EXPIRY_LOCAL_STORAGE_KEY, - String( - Date.now() + - SettingsService.getInstance().configTimeToLive * 60 * 60 * 1000 - ) - ); -} - -const COULD_NOT_GET_CONFIG_MSG = - 'Could not fetch config, will use default configs'; - -function getRemoteConfig( - performanceController: PerformanceController, - iid: string -): Promise { - // Perf needs auth token only to retrieve remote config. - return getAuthTokenPromise(performanceController.installations) - .then(authToken => { - const projectId = getProjectId(performanceController.app); - const apiKey = getApiKey(performanceController.app); - const configEndPoint = `https://firebaseremoteconfig.googleapis.com/v1/projects/${projectId}/namespaces/fireperf:fetch?key=${apiKey}`; - const request = new Request(configEndPoint, { - method: 'POST', - headers: { Authorization: `${FIS_AUTH_PREFIX} ${authToken}` }, - /* eslint-disable camelcase */ - body: JSON.stringify({ - app_instance_id: iid, - app_instance_id_token: authToken, - app_id: getAppId(performanceController.app), - app_version: SDK_VERSION, - sdk_version: REMOTE_CONFIG_SDK_VERSION - }) - /* eslint-enable camelcase */ - }); - return fetch(request).then(response => { - if (response.ok) { - return response.json() as RemoteConfigResponse; - } - // In case response is not ok. This will be caught by catch. - throw ERROR_FACTORY.create(ErrorCode.RC_NOT_OK); - }); - }) - .catch(() => { - consoleLogger.info(COULD_NOT_GET_CONFIG_MSG); - return undefined; - }); -} - -/** - * Processes config coming either from calling RC or from local storage. - * This method only runs if call is successful or config in storage - * is valid. - */ -function processConfig( - config?: RemoteConfigResponse -): RemoteConfigResponse | undefined { - if (!config) { - return config; - } - const settingsServiceInstance = SettingsService.getInstance(); - const entries = config.entries || {}; - if (entries.fpr_enabled !== undefined) { - // TODO: Change the assignment of loggingEnabled once the received type is - // known. - settingsServiceInstance.loggingEnabled = - String(entries.fpr_enabled) === 'true'; - } else if (DEFAULT_CONFIGS.loggingEnabled !== undefined) { - // Config retrieved successfully, but there is no fpr_enabled in template. - // Use secondary configs value. - settingsServiceInstance.loggingEnabled = DEFAULT_CONFIGS.loggingEnabled; - } - if (entries.fpr_log_source) { - settingsServiceInstance.logSource = Number(entries.fpr_log_source); - } else if (DEFAULT_CONFIGS.logSource) { - settingsServiceInstance.logSource = DEFAULT_CONFIGS.logSource; - } - - if (entries.fpr_log_endpoint_url) { - settingsServiceInstance.logEndPointUrl = entries.fpr_log_endpoint_url; - } else if (DEFAULT_CONFIGS.logEndPointUrl) { - settingsServiceInstance.logEndPointUrl = DEFAULT_CONFIGS.logEndPointUrl; - } - - // Key from Remote Config has to be non-empty string, otherwsie use local value. - if (entries.fpr_log_transport_key) { - settingsServiceInstance.transportKey = entries.fpr_log_transport_key; - } else if (DEFAULT_CONFIGS.transportKey) { - settingsServiceInstance.transportKey = DEFAULT_CONFIGS.transportKey; - } - - if (entries.fpr_vc_network_request_sampling_rate !== undefined) { - settingsServiceInstance.networkRequestsSamplingRate = Number( - entries.fpr_vc_network_request_sampling_rate - ); - } else if (DEFAULT_CONFIGS.networkRequestsSamplingRate !== undefined) { - settingsServiceInstance.networkRequestsSamplingRate = - DEFAULT_CONFIGS.networkRequestsSamplingRate; - } - if (entries.fpr_vc_trace_sampling_rate !== undefined) { - settingsServiceInstance.tracesSamplingRate = Number( - entries.fpr_vc_trace_sampling_rate - ); - } else if (DEFAULT_CONFIGS.tracesSamplingRate !== undefined) { - settingsServiceInstance.tracesSamplingRate = - DEFAULT_CONFIGS.tracesSamplingRate; - } - // Set the per session trace and network logging flags. - settingsServiceInstance.logTraceAfterSampling = shouldLogAfterSampling( - settingsServiceInstance.tracesSamplingRate - ); - settingsServiceInstance.logNetworkAfterSampling = shouldLogAfterSampling( - settingsServiceInstance.networkRequestsSamplingRate - ); - return config; -} - -function configValid(expiry: string): boolean { - return Number(expiry) > Date.now(); -} - -function shouldLogAfterSampling(samplingRate: number): boolean { - return Math.random() <= samplingRate; -} diff --git a/packages-exp/performance-exp/src/services/settings_service.ts b/packages-exp/performance-exp/src/services/settings_service.ts deleted file mode 100644 index 83e08bd53d5..00000000000 --- a/packages-exp/performance-exp/src/services/settings_service.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { mergeStrings } from '../utils/string_merger'; - -let settingsServiceInstance: SettingsService | undefined; - -export class SettingsService { - // The variable which controls logging of automatic traces and HTTP/S network monitoring. - instrumentationEnabled = true; - - // The variable which controls logging of custom traces. - dataCollectionEnabled = true; - - // Configuration flags set through remote config. - loggingEnabled = false; - // Sampling rate between 0 and 1. - tracesSamplingRate = 1; - networkRequestsSamplingRate = 1; - - // Address of logging service. - logEndPointUrl = - 'https://firebaselogging.googleapis.com/v0cc/log?format=json_proto'; - // Performance event transport endpoint URL which should be compatible with proto3. - // New Address for transport service, not configurable via Remote Config. - flTransportEndpointUrl = mergeStrings( - 'hts/frbslgigp.ogepscmv/ieo/eaylg', - 'tp:/ieaeogn-agolai.o/1frlglgc/o' - ); - - transportKey = mergeStrings('AzSC8r6ReiGqFMyfvgow', 'Iayx0u-XT3vksVM-pIV'); - - // Source type for performance event logs. - logSource = 462; - - // Flags which control per session logging of traces and network requests. - logTraceAfterSampling = false; - logNetworkAfterSampling = false; - - // TTL of config retrieved from remote config in hours. - configTimeToLive = 12; - - getFlTransportFullUrl(): string { - return this.flTransportEndpointUrl.concat('?key=', this.transportKey); - } - - static getInstance(): SettingsService { - if (settingsServiceInstance === undefined) { - settingsServiceInstance = new SettingsService(); - } - return settingsServiceInstance; - } -} diff --git a/packages-exp/performance-exp/src/services/transport_service.test.ts b/packages-exp/performance-exp/src/services/transport_service.test.ts deleted file mode 100644 index 43986e00817..00000000000 --- a/packages-exp/performance-exp/src/services/transport_service.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { stub, useFakeTimers, SinonStub, SinonFakeTimers, match } from 'sinon'; -import { use, expect } from 'chai'; -import * as sinonChai from 'sinon-chai'; -import { - transportHandler, - setupTransportService, - resetTransportService -} from './transport_service'; -import { SettingsService } from './settings_service'; - -use(sinonChai); - -describe('Firebase Performance > transport_service', () => { - let fetchStub: SinonStub<[RequestInfo, RequestInit?], Promise>; - const INITIAL_SEND_TIME_DELAY_MS = 5.5 * 1000; - const DEFAULT_SEND_INTERVAL_MS = 10 * 1000; - const MAX_EVENT_COUNT_PER_REQUEST = 1000; - const TRANSPORT_DELAY_INTERVAL = 10000; - // Starts date at timestamp 1 instead of 0, otherwise it causes validation errors. - let clock: SinonFakeTimers; - const testTransportHandler = transportHandler((...args) => { - return args[0]; - }); - - beforeEach(() => { - fetchStub = stub(window, 'fetch'); - clock = useFakeTimers(1); - setupTransportService(); - }); - - afterEach(() => { - fetchStub.restore(); - clock.restore(); - resetTransportService(); - }); - - it('throws an error when logging an empty message', () => { - expect(() => { - testTransportHandler(''); - }).to.throw; - }); - - it('does not attempt to log an event after INITIAL_SEND_TIME_DELAY_MS if queue is empty', () => { - fetchStub.resolves( - new Response('', { - status: 200, - headers: { 'Content-type': 'application/json' } - }) - ); - - clock.tick(INITIAL_SEND_TIME_DELAY_MS); - expect(fetchStub).to.not.have.been.called; - }); - - it('attempts to log an event after DEFAULT_SEND_INTERVAL_MS if queue not empty', async () => { - fetchStub.resolves( - new Response('', { - status: 200, - headers: { 'Content-type': 'application/json' } - }) - ); - - clock.tick(INITIAL_SEND_TIME_DELAY_MS); - testTransportHandler('someEvent'); - clock.tick(DEFAULT_SEND_INTERVAL_MS); - expect(fetchStub).to.have.been.calledOnce; - }); - - it('successful send a meesage to transport', () => { - const setting = SettingsService.getInstance(); - const flTransportFullUrl = - setting.flTransportEndpointUrl + '?key=' + setting.transportKey; - fetchStub.withArgs(flTransportFullUrl, match.any).resolves( - // DELETE_REQUEST means event dispatch is successful. - generateSuccessResponse() - ); - - testTransportHandler('event1'); - clock.tick(INITIAL_SEND_TIME_DELAY_MS); - expect(fetchStub).to.have.been.calledOnce; - }); - - it('sends up to the maximum event limit in one request', async () => { - // Arrange - const setting = SettingsService.getInstance(); - const flTransportFullUrl = - setting.flTransportEndpointUrl + '?key=' + setting.transportKey; - - // Returns successful response from fl for logRequests. - const response = generateSuccessResponse(); - stub(response, 'json').resolves(JSON.parse(generateSuccessResponseBody())); - fetchStub.resolves(response); - - // Act - // Generate 1020 events, which should be dispatched in two batches (1000 events and 20 events). - for (let i = 0; i < 1020; i++) { - testTransportHandler('event' + i); - } - // Wait for first and second event dispatch to happen. - clock.tick(INITIAL_SEND_TIME_DELAY_MS); - // This is to resolve the floating promise chain in transport service. - await Promise.resolve().then().then().then(); - clock.tick(DEFAULT_SEND_INTERVAL_MS); - - // Assert - // Expects the first logRequest which contains first 1000 events. - const firstLogRequest = generateLogRequest('5501'); - for (let i = 0; i < MAX_EVENT_COUNT_PER_REQUEST; i++) { - firstLogRequest['log_event'].push({ - 'source_extension_json_proto3': 'event' + i, - 'event_time_ms': '1' - }); - } - expect(fetchStub).which.to.have.been.calledWith(flTransportFullUrl, { - method: 'POST', - body: JSON.stringify(firstLogRequest) - }); - // Expects the second logRequest which contains remaining 20 events; - const secondLogRequest = generateLogRequest('15501'); - for (let i = 0; i < 20; i++) { - secondLogRequest['log_event'].push({ - 'source_extension_json_proto3': - 'event' + (MAX_EVENT_COUNT_PER_REQUEST + i), - 'event_time_ms': '1' - }); - } - expect(fetchStub).calledWith(flTransportFullUrl, { - method: 'POST', - body: JSON.stringify(secondLogRequest) - }); - }); - - function generateLogRequest(requestTimeMs: string): any { - return { - 'request_time_ms': requestTimeMs, - 'client_info': { - 'client_type': 1, - 'js_client_info': {} - }, - 'log_source': 462, - 'log_event': [] as any - }; - } - - function generateSuccessResponse(): Response { - return new Response(generateSuccessResponseBody(), { - status: 200, - headers: { 'Content-type': 'application/json' } - }); - } - - function generateSuccessResponseBody(): string { - return ( - '{\ - "nextRequestWaitMillis": "' + - TRANSPORT_DELAY_INTERVAL + - '",\ - "logResponseDetails": [\ - {\ - "responseAction": "DELETE_REQUEST"\ - }\ - ]\ - }' - ); - } -}); diff --git a/packages-exp/performance-exp/src/services/transport_service.ts b/packages-exp/performance-exp/src/services/transport_service.ts deleted file mode 100644 index 87f8efab773..00000000000 --- a/packages-exp/performance-exp/src/services/transport_service.ts +++ /dev/null @@ -1,192 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SettingsService } from './settings_service'; -import { ERROR_FACTORY, ErrorCode } from '../utils/errors'; -import { consoleLogger } from '../utils/console_logger'; - -const DEFAULT_SEND_INTERVAL_MS = 10 * 1000; -const INITIAL_SEND_TIME_DELAY_MS = 5.5 * 1000; -// If end point does not work, the call will be tried for these many times. -const DEFAULT_REMAINING_TRIES = 3; -const MAX_EVENT_COUNT_PER_REQUEST = 1000; -let remainingTries = DEFAULT_REMAINING_TRIES; - -interface LogResponseDetails { - responseAction?: string; -} - -interface BatchEvent { - message: string; - eventTime: number; -} - -/* eslint-disable camelcase */ -// CC/Fl accepted log format. -interface TransportBatchLogFormat { - request_time_ms: string; - client_info: ClientInfo; - log_source: number; - log_event: Log[]; -} - -interface ClientInfo { - client_type: number; - js_client_info: {}; -} - -interface Log { - source_extension_json_proto3: string; - event_time_ms: string; -} -/* eslint-enable camelcase */ - -let queue: BatchEvent[] = []; - -let isTransportSetup: boolean = false; - -export function setupTransportService(): void { - if (!isTransportSetup) { - processQueue(INITIAL_SEND_TIME_DELAY_MS); - isTransportSetup = true; - } -} - -/** - * Utilized by testing to clean up message queue and un-initialize transport service. - */ -export function resetTransportService(): void { - isTransportSetup = false; - queue = []; -} - -function processQueue(timeOffset: number): void { - setTimeout(() => { - // If there is no remainingTries left, stop retrying. - if (remainingTries === 0) { - return; - } - - // If there are no events to process, wait for DEFAULT_SEND_INTERVAL_MS and try again. - if (!queue.length) { - return processQueue(DEFAULT_SEND_INTERVAL_MS); - } - - dispatchQueueEvents(); - }, timeOffset); -} - -function dispatchQueueEvents(): void { - // Extract events up to the maximum cap of single logRequest from top of "official queue". - // The staged events will be used for current logRequest attempt, remaining events will be kept - // for next attempt. - const staged = queue.splice(0, MAX_EVENT_COUNT_PER_REQUEST); - - /* eslint-disable camelcase */ - // We will pass the JSON serialized event to the backend. - const log_event: Log[] = staged.map(evt => ({ - source_extension_json_proto3: evt.message, - event_time_ms: String(evt.eventTime) - })); - - const data: TransportBatchLogFormat = { - request_time_ms: String(Date.now()), - client_info: { - client_type: 1, // 1 is JS - js_client_info: {} - }, - log_source: SettingsService.getInstance().logSource, - log_event - }; - /* eslint-enable camelcase */ - - sendEventsToFl(data, staged).catch(() => { - // If the request fails for some reason, add the events that were attempted - // back to the primary queue to retry later. - queue = [...staged, ...queue]; - remainingTries--; - consoleLogger.info(`Tries left: ${remainingTries}.`); - processQueue(DEFAULT_SEND_INTERVAL_MS); - }); -} - -function sendEventsToFl( - data: TransportBatchLogFormat, - staged: BatchEvent[] -): Promise { - return postToFlEndpoint(data) - .then(res => { - if (!res.ok) { - consoleLogger.info('Call to Firebase backend failed.'); - } - return res.json(); - }) - .then(res => { - // Find the next call wait time from the response. - const transportWait = Number(res.nextRequestWaitMillis); - let requestOffset = DEFAULT_SEND_INTERVAL_MS; - if (!isNaN(transportWait)) { - requestOffset = Math.max(transportWait, requestOffset); - } - - // Delete request if response include RESPONSE_ACTION_UNKNOWN or DELETE_REQUEST action. - // Otherwise, retry request using normal scheduling if response include RETRY_REQUEST_LATER. - const logResponseDetails: LogResponseDetails[] = res.logResponseDetails; - if ( - Array.isArray(logResponseDetails) && - logResponseDetails.length > 0 && - logResponseDetails[0].responseAction === 'RETRY_REQUEST_LATER' - ) { - queue = [...staged, ...queue]; - consoleLogger.info(`Retry transport request later.`); - } - - remainingTries = DEFAULT_REMAINING_TRIES; - // Schedule the next process. - processQueue(requestOffset); - }); -} - -function postToFlEndpoint(data: TransportBatchLogFormat): Promise { - const flTransportFullUrl = SettingsService.getInstance().getFlTransportFullUrl(); - return fetch(flTransportFullUrl, { - method: 'POST', - body: JSON.stringify(data) - }); -} - -function addToQueue(evt: BatchEvent): void { - if (!evt.eventTime || !evt.message) { - throw ERROR_FACTORY.create(ErrorCode.INVALID_CC_LOG); - } - // Add the new event to the queue. - queue = [...queue, evt]; -} - -/** Log handler for cc service to send the performance logs to the server. */ -export function transportHandler( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - serializer: (...args: any[]) => string -): (...args: unknown[]) => void { - return (...args) => { - const message = serializer(...args); - addToQueue({ - message, - eventTime: Date.now() - }); - }; -} diff --git a/packages-exp/performance-exp/src/utils/attribute_utils.test.ts b/packages-exp/performance-exp/src/utils/attribute_utils.test.ts deleted file mode 100644 index bda20b3e165..00000000000 --- a/packages-exp/performance-exp/src/utils/attribute_utils.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF unknown KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { restore, stub } from 'sinon'; -import { expect } from 'chai'; -import { Api } from '../services/api_service'; - -import { - getVisibilityState, - VisibilityState, - getServiceWorkerStatus, - getEffectiveConnectionType, - isValidCustomAttributeName, - isValidCustomAttributeValue -} from './attributes_utils'; - -import '../../test/setup'; - -describe('Firebase Performance > attribute_utils', () => { - describe('#getServiceWorkerStatus', () => { - it('returns unsupported when service workers unsupported', () => { - stub(Api, 'getInstance').returns(({ - navigator: {} - } as unknown) as Api); - - expect(getServiceWorkerStatus()).to.be.eql(1); - }); - - it('returns controlled when service workers controlled', () => { - stub(Api, 'getInstance').returns(({ - navigator: { - serviceWorker: { - controller: {} - } - } - } as unknown) as Api); - - expect(getServiceWorkerStatus()).to.be.eql(2); - }); - - it('returns uncontrolled when service workers uncontrolled', () => { - stub(Api, 'getInstance').returns(({ - navigator: { - serviceWorker: {} - } - } as unknown) as Api); - - expect(getServiceWorkerStatus()).to.be.eql(3); - }); - }); - - describe('#getVisibilityState', () => { - afterEach(() => { - restore(); - }); - - it('returns visible when document is visible', () => { - stub(Api, 'getInstance').returns(({ - document: { - visibilityState: 'visible' - } - } as unknown) as Api); - expect(getVisibilityState()).to.be.eql(VisibilityState.VISIBLE); - }); - - it('returns hidden when document is hidden', () => { - stub(Api, 'getInstance').returns(({ - document: { - visibilityState: 'hidden' - } - } as unknown) as Api); - expect(getVisibilityState()).to.be.eql(VisibilityState.HIDDEN); - }); - - it('returns unknown when document is unknown', () => { - stub(Api, 'getInstance').returns(({ - document: { - visibilityState: 'unknown' - } - } as unknown) as Api); - expect(getVisibilityState()).to.be.eql(VisibilityState.UNKNOWN); - }); - }); - - describe('#getEffectiveConnectionType', () => { - afterEach(() => { - restore(); - }); - - it('returns EffectiveConnectionType.CONNECTION_SLOW_2G when slow-2g', () => { - stub(Api, 'getInstance').returns(({ - navigator: { - connection: { - effectiveType: 'slow-2g' - } - } - } as unknown) as Api); - expect(getEffectiveConnectionType()).to.be.eql(1); - }); - - it('returns EffectiveConnectionType.CONNECTION_2G when 2g', () => { - stub(Api, 'getInstance').returns(({ - navigator: { - connection: { - effectiveType: '2g' - } - } - } as unknown) as Api); - expect(getEffectiveConnectionType()).to.be.eql(2); - }); - - it('returns EffectiveConnectionType.CONNECTION_3G when 3g', () => { - stub(Api, 'getInstance').returns(({ - navigator: { - connection: { - effectiveType: '3g' - } - } - } as unknown) as Api); - expect(getEffectiveConnectionType()).to.be.eql(3); - }); - - it('returns EffectiveConnectionType.CONNECTION_4G when 4g', () => { - stub(Api, 'getInstance').returns(({ - navigator: { - connection: { - effectiveType: '4g' - } - } - } as unknown) as Api); - expect(getEffectiveConnectionType()).to.be.eql(4); - }); - - it('returns EffectiveConnectionType.UNKNOWN when unknown connection type', () => { - stub(Api, 'getInstance').returns(({ - navigator: { - connection: { - effectiveType: '5g' - } - } - } as unknown) as Api); - expect(getEffectiveConnectionType()).to.be.eql(0); - }); - - it('returns EffectiveConnectionType.UNKNOWN when no effective type', () => { - stub(Api, 'getInstance').returns(({ - navigator: { - connection: {} - } - } as unknown) as Api); - expect(getEffectiveConnectionType()).to.be.eql(0); - }); - }); - - describe('#isValidCustomAttributeName', () => { - it('returns true when name is valid', () => { - expect(isValidCustomAttributeName('validCustom_Attribute_Name')).to.be - .true; - }); - - it('returns false when name is blank', () => { - expect(isValidCustomAttributeName('')).to.be.false; - }); - - it('returns false when name is too long', () => { - expect( - isValidCustomAttributeName('invalid_custom_name_over_forty_characters') - ).to.be.false; - }); - - it('returns false when name starts with a reserved prefix', () => { - expect(isValidCustomAttributeName('firebase_invalidCustomName')).to.be - .false; - }); - - it('returns false when name does not begin with a letter', () => { - expect(isValidCustomAttributeName('_invalidCustomName')).to.be.false; - }); - - it('returns false when name contains prohibited characters', () => { - expect(isValidCustomAttributeName('invalidCustomName&')).to.be.false; - }); - }); - - describe('#isValidCustomAttributeValue', () => { - it('returns true when value is valid', () => { - expect(isValidCustomAttributeValue('valid_attribute_value')).to.be.true; - }); - - it('returns false when value is blank', () => { - expect(isValidCustomAttributeValue('')).to.be.false; - }); - - it('returns false when value is too long', () => { - const longAttributeValue = - 'too_long_attribute_value_over_one_hundred_characters_too_long_attribute_value_over_one_' + - 'hundred_charac'; - expect(isValidCustomAttributeValue(longAttributeValue)).to.be.false; - }); - }); -}); diff --git a/packages-exp/performance-exp/src/utils/attributes_utils.ts b/packages-exp/performance-exp/src/utils/attributes_utils.ts deleted file mode 100644 index ef3f499e23b..00000000000 --- a/packages-exp/performance-exp/src/utils/attributes_utils.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Api } from '../services/api_service'; - -// The values and orders of the following enums should not be changed. -const enum ServiceWorkerStatus { - UNKNOWN = 0, - UNSUPPORTED = 1, - CONTROLLED = 2, - UNCONTROLLED = 3 -} - -export enum VisibilityState { - UNKNOWN = 0, - VISIBLE = 1, - HIDDEN = 2 -} - -const enum EffectiveConnectionType { - UNKNOWN = 0, - CONNECTION_SLOW_2G = 1, - CONNECTION_2G = 2, - CONNECTION_3G = 3, - CONNECTION_4G = 4 -} - -/** - * NetworkInformation - * - * ref: https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation - */ -interface NetworkInformation { - readonly effectiveType?: 'slow-2g' | '2g' | '3g' | '4g'; -} - -interface NavigatorWithConnection extends Navigator { - readonly connection: NetworkInformation; -} - -const RESERVED_ATTRIBUTE_PREFIXES = ['firebase_', 'google_', 'ga_']; -const ATTRIBUTE_FORMAT_REGEX = new RegExp('^[a-zA-Z]\\w*$'); -const MAX_ATTRIBUTE_NAME_LENGTH = 40; -const MAX_ATTRIBUTE_VALUE_LENGTH = 100; - -export function getServiceWorkerStatus(): ServiceWorkerStatus { - const navigator = Api.getInstance().navigator; - if ('serviceWorker' in navigator) { - if (navigator.serviceWorker.controller) { - return ServiceWorkerStatus.CONTROLLED; - } else { - return ServiceWorkerStatus.UNCONTROLLED; - } - } else { - return ServiceWorkerStatus.UNSUPPORTED; - } -} - -export function getVisibilityState(): VisibilityState { - const document = Api.getInstance().document; - const visibilityState = document.visibilityState; - switch (visibilityState) { - case 'visible': - return VisibilityState.VISIBLE; - case 'hidden': - return VisibilityState.HIDDEN; - default: - return VisibilityState.UNKNOWN; - } -} - -export function getEffectiveConnectionType(): EffectiveConnectionType { - const navigator = Api.getInstance().navigator; - const navigatorConnection = (navigator as NavigatorWithConnection).connection; - const effectiveType = - navigatorConnection && navigatorConnection.effectiveType; - switch (effectiveType) { - case 'slow-2g': - return EffectiveConnectionType.CONNECTION_SLOW_2G; - case '2g': - return EffectiveConnectionType.CONNECTION_2G; - case '3g': - return EffectiveConnectionType.CONNECTION_3G; - case '4g': - return EffectiveConnectionType.CONNECTION_4G; - default: - return EffectiveConnectionType.UNKNOWN; - } -} - -export function isValidCustomAttributeName(name: string): boolean { - if (name.length === 0 || name.length > MAX_ATTRIBUTE_NAME_LENGTH) { - return false; - } - const matchesReservedPrefix = RESERVED_ATTRIBUTE_PREFIXES.some(prefix => - name.startsWith(prefix) - ); - return !matchesReservedPrefix && !!name.match(ATTRIBUTE_FORMAT_REGEX); -} - -export function isValidCustomAttributeValue(value: string): boolean { - return value.length !== 0 && value.length <= MAX_ATTRIBUTE_VALUE_LENGTH; -} diff --git a/packages-exp/performance-exp/src/utils/console_logger.ts b/packages-exp/performance-exp/src/utils/console_logger.ts deleted file mode 100644 index 2f97aeb386b..00000000000 --- a/packages-exp/performance-exp/src/utils/console_logger.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Logger, LogLevel } from '@firebase/logger'; -import { SERVICE_NAME } from '../constants'; - -export const consoleLogger = new Logger(SERVICE_NAME); -consoleLogger.logLevel = LogLevel.INFO; diff --git a/packages-exp/performance-exp/src/utils/errors.ts b/packages-exp/performance-exp/src/utils/errors.ts deleted file mode 100644 index 942a5571d87..00000000000 --- a/packages-exp/performance-exp/src/utils/errors.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ErrorFactory } from '@firebase/util'; -import { SERVICE, SERVICE_NAME } from '../constants'; - -export const enum ErrorCode { - TRACE_STARTED_BEFORE = 'trace started', - TRACE_STOPPED_BEFORE = 'trace stopped', - NONPOSITIVE_TRACE_START_TIME = 'nonpositive trace startTime', - NONPOSITIVE_TRACE_DURATION = 'nonpositive trace duration', - NO_WINDOW = 'no window', - NO_APP_ID = 'no app id', - NO_PROJECT_ID = 'no project id', - NO_API_KEY = 'no api key', - INVALID_CC_LOG = 'invalid cc log', - FB_NOT_DEFAULT = 'FB not default', - RC_NOT_OK = 'RC response not ok', - INVALID_ATTRIBUTE_NAME = 'invalid attribute name', - INVALID_ATTRIBUTE_VALUE = 'invalid attribute value', - INVALID_CUSTOM_METRIC_NAME = 'invalid custom metric name', - INVALID_STRING_MERGER_PARAMETER = 'invalid String merger input' -} - -const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { - [ErrorCode.TRACE_STARTED_BEFORE]: 'Trace {$traceName} was started before.', - [ErrorCode.TRACE_STOPPED_BEFORE]: 'Trace {$traceName} is not running.', - [ErrorCode.NONPOSITIVE_TRACE_START_TIME]: - 'Trace {$traceName} startTime should be positive.', - [ErrorCode.NONPOSITIVE_TRACE_DURATION]: - 'Trace {$traceName} duration should be positive.', - [ErrorCode.NO_WINDOW]: 'Window is not available.', - [ErrorCode.NO_APP_ID]: 'App id is not available.', - [ErrorCode.NO_PROJECT_ID]: 'Project id is not available.', - [ErrorCode.NO_API_KEY]: 'Api key is not available.', - [ErrorCode.INVALID_CC_LOG]: 'Attempted to queue invalid cc event', - [ErrorCode.FB_NOT_DEFAULT]: - 'Performance can only start when Firebase app instance is the default one.', - [ErrorCode.RC_NOT_OK]: 'RC response is not ok', - [ErrorCode.INVALID_ATTRIBUTE_NAME]: - 'Attribute name {$attributeName} is invalid.', - [ErrorCode.INVALID_ATTRIBUTE_VALUE]: - 'Attribute value {$attributeValue} is invalid.', - [ErrorCode.INVALID_CUSTOM_METRIC_NAME]: - 'Custom metric name {$customMetricName} is invalid', - [ErrorCode.INVALID_STRING_MERGER_PARAMETER]: - 'Input for String merger is invalid, contact support team to resolve.' -}; - -interface ErrorParams { - [ErrorCode.TRACE_STARTED_BEFORE]: { traceName: string }; - [ErrorCode.TRACE_STOPPED_BEFORE]: { traceName: string }; - [ErrorCode.NONPOSITIVE_TRACE_START_TIME]: { traceName: string }; - [ErrorCode.NONPOSITIVE_TRACE_DURATION]: { traceName: string }; - [ErrorCode.INVALID_ATTRIBUTE_NAME]: { attributeName: string }; - [ErrorCode.INVALID_ATTRIBUTE_VALUE]: { attributeValue: string }; - [ErrorCode.INVALID_CUSTOM_METRIC_NAME]: { customMetricName: string }; -} - -export const ERROR_FACTORY = new ErrorFactory( - SERVICE, - SERVICE_NAME, - ERROR_DESCRIPTION_MAP -); diff --git a/packages-exp/performance-exp/src/utils/metric_utils.test.ts b/packages-exp/performance-exp/src/utils/metric_utils.test.ts deleted file mode 100644 index fea213911f7..00000000000 --- a/packages-exp/performance-exp/src/utils/metric_utils.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF unknown KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; - -import { isValidMetricName } from './metric_utils'; -import { - FIRST_PAINT_COUNTER_NAME, - FIRST_CONTENTFUL_PAINT_COUNTER_NAME, - FIRST_INPUT_DELAY_COUNTER_NAME -} from '../constants'; -import '../../test/setup'; - -describe('Firebase Performance > metric_utils', () => { - describe('#isValidMetricName', () => { - it('returns true when name is valid', () => { - expect(isValidMetricName('validCustom_Metric_Name')).to.be.true; - }); - - it('returns false when name is blank', () => { - expect(isValidMetricName('')).to.be.false; - }); - - it('returns false when name is too long', () => { - const longMetricName = - 'too_long_metric_name_over_one_hundred_characters_too_long_metric_name_over_one_' + - 'hundred_characters_too'; - expect(isValidMetricName(longMetricName)).to.be.false; - }); - - it('returns false when name starts with a reserved prefix', () => { - expect(isValidMetricName('_invalidMetricName')).to.be.false; - }); - - it('returns true for first paint metric', () => { - expect( - isValidMetricName(FIRST_PAINT_COUNTER_NAME, '_wt_http://example.com') - ).to.be.true; - }); - - it('returns true for first contentful paint metric', () => { - expect( - isValidMetricName( - FIRST_CONTENTFUL_PAINT_COUNTER_NAME, - '_wt_http://example.com' - ) - ).to.be.true; - }); - - it('returns true for first input delay metric', () => { - expect( - isValidMetricName( - FIRST_INPUT_DELAY_COUNTER_NAME, - '_wt_http://example.com' - ) - ).to.be.true; - }); - - it('returns false if first paint metric name is used outside of page load traces', () => { - expect(isValidMetricName(FIRST_PAINT_COUNTER_NAME, 'some_randome_trace')) - .to.be.false; - }); - - it('returns false if first contentful paint metric name is used outside of page load traces', () => { - expect( - isValidMetricName( - FIRST_CONTENTFUL_PAINT_COUNTER_NAME, - 'some_randome_trace' - ) - ).to.be.false; - }); - - it('returns false if first input delay metric name is used outside of page load traces', () => { - expect( - isValidMetricName(FIRST_INPUT_DELAY_COUNTER_NAME, 'some_randome_trace') - ).to.be.false; - }); - }); -}); diff --git a/packages-exp/performance-exp/src/utils/metric_utils.ts b/packages-exp/performance-exp/src/utils/metric_utils.ts deleted file mode 100644 index 9bbc4886aef..00000000000 --- a/packages-exp/performance-exp/src/utils/metric_utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - FIRST_PAINT_COUNTER_NAME, - FIRST_CONTENTFUL_PAINT_COUNTER_NAME, - FIRST_INPUT_DELAY_COUNTER_NAME, - OOB_TRACE_PAGE_LOAD_PREFIX -} from '../constants'; -import { consoleLogger } from '../utils/console_logger'; - -const MAX_METRIC_NAME_LENGTH = 100; -const RESERVED_AUTO_PREFIX = '_'; -const oobMetrics = [ - FIRST_PAINT_COUNTER_NAME, - FIRST_CONTENTFUL_PAINT_COUNTER_NAME, - FIRST_INPUT_DELAY_COUNTER_NAME -]; - -/** - * Returns true if the metric is custom and does not start with reserved prefix, or if - * the metric is one of out of the box page load trace metrics. - */ -export function isValidMetricName(name: string, traceName?: string): boolean { - if (name.length === 0 || name.length > MAX_METRIC_NAME_LENGTH) { - return false; - } - return ( - (traceName && - traceName.startsWith(OOB_TRACE_PAGE_LOAD_PREFIX) && - oobMetrics.indexOf(name) > -1) || - !name.startsWith(RESERVED_AUTO_PREFIX) - ); -} - -/** - * Converts the provided value to an integer value to be used in case of a metric. - * @param providedValue Provided number value of the metric that needs to be converted to an integer. - * - * @returns Converted integer number to be set for the metric. - */ -export function convertMetricValueToInteger(providedValue: number): number { - const valueAsInteger: number = Math.floor(providedValue); - if (valueAsInteger < providedValue) { - consoleLogger.info( - `Metric value should be an Integer, setting the value as : ${valueAsInteger}.` - ); - } - return valueAsInteger; -} diff --git a/packages-exp/performance-exp/src/utils/string_merger.test.ts b/packages-exp/performance-exp/src/utils/string_merger.test.ts deleted file mode 100644 index ded5472e580..00000000000 --- a/packages-exp/performance-exp/src/utils/string_merger.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF unknown KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; - -import { mergeStrings } from './string_merger'; -import { FirebaseError } from '@firebase/util'; -// import { ERROR_FACTORY, ErrorCode } from './errors'; -import '../../test/setup'; - -describe('Firebase Performance > string_merger', () => { - describe('#mergeStrings', () => { - it('Throws exception when string length has | diff | > 1', () => { - // const expectedError = ERROR_FACTORY.create(ErrorCode.INVALID_STRING_MERGER_PARAMETER); - expect(() => mergeStrings('', '123')).to.throw( - FirebaseError, - 'performance/invalid String merger input' - ); - }); - - it('returns empty string when both inputs are empty', () => { - expect(mergeStrings('', '')).equal(''); - }); - - it('returns merge result string when both inputs have same length', () => { - expect(mergeStrings('12345', 'abcde')).equal('1a2b3c4d5e'); - }); - - it('returns merge result string when input length diff == 1', () => { - expect(() => mergeStrings('1234', 'abcde')).to.throw( - FirebaseError, - 'performance/invalid String merger input' - ); - }); - - it('returns merge result string when input length diff == -1', () => { - expect(mergeStrings('12345', 'abcd')).equal('1a2b3c4d5'); - }); - }); -}); diff --git a/packages-exp/performance-exp/src/utils/string_merger.ts b/packages-exp/performance-exp/src/utils/string_merger.ts deleted file mode 100644 index 620488a7416..00000000000 --- a/packages-exp/performance-exp/src/utils/string_merger.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ERROR_FACTORY, ErrorCode } from './errors'; - -export function mergeStrings(part1: string, part2: string): string { - const sizeDiff = part1.length - part2.length; - if (sizeDiff < 0 || sizeDiff > 1) { - throw ERROR_FACTORY.create(ErrorCode.INVALID_STRING_MERGER_PARAMETER); - } - - const resultArray = []; - for (let i = 0; i < part1.length; i++) { - resultArray.push(part1.charAt(i)); - if (part2.length > i) { - resultArray.push(part2.charAt(i)); - } - } - - return resultArray.join(''); -} diff --git a/packages-exp/performance-exp/test/setup.ts b/packages-exp/performance-exp/test/setup.ts deleted file mode 100644 index 11f8e4cec16..00000000000 --- a/packages-exp/performance-exp/test/setup.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { restore } from 'sinon'; -import { use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as sinonChai from 'sinon-chai'; - -use(chaiAsPromised); -use(sinonChai); -afterEach(() => { - restore(); -}); diff --git a/packages-exp/performance-exp/tsconfig.json b/packages-exp/performance-exp/tsconfig.json deleted file mode 100644 index a3c61dfddfd..00000000000 --- a/packages-exp/performance-exp/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "resolveJsonModule": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "exclude": ["dist/**/*"] -} diff --git a/packages-exp/performance-types-exp/api-extractor.json b/packages-exp/performance-types-exp/api-extractor.json deleted file mode 100644 index 42f37a88c4b..00000000000 --- a/packages-exp/performance-types-exp/api-extractor.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/index.d.ts" -} \ No newline at end of file diff --git a/packages-exp/performance-types-exp/index.d.ts b/packages-exp/performance-types-exp/index.d.ts deleted file mode 100644 index cf65771a0f1..00000000000 --- a/packages-exp/performance-types-exp/index.d.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @public - */ -export interface PerformanceSettings { - /** Whether to collect custom events. */ - dataCollectionEnabled?: boolean; - - /** Whether to collect out of the box events. */ - instrumentationEnabled?: boolean; -} - -export interface FirebasePerformance { - /** - * Controls the logging of automatic traces and HTTP/S network monitoring. - */ - instrumentationEnabled: boolean; - - /** - * Controls the logging of custom traces. - */ - dataCollectionEnabled: boolean; -} - -export interface PerformanceTrace { - /** - * Starts the timing for the trace instance. - */ - start(): void; - /** - * Stops the timing of the trace instance and logs the data of the instance. - */ - stop(): void; - /** - * Records a trace from given parameters. This provides a direct way to use trace without a need to - * start/stop. This is useful for use cases in which the trace cannot directly be used - * (e.g. if the duration was captured before the Performance SDK was loaded). - * - * @param startTime trace start time since epoch in millisec. - * @param duration The duraction of the trace in millisec. - * @param options An object which can optionally hold maps of custom metrics and - * custom attributes. - */ - record( - startTime: number, - duration: number, - options?: { - metrics?: { [key: string]: number }; - attributes?: { [key: string]: string }; - } - ): void; - /** - * Adds to the value of a custom metric. If a custom metric with the provided name does not - * exist, it creates one with that name and the value equal to the given number. The value will be floored down to an - * integer. - * - * @param metricName The name of the custom metric. - * @param num The number to be added to the value of the custom metric. If not provided, it - * uses a default value of one. - */ - incrementMetric(metricName: string, num?: number): void; - /** - * Sets the value of the specified custom metric to the given number regardless of whether - * a metric with that name already exists on the trace instance or not. The value will be floored down to an - * integer. - * - * @param metricName Name of the custom metric. - * @param num Value to of the custom metric. - */ - putMetric(metricName: string, num: number): void; - /** - * Returns the value of the custom metric by that name. If a custom metric with that name does - * not exist will return zero. - * - * @param metricName Name of the custom metric. - */ - getMetric(metricName: string): number; - /** - * Set a custom attribute of a trace to a certain value. - * - * @param attr Name of the custom attribute. - * @param value Value of the custom attribute. - */ - putAttribute(attr: string, value: string): void; - /** - * Retrieves the value which a custom attribute is set to. - * - * @param attr Name of the custom attribute. - */ - getAttribute(attr: string): string | undefined; - /** - * Removes the specified custom attribute from a trace instance. - * - * @param attr Name of the custom attribute. - */ - removeAttribute(attr: string): void; - /** - * Returns a map of all custom attributes of a trace instance. - */ - getAttributes(): { [key: string]: string }; -} - -declare module '@firebase/component' { - interface NameServiceMapping { - 'performance-exp': FirebasePerformance; - } -} diff --git a/packages-exp/performance-types-exp/package.json b/packages-exp/performance-types-exp/package.json deleted file mode 100644 index ead3e529949..00000000000 --- a/packages-exp/performance-types-exp/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@firebase/performance-types-exp", - "version": "0.0.900", - "description": "@firebase/performance Types", - "author": "Firebase (https://firebase.google.com/)", - "private": true, - "license": "Apache-2.0", - "scripts": { - "test": "tsc", - "test:ci": "node ../../scripts/run_tests_in_ci.js", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn api-report && yarn doc" - }, - "files": [ - "index.d.ts" - ], - "devDependencies": { - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages/performance-types-exp", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - } -} diff --git a/packages-exp/remote-config-compat/.eslintrc.js b/packages-exp/remote-config-compat/.eslintrc.js deleted file mode 100644 index ca80aa0f69a..00000000000 --- a/packages-exp/remote-config-compat/.eslintrc.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module.exports = { - extends: '../../config/.eslintrc.js', - parserOptions: { - project: 'tsconfig.json', - // to make vscode-eslint work with monorepo - // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 - tsconfigRootDir: __dirname - } -}; diff --git a/packages-exp/remote-config-compat/README.md b/packages-exp/remote-config-compat/README.md deleted file mode 100644 index 2813f0ae9d4..00000000000 --- a/packages-exp/remote-config-compat/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# @firebase/remote-compat - -This is the compat package that recreates the v8 APIs. - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/remote-config-compat/package.json b/packages-exp/remote-config-compat/package.json deleted file mode 100644 index ae6bea83c0c..00000000000 --- a/packages-exp/remote-config-compat/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@firebase/remote-config-compat", - "version": "0.0.900", - "description": "The compatibility package of Remote Config", - "author": "Firebase (https://firebase.google.com/)", - "private": true, - "main": "dist/index.cjs.js", - "browser": "dist/index.esm5.js", - "module": "dist/index.esm5.js", - "esm2017": "dist/index.esm2017.js", - "files": ["dist"], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c", - "build:release": "rollup -c rollup.config.release.js", - "build:deps": "lerna run --scope @firebase/remote-config-compat --include-dependencies build", - "dev": "rollup -c -w", - "test": "run-p lint test:all", - "test:all": "run-p test:browser", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", - "test:browser": "karma start --single-run", - "test:browser:debug": "karma start --browsers Chrome --auto-watch" - }, - "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app-compat": "0.x", - "@firebase/app-types": "0.x" - }, - "dependencies": { - "@firebase/remote-config-exp": "0.0.900", - "@firebase/remote-config-types-exp": "0.0.900", - "@firebase/util": "0.3.4", - "@firebase/logger": "0.2.6", - "@firebase/component": "0.1.21", - "tslib": "^1.11.1" - }, - "devDependencies": { - "rollup": "2.35.1", - "@rollup/plugin-json": "4.1.0", - "rollup-plugin-replace": "2.2.0", - "rollup-plugin-typescript2": "0.29.0", - "typescript": "4.0.5", - "@firebase/app-compat": "0.0.900" - }, - "repository": { - "directory": "packages-exp/remote-config-compat", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "dist/src/index.d.ts", - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/remote-config-compat/rollup.config.js b/packages-exp/remote-config-compat/rollup.config.js deleted file mode 100644 index 4df9d8a2f48..00000000000 --- a/packages-exp/remote-config-compat/rollup.config.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import json from '@rollup/plugin-json'; -import { es5BuildsNoPlugin, es2017BuildsNoPlugin } from './rollup.shared.js'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - } - }), - json({ - preferConst: true - }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/remote-config-compat/rollup.config.release.js b/packages-exp/remote-config-compat/rollup.config.release.js deleted file mode 100644 index b0b1dc5154a..00000000000 --- a/packages-exp/remote-config-compat/rollup.config.release.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import json from '@rollup/plugin-json'; -import { es5BuildsNoPlugin, es2017BuildsNoPlugin } from './rollup.shared.js'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - clean: true, - abortOnError: false, - transformers: [importPathTransformer] - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - clean: true, - abortOnError: false, - transformers: [importPathTransformer] - }), - json({ - preferConst: true - }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/remote-config-compat/rollup.shared.js b/packages-exp/remote-config-compat/rollup.shared.js deleted file mode 100644 index 5474a15ba7c..00000000000 --- a/packages-exp/remote-config-compat/rollup.shared.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import pkg from './package.json'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -export const es5BuildsNoPlugin = [ - /** - * Browser Builds - */ - { - input: 'src/index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.browser, format: 'es', sourcemap: true } - ], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -export const es2017BuildsNoPlugin = [ - /** - * Browser Builds - */ - { - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; diff --git a/packages-exp/remote-config-compat/src/index.ts b/packages-exp/remote-config-compat/src/index.ts deleted file mode 100644 index 6587a1cc8ed..00000000000 --- a/packages-exp/remote-config-compat/src/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import firebase from '@firebase/app-compat'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { - Component, - ComponentContainer, - ComponentType -} from '@firebase/component'; -import { RemoteConfigCompatImpl } from './remoteConfig'; -import { name as packageName, version } from '../package.json'; -import { RemoteConfig as RemoteConfigCompat } from '@firebase/remote-config-types'; - -// TODO: move it to the future remote-config-compat-types package -declare module '@firebase/component' { - interface NameServiceMapping { - 'remote-config-compat': RemoteConfigCompat; - } -} - -function registerRemoteConfigCompat( - firebaseInstance: _FirebaseNamespace -): void { - firebaseInstance.INTERNAL.registerComponent( - new Component( - 'remote-config-compat', - remoteConfigFactory, - ComponentType.PUBLIC - ).setMultipleInstances(true) - ); - - firebaseInstance.registerVersion(packageName, version); -} - -function remoteConfigFactory( - container: ComponentContainer, - namespace?: string -): RemoteConfigCompatImpl { - // TODO: change 'app' to 'app-compat' before the official release - const app = container.getProvider('app').getImmediate(); - // The following call will always succeed because rc `import {...} from '@firebase/remote-config-exp'` - const remoteConfig = container.getProvider('remote-config-exp').getImmediate({ - identifier: namespace - }); - - return new RemoteConfigCompatImpl(app, remoteConfig); -} - -registerRemoteConfigCompat(firebase as _FirebaseNamespace); diff --git a/packages-exp/remote-config-compat/src/remoteConfig.ts b/packages-exp/remote-config-compat/src/remoteConfig.ts deleted file mode 100644 index ab5f0da8105..00000000000 --- a/packages-exp/remote-config-compat/src/remoteConfig.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types'; -import { RemoteConfig } from '@firebase/remote-config-types-exp'; -import { - Value as ValueCompat, - FetchStatus as FetchSTatusCompat, - Settings as SettingsCompat, - LogLevel as RemoteConfigLogLevel, - RemoteConfig as RemoteConfigCompat -} from '@firebase/remote-config-types'; -import { - setLogLevel, - activate, - ensureInitialized, - fetchAndActivate, - fetchConfig, - getAll, - getBoolean, - getNumber, - getString, - getValue -} from '@firebase/remote-config-exp'; -import { FirebaseService } from '@firebase/app-types/private'; - -export class RemoteConfigCompatImpl - implements RemoteConfigCompat, FirebaseService { - constructor(public app: FirebaseApp, private _remoteConfig: RemoteConfig) {} - - get defaultConfig(): { [key: string]: string | number | boolean } { - return this._remoteConfig.defaultConfig; - } - - set defaultConfig(value: { [key: string]: string | number | boolean }) { - this._remoteConfig.defaultConfig = value; - } - - get fetchTimeMillis(): number { - return this._remoteConfig.fetchTimeMillis; - } - - get lastFetchStatus(): FetchSTatusCompat { - return this._remoteConfig.lastFetchStatus; - } - - get settings(): SettingsCompat { - return this._remoteConfig.settings; - } - - set settings(value: SettingsCompat) { - this._remoteConfig.settings = value; - } - - activate(): Promise { - return activate(this._remoteConfig); - } - - ensureInitialized(): Promise { - return ensureInitialized(this._remoteConfig); - } - - /** - * @throws a {@link ErrorCode.FETCH_CLIENT_TIMEOUT} if the request takes longer than - * {@link Settings.fetchTimeoutInSeconds} or - * {@link DEFAULT_FETCH_TIMEOUT_SECONDS}. - */ - fetch(): Promise { - return fetchConfig(this._remoteConfig); - } - - fetchAndActivate(): Promise { - return fetchAndActivate(this._remoteConfig); - } - - getAll(): { [key: string]: ValueCompat } { - return getAll(this._remoteConfig); - } - - getBoolean(key: string): boolean { - return getBoolean(this._remoteConfig, key); - } - - getNumber(key: string): number { - return getNumber(this._remoteConfig, key); - } - - getString(key: string): string { - return getString(this._remoteConfig, key); - } - - getValue(key: string): ValueCompat { - return getValue(this._remoteConfig, key); - } - - // Based on packages/firestore/src/util/log.ts but not static because we need per-instance levels - // to differentiate 2p and 3p use-cases. - setLogLevel(logLevel: RemoteConfigLogLevel): void { - setLogLevel(this._remoteConfig, logLevel); - } -} diff --git a/packages-exp/remote-config-compat/test/setup.ts b/packages-exp/remote-config-compat/test/setup.ts deleted file mode 100644 index b1e3136529f..00000000000 --- a/packages-exp/remote-config-compat/test/setup.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { use } from 'chai'; -import { restore } from 'sinon'; -import * as sinonChai from 'sinon-chai'; - -use(sinonChai); - -afterEach(async () => { - restore(); -}); diff --git a/packages-exp/remote-config-compat/test/util.ts b/packages-exp/remote-config-compat/test/util.ts deleted file mode 100644 index 7719b28c7b5..00000000000 --- a/packages-exp/remote-config-compat/test/util.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types'; -import { RemoteConfig } from '@firebase/remote-config-types-exp'; - -export function getFakeApp(): FirebaseApp { - return { - name: 'appName', - options: { - apiKey: 'apiKey', - projectId: 'projectId', - authDomain: 'authDomain', - messagingSenderId: 'messagingSenderId', - databaseURL: 'databaseUrl', - storageBucket: 'storageBucket', - appId: '1:777777777777:web:d93b5ca1475efe57' - }, - automaticDataCollectionEnabled: true, - delete: async () => {} - }; -} - -export function getFakeModularRemoteConfig(): RemoteConfig { - return { - defaultConfig: {}, - fetchTimeMillis: 0, - lastFetchStatus: 'no-fetch-yet', - settings: { - fetchTimeoutMillis: 0, - minimumFetchIntervalMillis: 0 - } - }; -} diff --git a/packages-exp/remote-config-exp/.eslintrc.js b/packages-exp/remote-config-exp/.eslintrc.js deleted file mode 100644 index 5a8c4b909c2..00000000000 --- a/packages-exp/remote-config-exp/.eslintrc.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module.exports = { - 'extends': '../../config/.eslintrc.js', - 'parserOptions': { - project: 'tsconfig.json', - // to make vscode-eslint work with monorepo - // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 - tsconfigRootDir: __dirname - } -}; diff --git a/packages-exp/remote-config-exp/.npmignore b/packages-exp/remote-config-exp/.npmignore deleted file mode 100644 index 6de0b6d2896..00000000000 --- a/packages-exp/remote-config-exp/.npmignore +++ /dev/null @@ -1 +0,0 @@ -# This file is left intentionally blank \ No newline at end of file diff --git a/packages-exp/remote-config-exp/README.md b/packages-exp/remote-config-exp/README.md deleted file mode 100644 index 1b21bbecc51..00000000000 --- a/packages-exp/remote-config-exp/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# @firebase/remote-config - -This is the [Remote Config](https://firebase.google.com/docs/remote-config/) component of the -[Firebase JS SDK](https://www.npmjs.com/package/firebase). - -**This package is not intended for direct usage, and should only be used via the officially -supported [firebase](https://www.npmjs.com/package/firebase) package.** - -## Contributing - -Setup: - -1. Run `yarn` in repo root - -Format: - -1. Run `yarn prettier` in RC package - -Unit test: - -1. Run `yarn test` in RC package - -End-to-end test: - -1. Run `yarn build` in RC package -1. Run `yarn build` in Firebase package -1. Open test_app/index.html in a browser diff --git a/packages-exp/remote-config-exp/api-extractor.json b/packages-exp/remote-config-exp/api-extractor.json deleted file mode 100644 index f291311f711..00000000000 --- a/packages-exp/remote-config-exp/api-extractor.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/dist/src/index.d.ts", - "dtsRollup": { - "enabled": true - } -} \ No newline at end of file diff --git a/packages-exp/remote-config-exp/karma.conf.js b/packages-exp/remote-config-exp/karma.conf.js deleted file mode 100644 index 5006cd5a4ea..00000000000 --- a/packages-exp/remote-config-exp/karma.conf.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const karma = require('karma'); -const path = require('path'); -const karmaBase = require('../../config/karma.base'); - -const files = [`test/**/*`]; - -module.exports = function (config) { - const karmaConfig = Object.assign({}, karmaBase, { - // files to load into karma - files: files, - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha'] - }); - - config.set(karmaConfig); -}; - -module.exports.files = files; diff --git a/packages-exp/remote-config-exp/package.json b/packages-exp/remote-config-exp/package.json deleted file mode 100644 index 354b394266d..00000000000 --- a/packages-exp/remote-config-exp/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@firebase/remote-config-exp", - "version": "0.0.900", - "description": "The Remote Config package of the Firebase JS SDK", - "author": "Firebase (https://firebase.google.com/)", - "private": true, - "main": "dist/index.cjs.js", - "browser": "dist/index.esm.js", - "module": "dist/index.esm.js", - "esm2017": "dist/index.esm2017.js", - "files": ["dist"], - "scripts": { - "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c && yarn api-report", - "build:deps": "lerna run --scope @firebase/remote-config-exp --include-dependencies build", - "build:release": "rollup -c rollup.config.release.js && yarn api-report", - "dev": "rollup -c -w", - "test": "run-p lint test:browser", - "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:browser", - "test:browser": "karma start --single-run", - "test:debug": "karma start --browsers=Chrome --auto-watch", - "prettier": "prettier --write '{src,test}/**/*.{js,ts}'", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn build && yarn doc" - }, - "peerDependencies": { - "@firebase/app-exp": "0.x", - "@firebase/app-types-exp": "0.x" - }, - "dependencies": { - "@firebase/installations-exp": "0.0.900", - "@firebase/logger": "0.2.6", - "@firebase/remote-config-types-exp": "0.0.900", - "@firebase/util": "0.3.4", - "@firebase/component": "0.1.21", - "tslib": "^1.11.1" - }, - "license": "Apache-2.0", - "devDependencies": { - "@firebase/app-exp": "0.0.900", - "rollup": "2.35.1", - "rollup-plugin-typescript2": "0.29.0", - "typescript": "4.0.5" - }, - "repository": { - "directory": "packages/remote-config", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "typings": "dist/remote-config-exp.d.ts", - "nyc": { - "extension": [ - ".ts" - ], - "reportDir": "./coverage/node" - } -} diff --git a/packages-exp/remote-config-exp/rollup.config.js b/packages-exp/remote-config-exp/rollup.config.js deleted file mode 100644 index b57e5ad73ee..00000000000 --- a/packages-exp/remote-config-exp/rollup.config.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import json from '@rollup/plugin-json'; // Enables package.json import in TypeScript. -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - } - }), - json({ preferConst: true }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/remote-config-exp/rollup.config.release.js b/packages-exp/remote-config-exp/rollup.config.release.js deleted file mode 100644 index 1e3b338e4b5..00000000000 --- a/packages-exp/remote-config-exp/rollup.config.release.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import typescriptPlugin from 'rollup-plugin-typescript2'; -import typescript from 'typescript'; -import json from '@rollup/plugin-json'; -import { importPathTransformer } from '../../scripts/exp/ts-transform-import-path'; -import { es2017BuildsNoPlugin, es5BuildsNoPlugin } from './rollup.shared'; - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript, - clean: true, - abortOnError: false, - transformers: [importPathTransformer] - }), - json() -]; - -const es5Builds = es5BuildsNoPlugin.map(build => ({ - ...build, - plugins: es5BuildPlugins, - treeshake: { - moduleSideEffects: false - } -})); - -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ - typescriptPlugin({ - typescript, - tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } - }, - abortOnError: false, - clean: true, - transformers: [importPathTransformer] - }), - json({ - preferConst: true - }) -]; - -const es2017Builds = es2017BuildsNoPlugin.map(build => ({ - ...build, - plugins: es2017BuildPlugins, - treeshake: { - moduleSideEffects: false - } -})); - -export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/remote-config-exp/rollup.shared.js b/packages-exp/remote-config-exp/rollup.shared.js deleted file mode 100644 index c35a655a498..00000000000 --- a/packages-exp/remote-config-exp/rollup.shared.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import pkg from './package.json'; - -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -export const es5BuildsNoPlugin = [ - /** - * Browser Builds - */ - { - input: 'src/index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } - ], - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; - -/** - * ES2017 Builds - */ -export const es2017BuildsNoPlugin = [ - { - /** - * Browser Build - */ - input: 'src/index.ts', - output: { - file: pkg.esm2017, - format: 'es', - sourcemap: true - }, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } -]; diff --git a/packages-exp/remote-config-exp/src/api.ts b/packages-exp/remote-config-exp/src/api.ts deleted file mode 100644 index 466cd9e2d9b..00000000000 --- a/packages-exp/remote-config-exp/src/api.ts +++ /dev/null @@ -1,259 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { _getProvider } from '@firebase/app-exp'; -import { FirebaseApp } from '@firebase/app-types-exp'; -import { - LogLevel as RemoteConfigLogLevel, - RemoteConfig, - Value as ValueType -} from '@firebase/remote-config-types-exp'; -import { RemoteConfigAbortSignal } from './client/remote_config_fetch_client'; -import { RC_COMPONENT_NAME } from './constants'; -import { ErrorCode, hasErrorCode } from './errors'; -import { RemoteConfig as RemoteConfigImpl } from './remote_config'; -import { Value } from './value'; -import { LogLevel as FirebaseLogLevel } from '@firebase/logger'; - -/** - * - * @param app - the firebase app instance - * @returns a remote config instance - * - * @public - */ -export function getRemoteConfig(app: FirebaseApp): RemoteConfig { - const rcProvider = _getProvider(app, RC_COMPONENT_NAME); - return rcProvider.getImmediate(); -} - -/** - * Makes the last fetched config available to the getters. - * @param remoteConfig - the remote config instance - * @returns A promise which resolves to true if the current call activated the fetched configs. - * If the fetched configs were already activated, the promise will resolve to false. - * - * @public - */ -export async function activate(remoteConfig: RemoteConfig): Promise { - const rc = remoteConfig as RemoteConfigImpl; - const [lastSuccessfulFetchResponse, activeConfigEtag] = await Promise.all([ - rc._storage.getLastSuccessfulFetchResponse(), - rc._storage.getActiveConfigEtag() - ]); - if ( - !lastSuccessfulFetchResponse || - !lastSuccessfulFetchResponse.config || - !lastSuccessfulFetchResponse.eTag || - lastSuccessfulFetchResponse.eTag === activeConfigEtag - ) { - // Either there is no successful fetched config, or is the same as current active - // config. - return false; - } - await Promise.all([ - rc._storageCache.setActiveConfig(lastSuccessfulFetchResponse.config), - rc._storage.setActiveConfigEtag(lastSuccessfulFetchResponse.eTag) - ]); - return true; -} - -/** - * Ensures the last activated config are available to the getters. - * @param remoteConfig - the remote config instance - * - * @returns A promise that resolves when the last activated config is available to the getters - * @public - */ -export function ensureInitialized(remoteConfig: RemoteConfig): Promise { - const rc = remoteConfig as RemoteConfigImpl; - if (!rc._initializePromise) { - rc._initializePromise = rc._storageCache.loadFromStorage().then(() => { - rc._isInitializationComplete = true; - }); - } - return rc._initializePromise; -} - -/** - * Fetches and caches configuration from the Remote Config service. - * @param remoteConfig - the remote config instance - * @public - */ -export async function fetchConfig(remoteConfig: RemoteConfig): Promise { - const rc = remoteConfig as RemoteConfigImpl; - // Aborts the request after the given timeout, causing the fetch call to - // reject with an AbortError. - // - //

Aborting after the request completes is a no-op, so we don't need a - // corresponding clearTimeout. - // - // Locating abort logic here because: - // * it uses a developer setting (timeout) - // * it applies to all retries (like curl's max-time arg) - // * it is consistent with the Fetch API's signal input - const abortSignal = new RemoteConfigAbortSignal(); - - setTimeout(async () => { - // Note a very low delay, eg < 10ms, can elapse before listeners are initialized. - abortSignal.abort(); - }, rc.settings.fetchTimeoutMillis); - - // Catches *all* errors thrown by client so status can be set consistently. - try { - await rc._client.fetch({ - cacheMaxAgeMillis: rc.settings.minimumFetchIntervalMillis, - signal: abortSignal - }); - - await rc._storageCache.setLastFetchStatus('success'); - } catch (e) { - const lastFetchStatus = hasErrorCode(e, ErrorCode.FETCH_THROTTLE) - ? 'throttle' - : 'failure'; - await rc._storageCache.setLastFetchStatus(lastFetchStatus); - throw e; - } -} - -/** - * Gets all config. - * - * @param remoteConfig - the remote config instance - * @returns all config - * - * @public - */ -export function getAll(remoteConfig: RemoteConfig): Record { - const rc = remoteConfig as RemoteConfigImpl; - return getAllKeys( - rc._storageCache.getActiveConfig(), - rc.defaultConfig - ).reduce((allConfigs, key) => { - allConfigs[key] = getValue(remoteConfig, key); - return allConfigs; - }, {} as Record); -} - -/** - * Gets the value for the given key as a boolean. - * - * Convenience method for calling remoteConfig.getValue(key).asBoolean(). - * - * @param remoteConfig - the remote config instance - * @param key - the name of the parameter - * - * @returns the value for the given key as a boolean - * @public - */ -export function getBoolean(remoteConfig: RemoteConfig, key: string): boolean { - return getValue(remoteConfig, key).asBoolean(); -} - -/** - * Gets the value for the given key as a number. - * - * Convenience method for calling remoteConfig.getValue(key).asNumber(). - * - * @param remoteConfig - the remote config instance - * @param key - the name of the parameter - * - * @returns the value for the given key as a number - * - * @public - */ -export function getNumber(remoteConfig: RemoteConfig, key: string): number { - return getValue(remoteConfig, key).asNumber(); -} - -/** - * Gets the value for the given key as a String. - * Convenience method for calling remoteConfig.getValue(key).asString(). - * - * @param remoteConfig - the remote config instance - * @param key - the name of the parameter - * - * @returns the value for the given key as a String - * - * @public - */ -export function getString(remoteConfig: RemoteConfig, key: string): string { - return getValue(remoteConfig, key).asString(); -} - -/** - * Gets the {@link @firebase/remote-config-types#Value} for the given key. - * - * @param remoteConfig - the remote config instance - * @param key - the name of the parameter - * - * @returns the value for the given key - * - * @public - */ -export function getValue(remoteConfig: RemoteConfig, key: string): ValueType { - const rc = remoteConfig as RemoteConfigImpl; - if (!rc._isInitializationComplete) { - rc._logger.debug( - `A value was requested for key "${key}" before SDK initialization completed.` + - ' Await on ensureInitialized if the intent was to get a previously activated value.' - ); - } - const activeConfig = rc._storageCache.getActiveConfig(); - if (activeConfig && activeConfig[key] !== undefined) { - return new Value('remote', activeConfig[key]); - } else if (rc.defaultConfig && rc.defaultConfig[key] !== undefined) { - return new Value('default', String(rc.defaultConfig[key])); - } - rc._logger.debug( - `Returning static value for key "${key}".` + - ' Define a default or remote value if this is unintentional.' - ); - return new Value('static'); -} - -/** - * Defines the log level to use. - * - * @param remoteConfig - the remote config instance - * @param logLevel - the log level to set - * - * @public - */ -export function setLogLevel( - remoteConfig: RemoteConfig, - logLevel: RemoteConfigLogLevel -): void { - const rc = remoteConfig as RemoteConfigImpl; - switch (logLevel) { - case 'debug': - rc._logger.logLevel = FirebaseLogLevel.DEBUG; - break; - case 'silent': - rc._logger.logLevel = FirebaseLogLevel.SILENT; - break; - default: - rc._logger.logLevel = FirebaseLogLevel.ERROR; - } -} - -/** - * Dedupes and returns an array of all the keys of the received objects. - */ -function getAllKeys(obj1: {} = {}, obj2: {} = {}): string[] { - return Object.keys({ ...obj1, ...obj2 }); -} diff --git a/packages-exp/remote-config-exp/src/api2.ts b/packages-exp/remote-config-exp/src/api2.ts deleted file mode 100644 index 8a532613186..00000000000 --- a/packages-exp/remote-config-exp/src/api2.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { RemoteConfig } from '@firebase/remote-config-types-exp'; -import { activate, fetchConfig } from './api'; - -// This API is put in a separate file, so we can stub fetchConfig and activate in tests. -// It's not possible to stub standalone functions from the same module. -/** - * - * Performs fetch and activate operations, as a convenience. - * - * @param remoteConfig - the remote config instance - * - * @returns A promise which resolves to true if the current call activated the fetched configs. - * If the fetched configs were already activated, the promise will resolve to false. - * - * @public - */ -export async function fetchAndActivate( - remoteConfig: RemoteConfig -): Promise { - await fetchConfig(remoteConfig); - return activate(remoteConfig); -} diff --git a/packages-exp/remote-config-exp/src/client/caching_client.ts b/packages-exp/remote-config-exp/src/client/caching_client.ts deleted file mode 100644 index aea61acfd1f..00000000000 --- a/packages-exp/remote-config-exp/src/client/caching_client.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { StorageCache } from '../storage/storage_cache'; -import { - FetchResponse, - RemoteConfigFetchClient, - FetchRequest -} from './remote_config_fetch_client'; -import { Storage } from '../storage/storage'; -import { Logger } from '@firebase/logger'; - -/** - * Implements the {@link RemoteConfigClient} abstraction with success response caching. - * - *

Comparable to the browser's Cache API for responses, but the Cache API requires a Service - * Worker, which requires HTTPS, which would significantly complicate SDK installation. Also, the - * Cache API doesn't support matching entries by time. - */ -export class CachingClient implements RemoteConfigFetchClient { - constructor( - private readonly client: RemoteConfigFetchClient, - private readonly storage: Storage, - private readonly storageCache: StorageCache, - private readonly logger: Logger - ) {} - - /** - * Returns true if the age of the cached fetched configs is less than or equal to - * {@link Settings#minimumFetchIntervalInSeconds}. - * - *

This is comparable to passing `headers = { 'Cache-Control': max-age }` to the - * native Fetch API. - * - *

Visible for testing. - */ - isCachedDataFresh( - cacheMaxAgeMillis: number, - lastSuccessfulFetchTimestampMillis: number | undefined - ): boolean { - // Cache can only be fresh if it's populated. - if (!lastSuccessfulFetchTimestampMillis) { - this.logger.debug('Config fetch cache check. Cache unpopulated.'); - return false; - } - - // Calculates age of cache entry. - const cacheAgeMillis = Date.now() - lastSuccessfulFetchTimestampMillis; - - const isCachedDataFresh = cacheAgeMillis <= cacheMaxAgeMillis; - - this.logger.debug( - 'Config fetch cache check.' + - ` Cache age millis: ${cacheAgeMillis}.` + - ` Cache max age millis (minimumFetchIntervalMillis setting): ${cacheMaxAgeMillis}.` + - ` Is cache hit: ${isCachedDataFresh}.` - ); - - return isCachedDataFresh; - } - - async fetch(request: FetchRequest): Promise { - // Reads from persisted storage to avoid cache miss if callers don't wait on initialization. - const [ - lastSuccessfulFetchTimestampMillis, - lastSuccessfulFetchResponse - ] = await Promise.all([ - this.storage.getLastSuccessfulFetchTimestampMillis(), - this.storage.getLastSuccessfulFetchResponse() - ]); - - // Exits early on cache hit. - if ( - lastSuccessfulFetchResponse && - this.isCachedDataFresh( - request.cacheMaxAgeMillis, - lastSuccessfulFetchTimestampMillis - ) - ) { - return lastSuccessfulFetchResponse; - } - - // Deviates from pure decorator by not honoring a passed ETag since we don't have a public API - // that allows the caller to pass an ETag. - request.eTag = - lastSuccessfulFetchResponse && lastSuccessfulFetchResponse.eTag; - - // Falls back to service on cache miss. - const response = await this.client.fetch(request); - - // Fetch throws for non-success responses, so success is guaranteed here. - - const storageOperations = [ - // Uses write-through cache for consistency with synchronous public API. - this.storageCache.setLastSuccessfulFetchTimestampMillis(Date.now()) - ]; - - if (response.status === 200) { - // Caches response only if it has changed, ie non-304 responses. - storageOperations.push( - this.storage.setLastSuccessfulFetchResponse(response) - ); - } - - await Promise.all(storageOperations); - - return response; - } -} diff --git a/packages-exp/remote-config-exp/src/client/remote_config_fetch_client.ts b/packages-exp/remote-config-exp/src/client/remote_config_fetch_client.ts deleted file mode 100644 index 25e00299855..00000000000 --- a/packages-exp/remote-config-exp/src/client/remote_config_fetch_client.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Defines a client, as in https://en.wikipedia.org/wiki/Client%E2%80%93server_model, for the - * Remote Config server (https://firebase.google.com/docs/reference/remote-config/rest). - * - *

Abstracts throttle, response cache and network implementation details. - * - *

Modeled after the native {@link GlobalFetch} interface, which is relatively modern and - * convenient, but simplified for Remote Config's use case. - * - * Disambiguation: {@link GlobalFetch} interface and the Remote Config service define "fetch" - * methods. The RestClient uses the former to make HTTP calls. This interface abstracts the latter. - */ -export interface RemoteConfigFetchClient { - /** - * @throws if response status is not 200 or 304. - */ - fetch(request: FetchRequest): Promise; -} - -/** - * Defines a self-descriptive reference for config key-value pairs. - */ -export interface FirebaseRemoteConfigObject { - [key: string]: string; -} - -/** - * Shims a minimal AbortSignal. - * - *

AbortController's AbortSignal conveniently decouples fetch timeout logic from other aspects - * of networking, such as retries. Firebase doesn't use AbortController enough to justify a - * polyfill recommendation, like we do with the Fetch API, but this minimal shim can easily be - * swapped out if/when we do. - */ -export class RemoteConfigAbortSignal { - listeners: Array<() => void> = []; - addEventListener(listener: () => void): void { - this.listeners.push(listener); - } - abort(): void { - this.listeners.forEach(listener => listener()); - } -} - -/** - * Defines per-request inputs for the Remote Config fetch request. - * - *

Modeled after the native {@link Request} interface, but simplified for Remote Config's - * use case. - */ -export interface FetchRequest { - /** - * Uses cached config if it is younger than this age. - * - *

Required because it's defined by settings, which always have a value. - * - *

Comparable to passing `headers = { 'Cache-Control': max-age }` to the native - * Fetch API. - */ - cacheMaxAgeMillis: number; - - /** - * An event bus for the signal to abort a request. - * - *

Required because all requests should be abortable. - * - *

Comparable to the native - * Fetch API's "signal" field on its request configuration object - * https://fetch.spec.whatwg.org/#dom-requestinit-signal. - * - *

Disambiguation: Remote Config commonly refers to API inputs as - * "signals". See the private ConfigFetchRequestBody interface for those: - * http://google3/firebase/remote_config/web/src/core/rest_client.ts?l=14&rcl=255515243. - */ - signal: RemoteConfigAbortSignal; - - /** - * The ETag header value from the last response. - * - *

Optional in case this is the first request. - * - *

Comparable to passing `headers = { 'If-None-Match': }` to the native Fetch API. - */ - eTag?: string; -} - -/** - * Defines a successful response (200 or 304). - * - *

Modeled after the native {@link Response} interface, but simplified for Remote Config's - * use case. - */ -export interface FetchResponse { - /** - * The HTTP status, which is useful for differentiating success responses with data from - * those without. - * - *

{@link RemoteConfigClient} is modeled after the native {@link GlobalFetch} interface, so - * HTTP status is first-class. - * - *

Disambiguation: the fetch response returns a legacy "state" value that is redundant with the - * HTTP status code. The former is normalized into the latter. - */ - status: number; - - /** - * Defines the ETag response header value. - * - *

Only defined for 200 and 304 responses. - */ - eTag?: string; - - /** - * Defines the map of parameters returned as "entries" in the fetch response body. - * - *

Only defined for 200 responses. - */ - config?: FirebaseRemoteConfigObject; - - // Note: we're not extracting experiment metadata until - // ABT and Analytics have Web SDKs. -} diff --git a/packages-exp/remote-config-exp/src/client/rest_client.ts b/packages-exp/remote-config-exp/src/client/rest_client.ts deleted file mode 100644 index a5b521a421e..00000000000 --- a/packages-exp/remote-config-exp/src/client/rest_client.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - FetchResponse, - RemoteConfigFetchClient, - FirebaseRemoteConfigObject, - FetchRequest -} from './remote_config_fetch_client'; -import { ERROR_FACTORY, ErrorCode } from '../errors'; -import { getUserLanguage } from '../language'; -import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; - -/** - * Defines request body parameters required to call the fetch API: - * https://firebase.google.com/docs/reference/remote-config/rest - * - *

Not exported because this file encapsulates REST API specifics. - * - *

Not passing User Properties because Analytics' source of truth on Web is server-side. - */ -interface FetchRequestBody { - // Disables camelcase linting for request body params. - /* eslint-disable camelcase*/ - sdk_version: string; - app_instance_id: string; - app_instance_id_token: string; - app_id: string; - language_code: string; - /* eslint-enable camelcase */ -} - -/** - * Implements the Client abstraction for the Remote Config REST API. - */ -export class RestClient implements RemoteConfigFetchClient { - constructor( - private readonly firebaseInstallations: _FirebaseInstallationsInternal, - private readonly sdkVersion: string, - private readonly namespace: string, - private readonly projectId: string, - private readonly apiKey: string, - private readonly appId: string - ) {} - - /** - * Fetches from the Remote Config REST API. - * - * @throws a {@link ErrorCode.FETCH_NETWORK} error if {@link GlobalFetch#fetch} can't - * connect to the network. - * @throws a {@link ErrorCode.FETCH_PARSE} error if {@link Response#json} can't parse the - * fetch response. - * @throws a {@link ErrorCode.FETCH_STATUS} error if the service returns an HTTP error status. - */ - async fetch(request: FetchRequest): Promise { - const [installationId, installationToken] = await Promise.all([ - this.firebaseInstallations.getId(), - this.firebaseInstallations.getToken() - ]); - - const urlBase = - window.FIREBASE_REMOTE_CONFIG_URL_BASE || - 'https://firebaseremoteconfig.googleapis.com'; - - const url = `${urlBase}/v1/projects/${this.projectId}/namespaces/${this.namespace}:fetch?key=${this.apiKey}`; - - const headers = { - 'Content-Type': 'application/json', - 'Content-Encoding': 'gzip', - // Deviates from pure decorator by not passing max-age header since we don't currently have - // service behavior using that header. - 'If-None-Match': request.eTag || '*' - }; - - const requestBody: FetchRequestBody = { - /* eslint-disable camelcase */ - sdk_version: this.sdkVersion, - app_instance_id: installationId, - app_instance_id_token: installationToken, - app_id: this.appId, - language_code: getUserLanguage() - /* eslint-enable camelcase */ - }; - - const options = { - method: 'POST', - headers, - body: JSON.stringify(requestBody) - }; - - // This logic isn't REST-specific, but shimming abort logic isn't worth another decorator. - const fetchPromise = fetch(url, options); - const timeoutPromise = new Promise((_resolve, reject) => { - // Maps async event listener to Promise API. - request.signal.addEventListener(() => { - // Emulates https://heycam.github.io/webidl/#aborterror - const error = new Error('The operation was aborted.'); - error.name = 'AbortError'; - reject(error); - }); - }); - - let response; - try { - await Promise.race([fetchPromise, timeoutPromise]); - response = await fetchPromise; - } catch (originalError) { - let errorCode = ErrorCode.FETCH_NETWORK; - if (originalError.name === 'AbortError') { - errorCode = ErrorCode.FETCH_TIMEOUT; - } - throw ERROR_FACTORY.create(errorCode, { - originalErrorMessage: originalError.message - }); - } - - let status = response.status; - - // Normalizes nullable header to optional. - const responseEtag = response.headers.get('ETag') || undefined; - - let config: FirebaseRemoteConfigObject | undefined; - let state: string | undefined; - - // JSON parsing throws SyntaxError if the response body isn't a JSON string. - // Requesting application/json and checking for a 200 ensures there's JSON data. - if (response.status === 200) { - let responseBody; - try { - responseBody = await response.json(); - } catch (originalError) { - throw ERROR_FACTORY.create(ErrorCode.FETCH_PARSE, { - originalErrorMessage: originalError.message - }); - } - config = responseBody['entries']; - state = responseBody['state']; - } - - // Normalizes based on legacy state. - if (state === 'INSTANCE_STATE_UNSPECIFIED') { - status = 500; - } else if (state === 'NO_CHANGE') { - status = 304; - } else if (state === 'NO_TEMPLATE' || state === 'EMPTY_CONFIG') { - // These cases can be fixed remotely, so normalize to safe value. - config = {}; - } - - // Normalize to exception-based control flow for non-success cases. - // Encapsulates HTTP specifics in this class as much as possible. Status is still the best for - // differentiating success states (200 from 304; the state body param is undefined in a - // standard 304). - if (status !== 304 && status !== 200) { - throw ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { - httpStatus: status - }); - } - - return { status, eTag: responseEtag, config }; - } -} diff --git a/packages-exp/remote-config-exp/src/client/retrying_client.ts b/packages-exp/remote-config-exp/src/client/retrying_client.ts deleted file mode 100644 index fe1737023df..00000000000 --- a/packages-exp/remote-config-exp/src/client/retrying_client.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - RemoteConfigAbortSignal, - RemoteConfigFetchClient, - FetchResponse, - FetchRequest -} from './remote_config_fetch_client'; -import { ThrottleMetadata, Storage } from '../storage/storage'; -import { ErrorCode, ERROR_FACTORY } from '../errors'; -import { FirebaseError, calculateBackoffMillis } from '@firebase/util'; - -/** - * Supports waiting on a backoff by: - * - *

    - *
  • Promisifying setTimeout, so we can set a timeout in our Promise chain
  • - *
  • Listening on a signal bus for abort events, just like the Fetch API
  • - *
  • Failing in the same way the Fetch API fails, so timing out a live request and a throttled - * request appear the same.
  • - *
- * - *

Visible for testing. - */ -export function setAbortableTimeout( - signal: RemoteConfigAbortSignal, - throttleEndTimeMillis: number -): Promise { - return new Promise((resolve, reject) => { - // Derives backoff from given end time, normalizing negative numbers to zero. - const backoffMillis = Math.max(throttleEndTimeMillis - Date.now(), 0); - - const timeout = setTimeout(resolve, backoffMillis); - - // Adds listener, rather than sets onabort, because signal is a shared object. - signal.addEventListener(() => { - clearTimeout(timeout); - - // If the request completes before this timeout, the rejection has no effect. - reject( - ERROR_FACTORY.create(ErrorCode.FETCH_THROTTLE, { - throttleEndTimeMillis - }) - ); - }); - }); -} - -type RetriableError = FirebaseError & { customData: { httpStatus: string } }; -/** - * Returns true if the {@link Error} indicates a fetch request may succeed later. - */ -function isRetriableError(e: Error): e is RetriableError { - if (!(e instanceof FirebaseError) || !e.customData) { - return false; - } - - // Uses string index defined by ErrorData, which FirebaseError implements. - const httpStatus = Number(e.customData['httpStatus']); - - return ( - httpStatus === 429 || - httpStatus === 500 || - httpStatus === 503 || - httpStatus === 504 - ); -} - -/** - * Decorates a Client with retry logic. - * - *

Comparable to CachingClient, but uses backoff logic instead of cache max age and doesn't cache - * responses (because the SDK has no use for error responses). - */ -export class RetryingClient implements RemoteConfigFetchClient { - constructor( - private readonly client: RemoteConfigFetchClient, - private readonly storage: Storage - ) {} - - async fetch(request: FetchRequest): Promise { - const throttleMetadata = (await this.storage.getThrottleMetadata()) || { - backoffCount: 0, - throttleEndTimeMillis: Date.now() - }; - - return this.attemptFetch(request, throttleMetadata); - } - - /** - * A recursive helper for attempting a fetch request repeatedly. - * - * @throws any non-retriable errors. - */ - async attemptFetch( - request: FetchRequest, - { throttleEndTimeMillis, backoffCount }: ThrottleMetadata - ): Promise { - // Starts with a (potentially zero) timeout to support resumption from stored state. - // Ensures the throttle end time is honored if the last attempt timed out. - // Note the SDK will never make a request if the fetch timeout expires at this point. - await setAbortableTimeout(request.signal, throttleEndTimeMillis); - - try { - const response = await this.client.fetch(request); - - // Note the SDK only clears throttle state if response is success or non-retriable. - await this.storage.deleteThrottleMetadata(); - - return response; - } catch (e) { - if (!isRetriableError(e)) { - throw e; - } - - // Increments backoff state. - const throttleMetadata = { - throttleEndTimeMillis: - Date.now() + calculateBackoffMillis(backoffCount), - backoffCount: backoffCount + 1 - }; - - // Persists state. - await this.storage.setThrottleMetadata(throttleMetadata); - - return this.attemptFetch(request, throttleMetadata); - } - } -} diff --git a/packages-exp/remote-config-exp/src/constants.ts b/packages-exp/remote-config-exp/src/constants.ts deleted file mode 100644 index 6bc7d1d5547..00000000000 --- a/packages-exp/remote-config-exp/src/constants.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const RC_COMPONENT_NAME = 'remote-config-exp'; diff --git a/packages-exp/remote-config-exp/src/errors.ts b/packages-exp/remote-config-exp/src/errors.ts deleted file mode 100644 index d4be9a09f76..00000000000 --- a/packages-exp/remote-config-exp/src/errors.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ErrorFactory, FirebaseError } from '@firebase/util'; - -export const enum ErrorCode { - REGISTRATION_WINDOW = 'registration-window', - REGISTRATION_PROJECT_ID = 'registration-project-id', - REGISTRATION_API_KEY = 'registration-api-key', - REGISTRATION_APP_ID = 'registration-app-id', - STORAGE_OPEN = 'storage-open', - STORAGE_GET = 'storage-get', - STORAGE_SET = 'storage-set', - STORAGE_DELETE = 'storage-delete', - FETCH_NETWORK = 'fetch-client-network', - FETCH_TIMEOUT = 'fetch-timeout', - FETCH_THROTTLE = 'fetch-throttle', - FETCH_PARSE = 'fetch-client-parse', - FETCH_STATUS = 'fetch-status' -} - -const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { - [ErrorCode.REGISTRATION_WINDOW]: - 'Undefined window object. This SDK only supports usage in a browser environment.', - [ErrorCode.REGISTRATION_PROJECT_ID]: - 'Undefined project identifier. Check Firebase app initialization.', - [ErrorCode.REGISTRATION_API_KEY]: - 'Undefined API key. Check Firebase app initialization.', - [ErrorCode.REGISTRATION_APP_ID]: - 'Undefined app identifier. Check Firebase app initialization.', - [ErrorCode.STORAGE_OPEN]: - 'Error thrown when opening storage. Original error: {$originalErrorMessage}.', - [ErrorCode.STORAGE_GET]: - 'Error thrown when reading from storage. Original error: {$originalErrorMessage}.', - [ErrorCode.STORAGE_SET]: - 'Error thrown when writing to storage. Original error: {$originalErrorMessage}.', - [ErrorCode.STORAGE_DELETE]: - 'Error thrown when deleting from storage. Original error: {$originalErrorMessage}.', - [ErrorCode.FETCH_NETWORK]: - 'Fetch client failed to connect to a network. Check Internet connection.' + - ' Original error: {$originalErrorMessage}.', - [ErrorCode.FETCH_TIMEOUT]: - 'The config fetch request timed out. ' + - ' Configure timeout using "fetchTimeoutMillis" SDK setting.', - [ErrorCode.FETCH_THROTTLE]: - 'The config fetch request timed out while in an exponential backoff state.' + - ' Configure timeout using "fetchTimeoutMillis" SDK setting.' + - ' Unix timestamp in milliseconds when fetch request throttling ends: {$throttleEndTimeMillis}.', - [ErrorCode.FETCH_PARSE]: - 'Fetch client could not parse response.' + - ' Original error: {$originalErrorMessage}.', - [ErrorCode.FETCH_STATUS]: - 'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.' -}; - -// Note this is effectively a type system binding a code to params. This approach overlaps with the -// role of TS interfaces, but works well for a few reasons: -// 1) JS is unaware of TS interfaces, eg we can't test for interface implementation in JS -// 2) callers should have access to a human-readable summary of the error and this interpolates -// params into an error message; -// 3) callers should be able to programmatically access data associated with an error, which -// ErrorData provides. -interface ErrorParams { - [ErrorCode.STORAGE_OPEN]: { originalErrorMessage: string | undefined }; - [ErrorCode.STORAGE_GET]: { originalErrorMessage: string | undefined }; - [ErrorCode.STORAGE_SET]: { originalErrorMessage: string | undefined }; - [ErrorCode.STORAGE_DELETE]: { originalErrorMessage: string | undefined }; - [ErrorCode.FETCH_NETWORK]: { originalErrorMessage: string }; - [ErrorCode.FETCH_THROTTLE]: { throttleEndTimeMillis: number }; - [ErrorCode.FETCH_PARSE]: { originalErrorMessage: string }; - [ErrorCode.FETCH_STATUS]: { httpStatus: number }; -} - -export const ERROR_FACTORY = new ErrorFactory( - 'remoteconfig' /* service */, - 'Remote Config' /* service name */, - ERROR_DESCRIPTION_MAP -); - -// Note how this is like typeof/instanceof, but for ErrorCode. -export function hasErrorCode(e: Error, errorCode: ErrorCode): boolean { - return e instanceof FirebaseError && e.code.indexOf(errorCode) !== -1; -} diff --git a/packages-exp/remote-config-exp/src/index.ts b/packages-exp/remote-config-exp/src/index.ts deleted file mode 100644 index 2ccde753bdf..00000000000 --- a/packages-exp/remote-config-exp/src/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { registerRemoteConfig } from './register'; - -// Facilitates debugging by enabling settings changes without rebuilding asset. -// Note these debug options are not part of a documented, supported API and can change at any time. -// Consolidates debug options for easier discovery. -// Uses transient variables on window to avoid lingering state causing panic. -declare global { - interface Window { - FIREBASE_REMOTE_CONFIG_URL_BASE: string; - } -} - -export * from './api'; -export * from './api2'; - -/** register component and version */ -registerRemoteConfig(); diff --git a/packages-exp/remote-config-exp/src/language.ts b/packages-exp/remote-config-exp/src/language.ts deleted file mode 100644 index 9c44ee275bf..00000000000 --- a/packages-exp/remote-config-exp/src/language.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Attempts to get the most accurate browser language setting. - * - *

Adapted from getUserLanguage in packages/auth/src/utils.js for TypeScript. - * - *

Defers default language specification to server logic for consistency. - * - * @param navigatorLanguage Enables tests to override read-only {@link NavigatorLanguage}. - */ -export function getUserLanguage( - navigatorLanguage: NavigatorLanguage = navigator -): string { - return ( - // Most reliable, but only supported in Chrome/Firefox. - (navigatorLanguage.languages && navigatorLanguage.languages[0]) || - // Supported in most browsers, but returns the language of the browser - // UI, not the language set in browser settings. - navigatorLanguage.language - // Polyfill otherwise. - ); -} diff --git a/packages-exp/remote-config-exp/src/register.ts b/packages-exp/remote-config-exp/src/register.ts deleted file mode 100644 index 257a5d68e55..00000000000 --- a/packages-exp/remote-config-exp/src/register.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - _registerComponent, - registerVersion, - SDK_VERSION -} from '@firebase/app-exp'; -import { - Component, - ComponentType, - ComponentContainer -} from '@firebase/component'; -import { Logger, LogLevel as FirebaseLogLevel } from '@firebase/logger'; -import { RemoteConfig } from '@firebase/remote-config-types-exp'; -import { name as packageName, version } from '../package.json'; -import { ensureInitialized } from './api'; -import { CachingClient } from './client/caching_client'; -import { RestClient } from './client/rest_client'; -import { RetryingClient } from './client/retrying_client'; -import { RC_COMPONENT_NAME } from './constants'; -import { ErrorCode, ERROR_FACTORY } from './errors'; -import { RemoteConfig as RemoteConfigImpl } from './remote_config'; -import { Storage } from './storage/storage'; -import { StorageCache } from './storage/storage_cache'; -// This needs to be in the same file that calls `getProvider()` on the component -// or it will get tree-shaken out. -import '@firebase/installations-exp'; - -export function registerRemoteConfig(): void { - _registerComponent( - new Component( - RC_COMPONENT_NAME, - remoteConfigFactory, - ComponentType.PUBLIC - ).setMultipleInstances(true) - ); - - registerVersion(packageName, version); - - function remoteConfigFactory( - container: ComponentContainer, - namespace?: string - ): RemoteConfig { - /* Dependencies */ - // getImmediate for FirebaseApp will always succeed - const app = container.getProvider('app-exp').getImmediate(); - // The following call will always succeed because rc has `import '@firebase/installations'` - const installations = container - .getProvider('installations-exp-internal') - .getImmediate(); - - // Guards against the SDK being used in non-browser environments. - if (typeof window === 'undefined') { - throw ERROR_FACTORY.create(ErrorCode.REGISTRATION_WINDOW); - } - - // Normalizes optional inputs. - const { projectId, apiKey, appId } = app.options; - if (!projectId) { - throw ERROR_FACTORY.create(ErrorCode.REGISTRATION_PROJECT_ID); - } - if (!apiKey) { - throw ERROR_FACTORY.create(ErrorCode.REGISTRATION_API_KEY); - } - if (!appId) { - throw ERROR_FACTORY.create(ErrorCode.REGISTRATION_APP_ID); - } - namespace = namespace || 'firebase'; - - const storage = new Storage(appId, app.name, namespace); - const storageCache = new StorageCache(storage); - - const logger = new Logger(packageName); - - // Sets ERROR as the default log level. - // See RemoteConfig#setLogLevel for corresponding normalization to ERROR log level. - logger.logLevel = FirebaseLogLevel.ERROR; - - const restClient = new RestClient( - installations, - // Uses the JS SDK version, by which the RC package version can be deduced, if necessary. - SDK_VERSION, - namespace, - projectId, - apiKey, - appId - ); - const retryingClient = new RetryingClient(restClient, storage); - const cachingClient = new CachingClient( - retryingClient, - storage, - storageCache, - logger - ); - - const remoteConfigInstance = new RemoteConfigImpl( - app, - cachingClient, - storageCache, - storage, - logger - ); - - // Starts warming cache. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - ensureInitialized(remoteConfigInstance); - - return remoteConfigInstance; - } -} diff --git a/packages-exp/remote-config-exp/src/remote_config.ts b/packages-exp/remote-config-exp/src/remote_config.ts deleted file mode 100644 index de7825b348b..00000000000 --- a/packages-exp/remote-config-exp/src/remote_config.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { - RemoteConfig as RemoteConfigType, - FetchStatus, - Settings -} from '@firebase/remote-config-types-exp'; -import { StorageCache } from './storage/storage_cache'; -import { RemoteConfigFetchClient } from './client/remote_config_fetch_client'; -import { Storage } from './storage/storage'; -import { Logger } from '@firebase/logger'; - -const DEFAULT_FETCH_TIMEOUT_MILLIS = 60 * 1000; // One minute -const DEFAULT_CACHE_MAX_AGE_MILLIS = 12 * 60 * 60 * 1000; // Twelve hours. - -/** - * Encapsulates business logic mapping network and storage dependencies to the public SDK API. - * - * See {@link https://github.com/FirebasePrivate/firebase-js-sdk/blob/master/packages/firebase/index.d.ts|interface documentation} for method descriptions. - */ -export class RemoteConfig implements RemoteConfigType { - /** - * Tracks completion of initialization promise. - * @internal - */ - _isInitializationComplete = false; - - /** - * De-duplicates initialization calls. - * @internal - */ - _initializePromise?: Promise; - - settings: Settings = { - fetchTimeoutMillis: DEFAULT_FETCH_TIMEOUT_MILLIS, - minimumFetchIntervalMillis: DEFAULT_CACHE_MAX_AGE_MILLIS - }; - - defaultConfig: { [key: string]: string | number | boolean } = {}; - - get fetchTimeMillis(): number { - return this._storageCache.getLastSuccessfulFetchTimestampMillis() || -1; - } - - get lastFetchStatus(): FetchStatus { - return this._storageCache.getLastFetchStatus() || 'no-fetch-yet'; - } - - constructor( - // Required by FirebaseServiceFactory interface. - readonly app: FirebaseApp, - // JS doesn't support private yet - // (https://github.com/tc39/proposal-class-fields#private-fields), so we hint using an - // underscore prefix. - /** - * @internal - */ - readonly _client: RemoteConfigFetchClient, - /** - * @internal - */ - readonly _storageCache: StorageCache, - /** - * @internal - */ - readonly _storage: Storage, - /** - * @internal - */ - readonly _logger: Logger - ) {} -} diff --git a/packages-exp/remote-config-exp/src/storage/storage.ts b/packages-exp/remote-config-exp/src/storage/storage.ts deleted file mode 100644 index f5f457161b1..00000000000 --- a/packages-exp/remote-config-exp/src/storage/storage.ts +++ /dev/null @@ -1,260 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FetchStatus } from '@firebase/remote-config-types'; -import { - FetchResponse, - FirebaseRemoteConfigObject -} from '../client/remote_config_fetch_client'; -import { ERROR_FACTORY, ErrorCode } from '../errors'; -import { FirebaseError } from '@firebase/util'; - -/** - * Converts an error event associated with a {@link IDBRequest} to a {@link FirebaseError}. - */ -function toFirebaseError(event: Event, errorCode: ErrorCode): FirebaseError { - const originalError = (event.target as IDBRequest).error || undefined; - return ERROR_FACTORY.create(errorCode, { - originalErrorMessage: originalError && originalError.message - }); -} - -/** - * A general-purpose store keyed by app + namespace + {@link - * ProjectNamespaceKeyFieldValue}. - * - *

The Remote Config SDK can be used with multiple app installations, and each app can interact - * with multiple namespaces, so this store uses app (ID + name) and namespace as common parent keys - * for a set of key-value pairs. See {@link Storage#createCompositeKey}. - * - *

Visible for testing. - */ -export const APP_NAMESPACE_STORE = 'app_namespace_store'; - -const DB_NAME = 'firebase_remote_config'; -const DB_VERSION = 1; - -/** - * Encapsulates metadata concerning throttled fetch requests. - */ -export interface ThrottleMetadata { - // The number of times fetch has backed off. Used for resuming backoff after a timeout. - backoffCount: number; - // The Unix timestamp in milliseconds when callers can retry a request. - throttleEndTimeMillis: number; -} - -/** - * Provides type-safety for the "key" field used by {@link APP_NAMESPACE_STORE}. - * - *

This seems like a small price to avoid potentially subtle bugs caused by a typo. - */ -type ProjectNamespaceKeyFieldValue = - | 'active_config' - | 'active_config_etag' - | 'last_fetch_status' - | 'last_successful_fetch_timestamp_millis' - | 'last_successful_fetch_response' - | 'settings' - | 'throttle_metadata'; - -// Visible for testing. -export function openDatabase(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, DB_VERSION); - request.onerror = event => { - reject(toFirebaseError(event, ErrorCode.STORAGE_OPEN)); - }; - request.onsuccess = event => { - resolve((event.target as IDBOpenDBRequest).result); - }; - request.onupgradeneeded = event => { - const db = (event.target as IDBOpenDBRequest).result; - - // We don't use 'break' in this switch statement, the fall-through - // behavior is what we want, because if there are multiple versions between - // the old version and the current version, we want ALL the migrations - // that correspond to those versions to run, not only the last one. - // eslint-disable-next-line default-case - switch (event.oldVersion) { - case 0: - db.createObjectStore(APP_NAMESPACE_STORE, { - keyPath: 'compositeKey' - }); - } - }; - }); -} - -/** - * Abstracts data persistence. - */ -export class Storage { - /** - * @param appId enables storage segmentation by app (ID + name). - * @param appName enables storage segmentation by app (ID + name). - * @param namespace enables storage segmentation by namespace. - */ - constructor( - private readonly appId: string, - private readonly appName: string, - private readonly namespace: string, - private readonly openDbPromise = openDatabase() - ) {} - - getLastFetchStatus(): Promise { - return this.get('last_fetch_status'); - } - - setLastFetchStatus(status: FetchStatus): Promise { - return this.set('last_fetch_status', status); - } - - // This is comparable to a cache entry timestamp. If we need to expire other data, we could - // consider adding timestamp to all storage records and an optional max age arg to getters. - getLastSuccessfulFetchTimestampMillis(): Promise { - return this.get('last_successful_fetch_timestamp_millis'); - } - - setLastSuccessfulFetchTimestampMillis(timestamp: number): Promise { - return this.set( - 'last_successful_fetch_timestamp_millis', - timestamp - ); - } - - getLastSuccessfulFetchResponse(): Promise { - return this.get('last_successful_fetch_response'); - } - - setLastSuccessfulFetchResponse(response: FetchResponse): Promise { - return this.set('last_successful_fetch_response', response); - } - - getActiveConfig(): Promise { - return this.get('active_config'); - } - - setActiveConfig(config: FirebaseRemoteConfigObject): Promise { - return this.set('active_config', config); - } - - getActiveConfigEtag(): Promise { - return this.get('active_config_etag'); - } - - setActiveConfigEtag(etag: string): Promise { - return this.set('active_config_etag', etag); - } - - getThrottleMetadata(): Promise { - return this.get('throttle_metadata'); - } - - setThrottleMetadata(metadata: ThrottleMetadata): Promise { - return this.set('throttle_metadata', metadata); - } - - deleteThrottleMetadata(): Promise { - return this.delete('throttle_metadata'); - } - - async get(key: ProjectNamespaceKeyFieldValue): Promise { - const db = await this.openDbPromise; - return new Promise((resolve, reject) => { - const transaction = db.transaction([APP_NAMESPACE_STORE], 'readonly'); - const objectStore = transaction.objectStore(APP_NAMESPACE_STORE); - const compositeKey = this.createCompositeKey(key); - try { - const request = objectStore.get(compositeKey); - request.onerror = event => { - reject(toFirebaseError(event, ErrorCode.STORAGE_GET)); - }; - request.onsuccess = event => { - const result = (event.target as IDBRequest).result; - if (result) { - resolve(result.value); - } else { - resolve(undefined); - } - }; - } catch (e) { - reject( - ERROR_FACTORY.create(ErrorCode.STORAGE_GET, { - originalErrorMessage: e && e.message - }) - ); - } - }); - } - - async set(key: ProjectNamespaceKeyFieldValue, value: T): Promise { - const db = await this.openDbPromise; - return new Promise((resolve, reject) => { - const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite'); - const objectStore = transaction.objectStore(APP_NAMESPACE_STORE); - const compositeKey = this.createCompositeKey(key); - try { - const request = objectStore.put({ - compositeKey, - value - }); - request.onerror = (event: Event) => { - reject(toFirebaseError(event, ErrorCode.STORAGE_SET)); - }; - request.onsuccess = () => { - resolve(); - }; - } catch (e) { - reject( - ERROR_FACTORY.create(ErrorCode.STORAGE_SET, { - originalErrorMessage: e && e.message - }) - ); - } - }); - } - - async delete(key: ProjectNamespaceKeyFieldValue): Promise { - const db = await this.openDbPromise; - return new Promise((resolve, reject) => { - const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite'); - const objectStore = transaction.objectStore(APP_NAMESPACE_STORE); - const compositeKey = this.createCompositeKey(key); - try { - const request = objectStore.delete(compositeKey); - request.onerror = (event: Event) => { - reject(toFirebaseError(event, ErrorCode.STORAGE_DELETE)); - }; - request.onsuccess = () => { - resolve(); - }; - } catch (e) { - reject( - ERROR_FACTORY.create(ErrorCode.STORAGE_DELETE, { - originalErrorMessage: e && e.message - }) - ); - } - }); - } - - // Facilitates composite key functionality (which is unsupported in IE). - createCompositeKey(key: ProjectNamespaceKeyFieldValue): string { - return [this.appId, this.appName, this.namespace, key].join(); - } -} diff --git a/packages-exp/remote-config-exp/src/storage/storage_cache.ts b/packages-exp/remote-config-exp/src/storage/storage_cache.ts deleted file mode 100644 index 5ffbdba20c0..00000000000 --- a/packages-exp/remote-config-exp/src/storage/storage_cache.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FetchStatus } from '@firebase/remote-config-types'; -import { FirebaseRemoteConfigObject } from '../client/remote_config_fetch_client'; -import { Storage } from './storage'; - -/** - * A memory cache layer over storage to support the SDK's synchronous read requirements. - */ -export class StorageCache { - constructor(private readonly storage: Storage) {} - - /** - * Memory caches. - */ - private lastFetchStatus?: FetchStatus; - private lastSuccessfulFetchTimestampMillis?: number; - private activeConfig?: FirebaseRemoteConfigObject; - - /** - * Memory-only getters - */ - getLastFetchStatus(): FetchStatus | undefined { - return this.lastFetchStatus; - } - - getLastSuccessfulFetchTimestampMillis(): number | undefined { - return this.lastSuccessfulFetchTimestampMillis; - } - - getActiveConfig(): FirebaseRemoteConfigObject | undefined { - return this.activeConfig; - } - - /** - * Read-ahead getter - */ - async loadFromStorage(): Promise { - const lastFetchStatusPromise = this.storage.getLastFetchStatus(); - const lastSuccessfulFetchTimestampMillisPromise = this.storage.getLastSuccessfulFetchTimestampMillis(); - const activeConfigPromise = this.storage.getActiveConfig(); - - // Note: - // 1. we consistently check for undefined to avoid clobbering defined values - // in memory - // 2. we defer awaiting to improve readability, as opposed to destructuring - // a Promise.all result, for example - - const lastFetchStatus = await lastFetchStatusPromise; - if (lastFetchStatus) { - this.lastFetchStatus = lastFetchStatus; - } - - const lastSuccessfulFetchTimestampMillis = await lastSuccessfulFetchTimestampMillisPromise; - if (lastSuccessfulFetchTimestampMillis) { - this.lastSuccessfulFetchTimestampMillis = lastSuccessfulFetchTimestampMillis; - } - - const activeConfig = await activeConfigPromise; - if (activeConfig) { - this.activeConfig = activeConfig; - } - } - - /** - * Write-through setters - */ - setLastFetchStatus(status: FetchStatus): Promise { - this.lastFetchStatus = status; - return this.storage.setLastFetchStatus(status); - } - - setLastSuccessfulFetchTimestampMillis( - timestampMillis: number - ): Promise { - this.lastSuccessfulFetchTimestampMillis = timestampMillis; - return this.storage.setLastSuccessfulFetchTimestampMillis(timestampMillis); - } - - setActiveConfig(activeConfig: FirebaseRemoteConfigObject): Promise { - this.activeConfig = activeConfig; - return this.storage.setActiveConfig(activeConfig); - } -} diff --git a/packages-exp/remote-config-exp/src/value.ts b/packages-exp/remote-config-exp/src/value.ts deleted file mode 100644 index f3fb6ff9581..00000000000 --- a/packages-exp/remote-config-exp/src/value.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Value as ValueType, ValueSource } from '@firebase/remote-config-types'; - -const DEFAULT_VALUE_FOR_BOOLEAN = false; -const DEFAULT_VALUE_FOR_STRING = ''; -const DEFAULT_VALUE_FOR_NUMBER = 0; - -const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on']; - -export class Value implements ValueType { - constructor( - private readonly _source: ValueSource, - private readonly _value: string = DEFAULT_VALUE_FOR_STRING - ) {} - - asString(): string { - return this._value; - } - - asBoolean(): boolean { - if (this._source === 'static') { - return DEFAULT_VALUE_FOR_BOOLEAN; - } - return BOOLEAN_TRUTHY_VALUES.indexOf(this._value.toLowerCase()) >= 0; - } - - asNumber(): number { - if (this._source === 'static') { - return DEFAULT_VALUE_FOR_NUMBER; - } - let num = Number(this._value); - if (isNaN(num)) { - num = DEFAULT_VALUE_FOR_NUMBER; - } - return num; - } - - getSource(): ValueSource { - return this._source; - } -} diff --git a/packages-exp/remote-config-exp/test/client/caching_client.test.ts b/packages-exp/remote-config-exp/test/client/caching_client.test.ts deleted file mode 100644 index a808dffb605..00000000000 --- a/packages-exp/remote-config-exp/test/client/caching_client.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '../setup'; -import { expect } from 'chai'; -import { - RemoteConfigFetchClient, - FetchResponse, - FetchRequest, - RemoteConfigAbortSignal -} from '../../src/client/remote_config_fetch_client'; -import * as sinon from 'sinon'; -import { CachingClient } from '../../src/client/caching_client'; -import { StorageCache } from '../../src/storage/storage_cache'; -import { Storage } from '../../src/storage/storage'; -import { Logger } from '@firebase/logger'; - -const DEFAULT_REQUEST: FetchRequest = { - // Invalidates cache by default. - cacheMaxAgeMillis: 0, - signal: new RemoteConfigAbortSignal() -}; - -describe('CachingClient', () => { - const backingClient = {} as RemoteConfigFetchClient; - const storageCache = {} as StorageCache; - const logger = {} as Logger; - const storage = {} as Storage; - let cachingClient: CachingClient; - let clock: sinon.SinonFakeTimers; - - beforeEach(() => { - logger.debug = sinon.stub(); - cachingClient = new CachingClient( - backingClient, - storage, - storageCache, - logger - ); - clock = sinon.useFakeTimers({ now: 3000 }); // Mocks Date.now as 3000. - }); - - afterEach(() => { - clock.restore(); - }); - - describe('isCacheDataFresh', () => { - it('returns false if cached response is older than max age', () => { - expect( - cachingClient.isCachedDataFresh( - // Mocks a cache set when Date.now was 1000, ie it's two seconds old. - 1000, - // Tolerates a cache one second old. - 1000 - ) - ).to.be.false; - }); - - it('returns true if cached response is equal to max age', () => { - expect(cachingClient.isCachedDataFresh(2000, 1000)).to.be.true; - }); - - it('returns true if cached response is younger than max age', () => { - expect(cachingClient.isCachedDataFresh(3000, 1000)).to.be.true; - }); - }); - - describe('fetch', () => { - beforeEach(() => { - storage.getLastSuccessfulFetchTimestampMillis = sinon - .stub() - .returns(1000); // Mocks a cache set when Date.now was 1000, ie it's two seconds old. - storageCache.setLastSuccessfulFetchTimestampMillis = sinon.stub(); - storage.getLastSuccessfulFetchResponse = sinon.stub(); - storage.setLastSuccessfulFetchResponse = sinon.stub(); - backingClient.fetch = sinon.stub().returns(Promise.resolve({})); - }); - - it('exits early on cache hit', async () => { - const expectedResponse = { config: { eTag: 'etag', color: 'taupe' } }; - storage.getLastSuccessfulFetchResponse = sinon - .stub() - .returns(expectedResponse); - - const actualResponse = await cachingClient.fetch({ - cacheMaxAgeMillis: 2000, - signal: new RemoteConfigAbortSignal() - }); - - expect(actualResponse).to.deep.eq(expectedResponse); - expect(backingClient.fetch).not.to.have.been.called; - }); - - it('fetches on cache miss', async () => { - await cachingClient.fetch(DEFAULT_REQUEST); - - expect(backingClient.fetch).to.have.been.called; - }); - - it('passes etag from last successful fetch', async () => { - const lastSuccessfulFetchResponse = { eTag: 'etag' } as FetchResponse; - storage.getLastSuccessfulFetchResponse = sinon - .stub() - .returns(lastSuccessfulFetchResponse); - - await cachingClient.fetch(DEFAULT_REQUEST); - - expect(backingClient.fetch).to.have.been.calledWith( - Object.assign({}, DEFAULT_REQUEST, { - eTag: lastSuccessfulFetchResponse.eTag - }) - ); - }); - - it('caches timestamp and response if status is 200', async () => { - const response = { - status: 200, - eTag: 'etag', - config: { color: 'clear' } - }; - backingClient.fetch = sinon.stub().returns(Promise.resolve(response)); - - await cachingClient.fetch(DEFAULT_REQUEST); - - expect( - storageCache.setLastSuccessfulFetchTimestampMillis - ).to.have.been.calledWith(3000); // Based on mock timer in beforeEach. - expect(storage.setLastSuccessfulFetchResponse).to.have.been.calledWith( - response - ); - }); - - it('sets timestamp, but not config, if 304', async () => { - backingClient.fetch = sinon - .stub() - .returns(Promise.resolve({ status: 304 })); - - await cachingClient.fetch(DEFAULT_REQUEST); - - expect( - storageCache.setLastSuccessfulFetchTimestampMillis - ).to.have.been.calledWith(3000); // Based on mock timer in beforeEach. - expect(storage.setLastSuccessfulFetchResponse).not.to.have.been.called; - }); - }); -}); diff --git a/packages-exp/remote-config-exp/test/client/rest_client.test.ts b/packages-exp/remote-config-exp/test/client/rest_client.test.ts deleted file mode 100644 index 89b745dacca..00000000000 --- a/packages-exp/remote-config-exp/test/client/rest_client.test.ts +++ /dev/null @@ -1,270 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '../setup'; -import { expect } from 'chai'; -import { RestClient } from '../../src/client/rest_client'; -import { FirebaseInstallations } from '@firebase/installations-types'; -import * as sinon from 'sinon'; -import { ERROR_FACTORY, ErrorCode } from '../../src/errors'; -import { FirebaseError } from '@firebase/util'; -import { - FetchRequest, - RemoteConfigAbortSignal -} from '../../src/client/remote_config_fetch_client'; - -const DEFAULT_REQUEST: FetchRequest = { - cacheMaxAgeMillis: 1, - signal: new RemoteConfigAbortSignal() -}; - -describe('RestClient', () => { - const firebaseInstallations = {} as FirebaseInstallations; - let client: RestClient; - - beforeEach(() => { - client = new RestClient( - firebaseInstallations, - 'sdk-version', - 'namespace', - 'project-id', - 'api-key', - 'app-id' - ); - firebaseInstallations.getId = sinon - .stub() - .returns(Promise.resolve('fis-id')); - firebaseInstallations.getToken = sinon - .stub() - .returns(Promise.resolve('fis-token')); - }); - - describe('fetch', () => { - let fetchStub: sinon.SinonStub< - [RequestInfo, RequestInit?], - Promise - >; - - beforeEach(() => { - fetchStub = sinon - .stub(window, 'fetch') - .returns(Promise.resolve(new Response('{}'))); - }); - - afterEach(() => { - fetchStub.restore(); - }); - - it('handles 200/UPDATE responses', async () => { - const expectedResponse = { - status: 200, - eTag: 'etag', - state: 'UPDATE', - entries: { color: 'sparkling' } - }; - - fetchStub.returns( - Promise.resolve({ - ok: true, - status: expectedResponse.status, - headers: new Headers({ ETag: expectedResponse.eTag }), - json: () => - Promise.resolve({ - entries: expectedResponse.entries, - state: expectedResponse.state - }) - } as Response) - ); - - const response = await client.fetch(DEFAULT_REQUEST); - - expect(response).to.deep.eq({ - status: expectedResponse.status, - eTag: expectedResponse.eTag, - config: expectedResponse.entries - }); - }); - - it('calls the correct endpoint', async () => { - await client.fetch(DEFAULT_REQUEST); - - expect(fetchStub).to.be.calledWith( - 'https://firebaseremoteconfig.googleapis.com/v1/projects/project-id/namespaces/namespace:fetch?key=api-key', - sinon.match.object - ); - }); - - it('passes injected params', async () => { - await client.fetch(DEFAULT_REQUEST); - - expect(fetchStub).to.be.calledWith( - sinon.match.string, - sinon.match({ - body: - '{"sdk_version":"sdk-version","app_instance_id":"fis-id","app_instance_id_token":"fis-token","app_id":"app-id","language_code":"en-US"}' - }) - ); - }); - - it('throws on network failure', async () => { - // The Fetch API throws a TypeError on network falure: - // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Exceptions - const originalError = new TypeError('Network request failed'); - fetchStub.returns(Promise.reject(originalError)); - - const fetchPromise = client.fetch(DEFAULT_REQUEST); - - const firebaseError = ERROR_FACTORY.create(ErrorCode.FETCH_NETWORK, { - originalErrorMessage: originalError.message - }); - - await expect(fetchPromise) - .to.eventually.be.rejectedWith(FirebaseError, firebaseError.message) - .with.nested.property( - 'customData.originalErrorMessage', - 'Network request failed' - ); - }); - - it('throws on JSON parse failure', async () => { - // JSON parsing throws a SyntaxError on failure: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Exceptions - const res = new Response(/* empty body */); - sinon - .stub(res, 'json') - .throws(new SyntaxError('Unexpected end of input')); - fetchStub.returns(Promise.resolve(res)); - - const fetchPromise = client.fetch(DEFAULT_REQUEST); - - const firebaseError = ERROR_FACTORY.create(ErrorCode.FETCH_PARSE, { - originalErrorMessage: 'Unexpected end of input' - }); - - await expect(fetchPromise) - .to.eventually.be.rejectedWith(FirebaseError, firebaseError.message) - .with.nested.property( - 'customData.originalErrorMessage', - 'Unexpected end of input' - ); - }); - - it('handles 304 status code and empty body', async () => { - fetchStub.returns( - Promise.resolve({ - status: 304, - headers: new Headers({ ETag: 'response-etag' }) - } as Response) - ); - - const response = await client.fetch( - Object.assign({}, DEFAULT_REQUEST, { - eTag: 'request-etag' - }) - ); - - expect(fetchStub).to.be.calledWith( - sinon.match.string, - sinon.match({ headers: { 'If-None-Match': 'request-etag' } }) - ); - - expect(response).to.deep.eq({ - status: 304, - eTag: 'response-etag', - config: undefined - }); - }); - - it('normalizes INSTANCE_STATE_UNSPECIFIED state to server error', async () => { - fetchStub.returns( - Promise.resolve({ - status: 200, - headers: new Headers({ ETag: 'etag' }), - json: async () => ({ state: 'INSTANCE_STATE_UNSPECIFIED' }) - } as Response) - ); - - const fetchPromise = client.fetch(DEFAULT_REQUEST); - - const error = ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { - httpStatus: 500 - }); - - await expect(fetchPromise) - .to.eventually.be.rejectedWith(FirebaseError, error.message) - .with.nested.property('customData.httpStatus', 500); - }); - - it('normalizes NO_CHANGE state to 304 status', async () => { - fetchStub.returns( - Promise.resolve({ - status: 200, - headers: new Headers({ ETag: 'etag' }), - json: async () => ({ state: 'NO_CHANGE' }) - } as Response) - ); - - const response = await client.fetch(DEFAULT_REQUEST); - - expect(response).to.deep.eq({ - status: 304, - eTag: 'etag', - config: undefined - }); - }); - - it('normalizes empty change states', async () => { - for (const state of ['NO_TEMPLATE', 'EMPTY_CONFIG']) { - fetchStub.returns( - Promise.resolve({ - status: 200, - headers: new Headers({ ETag: 'etag' }), - json: async () => ({ state }) - } as Response) - ); - - await expect(client.fetch(DEFAULT_REQUEST)).to.eventually.be.deep.eq({ - status: 200, - eTag: 'etag', - config: {} - }); - } - }); - - it('throws error on HTTP error status', async () => { - // Error codes from logs plus an arbitrary unexpected code (300) - for (const status of [300, 400, 403, 404, 415, 429, 500, 503, 504]) { - fetchStub.returns( - Promise.resolve({ - status, - headers: new Headers() - } as Response) - ); - - const fetchPromise = client.fetch(DEFAULT_REQUEST); - - const error = ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { - httpStatus: status - }); - - await expect(fetchPromise) - .to.eventually.be.rejectedWith(FirebaseError, error.message) - .with.nested.property('customData.httpStatus', status); - } - }); - }); -}); diff --git a/packages-exp/remote-config-exp/test/client/retrying_client.test.ts b/packages-exp/remote-config-exp/test/client/retrying_client.test.ts deleted file mode 100644 index 65641b438bd..00000000000 --- a/packages-exp/remote-config-exp/test/client/retrying_client.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { Storage, ThrottleMetadata } from '../../src/storage/storage'; -import { - RemoteConfigFetchClient, - FetchRequest, - FetchResponse, - RemoteConfigAbortSignal -} from '../../src/client/remote_config_fetch_client'; -import { - setAbortableTimeout, - RetryingClient -} from '../../src/client/retrying_client'; -import { ErrorCode, ERROR_FACTORY } from '../../src/errors'; -import '../setup'; - -const DEFAULT_REQUEST: FetchRequest = { - cacheMaxAgeMillis: 1, - signal: new RemoteConfigAbortSignal() -}; - -describe('RetryingClient', () => { - let backingClient: RemoteConfigFetchClient; - let storage: Storage; - let retryingClient: RetryingClient; - let abortSignal: RemoteConfigAbortSignal; - - beforeEach(() => { - backingClient = {} as RemoteConfigFetchClient; - storage = {} as Storage; - retryingClient = new RetryingClient(backingClient, storage); - storage.getThrottleMetadata = sinon.stub().returns(Promise.resolve()); - storage.deleteThrottleMetadata = sinon.stub().returns(Promise.resolve()); - storage.setThrottleMetadata = sinon.stub().returns(Promise.resolve()); - backingClient.fetch = sinon - .stub() - .returns(Promise.resolve({ status: 200 })); - abortSignal = new RemoteConfigAbortSignal(); - }); - - describe('setAbortableTimeout', () => { - let clock: sinon.SinonFakeTimers; - - beforeEach(() => { - // Sets Date.now() to zero. - clock = sinon.useFakeTimers(); - }); - - afterEach(() => { - clock.restore(); - }); - - it('Derives backoff from end time', async () => { - const setTimeoutSpy = sinon.spy(window, 'setTimeout'); - - const timeoutPromise = setAbortableTimeout(abortSignal, Date.now() + 1); - - // Advances mocked clock so setTimeout logic runs. - clock.runAll(); - - await timeoutPromise; - - expect(setTimeoutSpy).to.have.been.calledWith(sinon.match.any, 1); - }); - - it('Normalizes end time in the past to zero backoff', async () => { - const setTimeoutSpy = sinon.spy(window, 'setTimeout'); - - const timeoutPromise = setAbortableTimeout(abortSignal, Date.now() - 1); - - // Advances mocked clock so setTimeout logic runs. - clock.runAll(); - - await timeoutPromise; - - expect(setTimeoutSpy).to.have.been.calledWith(sinon.match.any, 0); - - setTimeoutSpy.restore(); - }); - - it('listens for abort event and rejects promise', async () => { - const throttleEndTimeMillis = 1000; - - const timeoutPromise = setAbortableTimeout( - abortSignal, - throttleEndTimeMillis - ); - - abortSignal.abort(); - - const expectedError = ERROR_FACTORY.create(ErrorCode.FETCH_THROTTLE, { - throttleEndTimeMillis - }); - - await expect(timeoutPromise).to.eventually.be.rejectedWith( - expectedError.message - ); - }); - }); - - describe('fetch', () => { - it('returns success response', async () => { - const setTimeoutSpy = sinon.spy(window, 'setTimeout'); - - const expectedResponse: FetchResponse = { - status: 200, - eTag: 'etag', - config: {} - }; - backingClient.fetch = sinon - .stub() - .returns(Promise.resolve(expectedResponse)); - - const actualResponse = retryingClient.fetch(DEFAULT_REQUEST); - - await expect(actualResponse).to.eventually.deep.eq(expectedResponse); - - // Asserts setTimeout is passed a zero delay, since throttleEndTimeMillis is set to Date.now, - // which is faked to be a constant. - expect(setTimeoutSpy).to.have.been.calledWith(sinon.match.any, 0); - - expect(storage.deleteThrottleMetadata).to.have.been.called; - - setTimeoutSpy.restore(); - }); - - it('rethrows unretriable errors rather than retrying', async () => { - const expectedError = ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { - httpStatus: 400 - }); - backingClient.fetch = sinon.stub().returns(Promise.reject(expectedError)); - - const fetchPromise = retryingClient.fetch(DEFAULT_REQUEST); - - await expect(fetchPromise).to.eventually.be.rejectedWith(expectedError); - }); - - it('retries on retriable errors', async () => { - // Configures Date.now() to advance clock from zero in 20ms increments, enabling - // tests to assert a known throttle end time and allow setTimeout to work. - const clock = sinon.useFakeTimers({ shouldAdvanceTime: true }); - - // Ensures backoff is always zero, which simplifies reasoning about timer. - const powSpy = sinon.stub(Math, 'pow').returns(0); - const randomSpy = sinon.stub(Math, 'random').returns(0.5); - - // Simulates a service call that returns errors several times before returning success. - // Error codes from logs. - const errorResponseStatuses = [429, 500, 503, 504]; - const errorResponseCount = errorResponseStatuses.length; - - backingClient.fetch = sinon.stub().callsFake(() => { - const httpStatus = errorResponseStatuses.pop(); - - if (httpStatus) { - // Triggers retry by returning a retriable status code. - const expectedError = ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { - httpStatus - }); - return Promise.reject(expectedError); - } - - // Halts retrying by returning success. - // Note backoff never terminates if the server always errors. - return Promise.resolve({ status: 200 }); - }); - - await retryingClient.fetch(DEFAULT_REQUEST); - - // Asserts throttle metadata was persisted after each error response. - for (let i = 1; i <= errorResponseCount; i++) { - expect(storage.setThrottleMetadata).to.have.been.calledWith({ - backoffCount: i, - throttleEndTimeMillis: i * 20 - }); - } - - powSpy.restore(); - randomSpy.restore(); - clock.restore(); - }); - }); - - describe('attemptFetch', () => { - it('honors metadata when initializing', async () => { - const clock = sinon.useFakeTimers({ shouldAdvanceTime: true }); - const setTimeoutSpy = sinon.spy(window, 'setTimeout'); - - const throttleMetadata = { - throttleEndTimeMillis: 123 - } as ThrottleMetadata; - - await retryingClient.attemptFetch(DEFAULT_REQUEST, throttleMetadata); - - expect(setTimeoutSpy).to.have.been.calledWith(sinon.match.any, 123); - - clock.restore(); - setTimeoutSpy.restore(); - }); - }); -}); diff --git a/packages-exp/remote-config-exp/test/errors.test.ts b/packages-exp/remote-config-exp/test/errors.test.ts deleted file mode 100644 index 67d41bfa505..00000000000 --- a/packages-exp/remote-config-exp/test/errors.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { hasErrorCode, ERROR_FACTORY, ErrorCode } from '../src/errors'; -import './setup'; - -describe('hasErrorCode', () => { - it('defaults false', () => { - const error = new Error(); - expect(hasErrorCode(error, ErrorCode.REGISTRATION_PROJECT_ID)).to.be.false; - }); - it('returns true for FirebaseError with given code', () => { - const error = ERROR_FACTORY.create(ErrorCode.REGISTRATION_PROJECT_ID); - expect(hasErrorCode(error, ErrorCode.REGISTRATION_PROJECT_ID)).to.be.true; - }); -}); diff --git a/packages-exp/remote-config-exp/test/language.test.ts b/packages-exp/remote-config-exp/test/language.test.ts deleted file mode 100644 index a92630467d4..00000000000 --- a/packages-exp/remote-config-exp/test/language.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { getUserLanguage } from '../src/language'; -import './setup'; - -// Adapts getUserLanguage tests from packages/auth/test/utils_test.js for TypeScript. -describe('getUserLanguage', () => { - it('prioritizes navigator.languages', () => { - expect( - getUserLanguage({ - languages: ['de', 'en'], - language: 'en' - }) - ).to.eq('de'); - }); - - it('falls back to navigator.language', () => { - expect( - getUserLanguage({ - language: 'en' - } as NavigatorLanguage) - ).to.eq('en'); - }); - - it('defaults undefined', () => { - expect(getUserLanguage({} as NavigatorLanguage)).to.be.undefined; - }); -}); diff --git a/packages-exp/remote-config-exp/test/remote_config.test.ts b/packages-exp/remote-config-exp/test/remote_config.test.ts deleted file mode 100644 index 128bcfff145..00000000000 --- a/packages-exp/remote-config-exp/test/remote_config.test.ts +++ /dev/null @@ -1,522 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FirebaseApp } from '@firebase/app-types-exp'; -import { - RemoteConfig as RemoteConfigType, - LogLevel as RemoteConfigLogLevel -} from '@firebase/remote-config-types-exp'; -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { StorageCache } from '../src/storage/storage_cache'; -import { Storage } from '../src/storage/storage'; -import { RemoteConfig } from '../src/remote_config'; -import { - RemoteConfigFetchClient, - FetchResponse -} from '../src/client/remote_config_fetch_client'; -import { Value } from '../src/value'; -import './setup'; -import { ERROR_FACTORY, ErrorCode } from '../src/errors'; -import { Logger, LogLevel as FirebaseLogLevel } from '@firebase/logger'; -import { - activate, - ensureInitialized, - getAll, - getBoolean, - getNumber, - getString, - getValue, - setLogLevel, - fetchConfig -} from '../src/api'; -import * as api from '../src/api'; -import { fetchAndActivate } from '../src'; -import { restore } from 'sinon'; - -describe('RemoteConfig', () => { - const ACTIVE_CONFIG = { - key1: 'active_config_value_1', - key2: 'active_config_value_2', - key3: 'true', - key4: '123' - }; - const DEFAULT_CONFIG = { - key1: 'default_config_value_1', - key2: 'default_config_value_2', - key3: 'false', - key4: '345', - test: 'test' - }; - - let app: FirebaseApp; - let client: RemoteConfigFetchClient; - let storageCache: StorageCache; - let storage: Storage; - let logger: Logger; - let rc: RemoteConfigType; - - let getActiveConfigStub: sinon.SinonStub; - let loggerDebugSpy: sinon.SinonSpy; - let loggerLogLevelSpy: any; - - beforeEach(() => { - // Clears stubbed behavior between each test. - app = {} as FirebaseApp; - client = {} as RemoteConfigFetchClient; - storageCache = {} as StorageCache; - storage = {} as Storage; - logger = new Logger('package-name'); - getActiveConfigStub = sinon.stub().returns(undefined); - storageCache.getActiveConfig = getActiveConfigStub; - loggerDebugSpy = sinon.spy(logger, 'debug'); - loggerLogLevelSpy = sinon.spy(logger, 'logLevel', ['set']); - rc = new RemoteConfig(app, client, storageCache, storage, logger); - }); - - afterEach(() => { - loggerDebugSpy.restore(); - loggerLogLevelSpy.restore(); - }); - - // Adapts getUserLanguage tests from packages/auth/test/utils_test.js for TypeScript. - describe('setLogLevel', () => { - it('proxies to the FirebaseLogger instance', () => { - setLogLevel(rc, 'debug'); - - // Casts spy to any because property setters aren't defined on the SinonSpy type. - expect(loggerLogLevelSpy.set).to.have.been.calledWith( - FirebaseLogLevel.DEBUG - ); - }); - - it('normalizes levels other than DEBUG and SILENT to ERROR', () => { - for (const logLevel of ['info', 'verbose', 'error', 'severe']) { - setLogLevel(rc, logLevel as RemoteConfigLogLevel); - - // Casts spy to any because property setters aren't defined on the SinonSpy type. - expect(loggerLogLevelSpy.set).to.have.been.calledWith( - FirebaseLogLevel.ERROR - ); - } - }); - }); - - describe('ensureInitialized', () => { - it('warms cache', async () => { - storageCache.loadFromStorage = sinon.stub().returns(Promise.resolve()); - - await ensureInitialized(rc); - - expect(storageCache.loadFromStorage).to.have.been.calledOnce; - }); - - it('de-duplicates repeated calls', async () => { - storageCache.loadFromStorage = sinon.stub().returns(Promise.resolve()); - - await ensureInitialized(rc); - await ensureInitialized(rc); - - expect(storageCache.loadFromStorage).to.have.been.calledOnce; - }); - }); - - describe('fetchTimeMillis', () => { - it('normalizes undefined values', async () => { - storageCache.getLastSuccessfulFetchTimestampMillis = sinon - .stub() - .returns(undefined); - - expect(rc.fetchTimeMillis).to.eq(-1); - }); - - it('reads from cache', async () => { - const lastFetchTimeMillis = 123; - - storageCache.getLastSuccessfulFetchTimestampMillis = sinon - .stub() - .returns(lastFetchTimeMillis); - - expect(rc.fetchTimeMillis).to.eq(lastFetchTimeMillis); - }); - }); - - describe('lastFetchStatus', () => { - it('normalizes undefined values', async () => { - storageCache.getLastFetchStatus = sinon.stub().returns(undefined); - - expect(rc.lastFetchStatus).to.eq('no-fetch-yet'); - }); - - it('reads from cache', async () => { - const lastFetchStatus = 'success'; - - storageCache.getLastFetchStatus = sinon.stub().returns(lastFetchStatus); - - expect(rc.lastFetchStatus).to.eq(lastFetchStatus); - }); - }); - - describe('getValue', () => { - it('returns the active value if available', () => { - getActiveConfigStub.returns(ACTIVE_CONFIG); - rc.defaultConfig = DEFAULT_CONFIG; - - expect(getValue(rc, 'key1')).to.deep.eq( - new Value('remote', ACTIVE_CONFIG.key1) - ); - }); - - it('returns the default value if active is not available', () => { - rc.defaultConfig = DEFAULT_CONFIG; - - expect(getValue(rc, 'key1')).to.deep.eq( - new Value('default', DEFAULT_CONFIG.key1) - ); - }); - - it('returns the stringified default boolean values if active is not available', () => { - const DEFAULTS = { trueVal: true, falseVal: false }; - rc.defaultConfig = DEFAULTS; - - expect(getValue(rc, 'trueVal')).to.deep.eq( - new Value('default', String(DEFAULTS.trueVal)) - ); - expect(getValue(rc, 'falseVal')).to.deep.eq( - new Value('default', String(DEFAULTS.falseVal)) - ); - }); - - it('returns the stringified default numeric values if active is not available', () => { - const DEFAULTS = { negative: -1, zero: 0, positive: 11 }; - rc.defaultConfig = DEFAULTS; - - expect(getValue(rc, 'negative')).to.deep.eq( - new Value('default', String(DEFAULTS.negative)) - ); - expect(getValue(rc, 'zero')).to.deep.eq( - new Value('default', String(DEFAULTS.zero)) - ); - expect(getValue(rc, 'positive')).to.deep.eq( - new Value('default', String(DEFAULTS.positive)) - ); - }); - - it('returns the static value if active and default are not available', () => { - expect(getValue(rc, 'key1')).to.deep.eq(new Value('static')); - - // Asserts debug message logged if static value is returned, per EAP feedback. - expect(logger.debug).to.have.been.called; - }); - - it('logs if initialization is incomplete', async () => { - // Defines default value to isolate initialization logging from static value logging. - rc.defaultConfig = { key1: 'val' }; - - // Gets value before initialization. - getValue(rc, 'key1'); - - // Asserts getValue logs. - expect(logger.debug).to.have.been.called; - - // Enables initialization to complete. - storageCache.loadFromStorage = sinon.stub().returns(Promise.resolve()); - - // Ensures initialization completes. - await ensureInitialized(rc); - - // Gets value after initialization. - getValue(rc, 'key1'); - - // Asserts getValue doesn't log after initialization is complete. - expect(logger.debug).to.have.been.calledOnce; - }); - }); - - describe('getBoolean', () => { - it('returns the active value if available', () => { - getActiveConfigStub.returns(ACTIVE_CONFIG); - rc.defaultConfig = DEFAULT_CONFIG; - - expect(getBoolean(rc, 'key3')).to.be.true; - }); - - it('returns the default value if active is not available', () => { - rc.defaultConfig = DEFAULT_CONFIG; - - expect(getBoolean(rc, 'key3')).to.be.false; - }); - - it('returns the static value if active and default are not available', () => { - expect(getBoolean(rc, 'key3')).to.be.false; - }); - }); - - describe('getString', () => { - it('returns the active value if available', () => { - getActiveConfigStub.returns(ACTIVE_CONFIG); - rc.defaultConfig = DEFAULT_CONFIG; - - expect(getString(rc, 'key1')).to.eq(ACTIVE_CONFIG.key1); - }); - - it('returns the default value if active is not available', () => { - rc.defaultConfig = DEFAULT_CONFIG; - - expect(getString(rc, 'key2')).to.eq(DEFAULT_CONFIG.key2); - }); - - it('returns the static value if active and default are not available', () => { - expect(getString(rc, 'key1')).to.eq(''); - }); - }); - - describe('getNumber', () => { - it('returns the active value if available', () => { - getActiveConfigStub.returns(ACTIVE_CONFIG); - rc.defaultConfig = DEFAULT_CONFIG; - - expect(getNumber(rc, 'key4')).to.eq(Number(ACTIVE_CONFIG.key4)); - }); - - it('returns the default value if active is not available', () => { - rc.defaultConfig = DEFAULT_CONFIG; - - expect(getNumber(rc, 'key4')).to.eq(Number(DEFAULT_CONFIG.key4)); - }); - - it('returns the static value if active and default are not available', () => { - expect(getNumber(rc, 'key1')).to.eq(0); - }); - }); - - describe('getAll', () => { - it('returns values for all keys included in active and default configs', () => { - getActiveConfigStub.returns(ACTIVE_CONFIG); - rc.defaultConfig = DEFAULT_CONFIG; - - expect(getAll(rc)).to.deep.eq({ - key1: new Value('remote', ACTIVE_CONFIG.key1), - key2: new Value('remote', ACTIVE_CONFIG.key2), - key3: new Value('remote', ACTIVE_CONFIG.key3), - key4: new Value('remote', ACTIVE_CONFIG.key4), - test: new Value('default', DEFAULT_CONFIG.test) - }); - }); - - it('returns values in default if active is not available', () => { - rc.defaultConfig = DEFAULT_CONFIG; - - expect(getAll(rc)).to.deep.eq({ - key1: new Value('default', DEFAULT_CONFIG.key1), - key2: new Value('default', DEFAULT_CONFIG.key2), - key3: new Value('default', DEFAULT_CONFIG.key3), - key4: new Value('default', DEFAULT_CONFIG.key4), - test: new Value('default', DEFAULT_CONFIG.test) - }); - }); - - it('returns empty object if both active and default configs are not defined', () => { - expect(getAll(rc)).to.deep.eq({}); - }); - }); - - describe('activate', () => { - const ETAG = 'etag'; - const CONFIG = { key: 'val' }; - const NEW_ETAG = 'new_etag'; - - let getLastSuccessfulFetchResponseStub: sinon.SinonStub; - let getActiveConfigEtagStub: sinon.SinonStub; - let setActiveConfigEtagStub: sinon.SinonStub; - let setActiveConfigStub: sinon.SinonStub; - - beforeEach(() => { - getLastSuccessfulFetchResponseStub = sinon.stub(); - getActiveConfigEtagStub = sinon.stub(); - setActiveConfigEtagStub = sinon.stub(); - setActiveConfigStub = sinon.stub(); - - storage.getLastSuccessfulFetchResponse = getLastSuccessfulFetchResponseStub; - storage.getActiveConfigEtag = getActiveConfigEtagStub; - storage.setActiveConfigEtag = setActiveConfigEtagStub; - storageCache.setActiveConfig = setActiveConfigStub; - }); - - it('does not activate if last successful fetch response is undefined', async () => { - getLastSuccessfulFetchResponseStub.returns(Promise.resolve()); - getActiveConfigEtagStub.returns(Promise.resolve(ETAG)); - - const activateResponse = await activate(rc); - - expect(activateResponse).to.be.false; - expect(storage.setActiveConfigEtag).to.not.have.been.called; - expect(storageCache.setActiveConfig).to.not.have.been.called; - }); - - it('does not activate if fetched and active etags are the same', async () => { - getLastSuccessfulFetchResponseStub.returns( - Promise.resolve({ config: {}, etag: ETAG }) - ); - getActiveConfigEtagStub.returns(Promise.resolve(ETAG)); - - const activateResponse = await activate(rc); - - expect(activateResponse).to.be.false; - expect(storage.setActiveConfigEtag).to.not.have.been.called; - expect(storageCache.setActiveConfig).to.not.have.been.called; - }); - - it('activates if fetched and active etags are different', async () => { - getLastSuccessfulFetchResponseStub.returns( - Promise.resolve({ config: CONFIG, eTag: NEW_ETAG }) - ); - getActiveConfigEtagStub.returns(Promise.resolve(ETAG)); - - const activateResponse = await activate(rc); - - expect(activateResponse).to.be.true; - expect(storage.setActiveConfigEtag).to.have.been.calledWith(NEW_ETAG); - expect(storageCache.setActiveConfig).to.have.been.calledWith(CONFIG); - }); - - it('activates if fetched is defined but active config is not', async () => { - getLastSuccessfulFetchResponseStub.returns( - Promise.resolve({ config: CONFIG, eTag: NEW_ETAG }) - ); - getActiveConfigEtagStub.returns(Promise.resolve()); - - const activateResponse = await activate(rc); - - expect(activateResponse).to.be.true; - expect(storage.setActiveConfigEtag).to.have.been.calledWith(NEW_ETAG); - expect(storageCache.setActiveConfig).to.have.been.calledWith(CONFIG); - }); - }); - - describe('fetchAndActivate', () => { - let rcActivateStub: sinon.SinonStub<[RemoteConfigType], Promise>; - - beforeEach(() => { - sinon.stub(api, 'fetchConfig').returns(Promise.resolve()); - rcActivateStub = sinon.stub(api, 'activate'); - }); - - afterEach(() => restore()); - - it('calls fetch and activate and returns activation boolean if true', async () => { - rcActivateStub.returns(Promise.resolve(true)); - - const response = await fetchAndActivate(rc); - - expect(response).to.be.true; - expect(api.fetchConfig).to.have.been.calledWith(rc); - expect(api.activate).to.have.been.calledWith(rc); - }); - - it('calls fetch and activate and returns activation boolean if false', async () => { - rcActivateStub.returns(Promise.resolve(false)); - - const response = await fetchAndActivate(rc); - - expect(response).to.be.false; - expect(api.fetchConfig).to.have.been.calledWith(rc); - expect(api.activate).to.have.been.calledWith(rc); - }); - }); - - describe('fetch', () => { - let timeoutStub: sinon.SinonStub<[ - (...args: any[]) => void, - number, - ...any[] - ]>; - beforeEach(() => { - client.fetch = sinon - .stub() - .returns(Promise.resolve({ status: 200 } as FetchResponse)); - storageCache.setLastFetchStatus = sinon.stub(); - timeoutStub = sinon.stub(window, 'setTimeout'); - }); - - afterEach(() => { - timeoutStub.restore(); - }); - - it('defines a default timeout', async () => { - await fetchConfig(rc); - - expect(timeoutStub).to.have.been.calledWith(sinon.match.any, 60000); - }); - - it('honors a custom timeout', async () => { - rc.settings.fetchTimeoutMillis = 1000; - - await fetchConfig(rc); - - expect(timeoutStub).to.have.been.calledWith(sinon.match.any, 1000); - }); - - it('sets success status', async () => { - for (const status of [200, 304]) { - client.fetch = sinon - .stub() - .returns(Promise.resolve({ status } as FetchResponse)); - - await fetchConfig(rc); - - expect(storageCache.setLastFetchStatus).to.have.been.calledWith( - 'success' - ); - } - }); - - it('sets throttle status', async () => { - storage.getThrottleMetadata = sinon.stub().returns(Promise.resolve({})); - - const error = ERROR_FACTORY.create(ErrorCode.FETCH_THROTTLE, { - throttleEndTimeMillis: 123 - }); - - client.fetch = sinon.stub().returns(Promise.reject(error)); - - const fetchPromise = fetchConfig(rc); - - await expect(fetchPromise).to.eventually.be.rejectedWith(error); - expect(storageCache.setLastFetchStatus).to.have.been.calledWith( - 'throttle' - ); - }); - - it('defaults to failure status', async () => { - storage.getThrottleMetadata = sinon.stub().returns(Promise.resolve()); - - const error = ERROR_FACTORY.create(ErrorCode.FETCH_STATUS, { - httpStatus: 400 - }); - - client.fetch = sinon.stub().returns(Promise.reject(error)); - - const fetchPromise = fetchConfig(rc); - - await expect(fetchPromise).to.eventually.be.rejectedWith(error); - expect(storageCache.setLastFetchStatus).to.have.been.calledWith( - 'failure' - ); - }); - }); -}); diff --git a/packages-exp/remote-config-exp/test/setup.ts b/packages-exp/remote-config-exp/test/setup.ts deleted file mode 100644 index 90d154f1400..00000000000 --- a/packages-exp/remote-config-exp/test/setup.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { use } from 'chai'; -import * as sinonChai from 'sinon-chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -// Normalizes Sinon assertions to Chai syntax. -use(sinonChai); - -// Adds Promise-friendly syntax to Chai. -use(chaiAsPromised); diff --git a/packages-exp/remote-config-exp/test/storage/storage.test.ts b/packages-exp/remote-config-exp/test/storage/storage.test.ts deleted file mode 100644 index 4543b574094..00000000000 --- a/packages-exp/remote-config-exp/test/storage/storage.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '../setup'; -import { expect } from 'chai'; -import { - Storage, - ThrottleMetadata, - openDatabase, - APP_NAMESPACE_STORE -} from '../../src/storage/storage'; -import { FetchResponse } from '../../src/client/remote_config_fetch_client'; - -// Clears global IndexedDB state. -async function clearDatabase(): Promise { - const db = await openDatabase(); - db.transaction([APP_NAMESPACE_STORE], 'readwrite') - .objectStore(APP_NAMESPACE_STORE) - .clear(); -} - -describe('Storage', () => { - const storage = new Storage('appId', 'appName', 'namespace'); - - beforeEach(async () => { - await clearDatabase(); - }); - - it('constructs a composite key', async () => { - // This is defensive, but the cost of accidentally changing the key composition is high. - expect(storage.createCompositeKey('throttle_metadata')).to.eq( - 'appId,appName,namespace,throttle_metadata' - ); - }); - - it('sets and gets last fetch attempt status', async () => { - const expectedStatus = 'success'; - - await storage.setLastFetchStatus(expectedStatus); - - const actualStatus = await storage.getLastFetchStatus(); - - expect(actualStatus).to.deep.eq(expectedStatus); - }); - - it('sets and gets last fetch success timestamp', async () => { - const lastSuccessfulFetchTimestampMillis = 123; - - await storage.setLastSuccessfulFetchTimestampMillis( - lastSuccessfulFetchTimestampMillis - ); - - const actualMetadata = await storage.getLastSuccessfulFetchTimestampMillis(); - - expect(actualMetadata).to.deep.eq(lastSuccessfulFetchTimestampMillis); - }); - - it('sets and gets last successful fetch response', async () => { - const lastSuccessfulFetchResponse = { status: 200 } as FetchResponse; - - await storage.setLastSuccessfulFetchResponse(lastSuccessfulFetchResponse); - - const actualConfig = await storage.getLastSuccessfulFetchResponse(); - - expect(actualConfig).to.deep.eq(lastSuccessfulFetchResponse); - }); - - it('sets and gets active config', async () => { - const expectedConfig = { key: 'value' }; - - await storage.setActiveConfig(expectedConfig); - - const storedConfig = await storage.getActiveConfig(); - - expect(storedConfig).to.deep.eq(expectedConfig); - }); - - it('sets and gets active config etag', async () => { - const expectedEtag = 'etag'; - - await storage.setActiveConfigEtag(expectedEtag); - - const storedConfigEtag = await storage.getActiveConfigEtag(); - - expect(storedConfigEtag).to.deep.eq(expectedEtag); - }); - - it('sets, gets and deletes throttle metadata', async () => { - const expectedMetadata = { - throttleEndTimeMillis: 1 - } as ThrottleMetadata; - - await storage.setThrottleMetadata(expectedMetadata); - - let actualMetadata = await storage.getThrottleMetadata(); - - expect(actualMetadata).to.deep.eq(expectedMetadata); - - await storage.deleteThrottleMetadata(); - - actualMetadata = await storage.getThrottleMetadata(); - - expect(actualMetadata).to.be.undefined; - }); -}); diff --git a/packages-exp/remote-config-exp/test/storage/storage_cache.test.ts b/packages-exp/remote-config-exp/test/storage/storage_cache.test.ts deleted file mode 100644 index e7cfb0ef0da..00000000000 --- a/packages-exp/remote-config-exp/test/storage/storage_cache.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import '../setup'; -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { Storage } from '../../src/storage/storage'; -import { StorageCache } from '../../src/storage/storage_cache'; - -describe('StorageCache', () => { - const storage = {} as Storage; - let storageCache: StorageCache; - - beforeEach(() => { - storageCache = new StorageCache(storage); - }); - - /** - * Read-ahead getter. - */ - describe('loadFromStorage', () => { - it('populates memory cache with persisted data', async () => { - const status = 'success'; - const lastSuccessfulFetchTimestampMillis = 123; - const activeConfig = { key: 'value' }; - - storage.getLastFetchStatus = sinon - .stub() - .returns(Promise.resolve(status)); - storage.getLastSuccessfulFetchTimestampMillis = sinon - .stub() - .returns(Promise.resolve(lastSuccessfulFetchTimestampMillis)); - storage.getActiveConfig = sinon - .stub() - .returns(Promise.resolve(activeConfig)); - - await storageCache.loadFromStorage(); - - expect(storage.getLastFetchStatus).to.have.been.called; - expect(storage.getLastSuccessfulFetchTimestampMillis).to.have.been.called; - expect(storage.getActiveConfig).to.have.been.called; - - expect(storageCache.getLastFetchStatus()).to.eq(status); - expect(storageCache.getLastSuccessfulFetchTimestampMillis()).to.deep.eq( - lastSuccessfulFetchTimestampMillis - ); - expect(storageCache.getActiveConfig()).to.deep.eq(activeConfig); - }); - }); - - describe('setActiveConfig', () => { - const activeConfig = { key: 'value2' }; - - beforeEach(() => { - storage.setActiveConfig = sinon.stub().returns(Promise.resolve()); - }); - - it('writes to memory cache', async () => { - await storageCache.setActiveConfig(activeConfig); - - expect(storageCache.getActiveConfig()).to.deep.eq(activeConfig); - }); - - it('writes to persistent storage', async () => { - await storageCache.setActiveConfig(activeConfig); - - expect(storage.setActiveConfig).to.have.been.calledWith(activeConfig); - }); - }); -}); diff --git a/packages-exp/remote-config-exp/test/value.test.ts b/packages-exp/remote-config-exp/test/value.test.ts deleted file mode 100644 index ead90ce25cf..00000000000 --- a/packages-exp/remote-config-exp/test/value.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import './setup'; -import { expect } from 'chai'; -import { Value } from '../src/value'; - -describe('Value', () => { - describe('asString', () => { - it('returns static default string if source is static', () => { - expect(new Value('static').asString()).to.eq(''); - }); - - it('returns the value as a string', () => { - const VALUE = 'test'; - const value = new Value('remote', VALUE); - - expect(value.asString()).to.eq(VALUE); - }); - }); - - describe('asBoolean', () => { - it('returns static default boolean if source is static', () => { - expect(new Value('static').asBoolean()).to.be.false; - }); - - it('returns true for a truthy values', () => { - expect(new Value('remote', '1').asBoolean()).to.be.true; - expect(new Value('remote', 'true').asBoolean()).to.be.true; - expect(new Value('remote', 't').asBoolean()).to.be.true; - expect(new Value('remote', 'yes').asBoolean()).to.be.true; - expect(new Value('remote', 'y').asBoolean()).to.be.true; - expect(new Value('remote', 'on').asBoolean()).to.be.true; - }); - - it('returns false for non-truthy values', () => { - expect(new Value('remote', '').asBoolean()).to.be.false; - expect(new Value('remote', 'false').asBoolean()).to.be.false; - expect(new Value('remote', 'random string').asBoolean()).to.be.false; - }); - }); - - describe('asNumber', () => { - it('returns static default number if source is static', () => { - expect(new Value('static').asNumber()).to.eq(0); - }); - - it('returns value as a number', () => { - expect(new Value('default', '33').asNumber()).to.eq(33); - expect(new Value('default', 'not a number').asNumber()).to.eq(0); - expect(new Value('default', '-10').asNumber()).to.eq(-10); - expect(new Value('default', '0').asNumber()).to.eq(0); - expect(new Value('default', '5.3').asNumber()).to.eq(5.3); - }); - }); - - describe('getSource', () => { - it('returns the source of the value', () => { - expect(new Value('default', 'test').getSource()).to.eq('default'); - expect(new Value('remote', 'test').getSource()).to.eq('remote'); - expect(new Value('static').getSource()).to.eq('static'); - }); - }); -}); diff --git a/packages-exp/remote-config-exp/test_app/index.html b/packages-exp/remote-config-exp/test_app/index.html deleted file mode 100644 index effc7bd1492..00000000000 --- a/packages-exp/remote-config-exp/test_app/index.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - Test App - - - - - -

-

Remote Config Test App

-
-
-
-
Firebase config
-
- -
-
-
RC defaults
-
- -
-
-
RC settings
-
- -
-
-
Log level
-
- -
-
-
-
-
Controls
-
- - - - -
-
-
-
Value getters
-
-
- key: - -
- - - - -
-
-
-
General getters
-
- - - - -
-
-
-
-
Output
-
-
-
-
-
-
- - - diff --git a/packages-exp/remote-config-exp/test_app/index.js b/packages-exp/remote-config-exp/test_app/index.js deleted file mode 100644 index 5fe575bbbb4..00000000000 --- a/packages-exp/remote-config-exp/test_app/index.js +++ /dev/null @@ -1,224 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const FB_CONFIG_PLACEHOLDER = `{ - apiKey: ..., - authDomain: ..., - databaseURL: ..., - projectId: ..., - storageBucket: ..., - messagingSenderId: ..., - appId: ... -}`; -const DEFAULTS_PLACEHOLDER = `{ - key1: 'value1', - key2: 'value2', - ... -}`; -const SETTINGS_PLACEHOLDER = `{ - minimumFetchIntervalMillis: 43200000, - fetchTimeoutMillis: 5000 -}`; -const SUCCESS_MESSAGE = 'Done '; - -let rcInstance; -const outputBox = document.getElementById('output-box'); - -window.onload = function () { - document.querySelector( - '#firebase-config' - ).placeholder = FB_CONFIG_PLACEHOLDER; - document.querySelector('#rc-defaults').placeholder = DEFAULTS_PLACEHOLDER; - document.querySelector('#rc-settings').placeholder = SETTINGS_PLACEHOLDER; -}; - -function initializeFirebase() { - const val = document.querySelector('#firebase-config').value; - const app = firebase.app.initializeApp(parseObjFromStr(val)); - rcInstance = firebase.remoteConfig.getRemoteConfig(app); - return Promise.resolve(); -} - -function setDefaults() { - rcInstance.defaultConfig = parseObjFromStr( - document.querySelector('#rc-defaults').value - ); - return SUCCESS_MESSAGE; -} - -function setSettings() { - const newSettings = parseObjFromStr( - document.querySelector('#rc-settings').value, - true - ); - const currentSettings = rcInstance.settings; - rcInstance.settings = Object.assign({}, currentSettings, newSettings); - return SUCCESS_MESSAGE; -} - -function setLogLevel() { - const newLogLevel = document.querySelector('#log-level-input').value; - firebase.remoteConfig.setLogLevel(rcInstance, newLogLevel); - return SUCCESS_MESSAGE; -} - -function activate() { - return firebase.remoteConfig.activate(rcInstance); -} - -function ensureInitialized() { - return firebase.remoteConfig.ensureInitialized(rcInstance); -} - -// Prefixed to avoid clobbering the browser's fetch function. -function rcFetch() { - return firebase.remoteConfig.fetchConfig(rcInstance); -} - -function fetchAndActivate() { - return firebase.remoteConfig.fetchAndActivate(rcInstance); -} - -function getString() { - return firebase.remoteConfig.getString(rcInstance, getKey()); -} - -function getBoolean() { - return firebase.remoteConfig.getBoolean(rcInstance, getKey()); -} - -function getNumber() { - return firebase.remoteConfig.getNumber(rcInstance, getKey()); -} - -function getValue() { - return firebase.remoteConfig.getValue(rcInstance, getKey()); -} - -function getAll() { - return firebase.remoteConfig.getAll(rcInstance); -} - -function getFetchTimeMillis() { - return rcInstance.fetchTimeMillis; -} - -function getLastFetchStatus() { - return rcInstance.lastFetchStatus; -} - -function getSettings() { - return rcInstance.settings; -} - -// Helper functions -function getKey() { - return document.querySelector('#key-input').value; -} - -function handlerWrapper(handler) { - try { - clearOutput(); - var returnValue = handler(); - if (returnValue instanceof Promise) { - handlePromise(returnValue); - } else if (returnValue instanceof Array) { - handleArray(returnValue); - } else if (returnValue instanceof Object) { - handleObject(returnValue); - } else { - displayOutput(returnValue); - } - } catch (error) { - displayOutputError(error); - } -} - -function clearOutput() { - outputBox.innerHTML = ''; -} - -function appendOutput(text) { - outputBox.innerHTML = outputBox.innerHTML + text; -} - -function displayOutput(text) { - outputBox.innerHTML = text; -} - -function displayOutputError(error) { - outputBox.innerHTML = `${error.message || error}`; -} - -function handlePromise(prom) { - const timerId = setInterval(() => appendOutput('.'), 400); - prom - .then(res => { - clearInterval(timerId); - appendOutput(SUCCESS_MESSAGE); - if (res != undefined) { - appendOutput('
'); - appendOutput('return value: ' + res); - } - }) - .catch(e => { - clearInterval(timerId); - appendOutput(`${e}`); - }); -} - -function handleArray(ar) { - displayOutput(ar.toString()); -} - -function handleObject(obj) { - appendOutput('{'); - for (const key in obj) { - if (obj[key] instanceof Function) { - appendOutput(`
${key}: ${obj[key]()},`); - } else if (obj[key] instanceof Object) { - appendOutput(key + ': '); - handleObject(obj[key]); - appendOutput(','); - } else { - appendOutput(`
${key}: ${obj[key]},`); - } - } - appendOutput('
}'); -} - -/** - * Parses string received from input elements into objects. These strings are not JSON - * compatible. - */ -function parseObjFromStr(str, valueIsNumber = false) { - const obj = {}; - str - .substring(str.indexOf('{') + 1, str.indexOf('}')) - .replace(/["']/g, '') // Get rid of curly braces and quotes - .trim() - .split(/[,]/g) // Results in a string of key value separated by a colon - .map(str => str.trim()) - .forEach(keyValue => { - const colIndex = keyValue.indexOf(':'); - const key = keyValue.substring(0, colIndex); - const val = keyValue.substring(colIndex + 1).trim(); - const value = valueIsNumber ? Number(val) : val; - obj[key] = value; - }); - return obj; -} diff --git a/packages-exp/remote-config-exp/tsconfig.json b/packages-exp/remote-config-exp/tsconfig.json deleted file mode 100644 index a4b8678284b..00000000000 --- a/packages-exp/remote-config-exp/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "resolveJsonModule": true, - }, - "exclude": [ - "dist/**/*" - ] -} \ No newline at end of file diff --git a/packages-exp/remote-config-types-exp/README.md b/packages-exp/remote-config-types-exp/README.md deleted file mode 100644 index 8e988de0262..00000000000 --- a/packages-exp/remote-config-types-exp/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @firebase/remoteconfig-types - -**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/remote-config-types-exp/api-extractor.json b/packages-exp/remote-config-types-exp/api-extractor.json deleted file mode 100644 index 42f37a88c4b..00000000000 --- a/packages-exp/remote-config-types-exp/api-extractor.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../config/api-extractor.json", - // Point it to your entry point d.ts file. - "mainEntryPointFilePath": "/index.d.ts" -} \ No newline at end of file diff --git a/packages-exp/remote-config-types-exp/index.d.ts b/packages-exp/remote-config-types-exp/index.d.ts deleted file mode 100644 index bd40447e08b..00000000000 --- a/packages-exp/remote-config-types-exp/index.d.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * The Firebase Remote Config service interface. - * - * @public - */ -export interface RemoteConfig { - /** - * Defines configuration for the Remote Config SDK. - */ - settings: Settings; - - /** - * Object containing default values for conigs. - */ - defaultConfig: { [key: string]: string | number | boolean }; - - /** - * The Unix timestamp in milliseconds of the last successful fetch, or negative one if - * the {@link RemoteConfig} instance either hasn't fetched or initialization - * is incomplete. - */ - fetchTimeMillis: number; - - /** - * The status of the last fetch attempt. - */ - lastFetchStatus: FetchStatus; -} - -/** - * Indicates the source of a value. - * - *
    - *
  • "static" indicates the value was defined by a static constant.
  • - *
  • "default" indicates the value was defined by default config.
  • - *
  • "remote" indicates the value was defined by fetched config.
  • - *
- * - * @public - */ -export type ValueSource = 'static' | 'default' | 'remote'; - -/** - * Wraps a value with metadata and type-safe getters. - * - * @public - */ -export interface Value { - /** - * Gets the value as a boolean. - * - * The following values (case insensitive) are interpreted as true: - * "1", "true", "t", "yes", "y", "on". Other values are interpreted as false. - */ - asBoolean(): boolean; - - /** - * Gets the value as a number. Comparable to calling Number(value) || 0. - */ - asNumber(): number; - - /** - * Gets the value as a string. - */ - asString(): string; - - /** - * Gets the {@link ValueSource} for the given key. - */ - getSource(): ValueSource; -} - -/** - * Defines configuration options for the Remote Config SDK. - * - * @public - */ -export interface Settings { - /** - * Defines the maximum age in milliseconds of an entry in the config cache before - * it is considered stale. Defaults to 43200000 (Twelve hours). - */ - minimumFetchIntervalMillis: number; - - /** - * Defines the maximum amount of milliseconds to wait for a response when fetching - * configuration from the Remote Config server. Defaults to 60000 (One minute). - */ - fetchTimeoutMillis: number; -} - -/** - * Summarizes the outcome of the last attempt to fetch config from the Firebase Remote Config server. - * - *
    - *
  • "no-fetch-yet" indicates the {@link RemoteConfig} instance has not yet attempted - * to fetch config, or that SDK initialization is incomplete.
  • - *
  • "success" indicates the last attempt succeeded.
  • - *
  • "failure" indicates the last attempt failed.
  • - *
  • "throttle" indicates the last attempt was rate-limited.
  • - *
- * - * @public - */ -export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle'; - -/** - * Defines levels of Remote Config logging. - * - * @public - */ -export type LogLevel = 'debug' | 'error' | 'silent'; - -declare module '@firebase/component' { - interface NameServiceMapping { - 'remote-config-exp': RemoteConfig; - } -} diff --git a/packages-exp/remote-config-types-exp/package.json b/packages-exp/remote-config-types-exp/package.json deleted file mode 100644 index 260fca6ef38..00000000000 --- a/packages-exp/remote-config-types-exp/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@firebase/remote-config-types-exp", - "version": "0.0.900", - "private": true, - "description": "@firebase/remote-config Types", - "author": "Firebase (https://firebase.google.com/)", - "license": "Apache-2.0", - "scripts": { - "test": "tsc", - "test:ci": "node ../../scripts/run_tests_in_ci.js", - "api-report": "api-extractor run --local --verbose", - "predoc": "node ../../scripts/exp/remove-exp.js temp", - "doc": "api-documenter markdown --input temp --output docs", - "build:doc": "yarn api-report && yarn doc" - }, - "files": [ - "index.d.ts" - ], - "repository": { - "directory": "packages/remote-config-types", - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" - }, - "devDependencies": { - "typescript": "4.0.5" - } -} diff --git a/packages-exp/remote-config-types-exp/tsconfig.json b/packages-exp/remote-config-types-exp/tsconfig.json deleted file mode 100644 index 9a785433d90..00000000000 --- a/packages-exp/remote-config-types-exp/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../config/tsconfig.base.json", - "compilerOptions": { - "noEmit": true - }, - "exclude": [ - "dist/**/*" - ] -} diff --git a/packages/ai/.eslintrc.js b/packages/ai/.eslintrc.js new file mode 100644 index 00000000000..1e8712b0633 --- /dev/null +++ b/packages/ai/.eslintrc.js @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const path = require('path'); + +module.exports = { + extends: '../../config/.eslintrc.js', + parserOptions: { + project: 'tsconfig.json', + // to make vscode-eslint work with monorepo + // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 + tsconfigRootDir: __dirname + }, + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + 'packageDir': [path.resolve(__dirname, '../../'), __dirname] + } + ] + } +}; diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md new file mode 100644 index 00000000000..1c3959ccdf1 --- /dev/null +++ b/packages/ai/CHANGELOG.md @@ -0,0 +1,303 @@ +# @firebase/ai + +## 2.5.0 + +### Minor Changes + +- [`22e0a1a`](https://github.com/firebase/firebase-js-sdk/commit/22e0a1adbc994196690bd020472d119c1a3d200b) [#9291](https://github.com/firebase/firebase-js-sdk/pull/9291) - Deprecate `sendMediaChunks()` and `sendMediaStream()`. Instead, use the new methods added to the `LiveSession` class. + Add `sendTextRealtime()`, `sendAudioReatime()`, and `sendVideoRealtime()` to the `LiveSession` class. + +- [`bc5a7c4`](https://github.com/firebase/firebase-js-sdk/commit/bc5a7c4a74e72e9218d1435bfe50711c77b47cbd) [#9330](https://github.com/firebase/firebase-js-sdk/pull/9330) - Add support for audio transcriptions in the Live API. + +- [`c8263c4`](https://github.com/firebase/firebase-js-sdk/commit/c8263c471db4df1b0e23f0d2a11c69fd6b920e2e) [#9315](https://github.com/firebase/firebase-js-sdk/pull/9315) - Add `inferenceSource` to the response from `generateContent` and `generateContentStream`. This property indicates whether on-device or in-cloud inference was used to generate the result. + +### Patch Changes + +- [`44d9891`](https://github.com/firebase/firebase-js-sdk/commit/44d9891f93298ab4bcef5170c40c235831af0276) [#9314](https://github.com/firebase/firebase-js-sdk/pull/9314) - Fix logic for merging default `onDeviceParams` with user-provided `onDeviceParams`. + +## 2.4.0 + +### Minor Changes + +- [`0bb2fe6`](https://github.com/firebase/firebase-js-sdk/commit/0bb2fe636c456628feabd10387673f4980c7ba9e) [#9272](https://github.com/firebase/firebase-js-sdk/pull/9272) - Added a `sendFunctionResponses` method to `LiveSession`, allowing function responses to be sent during realtime sessions. + Fixed an issue where function responses during audio conversations caused the WebSocket connection to close. See [GitHub Issue #9264](https://github.com/firebase/firebase-js-sdk/issues/9264). + + - **Breaking Change**: Changed the `functionCallingHandler` property in `StartAudioConversationOptions` so that it now must return a `Promise`. + This breaking change is allowed in a minor release since the Live API is in Public Preview. + +- [`0ffcb26`](https://github.com/firebase/firebase-js-sdk/commit/0ffcb26af7c597820370fab1223da330728bbb36) [#9254](https://github.com/firebase/firebase-js-sdk/pull/9254) - Added support for the URL context tool, which allows the model to access content from provided public web URLs to inform and enhance its responses. + +### Patch Changes + +- [`2596dd1`](https://github.com/firebase/firebase-js-sdk/commit/2596dd1b5072298da8814844a312681174fc2dca) [#9255](https://github.com/firebase/firebase-js-sdk/pull/9255) - Imagen Generation is now Generally Available (GA). + +- [`2596dd1`](https://github.com/firebase/firebase-js-sdk/commit/2596dd1b5072298da8814844a312681174fc2dca) [#9255](https://github.com/firebase/firebase-js-sdk/pull/9255) - The Gemini Developer API is now Generally Available (GA). + +- [`ea85128`](https://github.com/firebase/firebase-js-sdk/commit/ea8512812b994e5de081cb55a951b627fa0183b3) [#9262](https://github.com/firebase/firebase-js-sdk/pull/9262) - Updated SDK to handle empty parts when streaming. + +- [`7a7634f`](https://github.com/firebase/firebase-js-sdk/commit/7a7634f79c4cb0d9389747068b39a7968b5628a0) [#9274](https://github.com/firebase/firebase-js-sdk/pull/9274) - Tag code execution with beta tag (public preview). + +## 2.3.0 + +### Minor Changes + +- [`06ab5c4`](https://github.com/firebase/firebase-js-sdk/commit/06ab5c4f9b84085068381f6dff5e03b1b7cf4b2c) [#9236](https://github.com/firebase/firebase-js-sdk/pull/9236) - Added a new `InferenceMode` option for the hybrid on-device capability: `prefer_in_cloud`. When this mode is selected, the SDK will attempt to use a cloud-hosted model first. If the call to the cloud-hosted model fails with a network-related error, the SDK will fall back to the on-device model, if it's available. + +- [`9b8ab02`](https://github.com/firebase/firebase-js-sdk/commit/9b8ab02c543785226fafec056d39be7cf7ee03d1) [#9249](https://github.com/firebase/firebase-js-sdk/pull/9249) - Added Code Execution feature. + +### Patch Changes + +- [`a4848b4`](https://github.com/firebase/firebase-js-sdk/commit/a4848b401f6e8da16b0d0fdbfd064e8d68566555) [#9235](https://github.com/firebase/firebase-js-sdk/pull/9235) - Refactor component registration. + +- [`c123766`](https://github.com/firebase/firebase-js-sdk/commit/c1237662e6851936d2dd6017ab4bc7f0aa5112fd) [#9253](https://github.com/firebase/firebase-js-sdk/pull/9253) - Change documentation tags for hybrid inference from "EXPERIMENTAL" to "public preview". + +## 2.2.1 + +### Patch Changes + +- [`095c098`](https://github.com/firebase/firebase-js-sdk/commit/095c098de1e4399f3fb2993edae45060b2a8c6d0) [#9232](https://github.com/firebase/firebase-js-sdk/pull/9232) (fixes [#9231](https://github.com/firebase/firebase-js-sdk/issues/9231)) - Remove accidental `factory` export. + +## 2.2.0 + +### Minor Changes + +- [`984086b`](https://github.com/firebase/firebase-js-sdk/commit/984086b0b1bd607d3aac4cbb8400bc61416e2959) [#9224](https://github.com/firebase/firebase-js-sdk/pull/9224) - Add support for the Gemini Live API. + +- [`9b63cd6`](https://github.com/firebase/firebase-js-sdk/commit/9b63cd60efcd02b64b0d37f81affb3eabf70f9eb) [#9192](https://github.com/firebase/firebase-js-sdk/pull/9192) - Add `thoughtSummary()` convenience method to `EnhancedGenerateContentResponse`. + +- [`02280d7`](https://github.com/firebase/firebase-js-sdk/commit/02280d747863445fa1c21dfda01030412a6cecff) [#9201](https://github.com/firebase/firebase-js-sdk/pull/9201) - Add support for limited-use tokens with Firebase App Check. + These limited-use tokens are required for an upcoming optional feature called + _replay protection_. We recommend + [enabling the usage of limited-use tokens](https://firebase.google.com/docs/ai-logic/app-check) + now so that when replay protection becomes available, you can enable it sooner + because more of your users will be on versions of your app that send limited-use tokens. + +### Patch Changes + +- [`84b8bed`](https://github.com/firebase/firebase-js-sdk/commit/84b8bed35b69e4713fe8f677803cb06625525a61) [#9222](https://github.com/firebase/firebase-js-sdk/pull/9222) - Fixed an issue where `AIError` messages were too long after including an entire response body. + +- [`c5f08a9`](https://github.com/firebase/firebase-js-sdk/commit/c5f08a9bc5da0d2b0207802c972d53724ccef055) [#9216](https://github.com/firebase/firebase-js-sdk/pull/9216) - Add 'includeSafetyAttributes' field to Predict request payloads. + +- [`cbef6c6`](https://github.com/firebase/firebase-js-sdk/commit/cbef6c6e5b752c316104f9c834e0fe21b75c3ef1) [#9225](https://github.com/firebase/firebase-js-sdk/pull/9225) - Exclude ChromeAdapterImpl code from Node entry point. + +## 2.1.0 + +### Minor Changes + +- [`e25317f`](https://github.com/firebase/firebase-js-sdk/commit/e25317f9f3c58305bc093e4f2e676690feb16db0) [#9029](https://github.com/firebase/firebase-js-sdk/pull/9029) - Add hybrid inference options to the Firebase AI SDK. + +## 2.0.0 + +### Major Changes + +- [`5200f7b`](https://github.com/firebase/firebase-js-sdk/commit/5200f7bb777cf2260dcd396fbd19ac6cc7cb44c4) [#9042](https://github.com/firebase/firebase-js-sdk/pull/9042) - Add support for `anyOf` schemas + +- [`e59cd7d`](https://github.com/firebase/firebase-js-sdk/commit/e59cd7da1f375ec89f237ceb684c9f450d65cd34) [#9137](https://github.com/firebase/firebase-js-sdk/pull/9137) - Convert TS enums exports in Firebase AI into const variables. + +- [`cb19688`](https://github.com/firebase/firebase-js-sdk/commit/cb19688bf3d339a46c4964cb30b6263af08526e6) [#9079](https://github.com/firebase/firebase-js-sdk/pull/9079) - Remove GroundingAttribution + +- [`ec5f374`](https://github.com/firebase/firebase-js-sdk/commit/ec5f37403d9ebe28d3d71a7789d59edfb12762df) [#9063](https://github.com/firebase/firebase-js-sdk/pull/9063) - Remove `VertexAI` APIs. + +### Minor Changes + +- [`a4ccd25`](https://github.com/firebase/firebase-js-sdk/commit/a4ccd254dd1ecb63aa010ca010ad50d4b8a8316a) [#9068](https://github.com/firebase/firebase-js-sdk/pull/9068) - Add support for Grounding with Google Search. + +- [`6ab4e13`](https://github.com/firebase/firebase-js-sdk/commit/6ab4e13a1665dab4be89ecc141b4584a5a6df569) [#9156](https://github.com/firebase/firebase-js-sdk/pull/9156) - Add support for Thinking Budget. + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`ae976d0`](https://github.com/firebase/firebase-js-sdk/commit/ae976d02908a5a8913c5fcd4c0485fcf4b081fec) [#8948](https://github.com/firebase/firebase-js-sdk/pull/8948) (fixes [#8944](https://github.com/firebase/firebase-js-sdk/issues/8944)) - Fix typings for `functionDeclaration.parameters`. + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 1.4.1 + +### Patch Changes + +- [`b97eab3`](https://github.com/firebase/firebase-js-sdk/commit/b97eab36a3553c906c35f4751a0b17c717178b13) [#9090](https://github.com/firebase/firebase-js-sdk/pull/9090) - Add deprecation label to `totalBillableCharacters`. `totalTokens` should be used instead. + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + +## 1.4.0 + +### Minor Changes + +- [`1933324`](https://github.com/firebase/firebase-js-sdk/commit/1933324e0f3e4c8ed4d4d784f0c701fd0ec6ebc3) [#9026](https://github.com/firebase/firebase-js-sdk/pull/9026) - Add support for `minItems` and `maxItems` to `Schema`. + +- [`40be2db`](https://github.com/firebase/firebase-js-sdk/commit/40be2dbb884b8e1485862af8bb015e23db69ccbf) [#9047](https://github.com/firebase/firebase-js-sdk/pull/9047) - Add `title`, `maximum`, `minimum`, `propertyOrdering` to Schema builder + +## 1.3.0 + +### Minor Changes + +- [`e99683b`](https://github.com/firebase/firebase-js-sdk/commit/e99683b17cf75c581bd362a1d7cb85f0b9c110ba) [#8922](https://github.com/firebase/firebase-js-sdk/pull/8922) - Add support for Gemini multimodal output + +- [`d5082f9`](https://github.com/firebase/firebase-js-sdk/commit/d5082f9f2fc4de98a6bfd1c6a5af4571af4d0bc6) [#8931](https://github.com/firebase/firebase-js-sdk/pull/8931) - Add support for the Gemini Developer API, enabling usage in a free tier, and add new `AI` API to accomodate new product naming. + +### Patch Changes + +- [`050c1b6`](https://github.com/firebase/firebase-js-sdk/commit/050c1b6a099b87be1488b9207e4fad4da9f8f64b) [#8972](https://github.com/firebase/firebase-js-sdk/pull/8972) - Pass `GenerativeModel`'s `BaseParams` to created chat sessions. This fixes an issue where `GenerationConfig` would not be inherited from `ChatSession`. + +- Updated dependencies [[`8a03143`](https://github.com/firebase/firebase-js-sdk/commit/8a03143b9217effdd86d68bdf195493c0979aa27)]: + - @firebase/util@1.12.0 + - @firebase/component@0.6.17 + +## 1.2.4 + +### Patch Changes + +- Updated dependencies [[`9bcd1ea`](https://github.com/firebase/firebase-js-sdk/commit/9bcd1ea9b8cc5b55692765d40df000da8ddef02b)]: + - @firebase/util@1.11.3 + - @firebase/component@0.6.16 + +## 1.2.3 + +### Patch Changes + +- Updated dependencies [[`8593fa0`](https://github.com/firebase/firebase-js-sdk/commit/8593fa05bd884c2f1f6f3b4ae062efa48af93d24)]: + - @firebase/util@1.11.2 + - @firebase/component@0.6.15 + +## 1.2.2 + +### Patch Changes + +- Updated dependencies [[`ea1f913`](https://github.com/firebase/firebase-js-sdk/commit/ea1f9139e6baec0269fbb91233fd3f7f4b0d5875), [`0e12766`](https://github.com/firebase/firebase-js-sdk/commit/0e127664946ba324c6566a02b393dafd23fc1ddb)]: + - @firebase/util@1.11.1 + - @firebase/component@0.6.14 + +## 1.2.1 + +### Patch Changes + +- [`648de84`](https://github.com/firebase/firebase-js-sdk/commit/648de84b05c827d33d6b22aceb6eff01208ebdf0) [#8809](https://github.com/firebase/firebase-js-sdk/pull/8809) - Throw an error when initializing models if `appId` is not defined in the given `VertexAI` instance. + +- [`faaeb48`](https://github.com/firebase/firebase-js-sdk/commit/faaeb48e0c9dfddd014e5fb52088d39c895e9874) [#8832](https://github.com/firebase/firebase-js-sdk/pull/8832) - Label `GroundingAttribution` as deprecated. + +## 1.2.0 + +### Minor Changes + +- [`25985ac`](https://github.com/firebase/firebase-js-sdk/commit/25985ac3c3a797160e2dc3a2a28aba9f63fe6dfd) [#8827](https://github.com/firebase/firebase-js-sdk/pull/8827) - Add `systemInstruction`, `tools`, and `generationConfig` to `CountTokensRequest`. + +- [`058afa2`](https://github.com/firebase/firebase-js-sdk/commit/058afa280c8e9a72e27f3b1fbdb2921012dc65d3) [#8741](https://github.com/firebase/firebase-js-sdk/pull/8741) - Added missing `BlockReason` and `FinishReason` enum values. + +## 1.1.0 + +### Minor Changes + +- [`9d82665`](https://github.com/firebase/firebase-js-sdk/commit/9d826659334e1a43acd1126fab6e09a305e04936) [#8757](https://github.com/firebase/firebase-js-sdk/pull/8757) - Added support for modality-based token count. + +- [`ce2c775`](https://github.com/firebase/firebase-js-sdk/commit/ce2c77511210df109fdf381c7c02175173a6f7a2) [#8683](https://github.com/firebase/firebase-js-sdk/pull/8683) - **Public Preview** Added support for generating images using the Imagen 3 model. + +### Patch Changes + +- [`554c7bd`](https://github.com/firebase/firebase-js-sdk/commit/554c7bdc12cfde834ce5c4fa729a6cb790e1e5c2) [#8736](https://github.com/firebase/firebase-js-sdk/pull/8736) (fixes [#8714](https://github.com/firebase/firebase-js-sdk/issues/8714)) - Filter out empty text parts from streaming responses. + +- [`884cbd7`](https://github.com/firebase/firebase-js-sdk/commit/884cbd7d89d4dd9162858f108c39e75896c2db5a) [#8728](https://github.com/firebase/firebase-js-sdk/pull/8728) - Create Node CJS and ESM bundles. + +- Updated dependencies [[`777f465`](https://github.com/firebase/firebase-js-sdk/commit/777f465ff37495ff933a29583769ce8a6a2b59b5)]: + - @firebase/util@1.11.0 + - @firebase/component@0.6.13 + +## 1.0.4 + +### Patch Changes + +- [`97d48c7`](https://github.com/firebase/firebase-js-sdk/commit/97d48c7650e2d4273b7f94c8964dfcb44113952a) [#8651](https://github.com/firebase/firebase-js-sdk/pull/8651) - `FirebaseServerApp` can now be initalized with an App Check token instead of invoking the App Check + `getToken` method. This should unblock the use of App Check enforced products in SSR environments + where the App Check SDK cannot be initialized. + +## 1.0.3 + +### Patch Changes + +- Updated dependencies [[`25a6204c1`](https://github.com/firebase/firebase-js-sdk/commit/25a6204c1531b6c772e5368d12b2411ae1d21bbc)]: + - @firebase/util@1.10.3 + - @firebase/component@0.6.12 + +## 1.0.2 + +### Patch Changes + +- [`c540ba9ee`](https://github.com/firebase/firebase-js-sdk/commit/c540ba9eedd189ec8ac0932124d2cc400d1bd1d6) [#8663](https://github.com/firebase/firebase-js-sdk/pull/8663) - Clear fetch timeout after request completion. Fixes an issue that caused Node scripts to hang due to a pending timeout. + +## 1.0.1 + +### Patch Changes + +- [`052e438bc`](https://github.com/firebase/firebase-js-sdk/commit/052e438bc9abc5bfaf553a41edd2cde44dc70bc2) [#8589](https://github.com/firebase/firebase-js-sdk/pull/8589) - Update to new base URL in documentation + +- [`1f1ba3fee`](https://github.com/firebase/firebase-js-sdk/commit/1f1ba3feedf543a8ce42326dda077b0cdae21f2f) [#8587](https://github.com/firebase/firebase-js-sdk/pull/8587) - Remove indentation in VertexAI API Not Enabled error + +- [`4db3d3e7b`](https://github.com/firebase/firebase-js-sdk/commit/4db3d3e7be8b435b523d23b0910958a495c09ad8) [#8591](https://github.com/firebase/firebase-js-sdk/pull/8591) - Send App Check dummy token in header if there is an App Check getToken error. + +- [`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1) [#8604](https://github.com/firebase/firebase-js-sdk/pull/8604) - Upgrade to TypeScript 5.5.4 + +- Updated dependencies [[`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1)]: + - @firebase/app-check-interop-types@0.3.3 + - @firebase/component@0.6.11 + - @firebase/logger@0.4.4 + - @firebase/util@1.10.2 + +## 1.0.0 + +### Major Changes + +- [`479226bf3`](https://github.com/firebase/firebase-js-sdk/commit/479226bf3ebd99017bb12fa21440c75715658702) [#8475](https://github.com/firebase/firebase-js-sdk/pull/8475) - Release VertexAI in Firebase for general availability. + +### Patch Changes + +- [`479226bf3`](https://github.com/firebase/firebase-js-sdk/commit/479226bf3ebd99017bb12fa21440c75715658702) [#8475](https://github.com/firebase/firebase-js-sdk/pull/8475) - Remove ES5 bundles. The minimum required ES version is now ES2017. + +- Updated dependencies [[`479226bf3`](https://github.com/firebase/firebase-js-sdk/commit/479226bf3ebd99017bb12fa21440c75715658702)]: + - @firebase/component@0.6.10 + - @firebase/logger@0.4.3 + - @firebase/util@1.10.1 + +## 0.0.4 + +### Patch Changes + +- Updated dependencies [[`16d62d4fa`](https://github.com/firebase/firebase-js-sdk/commit/16d62d4fa16faddb8cb676c0af3f29b8a5824741)]: + - @firebase/util@1.10.0 + - @firebase/component@0.6.9 + +## 0.0.3 + +### Patch Changes + +- [`e7260e23d`](https://github.com/firebase/firebase-js-sdk/commit/e7260e23d186787d44c145829af245534db4d054) [#8240](https://github.com/firebase/firebase-js-sdk/pull/8240) - Add a publicly exported `VertexAIError` class. + +- Updated dependencies [[`192561b15`](https://github.com/firebase/firebase-js-sdk/commit/192561b1552a08840d8e341f30f3dbe275465558)]: + - @firebase/util@1.9.7 + - @firebase/component@0.6.8 + +## 0.0.2 + +### Patch Changes + +- [`3883133c3`](https://github.com/firebase/firebase-js-sdk/commit/3883133c33ba48027081eef9d946988f33b07606) [#8256](https://github.com/firebase/firebase-js-sdk/pull/8256) - Change `types` paths to point to rolled-up public `d.ts` files. This fixes some TypeScript compiler errors users are seeing. + +## 0.0.1 + +### Patch Changes + +- [`506b8a6ab`](https://github.com/firebase/firebase-js-sdk/commit/506b8a6abf662d74c2085fb729cace57d861ed17) [#8119](https://github.com/firebase/firebase-js-sdk/pull/8119) - Add the preview version of the VertexAI SDK. + +- [`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7) [#8237](https://github.com/firebase/firebase-js-sdk/pull/8237) - Bump all packages so staging works. + +- Updated dependencies [[`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7)]: + - @firebase/app-check-interop-types@0.3.2 + - @firebase/component@0.6.7 + - @firebase/logger@0.4.2 + - @firebase/util@1.9.6 diff --git a/packages/ai/README.md b/packages/ai/README.md new file mode 100644 index 00000000000..94c95e50b25 --- /dev/null +++ b/packages/ai/README.md @@ -0,0 +1,5 @@ +# @firebase/ai + +This is the Firebase AI component of the Firebase JS SDK. + +**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages/ai/api-extractor.json b/packages/ai/api-extractor.json new file mode 100644 index 00000000000..831039d9713 --- /dev/null +++ b/packages/ai/api-extractor.json @@ -0,0 +1,10 @@ +{ + "extends": "../../config/api-extractor.json", + // Point it to your entry point d.ts file. + "mainEntryPointFilePath": "/dist/src/index.d.ts", + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/.d.ts", + "betaTrimmedFilePath": "/dist/-public.d.ts" + } +} diff --git a/packages/ai/integration/chat.test.ts b/packages/ai/integration/chat.test.ts new file mode 100644 index 00000000000..b6772a38fb1 --- /dev/null +++ b/packages/ai/integration/chat.test.ts @@ -0,0 +1,161 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { + Content, + GenerationConfig, + HarmBlockThreshold, + HarmCategory, + SafetySetting, + getGenerativeModel +} from '../src'; +import { testConfigs, TOKEN_COUNT_DELTA } from './constants'; + +describe('Chat Session', () => { + testConfigs.forEach(testConfig => { + describe(`${testConfig.toString()}`, () => { + const commonGenerationConfig: GenerationConfig = { + temperature: 0, + topP: 0, + responseMimeType: 'text/plain' + }; + + const commonSafetySettings: SafetySetting[] = [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + }, + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + } + ]; + + const commonSystemInstruction: Content = { + role: 'system', + parts: [ + { + text: 'You are a friendly and helpful assistant.' + } + ] + }; + + it('startChat and sendMessage: text input, text output', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + systemInstruction: commonSystemInstruction + }); + + const chat = model.startChat(); + const result1 = await chat.sendMessage( + 'What is the capital of France?' + ); + const response1 = result1.response; + const result2 = await chat.sendMessage('And what about Italy?'); + const response2 = result2.response; + const history = await chat.getHistory(); + + expect(response1.text().trim().toLowerCase()).to.include('paris'); + expect(response1.usageMetadata).to.not.be.null; + expect(response2.text().trim().toLowerCase()).to.include('rome'); + expect(response2.usageMetadata).to.not.be.null; + expect(history.length).to.equal(4); + expect(history[0].role).to.equal('user'); + expect(history[0].parts[0].text).to.equal( + 'What is the capital of France?' + ); + expect(history[1].role).to.equal('model'); + expect(history[1].parts[0].text?.toLowerCase()).to.include('paris'); + expect(history[2].role).to.equal('user'); + expect(history[2].parts[0].text).to.equal('And what about Italy?'); + expect(history[3].role).to.equal('model'); + expect(history[3].parts[0].text?.toLowerCase()).to.include('rome'); + + if (model.model.includes('gemini-2.5-flash')) { + // Token counts can vary slightly in chat context + expect(response1.usageMetadata!.promptTokenCount).to.be.closeTo( + 17, // "What is the capital of France?" + system instruction + TOKEN_COUNT_DELTA + 2 // More variance for chat context + ); + expect(response1.usageMetadata!.candidatesTokenCount).to.be.closeTo( + 8, // "Paris" + TOKEN_COUNT_DELTA + ); + expect(response1.usageMetadata!.totalTokenCount).to.be.closeTo( + 49, // "What is the capital of France?" + system instruction + "Paris" + TOKEN_COUNT_DELTA + 3 // More variance for chat context + ); + expect(response1.usageMetadata!.totalTokenCount).to.be.closeTo( + 49, // "What is the capital of France?" + system instruction + "Paris" + TOKEN_COUNT_DELTA + 3 // More variance for chat context + ); + + expect(response2.usageMetadata!.promptTokenCount).to.be.closeTo( + 32, // History + "And what about Italy?" + system instruction + TOKEN_COUNT_DELTA + 5 // More variance for chat context with history + ); + expect(response2.usageMetadata!.candidatesTokenCount).to.be.closeTo( + 8, + TOKEN_COUNT_DELTA + ); + expect(response2.usageMetadata!.totalTokenCount).to.be.closeTo( + 68, + TOKEN_COUNT_DELTA + 2 + ); + } else if (model.model.includes('gemini-2.0-flash')) { + expect(response1.usageMetadata).to.not.be.null; + // Token counts can vary slightly in chat context + expect(response1.usageMetadata!.promptTokenCount).to.be.closeTo( + 15, // "What is the capital of France?" + system instruction + TOKEN_COUNT_DELTA + 2 // More variance for chat context + ); + expect(response1.usageMetadata!.candidatesTokenCount).to.be.closeTo( + 8, // "Paris" + TOKEN_COUNT_DELTA + ); + expect(response1.usageMetadata!.totalTokenCount).to.be.closeTo( + 23, // "What is the capital of France?" + system instruction + "Paris" + TOKEN_COUNT_DELTA + 3 // More variance for chat context + ); + expect(response2.usageMetadata!.promptTokenCount).to.be.closeTo( + 28, // History + "And what about Italy?" + system instruction + TOKEN_COUNT_DELTA + 5 // More variance for chat context with history + ); + expect(response2.usageMetadata!.candidatesTokenCount).to.be.closeTo( + 8, + TOKEN_COUNT_DELTA + ); + expect(response2.usageMetadata!.totalTokenCount).to.be.closeTo( + 36, + TOKEN_COUNT_DELTA + ); + } + }); + }); + }); +}); diff --git a/packages/ai/integration/constants.ts b/packages/ai/integration/constants.ts new file mode 100644 index 00000000000..f4a74e75039 --- /dev/null +++ b/packages/ai/integration/constants.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { initializeApp } from '@firebase/app'; +import { + AI, + Backend, + BackendType, + GoogleAIBackend, + VertexAIBackend, + getAI +} from '../src'; +import { FIREBASE_CONFIG } from './firebase-config'; + +const app = initializeApp(FIREBASE_CONFIG); + +/** + * Test config that all tests will be ran against. + */ +export type TestConfig = Readonly<{ + ai: AI; + model: string; + /** This will be used to output the test config at runtime */ + toString: () => string; +}>; + +function formatConfigAsString(config: { ai: AI; model: string }): string { + return `${backendNames.get(config.ai.backend.backendType)} ${config.model}`; +} + +const backends: readonly Backend[] = [ + new GoogleAIBackend(), + new VertexAIBackend() +]; + +const backendNames: Map = new Map([ + [BackendType.GOOGLE_AI, 'Google AI'], + [BackendType.VERTEX_AI, 'Vertex AI'] +]); + +const modelNames: readonly string[] = ['gemini-2.0-flash', 'gemini-2.5-flash']; + +// The Live API requires a different set of models, and they're different for each backend. +const liveModelNames: Map = new Map([ + [BackendType.GOOGLE_AI, ['gemini-live-2.5-flash-preview']], + [BackendType.VERTEX_AI, ['gemini-2.0-flash-exp']] +]); + +/** + * Array of test configurations that is iterated over to get full coverage + * of backends and models. Contains all combinations of backends and models. + */ +export const testConfigs: readonly TestConfig[] = backends.flatMap(backend => { + return modelNames.map(modelName => { + const ai = getAI(app, { backend }); + return { + ai: getAI(app, { backend }), + model: modelName, + toString: () => formatConfigAsString({ ai, model: modelName }) + }; + }); +}); + +/** + * Test configurations used for the Live API integration tests. + */ +export const liveTestConfigs: readonly TestConfig[] = backends.flatMap( + backend => { + const testConfigs: TestConfig[] = []; + liveModelNames.get(backend.backendType)!.forEach(modelName => { + const ai = getAI(app, { backend }); + testConfigs.push({ + ai, + model: modelName, + toString: () => formatConfigAsString({ ai, model: modelName }) + }); + }); + + return testConfigs; + } +); + +export const TINY_IMG_BASE64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII='; +export const IMAGE_MIME_TYPE = 'image/png'; +export const TINY_MP3_BASE64 = + 'SUQzBAAAAAAAIlRTU0UAAAAOAAADTGF2ZjYxLjcuMTAwAAAAAAAAAAAAAAD/+0DAAAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAAAUAAAK+AGhoaGhoaGhoaGhoaGhoaGhoaGiOjo6Ojo6Ojo6Ojo6Ojo6Ojo6OjrS0tLS0tLS0tLS0tLS0tLS0tLS02tra2tra2tra2tra2tra2tra2tr//////////////////////////wAAAABMYXZjNjEuMTkAAAAAAAAAAAAAAAAkAwYAAAAAAAACvhC6DYoAAAAAAP/7EMQAA8AAAaQAAAAgAAA0gAAABExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sQxCmDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+xDEUwPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7EMR8g8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sQxKYDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU='; +export const AUDIO_MIME_TYPE = 'audio/mpeg'; + +// Token counts are only expected to differ by at most this number of tokens. +// Set to 1 for whitespace that is not always present. +export const TOKEN_COUNT_DELTA = 1; diff --git a/packages/ai/integration/count-tokens.test.ts b/packages/ai/integration/count-tokens.test.ts new file mode 100644 index 00000000000..3256a9ed9d7 --- /dev/null +++ b/packages/ai/integration/count-tokens.test.ts @@ -0,0 +1,286 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { + Content, + GenerationConfig, + HarmBlockMethod, + HarmBlockThreshold, + HarmCategory, + Modality, + SafetySetting, + getGenerativeModel, + Part, + CountTokensRequest, + InlineDataPart, + FileDataPart, + BackendType +} from '../src'; +import { + AUDIO_MIME_TYPE, + IMAGE_MIME_TYPE, + TINY_IMG_BASE64, + TINY_MP3_BASE64, + testConfigs +} from './constants'; +import { FIREBASE_CONFIG } from './firebase-config'; + +describe('Count Tokens', () => { + testConfigs.forEach(testConfig => { + describe(`${testConfig.toString()}`, () => { + it('text input', async () => { + const generationConfig: GenerationConfig = { + temperature: 0, + topP: 0, + responseMimeType: 'text/plain' + }; + + const safetySettings: SafetySetting[] = [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + method: HarmBlockMethod.PROBABILITY + }, + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + method: HarmBlockMethod.SEVERITY + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + } + ]; + + const systemInstruction: Content = { + role: 'system', + parts: [ + { + text: 'You are a friendly and helpful assistant.' + } + ] + }; + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig, + systemInstruction, + safetySettings + }); + + const response = await model.countTokens('Why is the sky blue?'); + + expect(response.promptTokensDetails).to.exist; + expect(response.promptTokensDetails!.length).to.equal(1); + expect(response.promptTokensDetails![0].modality).to.equal( + Modality.TEXT + ); + if (testConfig.ai.backend.backendType === BackendType.GOOGLE_AI) { + expect(response.totalTokens).to.equal(7); + expect(response.totalBillableCharacters).to.be.undefined; + expect(response.promptTokensDetails![0].tokenCount).to.equal(7); + } else if ( + testConfig.ai.backend.backendType === BackendType.VERTEX_AI + ) { + expect(response.totalTokens).to.equal(6); + expect(response.totalBillableCharacters).to.equal(16); + expect(response.promptTokensDetails![0].tokenCount).to.equal(6); + } + }); + + it('image input', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model + }); + const imagePart: Part = { + inlineData: { + mimeType: IMAGE_MIME_TYPE, + data: TINY_IMG_BASE64 + } + }; + const response = await model.countTokens([imagePart]); + + if (testConfig.ai.backend.backendType === BackendType.GOOGLE_AI) { + const expectedImageTokens = 259; + expect(response.totalTokens).to.equal(expectedImageTokens); + expect(response.totalBillableCharacters).to.be.undefined; // Incorrect behavior + expect(response.promptTokensDetails!.length).to.equal(2); + expect(response.promptTokensDetails![0]).to.deep.equal({ + modality: Modality.TEXT, // Note: 1 unexpected text token observed for Google AI with image-only input. + tokenCount: 1 + }); + expect(response.promptTokensDetails![1]).to.deep.equal({ + modality: Modality.IMAGE, + tokenCount: 258 + }); + } else if ( + testConfig.ai.backend.backendType === BackendType.VERTEX_AI + ) { + const expectedImageTokens = 258; + expect(response.totalTokens).to.equal(expectedImageTokens); + expect(response.totalBillableCharacters).to.be.undefined; // Incorrect behavior + expect(response.promptTokensDetails!.length).to.equal(1); + // Note: No text tokens are present for Vertex AI with image-only input. + expect(response.promptTokensDetails![0]).to.deep.equal({ + modality: Modality.IMAGE, + tokenCount: 258 + }); + expect(response.promptTokensDetails![0].tokenCount).to.equal( + expectedImageTokens + ); + } + }); + + it('audio input', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model + }); + const audioPart: InlineDataPart = { + inlineData: { + mimeType: AUDIO_MIME_TYPE, + data: TINY_MP3_BASE64 + } + }; + + const response = await model.countTokens([audioPart]); + + expect(response.promptTokensDetails).to.exist; + const textDetails = response.promptTokensDetails!.find( + d => d.modality === Modality.TEXT + ); + const audioDetails = response.promptTokensDetails!.find( + d => d.modality === Modality.AUDIO + ); + + if (testConfig.ai.backend.backendType === BackendType.GOOGLE_AI) { + expect(response.totalTokens).to.equal(6); + expect(response.promptTokensDetails!.length).to.equal(2); + expect(textDetails).to.deep.equal({ + modality: Modality.TEXT, + tokenCount: 1 + }); + expect(audioDetails).to.deep.equal({ + modality: Modality.AUDIO, + tokenCount: 5 + }); + } else if ( + testConfig.ai.backend.backendType === BackendType.VERTEX_AI + ) { + expect(response.totalTokens).to.be.undefined; + expect(response.promptTokensDetails!.length).to.equal(1); // Note: Text modality details absent for Vertex AI with audio-only input. + expect(audioDetails).to.deep.equal({ modality: Modality.AUDIO }); // Note: Audio tokenCount is undefined for Vertex AI with audio-only input. + } + + expect(response.totalBillableCharacters).to.be.undefined; // Incorrect behavior + }); + + it('text, image, and audio input', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model + }); + const textPart: Part = { text: 'Describe these:' }; + const imagePart: Part = { + inlineData: { mimeType: IMAGE_MIME_TYPE, data: TINY_IMG_BASE64 } + }; + const audioPart: Part = { + inlineData: { mimeType: AUDIO_MIME_TYPE, data: TINY_MP3_BASE64 } + }; + + const request: CountTokensRequest = { + contents: [{ role: 'user', parts: [textPart, imagePart, audioPart] }] + }; + const response = await model.countTokens(request); + const textDetails = response.promptTokensDetails!.find( + d => d.modality === Modality.TEXT + ); + const imageDetails = response.promptTokensDetails!.find( + d => d.modality === Modality.IMAGE + ); + const audioDetails = response.promptTokensDetails!.find( + d => d.modality === Modality.AUDIO + ); + expect(response.promptTokensDetails).to.exist; + expect(response.promptTokensDetails!.length).to.equal(3); + + expect(imageDetails).to.deep.equal({ + modality: Modality.IMAGE, + tokenCount: 258 + }); + + if (testConfig.ai.backend.backendType === BackendType.GOOGLE_AI) { + expect(response.totalTokens).to.equal(267); + expect(response.totalBillableCharacters).to.be.undefined; + expect(textDetails).to.deep.equal({ + modality: Modality.TEXT, + tokenCount: 4 + }); + expect(audioDetails).to.deep.equal({ + modality: Modality.AUDIO, + tokenCount: 5 + }); + } else if ( + testConfig.ai.backend.backendType === BackendType.VERTEX_AI + ) { + expect(response.totalTokens).to.equal(261); + expect(textDetails).to.deep.equal({ + modality: Modality.TEXT, + tokenCount: 3 + }); + const expectedText = 'Describe these:'; + expect(response.totalBillableCharacters).to.equal( + expectedText.length - 1 + ); // Note: BillableCharacters observed as (text length - 1) for Vertex AI. + expect(audioDetails).to.deep.equal({ modality: Modality.AUDIO }); // Incorrect behavior because there's no tokenCount + } + }); + + it('public storage reference', async () => { + // This test is not expected to pass when using Google AI. + if (testConfig.ai.backend.backendType === BackendType.GOOGLE_AI) { + return; + } + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model + }); + const filePart: FileDataPart = { + fileData: { + mimeType: IMAGE_MIME_TYPE, + fileUri: `gs://${FIREBASE_CONFIG.storageBucket}/images/tree.png` + } + }; + + const response = await model.countTokens([filePart]); + + const expectedFileTokens = 258; + expect(response.totalTokens).to.equal(expectedFileTokens); + expect(response.totalBillableCharacters).to.be.undefined; + expect(response.promptTokensDetails).to.exist; + expect(response.promptTokensDetails!.length).to.equal(1); + expect(response.promptTokensDetails![0].modality).to.equal( + Modality.IMAGE + ); + expect(response.promptTokensDetails![0].tokenCount).to.equal( + expectedFileTokens + ); + }); + }); + }); +}); diff --git a/packages/ai/integration/firebase-config.ts b/packages/ai/integration/firebase-config.ts new file mode 100644 index 00000000000..2527f020530 --- /dev/null +++ b/packages/ai/integration/firebase-config.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as config from '../../../config/ci.config.json'; + +export const FIREBASE_CONFIG = config; diff --git a/packages/ai/integration/generate-content.test.ts b/packages/ai/integration/generate-content.test.ts new file mode 100644 index 00000000000..ffb1ecca698 --- /dev/null +++ b/packages/ai/integration/generate-content.test.ts @@ -0,0 +1,373 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { + BackendType, + Content, + GenerationConfig, + HarmBlockThreshold, + HarmCategory, + Language, + Modality, + Outcome, + SafetySetting, + URLRetrievalStatus, + getGenerativeModel +} from '../src'; +import { testConfigs, TOKEN_COUNT_DELTA } from './constants'; + +describe('Generate Content', function () { + this.timeout(20_000); + testConfigs.forEach(testConfig => { + describe(`${testConfig.toString()}`, () => { + const commonGenerationConfig: GenerationConfig = { + temperature: 0, + topP: 0, + responseMimeType: 'text/plain' + }; + + const commonSafetySettings: SafetySetting[] = [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + threshold: HarmBlockThreshold.BLOCK_NONE + }, + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_NONE + }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_NONE + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_NONE + } + ]; + + const commonSystemInstruction: Content = { + role: 'system', + parts: [ + { + text: 'You are a friendly and helpful assistant.' + } + ] + }; + + it('generateContent: text input, text output', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + systemInstruction: commonSystemInstruction + }); + + const result = await model.generateContent( + 'Where is Google headquarters located? Answer with the city name only.' + ); + const response = result.response; + + const trimmedText = response.text().trim(); + expect(trimmedText).to.equal('Mountain View'); + + expect(response.usageMetadata).to.not.be.null; + + if (model.model.includes('gemini-2.5-flash')) { + expect(response.usageMetadata!.promptTokenCount).to.be.closeTo( + 22, + TOKEN_COUNT_DELTA + ); + expect(response.usageMetadata!.candidatesTokenCount).to.be.closeTo( + 2, + TOKEN_COUNT_DELTA + ); + expect(response.usageMetadata!.thoughtsTokenCount).to.be.closeTo( + 30, + TOKEN_COUNT_DELTA * 2 + ); + expect(response.usageMetadata!.totalTokenCount).to.be.closeTo( + 55, + TOKEN_COUNT_DELTA * 2 + ); + expect(response.usageMetadata!.promptTokensDetails).to.not.be.null; + expect(response.usageMetadata!.promptTokensDetails!.length).to.equal( + 1 + ); + expect( + response.usageMetadata!.promptTokensDetails![0].modality + ).to.equal(Modality.TEXT); + expect( + response.usageMetadata!.promptTokensDetails![0].tokenCount + ).to.closeTo(22, TOKEN_COUNT_DELTA); + + // candidatesTokenDetails comes back about half the time, so let's just not test it. + } else if (model.model.includes('gemini-2.0-flash')) { + expect(response.usageMetadata!.promptTokenCount).to.be.closeTo( + 21, + TOKEN_COUNT_DELTA + ); + expect(response.usageMetadata!.candidatesTokenCount).to.be.closeTo( + 4, + TOKEN_COUNT_DELTA + ); + expect(response.usageMetadata!.totalTokenCount).to.be.closeTo( + 25, + TOKEN_COUNT_DELTA * 2 + ); + expect(response.usageMetadata!.promptTokensDetails).to.not.be.null; + expect(response.usageMetadata!.promptTokensDetails!.length).to.equal( + 1 + ); + expect( + response.usageMetadata!.promptTokensDetails![0].modality + ).to.equal(Modality.TEXT); + expect( + response.usageMetadata!.promptTokensDetails![0].tokenCount + ).to.equal(21); + expect(response.usageMetadata!.candidatesTokensDetails).to.not.be + .null; + expect( + response.usageMetadata!.candidatesTokensDetails!.length + ).to.equal(1); + expect( + response.usageMetadata!.candidatesTokensDetails![0].modality + ).to.equal(Modality.TEXT); + expect( + response.usageMetadata!.candidatesTokensDetails![0].tokenCount + ).to.be.closeTo(4, TOKEN_COUNT_DELTA); + } + }); + + it('generateContent: google search grounding', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + tools: [{ googleSearch: {} }] + }); + + const result = await model.generateContent( + 'What is the speed of light in a vaccuum in meters per second?' + ); + const response = result.response; + const trimmedText = response.text().trim(); + const groundingMetadata = response.candidates?.[0].groundingMetadata; + expect(trimmedText).to.contain('299,792,458'); + expect(groundingMetadata).to.exist; + expect(groundingMetadata!.searchEntryPoint?.renderedContent).to.contain( + 'div' + ); + expect( + groundingMetadata!.groundingChunks + ).to.have.length.greaterThanOrEqual(1); + groundingMetadata!.groundingChunks!.forEach(groundingChunk => { + expect(groundingChunk.web).to.exist; + expect(groundingChunk.web!.uri).to.exist; + }); + expect( + groundingMetadata?.groundingSupports + ).to.have.length.greaterThanOrEqual(1); + groundingMetadata!.groundingSupports!.forEach(groundingSupport => { + expect( + groundingSupport.groundingChunkIndices + ).to.have.length.greaterThanOrEqual(1); + expect(groundingSupport.segment).to.exist; + expect(groundingSupport.segment?.endIndex).to.exist; + expect(groundingSupport.segment?.text).to.exist; + // Since partIndex and startIndex are commonly 0, they may be omitted from responses. + }); + }); + + describe('URL Context', async () => { + // URL Context is not supported in Google AI for gemini-2.0-flash + if ( + testConfig.ai.backend.backendType === BackendType.GOOGLE_AI && + testConfig.model === 'gemini-2.0-flash' + ) { + return; + } + + it('generateContent: url context', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + tools: [{ urlContext: {} }] + }); + + const result = await model.generateContent( + 'Summarize this website https://berkshirehathaway.com' + ); + const response = result.response; + const urlContextMetadata = + response.candidates?.[0].urlContextMetadata; + expect(urlContextMetadata?.urlMetadata).to.exist; + expect( + urlContextMetadata?.urlMetadata.length + ).to.be.greaterThanOrEqual(1); + expect(urlContextMetadata?.urlMetadata[0].retrievedUrl).to.exist; + expect(urlContextMetadata?.urlMetadata[0].retrievedUrl).to.equal( + 'https://berkshirehathaway.com' + ); + expect( + urlContextMetadata?.urlMetadata[0].urlRetrievalStatus + ).to.equal(URLRetrievalStatus.URL_RETRIEVAL_STATUS_SUCCESS); + + const usageMetadata = response.usageMetadata; + expect(usageMetadata).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.be.greaterThan(0); + }); + + it('generateContent: url context and google search grounding', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + tools: [{ urlContext: {} }, { googleSearch: {} }] + }); + + const result = await model.generateContent( + 'According to https://info.cern.ch/hypertext/WWW/TheProject.html, what is the WorldWideWeb? Search the web for other definitions.' + ); + const response = result.response; + const trimmedText = response.text().trim(); + const urlContextMetadata = + response.candidates?.[0].urlContextMetadata; + const groundingMetadata = response.candidates?.[0].groundingMetadata; + expect(trimmedText).to.contain( + 'hypermedia information retrieval initiative' + ); + expect(urlContextMetadata?.urlMetadata).to.exist; + expect( + urlContextMetadata?.urlMetadata.length + ).to.be.greaterThanOrEqual(1); + expect(urlContextMetadata?.urlMetadata[0].retrievedUrl).to.exist; + expect(urlContextMetadata?.urlMetadata[0].retrievedUrl).to.equal( + 'https://info.cern.ch/hypertext/WWW/TheProject.html' + ); + expect( + urlContextMetadata?.urlMetadata[0].urlRetrievalStatus + ).to.equal(URLRetrievalStatus.URL_RETRIEVAL_STATUS_SUCCESS); + expect(groundingMetadata).to.exist; + expect(groundingMetadata?.groundingChunks).to.exist; + expect( + groundingMetadata?.groundingChunks!.length + ).to.be.greaterThanOrEqual(1); + expect( + groundingMetadata?.groundingSupports!.length + ).to.be.greaterThanOrEqual(1); + + const usageMetadata = response.usageMetadata; + expect(usageMetadata).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.be.greaterThan(0); + }); + + it('generateContent: url context and google search grounding without URLs in prompt', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + tools: [{ urlContext: {} }, { googleSearch: {} }] + }); + + const result = await model.generateContent( + 'Recommend 3 books for beginners to read to learn more about the latest advancements in Quantum Computing.' + ); + const response = result.response; + const urlContextMetadata = + response.candidates?.[0].urlContextMetadata; + const groundingMetadata = response.candidates?.[0].groundingMetadata; + if (testConfig.ai.backend.backendType === BackendType.GOOGLE_AI) { + expect(urlContextMetadata?.urlMetadata).to.exist; + expect( + urlContextMetadata?.urlMetadata.length + ).to.be.greaterThanOrEqual(1); + expect(urlContextMetadata?.urlMetadata[0].retrievedUrl).to.exist; + expect( + urlContextMetadata?.urlMetadata[0].urlRetrievalStatus + ).to.equal(URLRetrievalStatus.URL_RETRIEVAL_STATUS_SUCCESS); + expect(groundingMetadata).to.exist; + expect(groundingMetadata?.groundingChunks).to.exist; + + const usageMetadata = response.usageMetadata; + expect(usageMetadata).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.exist; + expect(usageMetadata?.toolUsePromptTokenCount).to.be.greaterThan(0); + } else { + // URL Context does not integrate with Google Search Grounding in Vertex AI + expect(urlContextMetadata?.urlMetadata).to.not.exist; + expect(groundingMetadata).to.exist; + expect(groundingMetadata?.groundingChunks).to.exist; + } + }); + }); + + it('generateContent: code execution', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + tools: [{ codeExecution: {} }] + }); + const prompt = + 'What is the sum of the first 50 prime numbers? ' + + 'Generate and run code for the calculation, and make sure you get all 50.'; + + const result = await model.generateContent(prompt); + const parts = result.response.candidates?.[0].content.parts; + expect( + parts?.some(part => part.executableCode?.language === Language.PYTHON) + ).to.be.true; + expect( + parts?.some(part => part.codeExecutionResult?.outcome === Outcome.OK) + ).to.be.true; + // Expect these to be truthy (!= null) + expect(parts?.some(part => part.executableCode?.code != null)).to.be + .true; + expect(parts?.some(part => part.codeExecutionResult?.output != null)).to + .be.true; + }); + + it('generateContentStream: text input, text output', async () => { + const model = getGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: commonGenerationConfig, + safetySettings: commonSafetySettings, + systemInstruction: commonSystemInstruction + }); + + const result = await model.generateContentStream( + 'Where is Google headquarters located? Answer with the city name only.' + ); + + let streamText = ''; + for await (const chunk of result.stream) { + streamText += chunk.text(); + } + expect(streamText.trim()).to.equal('Mountain View'); + + const response = await result.response; + const trimmedText = response.text().trim(); + expect(trimmedText).to.equal('Mountain View'); + expect(response.usageMetadata).to.be.undefined; // Note: This is incorrect behavior. + }); + }); + }); +}); diff --git a/packages/ai/integration/live.test.ts b/packages/ai/integration/live.test.ts new file mode 100644 index 00000000000..6b50fe65222 --- /dev/null +++ b/packages/ai/integration/live.test.ts @@ -0,0 +1,416 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { + BackendType, + getLiveGenerativeModel, + LiveGenerationConfig, + LiveServerContent, + LiveServerToolCall, + LiveServerToolCallCancellation, + ResponseModality +} from '../src'; +import { liveTestConfigs } from './constants'; +import { HELLO_AUDIO_PCM_BASE64 } from './sample-data/hello-audio'; + +// A helper function to consume the generator and collect text parts from one turn. +async function nextTurnText( + stream: AsyncGenerator< + LiveServerContent | LiveServerToolCall | LiveServerToolCallCancellation + > +): Promise { + let text = ''; + // We don't use `for await...of` on the generator, because that would automatically close the generator. + // We want to keep the generator open so that we can pass it to this function again to get the + // next turn's text. + let result = await stream.next(); + while (!result.done) { + const chunk = result.value as + | LiveServerContent + | LiveServerToolCall + | LiveServerToolCallCancellation; + switch (chunk.type) { + case 'serverContent': + if (chunk.turnComplete) { + return text; + } + + const parts = chunk.modelTurn?.parts; + if (parts) { + parts.forEach(part => { + if (part.text) { + text += part.text; + } else { + throw Error(`Expected TextPart but got ${JSON.stringify(part)}`); + } + }); + } + break; + default: + throw new Error(`Unexpected chunk type '${(chunk as any).type}'`); + } + + result = await stream.next(); + } + + return text; +} + +describe('Live', function () { + this.timeout(20000); + + const textLiveGenerationConfig: LiveGenerationConfig = { + responseModalities: [ResponseModality.TEXT], + temperature: 0, + topP: 0 + }; + + liveTestConfigs.forEach(testConfig => { + if (testConfig.ai.backend.backendType === BackendType.VERTEX_AI) { + return; + } + describe(`${testConfig.toString()}`, () => { + describe('Live', () => { + it('should connect, send a message, receive a response, and close', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + await session.send( + 'Where is Google headquarters located? Answer with the city name only.' + ); + const responseText = await responsePromise; + expect(responseText).to.exist; + expect(responseText).to.include('Mountain View'); + await session.close(); + }); + it('should handle multiple messages in a session', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const generator = session.receive(); + + await session.send( + 'Where is Google headquarters located? Answer with the city name only.' + ); + + const responsePromise1 = nextTurnText(generator); + const responseText1 = await responsePromise1; // Wait for the turn to complete + expect(responseText1).to.include('Mountain View'); + + await session.send( + 'What state is that in? Answer with the state name only.' + ); + + const responsePromise2 = nextTurnText(generator); + const responseText2 = await responsePromise2; // Wait for the second turn to complete + expect(responseText2).to.include('California'); + + await session.close(); + }); + + it('close() should be idempotent and terminate the stream', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model + }); + const session = await model.connect(); + const generator = session.receive(); + + // Start consuming but don't wait for it to finish yet + const consumptionPromise = (async () => { + // This loop should terminate cleanly when close() is called + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _ of generator) { + } + })(); + + await session.close(); + + // Calling it again should not throw an error + await session.close(); + + // Should resolve without timing out + await consumptionPromise; + }); + }); + + describe('sendTextRealtime()', () => { + it('should send a single text chunk and receive a response', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + await session.sendTextRealtime('Are you an AI? Yes or No.'); + + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + }); + + describe('sendAudioRealtime()', () => { + it('should send a single audio chunk and receive a response', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + await session.sendAudioRealtime({ + data: HELLO_AUDIO_PCM_BASE64, // "Hey, can you hear me?" + mimeType: 'audio/pcm' + }); + + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + }); + + describe('sendMediaChunks()', () => { + it('should send a single audio chunk and receive a response', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + await session.sendMediaChunks([ + { + data: HELLO_AUDIO_PCM_BASE64, // "Hey, can you hear me?" + mimeType: 'audio/pcm' + } + ]); + + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + + it('should send multiple audio chunks in a single batch call', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + // TODO (dlarocque): Pass two PCM files with different audio, and validate that the model + // heard both. + await session.sendMediaChunks([ + { data: HELLO_AUDIO_PCM_BASE64, mimeType: 'audio/pcm' }, + { data: HELLO_AUDIO_PCM_BASE64, mimeType: 'audio/pcm' } + ]); + + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + }); + + describe('sendMediaStream()', () => { + it('should consume a stream with multiple chunks and receive a response', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const responsePromise = nextTurnText(session.receive()); + + // TODO (dlarocque): Pass two PCM files with different audio, and validate that the model + // heard both. + const testStream = new ReadableStream({ + start(controller) { + controller.enqueue({ + data: HELLO_AUDIO_PCM_BASE64, + mimeType: 'audio/pcm' + }); + controller.enqueue({ + data: HELLO_AUDIO_PCM_BASE64, + mimeType: 'audio/pcm' + }); + controller.close(); + } + }); + + await session.sendMediaStream(testStream); + const responseText = await responsePromise; + expect(responseText).to.include('Yes'); + + await session.close(); + }); + }); + + describe('Transcripts', async () => { + it('should receive transcript of audio input', async () => { + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + generationConfig: { + responseModalities: [ResponseModality.AUDIO], + inputAudioTranscription: {}, + outputAudioTranscription: {} + } + }); + const session = await model.connect(); + const stream = session.receive(); + + await session.sendAudioRealtime({ + data: HELLO_AUDIO_PCM_BASE64, + mimeType: 'audio/pcm' + }); + + let aggregatedInputTranscription = ''; + let aggregatedOutputTranscription = ''; + let result = await stream.next(); + while (!result.done) { + const chunk = result.value as + | LiveServerContent + | LiveServerToolCall + | LiveServerToolCallCancellation; + if (chunk.type === 'serverContent') { + if (chunk.turnComplete) { + break; + } + + if (chunk.inputTranscription) { + aggregatedInputTranscription += chunk.inputTranscription?.text; + } + if (chunk.outputTranscription) { + aggregatedOutputTranscription += + chunk.outputTranscription?.text; + } + } + + result = await stream.next(); + } + + expect(aggregatedInputTranscription).to.not.be.empty; + expect(aggregatedOutputTranscription).to.not.be.empty; + + await session.close(); + }); + }); + + /** + * These tests are currently very unreliable. Their behavior seems to change frequently. + * Skipping them for now. + */ + /* + describe('function calling', () => { + // When this tests runs against the Google AI backend, the first message we get back + // has an `executableCode` part, and then + it('should trigger a function call', async () => { + const tool: FunctionDeclarationsTool = { + functionDeclarations: [ + { + name: 'fetchWeather', + description: + 'Get the weather conditions for a specific city on a specific date.', + parameters: Schema.object({ + properties: { + location: Schema.string({ + description: 'The city of the location' + }), + date: Schema.string({ + description: 'The date to fetch weather for.' + }) + } + }) + } + ] + }; + const model = getLiveGenerativeModel(testConfig.ai, { + model: testConfig.model, + tools: [tool], + generationConfig: textLiveGenerationConfig + }); + const session = await model.connect(); + const generator = session.receive(); + + const streamPromise = new Promise(async resolve => { + let text = ''; + let turnNum = 0; + for await (const chunk of generator) { + console.log('chunk', JSON.stringify(chunk)) + switch (chunk.type) { + case 'serverContent': + if (chunk.turnComplete) { + // Vertex AI only: + // For some unknown reason, the model's first turn will not be a toolCall, but + // will instead be an executableCode part in Google AI, and a groundingMetadata in Vertex AI. + // Let's skip this unexpected first message, waiting until the second turn to resolve with the text. This will definitely break if/when + // that bug is fixed. + if (turnNum === 0) { + turnNum = 1; + } else { + return resolve(text); + } + } else { + const parts = chunk.modelTurn?.parts; + if (parts) { + text += parts.flatMap(part => part.text).join(''); + } + } + break; + case 'toolCall': + // Send a fake function response + const functionResponse: FunctionResponsePart = { + functionResponse: { + id: chunk.functionCalls[0].id, // Only defined in Google AI + name: chunk.functionCalls[0].name, + response: { degrees: '22' } + } + }; + console.log('sending', JSON.stringify(functionResponse)) + await session.send([functionResponse]); + break; + case 'toolCallCancellation': + throw Error('Unexpected tool call cancellation'); + default: + throw Error('Unexpected chunk type'); + } + } + }); + + // Send a message that should trigger a function call to fetchWeather + await session.send('Whats the weather on June 15, 2025 in Toronto?'); + + const finalResponseText = await streamPromise; + expect(finalResponseText).to.include('22'); // Should include the result of our function call + + await session.close(); + }); + }); + */ + }); + }); +}); diff --git a/packages/ai/integration/sample-data/hello-audio.ts b/packages/ai/integration/sample-data/hello-audio.ts new file mode 100644 index 00000000000..7c3b8f2f693 --- /dev/null +++ b/packages/ai/integration/sample-data/hello-audio.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const HELLO_AUDIO_PCM_BASE64 = + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAP//AAAAAAAAAQABAAEAAQAAAAAAAAD///7//v8AAAIAAgAAAP7/AgAKAAcA7P+1/3r/Xf9x/67/8v8dACgAIAAZABgAGwAZABIADQAKAAkACAAGAAYACAALAAsACQAJAAsADwATABUAFQATABIAEAARABIAFAAVABMAEAANAAsACwALAAoACgAKAAoACgAKAAoACQAJAAkACAAIAAgACAAIAAgACAAHAAcABwAHAAYABgAGAAYABQAFAAUABQAFAAUABAAEAAQAAwADAAMAAwADAAMAAgACAAIAAgABAAEAAQAAAAAAAAAAAAAAAAAAAAAA/////////////////v/+//7//v/+//7//v/+//7//v/+//7//f/9//3//f/9//3//f/9//3//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z/+//7//v//P/8//v/+//7//v/+//7//v/+//7//v/+//8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//z//P/8//3//f/9//3//f/9//z//P/8//z//P/8//3//f/9//3//f/9//3//f/9//3//f/9//3//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v////////////////////7//v//////////////////////AAAAAAAA///9//3/AAADAAQAAQD9//r//P8BAAUABAAAAP3//P///wEAAQAAAP//AAACAAIAAgABAAEAAQABAAEAAQAAAAAAAQABAAEAAQABAAEAAQABAAEAAgACAAIAAgACAAIAAgABAAEAAQACAAIAAgACAAIAAgACAAEAAQABAAEAAQABAAEAAgACAAIAAgACAAIAAgACAAIAAgACAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgABAAEAAQABAAEAAQABAAEAAQACAAIAAgACAAIAAgABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////8AAP///////////////////////////////wAAAAD///////////////////////////////////////////////////////////////////////////////////////8AAAAAAAAAAAAAAAAAAAAA//////////////////8AAAAAAAD///////////////////////////////////////////7//v//////////////////////////////////////AAAAAAAAAQABAAEAAQABAAEAAQABAAIAAgACAAIAAgABAAEAAQABAAAAAAAAAAAAAAD//wAA//////7//v/+//7//f/9//z//f/9//3//f/9//z//P/7//z//P/8//z//P/8//z/+//6//r/+v/6//r/+v/6//n/+f/5//n/+f/5//n/+f/5//n/+P/3//f/9//3//j/+P/3//f/9v/2//f/+f/5//n/+v/7//v//f/9////AQABAAIABAADAAMABQAHAAgACQALAAwADAAMAAsACgAKAAkACwAMAAsACgAJAAkABwAFAAUABgAHAAUABQAFAAQABAACAAQABQACAAEAAQACAAIAAAABAAMAAgACAAMABgAEAAIAAwAEAAIAAQADAAYABQAEAAUABgAIAAYAAwAJAAwACgAJAA4AEAARABAADwAUABgAFQAUABwAHAAVABQAFwAWABEADwASABEACwAHAAYABQAGAAcACgAPABAADgANAA4ADgARABIAFgAaABsAGQAVABEAEwAXABgAFAATABcAEQAIAAoADwAMAAYAAgAAAAAA/v/7//v/+P/4//7/AAD3//D/8f/0//D/7v/1//r/9//z//X/+P/z/+v/7f/v/+z/6f/q/+3/7v/u/+r/5f/l/+f/6v/w//D/6P/p/+r/5f/m/+z/7f/p/+f/5P/f/+b/7f/w//L/7P/m/+7/9f/3//7/BAABAPr/+P/8/wEAAgADAAYABAD9//v///8GAAkAAwAAAAAA+P/x//b/9//0//T/+/8AAPr/9//+/wIA/f/2//r/AQD+//3/CwAYABMADwANAAoAEQAZABAACQALAA4AEQANAAEA/v8FAAMAAQAFAAAA9//u/+b/5//p/+L/4v/n/9v/zv/O/8//zf/O/9X/4f/m/9v/1//b/9//5P/m/+b/1//R/9T/3v/m/+P/2//O/8j/xf/B/9H/4//h/+v/8f/y//T/+/8MAAoABgAFAP7/EQAWAA8AJQAzACoANAA+AC0AHQATABsAKQAjABYAHQAWAAsAFwAkADUAPQAzACsAHwAMAAkAIAArACYAGgAIAAkADQAPABsALwA0ACsAKgArABwAGQAdABgAFAAHAO7/7f/v/+v/6f/3/+r/yf/I/8//yP/G/77/uP/H/8b/0f/Z/9T/1f/L/8D/zf/Q/83/2P/b/9j/2//i/9n/4v/u/97/4P/k/9j/9f/7/9v/6v/6//H/CQAnACoAEQAHABgAJAApACkAKAA4AEUAOgA1ACgAFwAQABoAAQDl/93/0f/C/8P/uf+1/6n/lf+V/7D/s/+2/+L/6f/U//P/+P/n//j/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UwBdAFQATAA/ADwALwAcAAkACgAAAOz/8v/6//X/7//v//b/5//T/83/yf+//7r/t/+o/7r/v/+2/7f/xf/A/7T/sv+9/8n/yP/C/8r/uv/B/8b/wv+6/7//wf+x/7P/uf+5/8D/vv+4/8H/u//Q/7r/vv/E/8b/y//X/+7/DQAmACoAVgBoAGsAeAByAHUAdQBoAHUAhgCEAIcAjQCRAIgAkACEAIYAfwB1AGUAVgBJAEkARAA9ACwAMAAuAC8AMwBDADsAQgBCADkAOgA5ADcANgBCAEcAQwBCAD4ARAAzACUAJAA2ACsAQAAyACYAJAAJAP3/DAAJAAkACQD6/+r/9P/1/+z/7v/o/+r/+v/2/wQAAQAQABoAIwAqACsAKwAxACoAJQAgABEADgAHAPz//P8IAA8AEQD7//b/6f/f/+P/1//a/8X/yf+3/8f/wf+9/7f/sf+5/7D/vP+u/7b/q/+r/7f/sf+m/53/rP+d/53/of+v/67/qv/A/8P/0//r////DgAMAB0AKAA3ADAAOgA1ADsAOwA1AB4AJwAmAB8AGwAJAAAA///o/+//8f8FAA4ADgATAPr/7//6//X/9P/m/+f/5//f/+f/1v/Y/9//4//l/+//9P/4//f/6//6//b/9//0//f/7f/l/9j/2P/e/8P/vf+3/7j/tP+8/7T/xv+x/67/o/+k/6n/sP+v/7X/rP+2/73/xP/B/77/yv/N/9P/2P/3//r//f8BAP//CwAVABcAIQApACwANgA0AD8ARQBAAEIARwBBADUAOgBBADsAPABKAFAASQBKAFQAVABUAGIAZgBeAFwAXABLAE8ARwBTAEwAOAAsADIALQAoADgAKQAuADEAKgA2ACsAMAArACMAKgApACcAMwAkAB4ADgASAAYA+/8BAAAAGQAHAAsACwADAPz/CwD9//f/7//Y/9X/1v/T/8j/0P/W/8v/5v/h//T/AAAIABcAEwAMABMAFwD8/woA/v/9//z/AAD9/+P/AADy/+7/+f////7/+v8MAAUABwANABQAGwAcADEAKQA5AEwATgBPAE0AXABNAFwAaABcAGAAVgBhAF0AUwBIADwAQQAvACsAKgAoACEAIgAhABgAHQATACUAHwAdAB4AGAAbABAACwAKAAIABQD0//7/+//+//b/+P/x/9j/3//U/93/zP/N/8z/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iv+N/5H/nP+j/6z/sf+4/77/xP/O/9n/6v/7/wgAEAAWACEAKgAuADIANwA2ADoAOwA4ACsAGwAWABEADwAKAAQAAQD3/+r/5f/g/+T/4//d/9z/1P/Y/9b/0//Q/83/x//F/8n/yP/H/8H/wf/F/8n/1P/C/9P/6//9/w0ACQATABMAGwAmACQAJAAmACwALwAqAC0AMAAzADEAJAAgABoAFwAVAA4ACwABAPz/9v/y//P/6f/o/+n/7P/z//3/CAAHAAQAAwD9//j/+v/4/+z/2v/S/9r/5//q/+H/1P/U/+H/7/8AAAAA/v/9/wYAIQA6AEcAPQAyADoAVQB2AIsAiQB+AIYAlgClAKUAnQCVAIkAewBwAGYAXgBWAEwATwBQAFAAUwBXAFIASgBIAEkASABFADQAIgAkAC8ALAAhABAACQAPABIADAAAAPv/+v/3//T/8P/u/+7/8v/8/wcAEwAeACMAIQAhACMAJwAiABwADQD///z/+//6//v/9v/5/wIADAARABIAFQAYAB8AKgAxADYAOgA9AD4AQABEAE0AWQBdAFcAVQBbAGYAawBlAGMAZABoAGsAYwBbAF0AXgBhAGIAWQBHAD0AMwApACEAHQAeACEAKwAyADMAKgAYAA4ABQD5//L/7P/h/83/xv/Y/+//8//Z/83/2v/l/+b/2f/I/8X/zf/F/6//ov+a/5j/k/+J/4L/hP+J/5L/nP+j/67/tv+1/7n/vP+3/7L/r/+1/7j/s/+v/6n/pP+p/7P/tv+5/8T/x//D/8L/yf/S/9X/1P/R/9n/4P/h/+T/5//w//b/8//q/+n/7P/t/+3/6//w//H/8v/0//P/7//o/+v/7//x//L/9P///wwAFwAaABkAGgAcABwAFAAGAPz//P////n/8P/w//f/+//+////AAABAAEAAgAIAA0ADgAPABEADgAQABIAHAAkACgAKwAnABwAEwAPAAkA/v/x/+n/6v/u//L/9P/1//L/8f/t/+b/4//g/+H/6P/u/+7/8P/4//7/AgAIAA8AGQAjACwAKgAlACEAHwAeABgAEgATABMAEgAPAA4ADQAKAAoACwANAA8AEwATABMAGQAaABgAGgAgACMAIAAnAC4ALwAyADEANgA7ADkAMAAlACAAIgAoACgAJgAfABUADAADAAAAAgACAAIA/P/6//j/8f/q/+T/4//Y/8z/xv++/7b/qf+g/5r/kf+J/4P/gf+E/4f/i/+R/57/pP+n/6z/sf+3/7//x//K/9D/0v/U/9f/2f/X/9P/z//K/8n/zP/V/9r/3//h/9v/2P/X/9X/1//d/+P/4//i/+P/4v/g/9z/1P/T/9L/1v/c/+P/7f/4//z//f8DAAIAAwAGAAMABwAMAAoADAAMAAwADAABAPn/+//+/wAA/v/5//f/+/8AAAEA/////wIABgAQABoAIQArADAAMwA3ADwARABMAEkAPwA5ADYAOwBCAEIAQABDAEUASgBMAEwAUABUAFUAVQBWAFkAYABfAFUATwBXAGQAaQBfAFQAWwBnAF8ARwA3AD8AUQBRADgAIAAcACMAGwAGAPv/+f/2/+7/4v/Z/9P/z//L/8r/zf/V/9f/0v/K/8b/zP/W/9T/zP/M/8f/vv+6/7v/u/+z/6z/qv+z/7z/vf+8/8X/1P/o//L/8P/w//b///8EABMAJAArACcAIwApADMAOAAzADEANwA4ADIAKgAkACQAIAAbAB4AHQAdABwAFQAOAA8ADAAMAAoAAAD2//T/9//5//f/9//9/wIACAAQABYAGgAfACwAOQA/AEYATQBNAFAAVgBdAGQAZwBpAGoAagBrAHEAcQBlAFYASQBAADkAMAAtACcAKAApACIAHgAgACkAKgAnACMAHwAZABEADAACAPv/8//m/9v/0f/J/8b/wP+1/6z/qP+g/5X/kP+Q/5P/j/+L/43/lP+c/6L/qP+x/77/wv/G/8z/0v/W/9L/zP/M/8v/zP/K/8b/wf/A/8L/vf+5/7j/uf+6/7z/v/++/77/u/+3/7b/tf+1/7n/vf/C/8P/yf/S/9b/0v/G/8D/wv/C/8L/x//M/9P/2f/Z/9v/5P/z/wQAFAAnADoATQBiAGYAYwBhAFgAVABVAFQAUQBPAE4ATgBIAEoATABNAE0ARwBGAEoASwBJAD8ANQA5AD0ANgAuACgAJAAgABwAGQAaAB0AIAAfABgAFgATABUAHQAjAC8ANgAzAC0AKgAbABYADwAJAA0ACQAHAAUACwAQAA0AEgARAA8ACgACAAQACAALABAADwACAPT/3//S/8f/wf/F/8n/zf/H/73/t/+v/6T/l/+T/5f/pf+v/67/sP+t/6z/rv+0/7r/wv/H/9D/3f/n//D/9v/2//P/8P/3//7/BgASACEALQAsACsAJgAmAC0AMwA+AFAAXgBrAHUAcgB0AHEAYQBaAE8ARgBGAEcAQwA5ADAAJgAdABgAEgAJAP7//f/4/+v/6v/l/9//5f/m/9r/4P/g/9X/1v/R/8b/xP+7/7H/rP+u/7f/w//L/83/0P/Q/8//z//V/+T/6//x//f/AgALAAsABwAHAAoADQASABgAFgAWABoAGwAaABAACwAMAAkACQAYACUAKgArAC8ALAAoACsALgAxADAAKgAmACAAHQAXAA8ADQAIAA0AHAAdABwAFgAQAAgA/v/1/+P/4f/c/9b/2P/V/9X/2P/W/8f/xf/M/8f/wv+4/7X/sv+t/63/pf+a/5f/nv+s/7v/xf/O/9f/2P/a/9//4f/f/+D/7P/1//j/8P/i/9j/0P/I/8H/wf/F/87/1P/V/9v/5f/t//L/8//2//3/AwALAA4AEQAZABsAHgAiACEAJgArAC8AMAAyADYANgA6AEAAQQA8ADkANwA3ADkAMwAtACsAGwAIAPX/6P/k/9r/1f/P/8P/vf+0/63/rv+z/7T/tf+z/63/qf+r/7D/sP+x/7j/wf/G/8f/zf/U/93/6//7/wYADQAWACAALAA2ADgAQgBOAE8AVQBZAF4AZQBjAFgASQBFAEgASwBLAEgARgBIAEUAQQBAADsAOAA+AD4ANAAwACkAJAAjABoAEwANAAQA/v/6//L/6//u/+3/5P/c/9H/zf/L/8v/0P/N/9H/0P/O/9X/zv/O/9P/1f/W/9D/zv/R/9j/3f/k/+r/6//t/+3/6//u//L/9f/2//n//f8EAA8AFwAeACAAJQAlACYALwAwACoAIgAhACQAIwAhAB8AJwAmACcAJwAgACcALwAyADMANgA6ADgAMQAkACEAJgAmACcALQA0ADcANQAvACUAKQAuAC4AKgAlACMAIAAeABoADgAJAAgACwAQABAAFgAWABYAGAAWABYAEgATABUAGQAZABgAGgAkACoAHwAeABsAHQAmACYAJgApADEAMgAwACoAJQAnACgAJAAoACkAIgAmACsAKwA1ADkAQABKAE4AUQBUAFYAVQBZAF4AYgBfAFwAUQBFADoAMQAmAB4AHAAWABEADQALAAkACAAKABEAEAANAAoA///0/+//5f/g/+P/4f/i/+H/3f/e/+X/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vv+9/73/t/+w/6b/of+n/7D/t/++/8b/zv/U/9r/3v/e/+D/3v/d/93/2//d/+L/4v/j/+L/3//Z/9X/0v/U/9j/1f/U/9P/0v/V/9b/1//X/9z/4//p/+n/6P/q/+//9f/7/wIABwANABQAHwAjAB8AFQAQABQAGwAgACMAJwAuAC4ALAAoACcAJwAsADMANwA3ADAALAApACMAHgAcABYAEAAQABUAHQAfACEAJgAoACgAJwAmACQAKAApACgAJgAkAB8AGwAbABoAGQAQAAwADQANAA8AEQARABIAFAASABEAFwAfACUAIwAjACUAJQAmACQAJwApACoAKwAtADUAOgA+AD8ARgBNAFAASwBFAEMAPgA4ADEAMAAtACcAHAAXABYAFQATABAADwAMAAoACgANAA0ADwAHAAIA/f/7//v/9v/z/+//7//u/+z/6f/x//r/AAD///j/9f/5//3//f8AAAYACAAIAAYABQAEAP///v/+//7/+f/w/+n/5v/k/93/2f/V/9L/0v/R/9P/0P/O/83/y//N/8v/y//L/9H/1//Z/9n/2P/a/9v/3v/b/9v/3v/g/+f/9P8AAAsAGAAjACoALQAqACgAJQAiAB8AIwAjACEAKAAxADYAPwBCAEQARgBLAE8AVQBeAGYAaABmAF4AVABIAD0AMQAnACQAHQAQAAEA9f/v/+b/1f/E/7//wf/F/8X/wv/D/8f/y//Q/9D/1P/b/93/3f/Z/9n/1f/R/9H/0f/N/8r/vv+4/7H/rP+k/53/nv+d/6L/pf+p/6z/q/+u/7H/sv+4/7//w//I/83/3P/p//D/+v/+/wIABQAEAAAA+f/2//X/8f/y//X/8f/u/+7/8P/u//H/9//4//n/9v/3//b/+P///wMA/v/3/+3/4//f/93/3//i/+P/4v/l/+f/6P/p/+f/5v/k/+H/3f/e/+D/6P/u//P/+v///wIAAwAHAAkACwAHAAUABgABAPz/+f/2//n/+//8/wMACgAUAB4AIwAkACYAKQApACYAIAAgAB4AHwAdABgADgAHAP///P/8//f/8f/w//D/8v/2/wIAEgAZAB0AIQAsADMAOAA0ADIAKgAfABMABAD+//b/7//k/9r/0f/I/8T/xP/H/83/zv/O/9H/2f/f/+L/6P/s//L/9P/z//T/8//t/+f/4//f/+D/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KgArACIAGgAZABUAEQAMAAoABAD5/+//7f/u/+X/2v/O/8H/s/+p/6f/p/+n/6n/sf+t/6D/nP+Y/5r/p/+r/6//v//I/87/zf/N/9X/3//f/9b/0//P/8v/z//T/9b/2f/Y/9n/3P/Z/93/4P/c/9v/3f/c/+L/5P/k/+P/3//c/+D/4f/g/+b/8v/2//n/+f/5//X/7P/n/+b/3f/Z/9z/2f/a/97/3//c/9X/zf/C/7T/p/+Y/5n/nf+Z/4v/f/99/3n/bf9o/2X/Zv9o/2z/bv9u/27/cP90/3T/eP91/3f/gP+G/4n/jP+O/47/lP+b/6T/pv+h/53/nv+e/6X/q/+s/7P/u//C/83/1f/U/9f/4//p//H/+P8BAAgABwAFAAgACQAKAAsACQAIAAEA8v/s/+f/4v/b/9X/0f/Q/9L/1P/U/9f/3P/b/9T/z//L/9H/2//b/9z/2//d/9z/4//l/+n/8P/t/+f/4f/W/9T/zv/G/8X/w/+8/7n/uP+3/7D/rf+t/6z/qP+i/6T/p/+r/7v/yv/N/8//0//R/9j/3v/k/+3/9f/7//z/AgAPAB0AJQAsADAALAAwADAALQApACkAKgAvADIAMQA0AD8ASABUAGQAbQB2AH0AggCQAJ4AogCxAMAAxgDMANEA2gDkAOoA8gD4APoA+QD3APMA9ADyAOoA6ADnAOIA4ADgAN0A3ADXANAAzQDLAMQAwQDCAL8AuACsAJ8AkwCKAIkAhgCDAIEAegBxAG4AagBjAFgAUQBDADUALgAmACMAHwAbABcAEgANAPr/5//f/+D/3v/X/9H/zv/K/8r/0f/R/8v/v//A/7j/sv+4/73/zf/V/9v/4f/m//L/+v/+/wkAGAAhACMALAA2ADQALgAsACwALgAtAC0ALwA1ADoAOwA5ADoAPQA+ADwAPAA9AD8APQA2ADUAMwA6AEEARgBOAFEAVQBgAGIAXwBgAGMAZgBgAFcATQBDAEEANgApACAAGwAWABAAAQDv/+3/7f/l/93/1P/J/77/sv+j/5r/kv+I/4T/f/+C/4f/iP+N/47/jf+H/3//gP+C/33/e/96/3//hP+K/47/i/+I/4X/hP+D/3v/cP9v/2j/Yf9h/17/Vv9S/1H/T/9W/13/Yv9m/2z/d/+D/5P/oP+r/7D/s/+2/7r/wf/H/83/1P/X/9j/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7//x//D/9v/3//L/8f8GABkAGQAaABoAIQAsADEAMgAvADYAOQA3AC0AKgAnACgAKgAdABcADwAGAPv/+P/3//r/8//u//b/+f8EAAMABwAAAPH/5//e/9T/0//a/9H/yP/E/7n/uf/B/77/xP/L/8X/wv/D/83/1P/V/9n/2v/g/+r/7P/0//j//v8GAAIA//8BAAQAAAD+/wEABwARABIAEgAaABwAHgAgACQALwAwACwAMQA3ADAAKwAnACcAIgAcABkAEwAUABAAEgAXAB0AIwAoADAANQA+AEAAOwA2ADMANAA1ADYALwAoACwAJAANAP7/9P/r/9b/xP++/6//qP+e/53/pf+k/6P/pP+q/6//qf+o/7T/uv+3/7H/s/+6/8X/yP/J/9P/3f/k/+n/7//5/wUADgAaACsANwBJAFEAUgBeAF4AYgBhAFUAVQBZAF4AXgBfAGUAZQBmAF4AWABZAFkATwBFAD4AOwA8AC8AJwAbABAAAwD3//T/8f/y/+z/5v/g/93/1P/Y/9//4v/i/93/2f/X/9T/2P/g/+P/8f/v/+n/9P/x/+b/5P/n/+3/7P/j/+P/6v/j/9//2v/a/9f/1v/e/+P/8f8EAAUACgAKAA0AEgAJAPn/7f/p/+T/3f/e/+T/3//T/8//z//K/8T/w//B/8T/yf/N/8//1P/Z/9j/1v/S/9X/2//e/9v/4P/e/9f/1v/T/9r/4//t//L//f8JABQAGwAgABkAEAAZABwAHwAhACYAJwAgABUACAAGAAkADAAKAA4AFQAaABQAGgAVABQADwAJAAwABwAJABAAEQAaAB8AIAAiACoAMgAtACoALwAmABgAGQAfAB0AFgAUABEADgAOAAwACAAGAAcABwAAAAsAGQAiAC0AKgAoACIAJwAuAC8AMgA4ADoAMQAqACMAJAAiAB8AFwABAPD/5f/b/9n/1P/T/9X/1P/V/+L/7f/7/wMAAgAOABgAFwAVABYAHAAaABYAGwAiAB8AGwARAAQABQALAAoABwD9//r//f8DAAMACAAfACgAJAAgACoALAAlACAAHAAdABsAFwASABIAFQATAA8AFQAaABAABwAAAPn/8//4//z/9v/8/wMAAwD9//z///8AAPL/7P/v//f//f/v/+b/6f/x/+//7P/q//X/9v/z//L/9/8CAAwAFgAQAAwA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EwAOABYAHgAcACUAJQAgACIAFQATABcAEAALAPz/8//v//P/8f/u/+L/3P/Z/8//zP/P/9L/1f/P/77/vf/H/83/x//I/8b/wf+2/7j/yv/Z/97/2f/e/+P/9v/7//7/CgAOABoAJQA/AFAAYABqAH0AiACLAJkAowCtALUAugDEAMEAwADFAMAAwwC+AMkAywDEALwAsgCyAKkAqgCgAJUAjACDAHQAXgBPAEoARQA7ADUAMQAtACwAHwAUABQAFQAaABsAGwAZABIADwAPAAsACAAAAAYABAACAAsADwAXABUAFQAOAA4ACwATABkAEgASABMAGQAcACIAHgAfABgADQAUABMAEQAaABAACAD//+//9v/0/+3/7P/e/9P/0f/P/87/zf+//7H/qv+g/5X/lP+I/4H/ff94/3b/ef9+/4X/j/+J/5X/nP+b/63/vP+//8v/0P/K/9r/3P/V/9r/4//r//b/8f/y//7/AgALAB0ALAA/AE0ASQBCADcANwA/AEUAPwA0ACsALgAtACoAIwAhABkACQD///f/+/8CAAIA8f/l/9r/xP+//7T/qP+a/4f/gP+D/4j/gf+B/3b/ff99/33/gP95/3v/c/9t/2j/a/9n/2n/c/9z/2v/dP92/27/cf9x/3j/f/+E/4P/k/+m/6j/uf/C/8r/0f/Y/97/5f/x//f/+v8AAAkABgAIAAcAAwALAAoAEQAdACgALAAmADEAPgA5AD0AQgBCAE8AUwBbAF8AYABWAEoASQBNAEMANQAtADUAPwBHAD8ALQAqACUAGgAOAPv/9f/s/+P/5f/h/9//2P/W/8//zv/V/9f/3v/V/9n/2//r//D/+P/7//b//P/8/wkACwAaACYAIgAeABUAFwAaABYAGAAcACMAHQAaABsAHAAmADAAKwAnACMAKgAvADEALwAlACkAHAAZACUANAA+AEAAQgA3ADcANgBDAEkATABSAFQARwBMAFUAVQBZAEsAQgA2ADwANwAwACkAIwAoACEAHgAXABUAGAAWABMA///9//L/4//m/9P/2f/f/+X/5v/Y/9H/xP/C/8n/1f/V/9T/1v/R/8v/yv/L/9H/4v/e/+L/5v/w//n/DAAVAB8AHAAaAB8AHgAnABoAGAAZABcAEwASABQADgAHAPr/9P/w//T/7//h/9z/1//j/9v/0v/R/9X/0//V/8//2//y//z/BQALABUAFAAfABsAGgAiAB8AKQA4AD8ATABTAFYAZQBiAGkAbABxAH8AgwCHAIMAfAB8AH8AfwCCAHsAeACCAHoAcQBnAFkAWABMAEMAQgAyACMAEwAFAP3/9P/n/+P/5v/m/+n/4v/S/87/w/+5/6z/p/+j/6T/ov+b/6b/uf/J/9D/yv/H/8r/zv/b/+f/6v/v//b/AgAOABUAJgAoADgASABWAFgAXQBuAHgAegBxAHEAaQBeAFsAVwBTAE0AQwAyAB4AHgAYAA0A+v/p/+f/7f/v/+r/7P/u/+f/2f/f/+v/7v/q/+D/3P/a/9n/1v/W/9f/0//E/7v/u/++/7n/s/+r/7L/tv+v/6r/r/+7/7v/s/+x/7n/wv/G/77/uf+1/7f/tf+4/8L/uv+y/6b/qP+q/7H/qP+m/6H/lv+S/5b/nv+i/5r/k/+c/6X/of+j/63/t/+8/7//x//K/8b/xf/H/8j/0v/X/9v/3f/V/8z/zP/N/8P/yv/S/9X/3f/d/9r/3f/W/9X/4f/f/+T/2f/W/9b/0v/N/8n/xP+9/7T/qP+k/6L/o/+Z/5D/gP97/3T/cP9r/23/c/9x/4D/hf+N/4//k/+V/5r/nP+a/5z/o/+x/7f/wv/I/9D/1v/d/+3/9P/y//j/BgAPAB8ALQAwADYASQBRAGAAbwB9AHwAdgB5AIUAhQCLAJQAmACVAIkAiwCGAJAAlQCUAJIAgwBzAHAAbgB0AHgAdQBwAGIAWQBYAFUAVgBOAE8AVABLAEQANwA2ADcAMQAnACIAIgAmACoALQA3ADwAOAA2AD0AQABHAEwATgBVAEwAPgBBAEwAVABaAFIAVwBcAGIAZQBmAGcAbAByAHAAcwB4AHcAeACAAIAAfQBwAGoAbgBvAHgAagBqAGgAZgBxAGwAdgBtAHMAcwBwAHcAdAB5AHkAdwB5AHQAeQB2AHUAgAB+AIUAiACIAIIAfgB4AHAAdgBpAFsAWABQAE8ARgBAAD0AOwA3ADMALgA4ADwAMQAmABcACAAGAP3//f/y/+b/4f/W/8v/wv+2/7P/tP+y/7D/s/+9/8j/0v/U/9P/1v/W/9r/3//o/+//8//6//r////7//b/5P/Z/9j/2P/i/9//1v/W/9L/y//F/77/wf/B/77/u//B/7z/s/+s/6P/p/+p/5z/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GAAdACMAKgAwADUANQAxAC4ALgAwADYAOwBAAEAAPwA/AEAAQgBFAEgASABHAEcATABTAFkAXQBfAF8AXQBaAFgAWABcAGEAYgBeAFkAVQBSAFAATwBSAFMAUABNAEoASwBRAFQAUgBOAEkAPwA0ACkAHwAYABAADAANABMAHgAkACcAKgApACkAJgAkACEAHgAXAA0ABQABAPv/7//m/+T/6P/r/+//8f/u/+r/6//v//j/+//5//r//f8HABEAFgAaABoAHgAmADAAOgBDAEgATQBRAFUAVgBWAE8AQgA5ADgAPgBCAEIARABHAEQAPgA8ADcAMAAsACUAGQAIAPf/6P/a/8z/wP+3/63/oP+X/5P/j/+N/4z/kP+W/5z/ov+n/6z/sf+6/8P/yf/P/9T/1v/Z/97/5//x//b/+/8CAAsAEwAWABkAGwAcABwAHQAgACAAGgAWABkAHQAcABcAFgAeACUAIwAWAAwACgAKAAQA+P/p/9//1//K/8D/u/+7/7T/q/+i/6D/p/+w/7f/u/++/7//vP+1/7L/uv/F/8v/0P/X/9//5P/q//T/+////wIADAAVABcAFgAZABkAFwASAAsABQABAAAA///7//H/6v/p/+v/5v/h/+D/4f/m/+b/5f/k/+T/4//h/97/3P/c/9n/1P/T/9T/2v/e/+X/6v/t//L/9f/5//v/+v/+/wMABwAKABAAFwAbACAAKAAvADIANQA1ADUANgA3ADYANQA2ADgAOwA7ADkANQAuACwAKwAsACwALQAoACAAGgAYABcAEgAJAAQABwAJAAQA///6//n/+P/3//r///8DAAQABAADAAEA/v8AAAQADAASABUAFgAcACIAJwAnACQAJgArADEANwA+AEUATQBQAFMAVgBbAF4AWwBZAFoAWABSAEsASQBLAEcAPQAzACkAHAASAAgA/f/1//T/8//u/+b/4f/h/+P/5f/l/9//1v/T/9T/0//R/9D/zf/K/8f/xP/A/7j/sf+r/6j/pP+k/6j/r/+z/7b/uP+4/7b/t/+7/8H/xP/B/8H/w//G/8f/yf/P/9T/2f/h/+b/6v/t//P/9//2//f/AAALAA8ADgAPABYAIQApADAANgA9AEIAQwBFAEcARQBAADoAOgA9AD8AOAAuACkAJwAmAB4AEQACAPn/8//x/+3/5P/b/9L/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9v/8//3/AAAJABcAJwA0ADwAQgBEAEQARQBEAD8AOgA4ADsAQwBMAFAAVABYAFsAWwBZAFcAVgBUAFIAUABNAFAAVgBdAFoAVABPAEsARgBBAD8APgBAAEMARQBIAE0AUQBPAEsARwBFAEUAQwA/ADoAMwAqAB4AFgATABIAFgAcAB8AIgAjACAAHgAcABgAEwARAA8ADwAKAAYABgACAPr/8//t/+r/5f/i/+D/4v/i/+P/4v/d/9T/x/+8/7L/q/+n/6T/ov+d/5j/kf+Q/5L/lP+U/5X/mf+e/6L/pv+u/7f/v//F/8//1v/b/+D/6P/v//H/9P/5//3/AwAOABQAFwAaAB4AIgAhAB0AHQAhAB4AFwASABIAEwAVABkAHAAiACkALQAwADMANQAzADMAOQA9AD4APAA3ADMANQA9AEEAQwBBAD8APgA/AD0AOQA4ADoAPgBHAE4AUwBQAEoARwBGAEIAOwAyACgAHgASAAgA/v/1//D/7v/o/97/1//R/8r/v/+z/6f/n/+c/5b/j/+I/4P/fv95/3b/df96/4D/iv+V/57/pP+n/6n/rP+u/6z/p/+o/6//u//E/8r/0P/V/9//6//y//f/+v/+/wIABAAFAAcACgAOABQAFwAYABcAFQAWABkAGAAWABQAFgAZABsAGAAQAAkACAALABAAEAANAAkABwAFAAAA+P/0//f//v8EAAsAFgAdACEAJQAsADEAMwA1ADUAMgAvACsAKQAkAB8AGwASAAoABwAIAAoACAAGAAYABQAAAP7//f/7//j/9P/u/+j/5v/q//P//f8JABQAHQAeABQABwD///n/+f/5//b/8f/r/+j/5f/k/+X/5f/i/9//3f/g/+b/7P/w//P/7//q/+T/4P/f/+H/5P/n/+j/4//f/+P/6//z//v/BAAMABIAEwATABQAFQAQAAgAAgABAAUABwAHAAcABQD+//b/8v/x//D/7f/s/+3/6//r/+r/5v/e/9b/zf/E/77/vv++/8H/xf/M/9f/4v/t//X//P8GABUAIwAuADgARABOAFMAUgBSAFAATgBPAFIAVABVAFUAUABGADwANgA1ADIALQAqACcAJwAoACkAKwAsAC4ALwAuAC4ALgAsACYAIQAiACAAGAAQAAwADAAQABEADgALAAsAEQAUABEADgAOAAsABAD7//f/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6P/3/wUAEgAWABYAFwAWABUAFAAcACMAJgAeABQACwAHAAQAAgD+//3//v/6//D/5//l/+P/3v/Z/9P/0P/V/97/5P/m/+f/5P/f/93/3P/a/9P/zf/K/8r/yP/C/7z/t/+0/7b/vv/I/87/z//P/9H/1//c/+L/6v/v//T/9P/y/+3/6v/o/+n/5//j/93/3P/e/+D/4f/i/+b/6P/r//D/9P/9/wUAEAAcACEAJgAuADgAPwBCAEQARgBGAEUARABFAEkARgBAADoAMgAuACwAMgA1ADUAOABAAEUARgBDAEUASwBPAFIAUgBXAFwAXQBeAFsAWABbAF4AYwBrAHcAhACMAJEAkQCOAIsAhQCAAHwAdQBuAGgAYQBZAFEATgBOAEwARwBAADYALAAdAAwA///5//j/9//z/+3/5//g/97/4P/l/+j/6v/w//L/9f/4//n/+f/6//v//f/7//r/9//1//X/8v/x//H/8v/2//r//P/8//z/+P/x/+n/4//f/9z/2P/V/9L/z//N/8//0//U/9X/2f/e/+D/4v/n/+j/6f/r/+//9v/9/wMABwAKABIAHAAlACsALgAzADwARABEAEIAQgBDAEQARQBGAEkASQBDADsALwAfAA8ABAD7//H/6v/l/+X/5f/l/+b/5P/i/+T/5P/h/+H/5v/r/+v/6f/l/+D/2//U/8//z//R/8//zf/H/8P/wP+7/7b/sv+v/6n/ov+b/5f/kv+M/4j/hv+F/4f/if+J/4X/gv9+/37/gP+B/4L/gP+B/4j/jP+Q/5b/of+y/8P/zf/P/9H/1//b/9//4f/i/+f/7P/t//D/7v/r/+f/4f/f/97/3//i/+f/7f/x/+//6P/l/+j/8f/9/woAEgAdACoAMgA4AD4ASABQAFQAVgBcAGMAaABoAGMAXQBbAFoAVwBSAE4ATgBNAEsASABFADwALgAiABsAFAAJAP//9//x/+r/3//U/8//zv/M/8b/v/+8/7v/uv+6/7r/uP+4/7f/uP+5/7v/wP/G/8r/zv/S/9f/3P/e/+H/5P/m/+f/6f/y//z/AwAIAAsADgASABQAFQAUABQAFwAeACIAIAAaABcAFgATAAcA/v/+/wMABAADAAcACwAOABAAFAAZABoAGQAaABwAHAAYABYAGQAeACUALQA3AEYAVgBjAG4AeQCBAIkA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kf+b/7r/uP+n/63/wv/k//j/6f8gAD0AJwD6//H/BwD6//n/4v/V/9j/zP8DAAQADQAcAA0ADwBQAFMAZwApAPf/BgD3//r//P8DAP//AgAFAAkACQAFAAEA/v/7//n/+v/5//3/AAABAAIABgAHAAcABgAHAAkACAAHAAsACwALAA4ADgAOAA0ACwAJAAYAAwABAAAA//8AAAIAAgAEAAYABwAHAAgACAAHAAUAAwAAAP7//P/6//n/+f/4//j/+P/5//r//P/+/wAAAQACAAMAAwACAAEA///+//7//f/9////AQAGAAoADQASABUAFgAXABYAFAARAA4ACwAIAAUABAADAAMAAwADAAQABAAEAAMAAgAAAP7//P/6//j/9//2//T/9P/1//X/9f/1//b/9//5//v//f///wEAAgADAAQABAADAAMAAwACAAIAAwAEAAYACQALAA4AEAARABIAEQAPAA0ACQAHAAQAAQD///7//v//////AAABAAIAAgABAAEAAAD///7//f/8//z/+//7//z//P/9//3//v///wAAAQACAAQABAAEAAMAAwADAAIAAQABAAAAAAAAAAEAAQACAAQABAAFAAUABgAGAAUABQAFAAMAAwAEAAIAAgACAAIAAgABAAAAAAD///3//v/+//7//v///wEAAwAFAAQAAQD9//3//P///wEABAD6//3/BwACAAUA/P///wEA/f/9/wIA/v8FAAMAAAD///3/AQD+////AwACAP3/+f8BAAQAAgD///7/AQADAAEAAQAAAP//AwADAP///v8CAAIA///+/wAAAQAAAAAAAQAAAAAAAQAAAAAAAAA='; diff --git a/packages/ai/karma.conf.js b/packages/ai/karma.conf.js new file mode 100644 index 00000000000..e64048d5f6d --- /dev/null +++ b/packages/ai/karma.conf.js @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const karmaBase = require('../../config/karma.base'); +const { argv } = require('yargs'); + +const files = [`src/**/*.test.ts`]; + +module.exports = function (config) { + const karmaConfig = { + ...karmaBase, + + preprocessors: { + ...karmaBase.preprocessors, + 'integration/**/*.ts': ['webpack', 'sourcemap'] + }, + + // files to load into karma + files: (() => { + if (argv.integration) { + return ['integration/**']; + } else { + return ['src/**/*.test.ts']; + } + })(), + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'] + }; + + config.set(karmaConfig); +}; + +module.exports.files = files; diff --git a/packages/ai/package.json b/packages/ai/package.json new file mode 100644 index 00000000000..dcb6f11fdbf --- /dev/null +++ b/packages/ai/package.json @@ -0,0 +1,85 @@ +{ + "name": "@firebase/ai", + "version": "2.5.0", + "description": "The Firebase AI SDK", + "author": "Firebase (https://firebase.google.com/)", + "engines": { + "node": ">=20.0.0" + }, + "main": "dist/index.cjs.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", + "exports": { + ".": { + "types": "./dist/ai-public.d.ts", + "node": { + "require": "./dist/index.node.cjs.js", + "import": "./dist/index.node.mjs" + }, + "browser": { + "require": "./dist/index.cjs.js", + "import": "./dist/esm/index.esm.js" + }, + "default": "./dist/esm/index.esm.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "scripts": { + "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "build": "rollup -c && yarn api-report", + "build:deps": "lerna run --scope @firebase/ai --include-dependencies build", + "dev": "rollup -c -w", + "update-responses": "../../scripts/update_vertexai_responses.sh", + "testsetup": "yarn update-responses && yarn ts-node ./test-utils/convert-mocks.ts", + "test": "run-p --npm-path npm lint type-check test:browser", + "test:ci": "yarn testsetup && node ../../scripts/run_tests_in_ci.js -s test", + "test:skip-clone": "karma start", + "test:browser": "yarn testsetup && karma start", + "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --require ts-node/register --require src/index.node.ts 'src/**/!(*-browser)*.test.ts' --config ../../config/mocharc.node.js", + "test:integration": "karma start --integration", + "test:integration:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha integration/**/*.test.ts --config ../../config/mocharc.node.js", + "api-report": "api-extractor run --local --verbose", + "typings:public": "node ../../scripts/build/use_typings.js ./dist/ai-public.d.ts", + "type-check": "yarn tsc --noEmit", + "trusted-type-check": "tsec -p tsconfig.json --noEmit" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + }, + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "license": "Apache-2.0", + "devDependencies": { + "@firebase/app": "0.14.5", + "@rollup/plugin-json": "6.1.0", + "rollup": "2.79.2", + "rollup-plugin-replace": "2.2.0", + "rollup-plugin-typescript2": "0.36.0", + "typescript": "5.5.4" + }, + "repository": { + "directory": "packages/ai", + "type": "git", + "url": "git+https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "typings": "dist/src/index.d.ts", + "nyc": { + "extension": [ + ".ts" + ], + "reportDir": "./coverage/node" + } +} diff --git a/packages/ai/rollup.config.js b/packages/ai/rollup.config.js new file mode 100644 index 00000000000..7ebbff4f2f5 --- /dev/null +++ b/packages/ai/rollup.config.js @@ -0,0 +1,118 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import json from '@rollup/plugin-json'; +import typescriptPlugin from 'rollup-plugin-typescript2'; +import replace from 'rollup-plugin-replace'; +import typescript from 'typescript'; +import pkg from './package.json'; +import tsconfig from './tsconfig.json'; +import { generateBuildTargetReplaceConfig } from '../../scripts/build/rollup_replace_build_target'; +import { emitModulePackageFile } from '../../scripts/build/rollup_emit_module_package_file'; + +const deps = Object.keys( + Object.assign({}, pkg.peerDependencies, pkg.dependencies) +); + +const buildPlugins = [ + typescriptPlugin({ + typescript, + tsconfigOverride: { + exclude: [ + ...tsconfig.exclude, + '**/*.test.ts', + 'test-utils', + 'integration' + ], + compilerOptions: { + target: 'es2020' + } + } + }), + json() +]; + +const browserBuilds = [ + { + input: 'src/index.ts', + output: { + file: pkg.module, + format: 'es', + sourcemap: true + }, + plugins: [ + ...buildPlugins, + replace({ + ...generateBuildTargetReplaceConfig('esm', 2020), + __PACKAGE_VERSION__: pkg.version + }), + emitModulePackageFile() + ], + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + }, + { + input: 'src/index.ts', + output: { + file: './dist/index.cjs.js', + format: 'cjs', + sourcemap: true + }, + plugins: [ + ...buildPlugins, + replace({ + ...generateBuildTargetReplaceConfig('cjs', 2020), + __PACKAGE_VERSION__: pkg.version + }) + ], + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } +]; + +const nodeBuilds = [ + { + input: 'src/index.node.ts', + output: { + file: pkg.exports['.'].node.import, + format: 'es', + sourcemap: true + }, + plugins: [ + ...buildPlugins, + replace({ + ...generateBuildTargetReplaceConfig('esm', 2020) + }) + ], + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + }, + { + input: 'src/index.node.ts', + output: { + file: pkg.exports['.'].node.require, + format: 'cjs', + sourcemap: true + }, + plugins: [ + ...buildPlugins, + replace({ + ...generateBuildTargetReplaceConfig('cjs', 2020) + }) + ], + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } +]; + +export default [...browserBuilds, ...nodeBuilds]; diff --git a/packages/ai/src/api-browser.test.ts b/packages/ai/src/api-browser.test.ts new file mode 100644 index 00000000000..74cfea05f4f --- /dev/null +++ b/packages/ai/src/api-browser.test.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { getAI, getGenerativeModel } from './api'; +import { expect } from 'chai'; +import { InferenceMode } from './public-types'; +import { getFullApp } from '../test-utils/get-fake-firebase-services'; +import { DEFAULT_HYBRID_IN_CLOUD_MODEL } from './constants'; +import { factory } from './factory-browser'; + +/** + * Browser-only top level API tests using a factory that provides + * a ChromeAdapter. + */ +describe('Top level API', () => { + describe('getAI()', () => { + it('getGenerativeModel with HybridParams sets a default model', () => { + const ai = getAI(getFullApp({ apiKey: 'key', appId: 'id' }, factory)); + const genModel = getGenerativeModel(ai, { + mode: InferenceMode.ONLY_ON_DEVICE + }); + expect(genModel.model).to.equal( + `models/${DEFAULT_HYBRID_IN_CLOUD_MODEL}` + ); + }); + it('getGenerativeModel with HybridParams honors a model override', () => { + const ai = getAI(getFullApp({ apiKey: 'key', appId: 'id' }, factory)); + const genModel = getGenerativeModel(ai, { + mode: InferenceMode.PREFER_ON_DEVICE, + inCloudParams: { model: 'my-model' } + }); + expect(genModel.model).to.equal('models/my-model'); + }); + }); +}); diff --git a/packages/ai/src/api.test.ts b/packages/ai/src/api.test.ts new file mode 100644 index 00000000000..3854f010fc7 --- /dev/null +++ b/packages/ai/src/api.test.ts @@ -0,0 +1,269 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ImagenModelParams, ModelParams, AIErrorCode } from './types'; +import { AIError } from './errors'; +import { + getAI, + ImagenModel, + LiveGenerativeModel, + getGenerativeModel, + getImagenModel, + getLiveGenerativeModel +} from './api'; +import { expect } from 'chai'; +import { AI } from './public-types'; +import { GenerativeModel } from './models/generative-model'; +import { GoogleAIBackend, VertexAIBackend } from './backend'; +import { getFullApp } from '../test-utils/get-fake-firebase-services'; +import { AI_TYPE } from './constants'; + +const fakeAI: AI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project', + appId: 'my-appid' + } + }, + backend: new VertexAIBackend('us-central1'), + location: 'us-central1' +}; + +describe('Top level API', () => { + describe('getAI()', () => { + it('works without options', () => { + const ai = getAI(getFullApp()); + expect(ai.backend).to.be.instanceOf(GoogleAIBackend); + }); + it('works with options: no backend, limited use token', () => { + const ai = getAI(getFullApp(), { useLimitedUseAppCheckTokens: true }); + expect(ai.backend).to.be.instanceOf(GoogleAIBackend); + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.true; + }); + it('works with options: backend specified, limited use token', () => { + const ai = getAI(getFullApp(), { + backend: new VertexAIBackend('us-central1'), + useLimitedUseAppCheckTokens: true + }); + expect(ai.backend).to.be.instanceOf(VertexAIBackend); + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.true; + }); + it('works with options: appCheck option is falsy', () => { + const ai = getAI(getFullApp(), { + backend: new VertexAIBackend('us-central1'), + useLimitedUseAppCheckTokens: undefined + }); + expect(ai.backend).to.be.instanceOf(VertexAIBackend); + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.false; + }); + it('works with options: backend specified only', () => { + const ai = getAI(getFullApp(), { + backend: new VertexAIBackend('us-central1') + }); + expect(ai.backend).to.be.instanceOf(VertexAIBackend); + expect(ai.options?.useLimitedUseAppCheckTokens).to.be.false; + }); + }); + it('getGenerativeModel throws if no model is provided', () => { + try { + getGenerativeModel(fakeAI, {} as ModelParams); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_MODEL); + expect((e as AIError).message).includes( + `AI: Must provide a model name. Example: ` + + `getGenerativeModel({ model: 'my-model-name' }) (${AI_TYPE}/${AIErrorCode.NO_MODEL})` + ); + } + }); + it('getGenerativeModel throws if no apiKey is provided', () => { + const fakeVertexNoApiKey = { + ...fakeAI, + app: { options: { projectId: 'my-project', appId: 'my-appid' } } + } as AI; + try { + getGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_API_KEY); + expect((e as AIError).message).equals( + `AI: The "apiKey" field is empty in the local ` + + `Firebase config. Firebase AI requires this field to` + + ` contain a valid API key. (${AI_TYPE}/${AIErrorCode.NO_API_KEY})` + ); + } + }); + it('getGenerativeModel throws if no projectId is provided', () => { + const fakeVertexNoProject = { + ...fakeAI, + app: { options: { apiKey: 'my-key', appId: 'my-appid' } } + } as AI; + try { + getGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_PROJECT_ID); + expect((e as AIError).message).equals( + `AI: The "projectId" field is empty in the local` + + ` Firebase config. Firebase AI requires this field ` + + `to contain a valid project ID. (${AI_TYPE}/${AIErrorCode.NO_PROJECT_ID})` + ); + } + }); + it('getGenerativeModel throws if no appId is provided', () => { + const fakeVertexNoProject = { + ...fakeAI, + app: { options: { apiKey: 'my-key', projectId: 'my-projectid' } } + } as AI; + try { + getGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_APP_ID); + expect((e as AIError).message).equals( + `AI: The "appId" field is empty in the local` + + ` Firebase config. Firebase AI requires this field ` + + `to contain a valid app ID. (${AI_TYPE}/${AIErrorCode.NO_APP_ID})` + ); + } + }); + it('getGenerativeModel gets a GenerativeModel', () => { + const genModel = getGenerativeModel(fakeAI, { model: 'my-model' }); + expect(genModel).to.be.an.instanceOf(GenerativeModel); + expect(genModel.model).to.equal('publishers/google/models/my-model'); + }); + it('getImagenModel throws if no model is provided', () => { + try { + getImagenModel(fakeAI, {} as ImagenModelParams); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_MODEL); + expect((e as AIError).message).includes( + `AI: Must provide a model name. Example: ` + + `getImagenModel({ model: 'my-model-name' }) (${AI_TYPE}/${AIErrorCode.NO_MODEL})` + ); + } + }); + it('getImagenModel throws if no apiKey is provided', () => { + const fakeVertexNoApiKey = { + ...fakeAI, + app: { options: { projectId: 'my-project', appId: 'my-appid' } } + } as AI; + try { + getImagenModel(fakeVertexNoApiKey, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_API_KEY); + expect((e as AIError).message).equals( + `AI: The "apiKey" field is empty in the local ` + + `Firebase config. Firebase AI requires this field to` + + ` contain a valid API key. (${AI_TYPE}/${AIErrorCode.NO_API_KEY})` + ); + } + }); + it('getImagenModel throws if no projectId is provided', () => { + const fakeVertexNoProject = { + ...fakeAI, + app: { options: { apiKey: 'my-key', appId: 'my-appid' } } + } as AI; + try { + getImagenModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_PROJECT_ID); + expect((e as AIError).message).equals( + `AI: The "projectId" field is empty in the local` + + ` Firebase config. Firebase AI requires this field ` + + `to contain a valid project ID. (${AI_TYPE}/${AIErrorCode.NO_PROJECT_ID})` + ); + } + }); + it('getImagenModel throws if no appId is provided', () => { + const fakeVertexNoProject = { + ...fakeAI, + app: { options: { apiKey: 'my-key', projectId: 'my-project' } } + } as AI; + try { + getImagenModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_APP_ID); + expect((e as AIError).message).equals( + `AI: The "appId" field is empty in the local` + + ` Firebase config. Firebase AI requires this field ` + + `to contain a valid app ID. (${AI_TYPE}/${AIErrorCode.NO_APP_ID})` + ); + } + }); + it('getImagenModel gets an ImagenModel', () => { + const genModel = getImagenModel(fakeAI, { model: 'my-model' }); + expect(genModel).to.be.an.instanceOf(ImagenModel); + expect(genModel.model).to.equal('publishers/google/models/my-model'); + }); + + it('getLiveGenerativeModel throws if no apiKey is provided', () => { + const fakeVertexNoApiKey = { + ...fakeAI, + app: { options: { projectId: 'my-project', appId: 'my-appid' } } + } as AI; + try { + getLiveGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_API_KEY); + expect((e as AIError).message).equals( + `AI: The "apiKey" field is empty in the local ` + + `Firebase config. Firebase AI requires this field to` + + ` contain a valid API key. (${AI_TYPE}/${AIErrorCode.NO_API_KEY})` + ); + } + }); + it('getLiveGenerativeModel throws if no projectId is provided', () => { + const fakeVertexNoProject = { + ...fakeAI, + app: { options: { apiKey: 'my-key', appId: 'my-appid' } } + } as AI; + try { + getLiveGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_PROJECT_ID); + expect((e as AIError).message).equals( + `AI: The "projectId" field is empty in the local` + + ` Firebase config. Firebase AI requires this field ` + + `to contain a valid project ID. (${AI_TYPE}/${AIErrorCode.NO_PROJECT_ID})` + ); + } + }); + it('getLiveGenerativeModel throws if no appId is provided', () => { + const fakeVertexNoProject = { + ...fakeAI, + app: { options: { apiKey: 'my-key', projectId: 'my-project' } } + } as AI; + try { + getLiveGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.NO_APP_ID); + expect((e as AIError).message).equals( + `AI: The "appId" field is empty in the local` + + ` Firebase config. Firebase AI requires this field ` + + `to contain a valid app ID. (${AI_TYPE}/${AIErrorCode.NO_APP_ID})` + ); + } + }); + it('getLiveGenerativeModel gets a LiveGenerativeModel', () => { + const liveGenerativeModel = getLiveGenerativeModel(fakeAI, { + model: 'my-model' + }); + expect(liveGenerativeModel).to.be.an.instanceOf(LiveGenerativeModel); + expect(liveGenerativeModel.model).to.equal( + 'publishers/google/models/my-model' + ); + }); +}); diff --git a/packages/ai/src/api.ts b/packages/ai/src/api.ts new file mode 100644 index 00000000000..6e56aea793c --- /dev/null +++ b/packages/ai/src/api.ts @@ -0,0 +1,204 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseApp, getApp, _getProvider } from '@firebase/app'; +import { Provider } from '@firebase/component'; +import { getModularInstance } from '@firebase/util'; +import { AI_TYPE, DEFAULT_HYBRID_IN_CLOUD_MODEL } from './constants'; +import { AIService } from './service'; +import { AI, AIOptions } from './public-types'; +import { + ImagenModelParams, + HybridParams, + ModelParams, + RequestOptions, + AIErrorCode, + LiveModelParams +} from './types'; +import { AIError } from './errors'; +import { + AIModel, + GenerativeModel, + LiveGenerativeModel, + ImagenModel +} from './models'; +import { encodeInstanceIdentifier } from './helpers'; +import { GoogleAIBackend } from './backend'; +import { WebSocketHandlerImpl } from './websocket'; + +export { ChatSession } from './methods/chat-session'; +export { LiveSession } from './methods/live-session'; +export * from './requests/schema-builder'; +export { ImagenImageFormat } from './requests/imagen-image-format'; +export { AIModel, GenerativeModel, LiveGenerativeModel, ImagenModel, AIError }; +export { Backend, VertexAIBackend, GoogleAIBackend } from './backend'; +export { + startAudioConversation, + AudioConversationController, + StartAudioConversationOptions +} from './methods/live-session-helpers'; + +declare module '@firebase/component' { + interface NameServiceMapping { + [AI_TYPE]: AIService; + } +} + +/** + * Returns the default {@link AI} instance that is associated with the provided + * {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new instance with the + * default settings. + * + * @example + * ```javascript + * const ai = getAI(app); + * ``` + * + * @example + * ```javascript + * // Get an AI instance configured to use the Gemini Developer API (via Google AI). + * const ai = getAI(app, { backend: new GoogleAIBackend() }); + * ``` + * + * @example + * ```javascript + * // Get an AI instance configured to use the Vertex AI Gemini API. + * const ai = getAI(app, { backend: new VertexAIBackend() }); + * ``` + * + * @param app - The {@link @firebase/app#FirebaseApp} to use. + * @param options - {@link AIOptions} that configure the AI instance. + * @returns The default {@link AI} instance for the given {@link @firebase/app#FirebaseApp}. + * + * @public + */ +export function getAI(app: FirebaseApp = getApp(), options?: AIOptions): AI { + app = getModularInstance(app); + // Dependencies + const AIProvider: Provider<'AI'> = _getProvider(app, AI_TYPE); + + const backend = options?.backend ?? new GoogleAIBackend(); + + const finalOptions: Omit = { + useLimitedUseAppCheckTokens: options?.useLimitedUseAppCheckTokens ?? false + }; + + const identifier = encodeInstanceIdentifier(backend); + const aiInstance = AIProvider.getImmediate({ + identifier + }); + + aiInstance.options = finalOptions; + + return aiInstance; +} + +/** + * Returns a {@link GenerativeModel} class with methods for inference + * and other functionality. + * + * @public + */ +export function getGenerativeModel( + ai: AI, + modelParams: ModelParams | HybridParams, + requestOptions?: RequestOptions +): GenerativeModel { + // Uses the existence of HybridParams.mode to clarify the type of the modelParams input. + const hybridParams = modelParams as HybridParams; + let inCloudParams: ModelParams; + if (hybridParams.mode) { + inCloudParams = hybridParams.inCloudParams || { + model: DEFAULT_HYBRID_IN_CLOUD_MODEL + }; + } else { + inCloudParams = modelParams as ModelParams; + } + + if (!inCloudParams.model) { + throw new AIError( + AIErrorCode.NO_MODEL, + `Must provide a model name. Example: getGenerativeModel({ model: 'my-model-name' })` + ); + } + + /** + * An AIService registered by index.node.ts will not have a + * chromeAdapterFactory() method. + */ + const chromeAdapter = (ai as AIService).chromeAdapterFactory?.( + hybridParams.mode, + typeof window === 'undefined' ? undefined : window, + hybridParams.onDeviceParams + ); + + return new GenerativeModel(ai, inCloudParams, requestOptions, chromeAdapter); +} + +/** + * Returns an {@link ImagenModel} class with methods for using Imagen. + * + * Only Imagen 3 models (named `imagen-3.0-*`) are supported. + * + * @param ai - An {@link AI} instance. + * @param modelParams - Parameters to use when making Imagen requests. + * @param requestOptions - Additional options to use when making requests. + * + * @throws If the `apiKey` or `projectId` fields are missing in your + * Firebase config. + * + * @public + */ +export function getImagenModel( + ai: AI, + modelParams: ImagenModelParams, + requestOptions?: RequestOptions +): ImagenModel { + if (!modelParams.model) { + throw new AIError( + AIErrorCode.NO_MODEL, + `Must provide a model name. Example: getImagenModel({ model: 'my-model-name' })` + ); + } + return new ImagenModel(ai, modelParams, requestOptions); +} + +/** + * Returns a {@link LiveGenerativeModel} class for real-time, bidirectional communication. + * + * The Live API is only supported in modern browser windows and Node >= 22. + * + * @param ai - An {@link AI} instance. + * @param modelParams - Parameters to use when setting up a {@link LiveSession}. + * @throws If the `apiKey` or `projectId` fields are missing in your + * Firebase config. + * + * @beta + */ +export function getLiveGenerativeModel( + ai: AI, + modelParams: LiveModelParams +): LiveGenerativeModel { + if (!modelParams.model) { + throw new AIError( + AIErrorCode.NO_MODEL, + `Must provide a model name for getLiveGenerativeModel. Example: getLiveGenerativeModel(ai, { model: 'my-model-name' })` + ); + } + const webSocketHandler = new WebSocketHandlerImpl(); + return new LiveGenerativeModel(ai, modelParams, webSocketHandler); +} diff --git a/packages/ai/src/backend.test.ts b/packages/ai/src/backend.test.ts new file mode 100644 index 00000000000..0c6609277e3 --- /dev/null +++ b/packages/ai/src/backend.test.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { GoogleAIBackend, VertexAIBackend } from './backend'; +import { BackendType } from './public-types'; +import { DEFAULT_LOCATION } from './constants'; + +describe('Backend', () => { + describe('GoogleAIBackend', () => { + it('sets backendType to GOOGLE_AI', () => { + const backend = new GoogleAIBackend(); + expect(backend.backendType).to.equal(BackendType.GOOGLE_AI); + }); + }); + describe('VertexAIBackend', () => { + it('set backendType to VERTEX_AI', () => { + const backend = new VertexAIBackend(); + expect(backend.backendType).to.equal(BackendType.VERTEX_AI); + expect(backend.location).to.equal(DEFAULT_LOCATION); + }); + it('sets custom location', () => { + const backend = new VertexAIBackend('test-location'); + expect(backend.backendType).to.equal(BackendType.VERTEX_AI); + expect(backend.location).to.equal('test-location'); + }); + it('uses default location if location is empty string', () => { + const backend = new VertexAIBackend(''); + expect(backend.backendType).to.equal(BackendType.VERTEX_AI); + expect(backend.location).to.equal(DEFAULT_LOCATION); + }); + it('uses default location if location is null', () => { + const backend = new VertexAIBackend(null as any); + expect(backend.backendType).to.equal(BackendType.VERTEX_AI); + expect(backend.location).to.equal(DEFAULT_LOCATION); + }); + }); +}); diff --git a/packages/ai/src/backend.ts b/packages/ai/src/backend.ts new file mode 100644 index 00000000000..7209828122b --- /dev/null +++ b/packages/ai/src/backend.ts @@ -0,0 +1,92 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DEFAULT_LOCATION } from './constants'; +import { BackendType } from './public-types'; + +/** + * Abstract base class representing the configuration for an AI service backend. + * This class should not be instantiated directly. Use its subclasses; {@link GoogleAIBackend} for + * the Gemini Developer API (via {@link https://ai.google/ | Google AI}), and + * {@link VertexAIBackend} for the Vertex AI Gemini API. + * + * @public + */ +export abstract class Backend { + /** + * Specifies the backend type. + */ + readonly backendType: BackendType; + + /** + * Protected constructor for use by subclasses. + * @param type - The backend type. + */ + protected constructor(type: BackendType) { + this.backendType = type; + } +} + +/** + * Configuration class for the Gemini Developer API. + * + * Use this with {@link AIOptions} when initializing the AI service via + * {@link getAI | getAI()} to specify the Gemini Developer API as the backend. + * + * @public + */ +export class GoogleAIBackend extends Backend { + /** + * Creates a configuration object for the Gemini Developer API backend. + */ + constructor() { + super(BackendType.GOOGLE_AI); + } +} + +/** + * Configuration class for the Vertex AI Gemini API. + * + * Use this with {@link AIOptions} when initializing the AI service via + * {@link getAI | getAI()} to specify the Vertex AI Gemini API as the backend. + * + * @public + */ +export class VertexAIBackend extends Backend { + /** + * The region identifier. + * See {@link https://firebase.google.com/docs/vertex-ai/locations#available-locations | Vertex AI locations} + * for a list of supported locations. + */ + readonly location: string; + + /** + * Creates a configuration object for the Vertex AI backend. + * + * @param location - The region identifier, defaulting to `us-central1`; + * see {@link https://firebase.google.com/docs/vertex-ai/locations#available-locations | Vertex AI locations} + * for a list of supported locations. + */ + constructor(location: string = DEFAULT_LOCATION) { + super(BackendType.VERTEX_AI); + if (!location) { + this.location = DEFAULT_LOCATION; + } else { + this.location = location; + } + } +} diff --git a/packages/ai/src/constants.ts b/packages/ai/src/constants.ts new file mode 100644 index 00000000000..82482527f3b --- /dev/null +++ b/packages/ai/src/constants.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { version } from '../package.json'; + +export const AI_TYPE = 'AI'; + +export const DEFAULT_LOCATION = 'us-central1'; + +export const DEFAULT_DOMAIN = 'firebasevertexai.googleapis.com'; + +export const DEFAULT_API_VERSION = 'v1beta'; + +export const PACKAGE_VERSION = version; + +export const LANGUAGE_TAG = 'gl-js'; + +export const DEFAULT_FETCH_TIMEOUT_MS = 180 * 1000; + +/** + * Defines the name of the default in-cloud model to use for hybrid inference. + */ +export const DEFAULT_HYBRID_IN_CLOUD_MODEL = 'gemini-2.0-flash-lite'; diff --git a/packages/ai/src/errors.ts b/packages/ai/src/errors.ts new file mode 100644 index 00000000000..8190510d0d8 --- /dev/null +++ b/packages/ai/src/errors.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseError } from '@firebase/util'; +import { AIErrorCode, CustomErrorData } from './types'; +import { AI_TYPE } from './constants'; + +/** + * Error class for the Firebase AI SDK. + * + * @public + */ +export class AIError extends FirebaseError { + /** + * Constructs a new instance of the `AIError` class. + * + * @param code - The error code from {@link (AIErrorCode:type)}. + * @param message - A human-readable message describing the error. + * @param customErrorData - Optional error data. + */ + constructor( + readonly code: AIErrorCode, + message: string, + readonly customErrorData?: CustomErrorData + ) { + // Match error format used by FirebaseError from ErrorFactory + const service = AI_TYPE; + const fullCode = `${service}/${code}`; + const fullMessage = `${service}: ${message} (${fullCode})`; + super(code, fullMessage); + + // FirebaseError initializes a stack trace, but it assumes the error is created from the error + // factory. Since we break this assumption, we set the stack trace to be originating from this + // constructor. + // This is only supported in V8. + if (Error.captureStackTrace) { + // Allows us to initialize the stack trace without including the constructor itself at the + // top level of the stack trace. + Error.captureStackTrace(this, AIError); + } + + // Allows instanceof AIError in ES5/ES6 + // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work + // TODO(dlarocque): Replace this with `new.target`: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + // which we can now use since we no longer target ES5. + Object.setPrototypeOf(this, AIError.prototype); + + // Since Error is an interface, we don't inherit toString and so we define it ourselves. + this.toString = () => fullMessage; + } +} diff --git a/packages/ai/src/factory-browser.ts b/packages/ai/src/factory-browser.ts new file mode 100644 index 00000000000..98b91812397 --- /dev/null +++ b/packages/ai/src/factory-browser.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ComponentContainer, + InstanceFactoryOptions +} from '@firebase/component'; +import { AIError } from './errors'; +import { decodeInstanceIdentifier } from './helpers'; +import { chromeAdapterFactory } from './methods/chrome-adapter'; +import { AIService } from './service'; +import { AIErrorCode } from './types'; + +export function factory( + container: ComponentContainer, + { instanceIdentifier }: InstanceFactoryOptions +): AIService { + if (!instanceIdentifier) { + throw new AIError( + AIErrorCode.ERROR, + 'AIService instance identifier is undefined.' + ); + } + + const backend = decodeInstanceIdentifier(instanceIdentifier); + + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app').getImmediate(); + const auth = container.getProvider('auth-internal'); + const appCheckProvider = container.getProvider('app-check-internal'); + + return new AIService( + app, + backend, + auth, + appCheckProvider, + chromeAdapterFactory + ); +} diff --git a/packages/ai/src/factory-node.ts b/packages/ai/src/factory-node.ts new file mode 100644 index 00000000000..212ca18a824 --- /dev/null +++ b/packages/ai/src/factory-node.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ComponentContainer, + InstanceFactoryOptions +} from '@firebase/component'; +import { AIError } from './errors'; +import { decodeInstanceIdentifier } from './helpers'; +import { AIService } from './service'; +import { AIErrorCode } from './types'; + +export function factory( + container: ComponentContainer, + { instanceIdentifier }: InstanceFactoryOptions +): AIService { + if (!instanceIdentifier) { + throw new AIError( + AIErrorCode.ERROR, + 'AIService instance identifier is undefined.' + ); + } + + const backend = decodeInstanceIdentifier(instanceIdentifier); + + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app').getImmediate(); + const auth = container.getProvider('auth-internal'); + const appCheckProvider = container.getProvider('app-check-internal'); + + return new AIService(app, backend, auth, appCheckProvider); +} diff --git a/packages/ai/src/googleai-mappers.test.ts b/packages/ai/src/googleai-mappers.test.ts new file mode 100644 index 00000000000..12f422625f5 --- /dev/null +++ b/packages/ai/src/googleai-mappers.test.ts @@ -0,0 +1,391 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import sinon, { restore, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import { + mapCountTokensRequest, + mapGenerateContentCandidates, + mapGenerateContentRequest, + mapGenerateContentResponse, + mapPromptFeedback +} from './googleai-mappers'; +import { + BlockReason, + Content, + CountTokensRequest, + GenerateContentRequest, + HarmBlockMethod, + HarmBlockThreshold, + HarmCategory, + HarmProbability, + HarmSeverity, + SafetyRating, + AIErrorCode, + FinishReason, + PromptFeedback +} from './types'; +import { + GoogleAIGenerateContentResponse, + GoogleAIGenerateContentCandidate, + GoogleAICountTokensRequest +} from './types/googleai'; +import { logger } from './logger'; +import { AIError } from './errors'; +import { getMockResponse } from '../test-utils/mock-response'; + +use(sinonChai); + +const fakeModel = 'models/gemini-pro'; + +const fakeContents: Content[] = [{ role: 'user', parts: [{ text: 'hello' }] }]; + +describe('Google AI Mappers', () => { + let loggerWarnStub: sinon.SinonStub; + + beforeEach(() => { + loggerWarnStub = stub(logger, 'warn'); + }); + + afterEach(() => { + restore(); + }); + + describe('mapGenerateContentRequest', () => { + it('should throw if safetySettings contain method', () => { + const request: GenerateContentRequest = { + contents: fakeContents, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + method: HarmBlockMethod.SEVERITY + } + ] + }; + expect(() => mapGenerateContentRequest(request)) + .to.throw(AIError, /SafetySetting.method is not supported/i) + .with.property('code', AIErrorCode.UNSUPPORTED); + }); + + it('should warn and round topK if present', () => { + const request: GenerateContentRequest = { + contents: fakeContents, + generationConfig: { + topK: 15.7 + } + }; + const mappedRequest = mapGenerateContentRequest(request); + expect(loggerWarnStub).to.have.been.calledOnceWith( + 'topK in GenerationConfig has been rounded to the nearest integer to match the format for requests to the Gemini Developer API.' + ); + expect(mappedRequest.generationConfig?.topK).to.equal(16); + }); + + it('should not modify topK if it is already an integer', () => { + const request: GenerateContentRequest = { + contents: fakeContents, + generationConfig: { + topK: 16 + } + }; + const mappedRequest = mapGenerateContentRequest(request); + expect(loggerWarnStub).to.not.have.been.called; + expect(mappedRequest.generationConfig?.topK).to.equal(16); + }); + + it('should return the request mostly unchanged if valid', () => { + const request: GenerateContentRequest = { + contents: fakeContents, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE + } + ], + generationConfig: { + temperature: 0.5 + } + }; + const mappedRequest = mapGenerateContentRequest({ ...request }); + expect(mappedRequest).to.deep.equal(request); + expect(loggerWarnStub).to.not.have.been.called; + }); + }); + + describe('mapGenerateContentResponse', () => { + it('should map a full Google AI response', async () => { + const googleAIMockResponse: GoogleAIGenerateContentResponse = await ( + getMockResponse('googleAI', 'unary-success-citations.json') as Response + ).json(); + const mappedResponse = mapGenerateContentResponse(googleAIMockResponse); + + expect(mappedResponse.candidates).to.exist; + expect(mappedResponse.candidates?.[0].content.parts[0].text).to.contain( + 'quantum mechanics' + ); + + // Mapped citations + expect( + mappedResponse.candidates?.[0].citationMetadata?.citations[0].startIndex + ).to.equal( + googleAIMockResponse.candidates?.[0].citationMetadata + ?.citationSources[0].startIndex + ); + expect( + mappedResponse.candidates?.[0].citationMetadata?.citations[0].endIndex + ).to.equal( + googleAIMockResponse.candidates?.[0].citationMetadata + ?.citationSources[0].endIndex + ); + + // Mapped safety ratings + expect( + mappedResponse.candidates?.[0].safetyRatings?.[0].probabilityScore + ).to.equal(0); + expect( + mappedResponse.candidates?.[0].safetyRatings?.[0].severityScore + ).to.equal(0); + expect( + mappedResponse.candidates?.[0].safetyRatings?.[0].severity + ).to.equal(HarmSeverity.HARM_SEVERITY_UNSUPPORTED); + + expect(mappedResponse.candidates?.[0].finishReason).to.equal( + FinishReason.STOP + ); + + // Check usage metadata passthrough + expect(mappedResponse.usageMetadata).to.deep.equal( + googleAIMockResponse.usageMetadata + ); + }); + + it('should handle missing candidates and promptFeedback', () => { + const googleAIResponse: GoogleAIGenerateContentResponse = { + // No candidates + // No promptFeedback + usageMetadata: { + promptTokenCount: 5, + candidatesTokenCount: 0, + totalTokenCount: 5 + } + }; + const mappedResponse = mapGenerateContentResponse(googleAIResponse); + expect(mappedResponse.candidates).to.be.undefined; + expect(mappedResponse.promptFeedback).to.be.undefined; // Mapped to undefined + expect(mappedResponse.usageMetadata).to.deep.equal( + googleAIResponse.usageMetadata + ); + }); + + it('should handle empty candidates array', () => { + const googleAIResponse: GoogleAIGenerateContentResponse = { + candidates: [], + usageMetadata: { + promptTokenCount: 5, + candidatesTokenCount: 0, + totalTokenCount: 5 + } + }; + const mappedResponse = mapGenerateContentResponse(googleAIResponse); + expect(mappedResponse.candidates).to.deep.equal([]); + expect(mappedResponse.promptFeedback).to.be.undefined; + expect(mappedResponse.usageMetadata).to.deep.equal( + googleAIResponse.usageMetadata + ); + }); + }); + + describe('mapCountTokensRequest', () => { + it('should map a Vertex AI CountTokensRequest to Google AI format', () => { + const vertexRequest: CountTokensRequest = { + contents: fakeContents, + systemInstruction: { role: 'system', parts: [{ text: 'Be nice' }] }, + tools: [ + { functionDeclarations: [{ name: 'foo', description: 'bar' }] } + ], + generationConfig: { temperature: 0.8 } + }; + + const expectedGoogleAIRequest: GoogleAICountTokensRequest = { + generateContentRequest: { + model: fakeModel, + contents: vertexRequest.contents, + systemInstruction: vertexRequest.systemInstruction, + tools: vertexRequest.tools, + generationConfig: vertexRequest.generationConfig + } + }; + + const mappedRequest = mapCountTokensRequest(vertexRequest, fakeModel); + expect(mappedRequest).to.deep.equal(expectedGoogleAIRequest); + }); + + it('should map a minimal Vertex AI CountTokensRequest', () => { + const vertexRequest: CountTokensRequest = { + contents: fakeContents, + systemInstruction: { role: 'system', parts: [{ text: 'Be nice' }] }, + generationConfig: { temperature: 0.8 } + }; + + const expectedGoogleAIRequest: GoogleAICountTokensRequest = { + generateContentRequest: { + model: fakeModel, + contents: vertexRequest.contents, + systemInstruction: { role: 'system', parts: [{ text: 'Be nice' }] }, + generationConfig: { temperature: 0.8 } + } + }; + + const mappedRequest = mapCountTokensRequest(vertexRequest, fakeModel); + expect(mappedRequest).to.deep.equal(expectedGoogleAIRequest); + }); + }); + + describe('mapGenerateContentCandidates', () => { + it('should map citationSources to citationMetadata.citations', () => { + const candidates: GoogleAIGenerateContentCandidate[] = [ + { + index: 0, + content: { role: 'model', parts: [{ text: 'Cited text' }] }, + citationMetadata: { + citationSources: [ + { startIndex: 0, endIndex: 5, uri: 'uri1', license: 'MIT' }, + { startIndex: 6, endIndex: 10, uri: 'uri2' } + ] + } + } + ]; + const mapped = mapGenerateContentCandidates(candidates); + expect(mapped[0].citationMetadata).to.exist; + expect(mapped[0].citationMetadata?.citations).to.deep.equal( + candidates[0].citationMetadata?.citationSources + ); + expect(mapped[0].citationMetadata?.citations[0].title).to.be.undefined; // Not in Google AI + expect(mapped[0].citationMetadata?.citations[0].publicationDate).to.be + .undefined; // Not in Google AI + }); + + it('should add default safety rating properties', () => { + const candidates: GoogleAIGenerateContentCandidate[] = [ + { + index: 0, + content: { role: 'model', parts: [{ text: 'Maybe unsafe' }] }, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + probability: HarmProbability.MEDIUM, + blocked: false + // Missing severity, probabilityScore, severityScore + } as any + ] + } + ]; + const mapped = mapGenerateContentCandidates(candidates); + expect(mapped[0].safetyRatings).to.exist; + const safetyRating = mapped[0].safetyRatings?.[0] as SafetyRating; // Type assertion + expect(safetyRating.severity).to.equal( + HarmSeverity.HARM_SEVERITY_UNSUPPORTED + ); + expect(safetyRating.probabilityScore).to.equal(0); + expect(safetyRating.severityScore).to.equal(0); + // Existing properties should be preserved + expect(safetyRating.category).to.equal( + HarmCategory.HARM_CATEGORY_HARASSMENT + ); + expect(safetyRating.probability).to.equal(HarmProbability.MEDIUM); + expect(safetyRating.blocked).to.be.false; + }); + + it('should throw if videoMetadata is present in parts', () => { + const candidates: GoogleAIGenerateContentCandidate[] = [ + { + index: 0, + content: { + role: 'model', + parts: [ + { + inlineData: { mimeType: 'video/mp4', data: 'base64==' }, + videoMetadata: { startOffset: '0s', endOffset: '5s' } // Unsupported + } + ] + } + } + ]; + expect(() => mapGenerateContentCandidates(candidates)) + .to.throw(AIError, /Part.videoMetadata is not supported/i) + .with.property('code', AIErrorCode.UNSUPPORTED); + }); + + it('should handle candidates without citation or safety ratings', () => { + const candidates: GoogleAIGenerateContentCandidate[] = [ + { + index: 0, + content: { role: 'model', parts: [{ text: 'Simple text' }] }, + finishReason: FinishReason.STOP + } + ]; + const mapped = mapGenerateContentCandidates(candidates); + expect(mapped[0].citationMetadata).to.be.undefined; + expect(mapped[0].safetyRatings).to.be.undefined; + expect(mapped[0].content.parts[0].text).to.equal('Simple text'); + expect(loggerWarnStub).to.not.have.been.called; + }); + + it('should handle empty candidate array', () => { + const candidates: GoogleAIGenerateContentCandidate[] = []; + const mapped = mapGenerateContentCandidates(candidates); + expect(mapped).to.deep.equal([]); + expect(loggerWarnStub).to.not.have.been.called; + }); + }); + + describe('mapPromptFeedback', () => { + it('should add default safety rating properties', () => { + const feedback: PromptFeedback = { + blockReason: BlockReason.OTHER, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + probability: HarmProbability.HIGH, + blocked: true + // Missing severity, probabilityScore, severityScore + } as any + ] + // Missing blockReasonMessage + }; + const mapped = mapPromptFeedback(feedback); + expect(mapped.safetyRatings).to.exist; + const safetyRating = mapped.safetyRatings[0] as SafetyRating; // Type assertion + expect(safetyRating.severity).to.equal( + HarmSeverity.HARM_SEVERITY_UNSUPPORTED + ); + expect(safetyRating.probabilityScore).to.equal(0); + expect(safetyRating.severityScore).to.equal(0); + // Existing properties should be preserved + expect(safetyRating.category).to.equal( + HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT + ); + expect(safetyRating.probability).to.equal(HarmProbability.HIGH); + expect(safetyRating.blocked).to.be.true; + // Other properties + expect(mapped.blockReason).to.equal(BlockReason.OTHER); + expect(mapped.blockReasonMessage).to.be.undefined; // Not present in input + }); + }); +}); diff --git a/packages/ai/src/googleai-mappers.ts b/packages/ai/src/googleai-mappers.ts new file mode 100644 index 00000000000..c6656c8318d --- /dev/null +++ b/packages/ai/src/googleai-mappers.ts @@ -0,0 +1,228 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from './errors'; +import { logger } from './logger'; +import { + CitationMetadata, + CountTokensRequest, + GenerateContentCandidate, + GenerateContentRequest, + GenerateContentResponse, + HarmSeverity, + InlineDataPart, + PromptFeedback, + SafetyRating, + AIErrorCode +} from './types'; +import { + GoogleAIGenerateContentResponse, + GoogleAIGenerateContentCandidate, + GoogleAICountTokensRequest +} from './types/googleai'; + +/** + * This SDK supports both the Vertex AI Gemini API and the Gemini Developer API (using Google AI). + * The public API prioritizes the format used by the Vertex AI Gemini API. + * We avoid having two sets of types by translating requests and responses between the two API formats. + * This translation allows developers to switch between the Vertex AI Gemini API and the Gemini Developer API + * with minimal code changes. + * + * In here are functions that map requests and responses between the two API formats. + * Requests in the Vertex AI format are mapped to the Google AI format before being sent. + * Responses from the Google AI backend are mapped back to the Vertex AI format before being returned to the user. + */ + +/** + * Maps a Vertex AI {@link GenerateContentRequest} to a format that can be sent to Google AI. + * + * @param generateContentRequest The {@link GenerateContentRequest} to map. + * @returns A {@link GenerateContentResponse} that conforms to the Google AI format. + * + * @throws If the request contains properties that are unsupported by Google AI. + * + * @internal + */ +export function mapGenerateContentRequest( + generateContentRequest: GenerateContentRequest +): GenerateContentRequest { + generateContentRequest.safetySettings?.forEach(safetySetting => { + if (safetySetting.method) { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'SafetySetting.method is not supported in the the Gemini Developer API. Please remove this property.' + ); + } + }); + + if (generateContentRequest.generationConfig?.topK) { + const roundedTopK = Math.round( + generateContentRequest.generationConfig.topK + ); + + if (roundedTopK !== generateContentRequest.generationConfig.topK) { + logger.warn( + 'topK in GenerationConfig has been rounded to the nearest integer to match the format for requests to the Gemini Developer API.' + ); + generateContentRequest.generationConfig.topK = roundedTopK; + } + } + + return generateContentRequest; +} + +/** + * Maps a {@link GenerateContentResponse} from Google AI to the format of the + * {@link GenerateContentResponse} that we get from VertexAI that is exposed in the public API. + * + * @param googleAIResponse The {@link GenerateContentResponse} from Google AI. + * @returns A {@link GenerateContentResponse} that conforms to the public API's format. + * + * @internal + */ +export function mapGenerateContentResponse( + googleAIResponse: GoogleAIGenerateContentResponse +): GenerateContentResponse { + const generateContentResponse = { + candidates: googleAIResponse.candidates + ? mapGenerateContentCandidates(googleAIResponse.candidates) + : undefined, + prompt: googleAIResponse.promptFeedback + ? mapPromptFeedback(googleAIResponse.promptFeedback) + : undefined, + usageMetadata: googleAIResponse.usageMetadata + }; + + return generateContentResponse; +} + +/** + * Maps a Vertex AI {@link CountTokensRequest} to a format that can be sent to Google AI. + * + * @param countTokensRequest The {@link CountTokensRequest} to map. + * @param model The model to count tokens with. + * @returns A {@link CountTokensRequest} that conforms to the Google AI format. + * + * @internal + */ +export function mapCountTokensRequest( + countTokensRequest: CountTokensRequest, + model: string +): GoogleAICountTokensRequest { + const mappedCountTokensRequest: GoogleAICountTokensRequest = { + generateContentRequest: { + model, + ...countTokensRequest + } + }; + + return mappedCountTokensRequest; +} + +/** + * Maps a Google AI {@link GoogleAIGenerateContentCandidate} to a format that conforms + * to the Vertex AI API format. + * + * @param candidates The {@link GoogleAIGenerateContentCandidate} to map. + * @returns A {@link GenerateContentCandidate} that conforms to the Vertex AI format. + * + * @throws If any {@link Part} in the candidates has a `videoMetadata` property. + * + * @internal + */ +export function mapGenerateContentCandidates( + candidates: GoogleAIGenerateContentCandidate[] +): GenerateContentCandidate[] { + const mappedCandidates: GenerateContentCandidate[] = []; + let mappedSafetyRatings: SafetyRating[]; + if (mappedCandidates) { + candidates.forEach(candidate => { + // Map citationSources to citations. + let citationMetadata: CitationMetadata | undefined; + if (candidate.citationMetadata) { + citationMetadata = { + citations: candidate.citationMetadata.citationSources + }; + } + + // Assign missing candidate SafetyRatings properties to their defaults if undefined. + if (candidate.safetyRatings) { + mappedSafetyRatings = candidate.safetyRatings.map(safetyRating => { + return { + ...safetyRating, + severity: + safetyRating.severity ?? HarmSeverity.HARM_SEVERITY_UNSUPPORTED, + probabilityScore: safetyRating.probabilityScore ?? 0, + severityScore: safetyRating.severityScore ?? 0 + }; + }); + } + + // videoMetadata is not supported. + // Throw early since developers may send a long video as input and only expect to pay + // for inference on a small portion of the video. + if ( + candidate.content?.parts?.some( + part => (part as InlineDataPart)?.videoMetadata + ) + ) { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'Part.videoMetadata is not supported in the Gemini Developer API. Please remove this property.' + ); + } + + const mappedCandidate = { + index: candidate.index, + content: candidate.content, + finishReason: candidate.finishReason, + finishMessage: candidate.finishMessage, + safetyRatings: mappedSafetyRatings, + citationMetadata, + groundingMetadata: candidate.groundingMetadata, + urlContextMetadata: candidate.urlContextMetadata + }; + mappedCandidates.push(mappedCandidate); + }); + } + + return mappedCandidates; +} + +export function mapPromptFeedback( + promptFeedback: PromptFeedback +): PromptFeedback { + // Assign missing SafetyRating properties to their defaults if undefined. + const mappedSafetyRatings: SafetyRating[] = []; + promptFeedback.safetyRatings.forEach(safetyRating => { + mappedSafetyRatings.push({ + category: safetyRating.category, + probability: safetyRating.probability, + severity: safetyRating.severity ?? HarmSeverity.HARM_SEVERITY_UNSUPPORTED, + probabilityScore: safetyRating.probabilityScore ?? 0, + severityScore: safetyRating.severityScore ?? 0, + blocked: safetyRating.blocked + }); + }); + + const mappedPromptFeedback: PromptFeedback = { + blockReason: promptFeedback.blockReason, + safetyRatings: mappedSafetyRatings, + blockReasonMessage: promptFeedback.blockReasonMessage + }; + return mappedPromptFeedback; +} diff --git a/packages/ai/src/helpers.test.ts b/packages/ai/src/helpers.test.ts new file mode 100644 index 00000000000..8f5f164d6b8 --- /dev/null +++ b/packages/ai/src/helpers.test.ts @@ -0,0 +1,121 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect } from 'chai'; +import { AI_TYPE, DEFAULT_LOCATION } from './constants'; +import { encodeInstanceIdentifier, decodeInstanceIdentifier } from './helpers'; +import { AIError } from './errors'; +import { AIErrorCode } from './types'; +import { GoogleAIBackend, VertexAIBackend } from './backend'; + +describe('Identifier Encoding/Decoding', () => { + describe('encodeInstanceIdentifier', () => { + it('should encode Vertex AI identifier with a specific location', () => { + const backend = new VertexAIBackend('us-east1'); + const expected = `${AI_TYPE}/vertexai/us-east1`; + expect(encodeInstanceIdentifier(backend)).to.equal(expected); + }); + + it('should encode Vertex AI identifier using default location if location is empty string', () => { + const backend = new VertexAIBackend(''); + const expected = `${AI_TYPE}/vertexai/${DEFAULT_LOCATION}`; + expect(encodeInstanceIdentifier(backend)).to.equal(expected); + }); + + it('should encode Google AI identifier', () => { + const backend = new GoogleAIBackend(); + const expected = `${AI_TYPE}/googleai`; + expect(encodeInstanceIdentifier(backend)).to.equal(expected); + }); + + it('should throw AIError for unknown backend type', () => { + expect(() => encodeInstanceIdentifier({} as any)).to.throw(AIError); + + try { + encodeInstanceIdentifier({} as any); + expect.fail('Expected encodeInstanceIdentifier to throw'); + } catch (e) { + expect(e).to.be.instanceOf(AIError); + const error = e as AIError; + expect(error.message).to.contain('Invalid backend'); + expect(error.code).to.equal(AIErrorCode.ERROR); + } + }); + }); + + describe('decodeInstanceIdentifier', () => { + it('should decode Vertex AI identifier with location', () => { + const encoded = `${AI_TYPE}/vertexai/europe-west1`; + const backend = new VertexAIBackend('europe-west1'); + expect(decodeInstanceIdentifier(encoded)).to.deep.equal(backend); + }); + + it('should throw an error if Vertex AI identifier string without explicit location part', () => { + const encoded = `${AI_TYPE}/vertexai`; + expect(() => decodeInstanceIdentifier(encoded)).to.throw(AIError); + + try { + decodeInstanceIdentifier(encoded); + expect.fail('Expected encodeInstanceIdentifier to throw'); + } catch (e) { + expect(e).to.be.instanceOf(AIError); + const error = e as AIError; + expect(error.message).to.contain( + `Invalid instance identifier, unknown location` + ); + expect(error.code).to.equal(AIErrorCode.ERROR); + } + }); + + it('should decode Google AI identifier', () => { + const encoded = `${AI_TYPE}/googleai`; + const backend = new GoogleAIBackend(); + expect(decodeInstanceIdentifier(encoded)).to.deep.equal(backend); + }); + + it('should throw AIError for invalid backend string', () => { + const encoded = `${AI_TYPE}/someotherbackend/location`; + expect(() => decodeInstanceIdentifier(encoded)).to.throw( + AIError, + `Invalid instance identifier string: '${encoded}'` + ); + try { + decodeInstanceIdentifier(encoded); + expect.fail('Expected decodeInstanceIdentifier to throw'); + } catch (e) { + expect(e).to.be.instanceOf(AIError); + expect((e as AIError).code).to.equal(AIErrorCode.ERROR); + } + }); + + it('should throw AIError for malformed identifier string (too few parts)', () => { + const encoded = AI_TYPE; + expect(() => decodeInstanceIdentifier(encoded)).to.throw( + AIError, + `Invalid instance identifier string: '${encoded}'` + ); + }); + + it('should throw AIError for malformed identifier string (incorrect prefix)', () => { + const encoded = 'firebase/AI/location'; + // This will also hit the default case in the switch statement + expect(() => decodeInstanceIdentifier(encoded)).to.throw( + AIError, + `Invalid instance identifier, unknown prefix 'firebase'` + ); + }); + }); +}); diff --git a/packages/ai/src/helpers.ts b/packages/ai/src/helpers.ts new file mode 100644 index 00000000000..709bf4369c5 --- /dev/null +++ b/packages/ai/src/helpers.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AI_TYPE } from './constants'; +import { AIError } from './errors'; +import { AIErrorCode } from './types'; +import { Backend, GoogleAIBackend, VertexAIBackend } from './backend'; + +/** + * Encodes a {@link Backend} into a string that will be used to uniquely identify {@link AI} + * instances by backend type. + * + * @internal + */ +export function encodeInstanceIdentifier(backend: Backend): string { + if (backend instanceof GoogleAIBackend) { + return `${AI_TYPE}/googleai`; + } else if (backend instanceof VertexAIBackend) { + return `${AI_TYPE}/vertexai/${backend.location}`; + } else { + throw new AIError( + AIErrorCode.ERROR, + `Invalid backend: ${JSON.stringify(backend.backendType)}` + ); + } +} + +/** + * Decodes an instance identifier string into a {@link Backend}. + * + * @internal + */ +export function decodeInstanceIdentifier(instanceIdentifier: string): Backend { + const identifierParts = instanceIdentifier.split('/'); + if (identifierParts[0] !== AI_TYPE) { + throw new AIError( + AIErrorCode.ERROR, + `Invalid instance identifier, unknown prefix '${identifierParts[0]}'` + ); + } + const backendType = identifierParts[1]; + switch (backendType) { + case 'vertexai': + const location: string | undefined = identifierParts[2]; + if (!location) { + throw new AIError( + AIErrorCode.ERROR, + `Invalid instance identifier, unknown location '${instanceIdentifier}'` + ); + } + return new VertexAIBackend(location); + case 'googleai': + return new GoogleAIBackend(); + default: + throw new AIError( + AIErrorCode.ERROR, + `Invalid instance identifier string: '${instanceIdentifier}'` + ); + } +} diff --git a/packages/ai/src/index.node.ts b/packages/ai/src/index.node.ts new file mode 100644 index 00000000000..07d95e7d30e --- /dev/null +++ b/packages/ai/src/index.node.ts @@ -0,0 +1,45 @@ +/** + * The Firebase AI Web SDK. + * + * @packageDocumentation + */ + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { registerVersion, _registerComponent } from '@firebase/app'; +import { AI_TYPE } from './constants'; +import { Component, ComponentType } from '@firebase/component'; +import { name, version } from '../package.json'; +import { factory } from './factory-node'; + +function registerAI(): void { + _registerComponent( + new Component(AI_TYPE, factory, ComponentType.PUBLIC).setMultipleInstances( + true + ) + ); + + registerVersion(name, version, 'node'); + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation + registerVersion(name, version, '__BUILD_TARGET__'); +} + +registerAI(); + +export * from './api'; +export * from './public-types'; diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts new file mode 100644 index 00000000000..9d787c832dd --- /dev/null +++ b/packages/ai/src/index.ts @@ -0,0 +1,52 @@ +/** + * The Firebase AI Web SDK. + * + * @packageDocumentation + */ + +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { registerVersion, _registerComponent } from '@firebase/app'; +import { AI_TYPE } from './constants'; +import { Component, ComponentType } from '@firebase/component'; +import { name, version } from '../package.json'; +import { LanguageModel } from './types/language-model'; +import { factory } from './factory-browser'; + +declare global { + interface Window { + LanguageModel: LanguageModel; + } +} + +function registerAI(): void { + _registerComponent( + new Component(AI_TYPE, factory, ComponentType.PUBLIC).setMultipleInstances( + true + ) + ); + + registerVersion(name, version); + // BUILD_TARGET will be replaced by values like esm, cjs, etc during the compilation + registerVersion(name, version, '__BUILD_TARGET__'); +} + +registerAI(); + +export * from './api'; +export * from './public-types'; diff --git a/packages/ai/src/logger.ts b/packages/ai/src/logger.ts new file mode 100644 index 00000000000..7664d6b4e31 --- /dev/null +++ b/packages/ai/src/logger.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Logger } from '@firebase/logger'; + +export const logger = new Logger('@firebase/vertexai'); diff --git a/packages/ai/src/methods/chat-session-helpers.test.ts b/packages/ai/src/methods/chat-session-helpers.test.ts new file mode 100644 index 00000000000..e64f3e84e2d --- /dev/null +++ b/packages/ai/src/methods/chat-session-helpers.test.ts @@ -0,0 +1,213 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { validateChatHistory } from './chat-session-helpers'; +import { expect } from 'chai'; +import { Content } from '../types'; +import { FirebaseError } from '@firebase/util'; + +describe('chat-session-helpers', () => { + describe('validateChatHistory', () => { + const TCS: Array<{ + history: Content[]; + isValid: boolean; + errorShouldInclude?: string; + }> = [ + { + history: [{ role: 'user', parts: [{ text: 'hi' }] }], + isValid: true + }, + { + history: [ + { + role: 'user', + parts: [ + { text: 'hi' }, + { inlineData: { mimeType: 'image/jpeg', data: 'base64==' } } + ] + } + ], + isValid: true + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { role: 'model', parts: [{ text: 'hi' }, { text: 'hi' }] } + ], + isValid: true + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }] + } + ], + isValid: true + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }] + }, + { + role: 'function', + parts: [ + { + functionResponse: { name: 'greet', response: { name: 'user' } } + } + ] + } + ], + isValid: true + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }] + }, + { + role: 'function', + parts: [ + { + functionResponse: { name: 'greet', response: { name: 'user' } } + } + ] + }, + { + role: 'model', + parts: [{ text: 'hi name' }] + } + ], + isValid: true + }, + { + //@ts-expect-error + history: [{ role: 'user', parts: '' }], + errorShouldInclude: `array of Parts`, + isValid: false + }, + { + //@ts-expect-error + history: [{ role: 'user' }], + errorShouldInclude: `array of Parts`, + isValid: false + }, + { + history: [{ role: 'user', parts: [] }], + errorShouldInclude: `at least one part`, + isValid: false + }, + { + history: [{ role: 'model', parts: [{ text: 'hi' }] }], + errorShouldInclude: `model`, + isValid: false + }, + { + history: [ + { + role: 'function', + parts: [ + { + functionResponse: { name: 'greet', response: { name: 'user' } } + } + ] + } + ], + errorShouldInclude: `function`, + isValid: false + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { role: 'user', parts: [{ text: 'hi' }] } + ], + errorShouldInclude: `can't follow 'user'`, + isValid: false + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { role: 'model', parts: [{ text: 'hi' }] }, + { role: 'model', parts: [{ text: 'hi' }] } + ], + errorShouldInclude: `can't follow 'model'`, + isValid: false + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [ + { text: 'hi' }, + { + text: 'thought about hi', + thought: true, + thoughtSignature: 'thought signature' + } + ] + } + ], + isValid: true + }, + { + history: [ + { + role: 'user', + parts: [{ text: 'hi', thought: true, thoughtSignature: 'sig' }] + }, + { + role: 'model', + parts: [ + { text: 'hi' }, + { + text: 'thought about hi', + thought: true, + thoughtSignature: 'thought signature' + } + ] + } + ], + errorShouldInclude: 'thought', + isValid: false + } + ]; + TCS.forEach((tc, index) => { + it(`case ${index}`, () => { + const fn = (): void => validateChatHistory(tc.history); + if (tc.isValid) { + expect(fn).to.not.throw(); + } else { + try { + fn(); + } catch (e) { + expect(e).to.be.instanceOf(FirebaseError); + if (e instanceof FirebaseError && tc.errorShouldInclude) { + expect(e.message).to.include(tc.errorShouldInclude); + } + } + } + }); + }); + }); +}); diff --git a/packages/ai/src/methods/chat-session-helpers.ts b/packages/ai/src/methods/chat-session-helpers.ts new file mode 100644 index 00000000000..3c0c58b7bf5 --- /dev/null +++ b/packages/ai/src/methods/chat-session-helpers.ts @@ -0,0 +1,124 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Content, POSSIBLE_ROLES, Part, Role, AIErrorCode } from '../types'; +import { AIError } from '../errors'; + +// https://ai.google.dev/api/rest/v1beta/Content#part + +const VALID_PART_FIELDS: Array = [ + 'text', + 'inlineData', + 'functionCall', + 'functionResponse', + 'thought', + 'thoughtSignature' +]; + +const VALID_PARTS_PER_ROLE: { [key in Role]: Array } = { + user: ['text', 'inlineData'], + function: ['functionResponse'], + model: ['text', 'functionCall', 'thought', 'thoughtSignature'], + // System instructions shouldn't be in history anyway. + system: ['text'] +}; + +const VALID_PREVIOUS_CONTENT_ROLES: { [key in Role]: Role[] } = { + user: ['model'], + function: ['model'], + model: ['user', 'function'], + // System instructions shouldn't be in history. + system: [] +}; + +export function validateChatHistory(history: Content[]): void { + let prevContent: Content | null = null; + for (const currContent of history) { + const { role, parts } = currContent; + if (!prevContent && role !== 'user') { + throw new AIError( + AIErrorCode.INVALID_CONTENT, + `First Content should be with role 'user', got ${role}` + ); + } + if (!POSSIBLE_ROLES.includes(role)) { + throw new AIError( + AIErrorCode.INVALID_CONTENT, + `Each item should include role field. Got ${role} but valid roles are: ${JSON.stringify( + POSSIBLE_ROLES + )}` + ); + } + + if (!Array.isArray(parts)) { + throw new AIError( + AIErrorCode.INVALID_CONTENT, + `Content should have 'parts' property with an array of Parts` + ); + } + + if (parts.length === 0) { + throw new AIError( + AIErrorCode.INVALID_CONTENT, + `Each Content should have at least one part` + ); + } + + const countFields: Record = { + text: 0, + inlineData: 0, + functionCall: 0, + functionResponse: 0, + thought: 0, + thoughtSignature: 0, + executableCode: 0, + codeExecutionResult: 0 + }; + + for (const part of parts) { + for (const key of VALID_PART_FIELDS) { + if (key in part) { + countFields[key] += 1; + } + } + } + const validParts = VALID_PARTS_PER_ROLE[role]; + for (const key of VALID_PART_FIELDS) { + if (!validParts.includes(key) && countFields[key] > 0) { + throw new AIError( + AIErrorCode.INVALID_CONTENT, + `Content with role '${role}' can't contain '${key}' part` + ); + } + } + + if (prevContent) { + const validPreviousContentRoles = VALID_PREVIOUS_CONTENT_ROLES[role]; + if (!validPreviousContentRoles.includes(prevContent.role)) { + throw new AIError( + AIErrorCode.INVALID_CONTENT, + `Content with role '${role}' can't follow '${ + prevContent.role + }'. Valid previous roles: ${JSON.stringify( + VALID_PREVIOUS_CONTENT_ROLES + )}` + ); + } + } + prevContent = currContent; + } +} diff --git a/packages/ai/src/methods/chat-session.test.ts b/packages/ai/src/methods/chat-session.test.ts new file mode 100644 index 00000000000..1273d02876c --- /dev/null +++ b/packages/ai/src/methods/chat-session.test.ts @@ -0,0 +1,160 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import { match, restore, stub, useFakeTimers } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import * as generateContentMethods from './generate-content'; +import { Content, GenerateContentStreamResult } from '../types'; +import { ChatSession } from './chat-session'; +import { ApiSettings } from '../types/internal'; +import { VertexAIBackend } from '../backend'; +import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services'; + +use(sinonChai); +use(chaiAsPromised); + +const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + appId: 'my-appid', + location: 'us-central1', + backend: new VertexAIBackend() +}; + +describe('ChatSession', () => { + afterEach(() => { + restore(); + }); + describe('sendMessage()', () => { + it('generateContent errors should be catchable', async () => { + const generateContentStub = stub( + generateContentMethods, + 'generateContent' + ).rejects('generateContent failed'); + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + fakeChromeAdapter + ); + await expect(chatSession.sendMessage('hello')).to.be.rejected; + expect(generateContentStub).to.be.calledWith( + fakeApiSettings, + 'a-model', + match.any + ); + }); + it('adds message and response to history', async () => { + const fakeContent: Content = { + role: 'model', + parts: [ + { text: 'hi' }, + { + text: 'thought about hi', + thoughtSignature: 'thought signature' + } + ] + }; + const fakeResponse = { + candidates: [ + { + index: 1, + content: fakeContent + } + ] + }; + const generateContentStub = stub( + generateContentMethods, + 'generateContent' + ).resolves({ + // @ts-ignore + response: fakeResponse + }); + const chatSession = new ChatSession(fakeApiSettings, 'a-model'); + const result = await chatSession.sendMessage('hello'); + // @ts-ignore + expect(result.response).to.equal(fakeResponse); + // Test: stores history correctly? + const history = await chatSession.getHistory(); + expect(history[0].role).to.equal('user'); + expect(history[0].parts[0].text).to.equal('hello'); + expect(history[1]).to.deep.equal(fakeResponse.candidates[0].content); + // Test: sends history correctly? + await chatSession.sendMessage('hello 2'); + expect(generateContentStub.args[1][2].contents[0].parts[0].text).to.equal( + 'hello' + ); + expect(generateContentStub.args[1][2].contents[1]).to.deep.equal( + fakeResponse.candidates[0].content + ); + expect(generateContentStub.args[1][2].contents[2].parts[0].text).to.equal( + 'hello 2' + ); + }); + }); + describe('sendMessageStream()', () => { + it('generateContentStream errors should be catchable', async () => { + const clock = useFakeTimers(); + const consoleStub = stub(console, 'error'); + const generateContentStreamStub = stub( + generateContentMethods, + 'generateContentStream' + ).rejects('generateContentStream failed'); + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + fakeChromeAdapter + ); + await expect(chatSession.sendMessageStream('hello')).to.be.rejected; + expect(generateContentStreamStub).to.be.calledWith( + fakeApiSettings, + 'a-model', + match.any + ); + await clock.runAllAsync(); + expect(consoleStub).to.not.be.called; + clock.restore(); + }); + it('downstream sendPromise errors should log but not throw', async () => { + const clock = useFakeTimers(); + const consoleStub = stub(console, 'error'); + // make response undefined so that response.candidates errors + const generateContentStreamStub = stub( + generateContentMethods, + 'generateContentStream' + ).resolves({} as unknown as GenerateContentStreamResult); + const chatSession = new ChatSession( + fakeApiSettings, + 'a-model', + fakeChromeAdapter + ); + await chatSession.sendMessageStream('hello'); + expect(generateContentStreamStub).to.be.calledWith( + fakeApiSettings, + 'a-model', + match.any + ); + await clock.runAllAsync(); + expect(consoleStub.args[0][1].toString()).to.include( + // Firefox has different wording when a property is undefined + 'undefined' + ); + clock.restore(); + }); + }); +}); diff --git a/packages/ai/src/methods/chat-session.ts b/packages/ai/src/methods/chat-session.ts new file mode 100644 index 00000000000..dac16430b7a --- /dev/null +++ b/packages/ai/src/methods/chat-session.ts @@ -0,0 +1,195 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Content, + GenerateContentRequest, + GenerateContentResult, + GenerateContentStreamResult, + Part, + RequestOptions, + StartChatParams +} from '../types'; +import { formatNewContent } from '../requests/request-helpers'; +import { formatBlockErrorMessage } from '../requests/response-helpers'; +import { validateChatHistory } from './chat-session-helpers'; +import { generateContent, generateContentStream } from './generate-content'; +import { ApiSettings } from '../types/internal'; +import { logger } from '../logger'; +import { ChromeAdapter } from '../types/chrome-adapter'; + +/** + * Do not log a message for this error. + */ +const SILENT_ERROR = 'SILENT_ERROR'; + +/** + * ChatSession class that enables sending chat messages and stores + * history of sent and received messages so far. + * + * @public + */ +export class ChatSession { + private _apiSettings: ApiSettings; + private _history: Content[] = []; + private _sendPromise: Promise = Promise.resolve(); + + constructor( + apiSettings: ApiSettings, + public model: string, + private chromeAdapter?: ChromeAdapter, + public params?: StartChatParams, + public requestOptions?: RequestOptions + ) { + this._apiSettings = apiSettings; + if (params?.history) { + validateChatHistory(params.history); + this._history = params.history; + } + } + + /** + * Gets the chat history so far. Blocked prompts are not added to history. + * Neither blocked candidates nor the prompts that generated them are added + * to history. + */ + async getHistory(): Promise { + await this._sendPromise; + return this._history; + } + + /** + * Sends a chat message and receives a non-streaming + * {@link GenerateContentResult} + */ + async sendMessage( + request: string | Array + ): Promise { + await this._sendPromise; + const newContent = formatNewContent(request); + const generateContentRequest: GenerateContentRequest = { + safetySettings: this.params?.safetySettings, + generationConfig: this.params?.generationConfig, + tools: this.params?.tools, + toolConfig: this.params?.toolConfig, + systemInstruction: this.params?.systemInstruction, + contents: [...this._history, newContent] + }; + let finalResult = {} as GenerateContentResult; + // Add onto the chain. + this._sendPromise = this._sendPromise + .then(() => + generateContent( + this._apiSettings, + this.model, + generateContentRequest, + this.chromeAdapter, + this.requestOptions + ) + ) + .then(result => { + if ( + result.response.candidates && + result.response.candidates.length > 0 + ) { + this._history.push(newContent); + const responseContent: Content = { + parts: result.response.candidates?.[0].content.parts || [], + // Response seems to come back without a role set. + role: result.response.candidates?.[0].content.role || 'model' + }; + this._history.push(responseContent); + } else { + const blockErrorMessage = formatBlockErrorMessage(result.response); + if (blockErrorMessage) { + logger.warn( + `sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.` + ); + } + } + finalResult = result; + }); + await this._sendPromise; + return finalResult; + } + + /** + * Sends a chat message and receives the response as a + * {@link GenerateContentStreamResult} containing an iterable stream + * and a response promise. + */ + async sendMessageStream( + request: string | Array + ): Promise { + await this._sendPromise; + const newContent = formatNewContent(request); + const generateContentRequest: GenerateContentRequest = { + safetySettings: this.params?.safetySettings, + generationConfig: this.params?.generationConfig, + tools: this.params?.tools, + toolConfig: this.params?.toolConfig, + systemInstruction: this.params?.systemInstruction, + contents: [...this._history, newContent] + }; + const streamPromise = generateContentStream( + this._apiSettings, + this.model, + generateContentRequest, + this.chromeAdapter, + this.requestOptions + ); + + // Add onto the chain. + this._sendPromise = this._sendPromise + .then(() => streamPromise) + // This must be handled to avoid unhandled rejection, but jump + // to the final catch block with a label to not log this error. + .catch(_ignored => { + throw new Error(SILENT_ERROR); + }) + .then(streamResult => streamResult.response) + .then(response => { + if (response.candidates && response.candidates.length > 0) { + this._history.push(newContent); + const responseContent = { ...response.candidates[0].content }; + // Response seems to come back without a role set. + if (!responseContent.role) { + responseContent.role = 'model'; + } + this._history.push(responseContent); + } else { + const blockErrorMessage = formatBlockErrorMessage(response); + if (blockErrorMessage) { + logger.warn( + `sendMessageStream() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.` + ); + } + } + }) + .catch(e => { + // Errors in streamPromise are already catchable by the user as + // streamPromise is returned. + // Avoid duplicating the error message in logs. + if (e.message !== SILENT_ERROR) { + // Users do not have access to _sendPromise to catch errors + // downstream from streamPromise, so they should not throw. + logger.error(e); + } + }); + return streamPromise; + } +} diff --git a/packages/ai/src/methods/chrome-adapter-browser.test.ts b/packages/ai/src/methods/chrome-adapter-browser.test.ts new file mode 100644 index 00000000000..e37a08bf1a9 --- /dev/null +++ b/packages/ai/src/methods/chrome-adapter-browser.test.ts @@ -0,0 +1,847 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { expect, use } from 'chai'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { chromeAdapterFactory, ChromeAdapterImpl } from './chrome-adapter'; +import { + Availability, + LanguageModel, + LanguageModelCreateOptions, + LanguageModelMessage +} from '../types/language-model'; +import { match, stub } from 'sinon'; +import { GenerateContentRequest, AIErrorCode, InferenceMode } from '../types'; +import { Schema } from '../api'; + +use(sinonChai); +use(chaiAsPromised); + +/** + * Converts the ReadableStream from response.body to an array of strings. + */ +async function toStringArray( + stream: ReadableStream +): Promise { + const decoder = new TextDecoder(); + const actual = []; + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + actual.push(decoder.decode(value)); + } + return actual; +} + +describe('ChromeAdapter', () => { + describe('constructor', () => { + it('sets image as expected input type by default', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.AVAILABLE) + } as LanguageModel; + const availabilityStub = stub( + languageModelProvider, + 'availability' + ).resolves(Availability.AVAILABLE); + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [{ text: 'hi' }] + } + ] + }); + expect(availabilityStub).to.have.been.calledWith({ + expectedInputs: [{ type: 'image' }] + }); + }); + it('sets image as expected input type by default even if other onDeviceParams params are set', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.AVAILABLE) + } as LanguageModel; + const availabilityStub = stub( + languageModelProvider, + 'availability' + ).resolves(Availability.AVAILABLE); + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { + promptOptions: {} + } + ); + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [{ text: 'hi' }] + } + ] + }); + expect(availabilityStub).to.have.been.calledWith({ + expectedInputs: [{ type: 'image' }] + }); + }); + it('sets image as expected input type by default even if other createOptions params are set', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.AVAILABLE) + } as LanguageModel; + const availabilityStub = stub( + languageModelProvider, + 'availability' + ).resolves(Availability.AVAILABLE); + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { + createOptions: { + topK: 22 + } + } + ); + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [{ text: 'hi' }] + } + ] + }); + expect(availabilityStub).to.have.been.calledWith({ + topK: 22, + expectedInputs: [{ type: 'image' }] + }); + }); + it('honors explicitly set expected inputs', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.AVAILABLE) + } as LanguageModel; + const availabilityStub = stub( + languageModelProvider, + 'availability' + ).resolves(Availability.AVAILABLE); + const createOptions = { + // Explicitly sets expected inputs. + expectedInputs: [{ type: 'text' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { + createOptions + } + ); + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [{ text: 'hi' }] + } + ] + }); + expect(availabilityStub).to.have.been.calledWith(createOptions); + }); + }); + describe('isAvailable', () => { + it('returns false if mode is only cloud', async () => { + const adapter = new ChromeAdapterImpl( + {} as LanguageModel, + InferenceMode.ONLY_IN_CLOUD + ); + expect( + await adapter.isAvailable({ + contents: [] + }) + ).to.be.false; + }); + it('returns true if mode is only on device and is available', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.AVAILABLE + } as LanguageModel, + InferenceMode.ONLY_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [] + }) + ).to.be.true; + }); + it('throws if mode is only on device and is unavailable', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.UNAVAILABLE + } as LanguageModel, + InferenceMode.ONLY_ON_DEVICE + ); + await expect( + adapter.isAvailable({ + contents: [] + }) + ).to.be.rejected; + }); + it('returns true after waiting for download if mode is only on device', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.DOWNLOADING, + create: ({}: LanguageModelCreateOptions) => + Promise.resolve({} as LanguageModel) + } as LanguageModel, + InferenceMode.ONLY_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [] + }) + ).to.be.true; + }); + it('returns false if LanguageModel API is undefined', async () => { + const adapter = new ChromeAdapterImpl( + // @ts-expect-error + undefined, + InferenceMode.PREFER_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [] + }) + ).to.be.false; + }); + it('returns false if request contents empty', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.AVAILABLE + } as LanguageModel, + InferenceMode.PREFER_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [] + }) + ).to.be.false; + }); + it('returns false if request content has "function" role', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.AVAILABLE + } as LanguageModel, + InferenceMode.PREFER_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [ + { + role: 'function', + parts: [] + } + ] + }) + ).to.be.false; + }); + it('returns true if request has image with supported mime type', async () => { + const adapter = new ChromeAdapterImpl( + { + availability: async () => Availability.AVAILABLE + } as LanguageModel, + InferenceMode.PREFER_ON_DEVICE + ); + for (const mimeType of ChromeAdapterImpl.SUPPORTED_MIME_TYPES) { + expect( + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [ + { + inlineData: { + mimeType, + data: '' + } + } + ] + } + ] + }) + ).to.be.true; + } + }); + it('returns true if model is readily available', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.AVAILABLE) + } as LanguageModel; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [ + { + role: 'user', + parts: [ + { text: 'describe this image' }, + { inlineData: { mimeType: 'image/jpeg', data: 'asd' } } + ] + } + ] + }) + ).to.be.true; + }); + it('returns false and triggers download when model is available after download', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.DOWNLOADABLE), + create: () => Promise.resolve({}) + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + {} as LanguageModel + ); + const createOptions = { + expectedInputs: [{ type: 'image' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { createOptions } + ); + expect( + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }) + ).to.be.false; + expect(createStub).to.have.been.calledOnceWith(createOptions); + }); + it('avoids redundant downloads', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.DOWNLOADABLE), + create: () => Promise.resolve({}) + } as LanguageModel; + const downloadPromise = new Promise(() => { + /* never resolves */ + }); + const createStub = stub(languageModelProvider, 'create').returns( + downloadPromise + ); + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }); + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }); + expect(createStub).to.have.been.calledOnce; + }); + it('clears state when download completes', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.DOWNLOADABLE), + create: () => Promise.resolve({}) + } as LanguageModel; + let resolveDownload; + const downloadPromise = new Promise(resolveCallback => { + resolveDownload = resolveCallback; + }); + const createStub = stub(languageModelProvider, 'create').returns( + downloadPromise + ); + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }); + resolveDownload!(); + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }); + expect(createStub).to.have.been.calledTwice; + }); + it('returns false when model is never available', async () => { + const languageModelProvider = { + availability: () => Promise.resolve(Availability.UNAVAILABLE), + create: () => Promise.resolve({}) + } as LanguageModel; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + expect( + await adapter.isAvailable({ + contents: [{ role: 'user', parts: [{ text: 'hi' }] }] + }) + ).to.be.false; + }); + }); + describe('generateContent', () => { + it('throws if Chrome API is undefined', async () => { + const adapter = new ChromeAdapterImpl( + // @ts-expect-error + undefined, + InferenceMode.ONLY_ON_DEVICE + ); + await expect( + adapter.generateContent({ + contents: [] + }) + ) + .to.eventually.be.rejectedWith( + AIError, + 'Chrome AI requested for unsupported browser version.' + ) + .and.have.property('code', AIErrorCode.UNSUPPORTED); + }); + it('generates content', async () => { + const languageModelProvider = { + create: () => Promise.resolve({}) + } as LanguageModel; + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + prompt: (p: LanguageModelMessage[]) => Promise.resolve('') + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + languageModel + ); + const promptOutput = 'hi'; + const promptStub = stub(languageModel, 'prompt').resolves(promptOutput); + const createOptions = { + systemPrompt: 'be yourself', + expectedInputs: [{ type: 'image' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { createOptions } + ); + const request = { + contents: [{ role: 'user', parts: [{ text: 'anything' }] }] + } as GenerateContentRequest; + const response = await adapter.generateContent(request); + // Asserts initialization params are proxied. + expect(createStub).to.have.been.calledOnceWith(createOptions); + // Asserts Vertex input type is mapped to Chrome type. + expect(promptStub).to.have.been.calledOnceWith([ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ]); + // Asserts expected output. + expect(await response.json()).to.deep.equal({ + candidates: [ + { + content: { + parts: [{ text: promptOutput }] + } + } + ] + }); + }); + it('generates content using image type input', async () => { + const languageModelProvider = { + create: () => Promise.resolve({}) + } as LanguageModel; + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + prompt: (p: LanguageModelMessage[]) => Promise.resolve('') + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + languageModel + ); + const promptOutput = 'hi'; + const promptStub = stub(languageModel, 'prompt').resolves(promptOutput); + const createOptions = { + systemPrompt: 'be yourself', + expectedInputs: [{ type: 'image' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { createOptions } + ); + const request = { + contents: [ + { + role: 'user', + parts: [ + { text: 'anything' }, + { + inlineData: { + data: sampleBase64EncodedImage, + mimeType: 'image/jpeg' + } + } + ] + } + ] + } as GenerateContentRequest; + const response = await adapter.generateContent(request); + // Asserts initialization params are proxied. + expect(createStub).to.have.been.calledOnceWith(createOptions); + // Asserts Vertex input type is mapped to Chrome type. + expect(promptStub).to.have.been.calledOnceWith([ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + }, + { + type: 'image', + value: match.instanceOf(ImageBitmap) + } + ] + } + ]); + // Asserts expected output. + expect(await response.json()).to.deep.equal({ + candidates: [ + { + content: { + parts: [{ text: promptOutput }] + } + } + ] + }); + }); + it('honors prompt options', async () => { + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + prompt: (p: LanguageModelMessage[]) => Promise.resolve('') + } as LanguageModel; + const languageModelProvider = { + create: () => Promise.resolve(languageModel) + } as LanguageModel; + const promptOutput = '{}'; + const promptStub = stub(languageModel, 'prompt').resolves(promptOutput); + const promptOptions = { + responseConstraint: Schema.object({ + properties: {} + }) + }; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { promptOptions } + ); + const request = { + contents: [{ role: 'user', parts: [{ text: 'anything' }] }] + } as GenerateContentRequest; + await adapter.generateContent(request); + expect(promptStub).to.have.been.calledOnceWith( + [ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ], + promptOptions + ); + }); + it('normalizes roles', async () => { + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + prompt: (p: LanguageModelMessage[]) => Promise.resolve('unused') + } as LanguageModel; + const promptStub = stub(languageModel, 'prompt').resolves('unused'); + const languageModelProvider = { + create: () => Promise.resolve(languageModel) + } as LanguageModel; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + const request = { + contents: [{ role: 'model', parts: [{ text: 'unused' }] }] + } as GenerateContentRequest; + await adapter.generateContent(request); + expect(promptStub).to.have.been.calledOnceWith([ + { + // Asserts Vertex's "model" role normalized to Chrome's "assistant" role. + role: 'assistant', + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ]); + }); + }); + describe('countTokens', () => { + it('counts tokens is not yet available', async () => { + const inputText = 'first'; + // setting up stubs + const languageModelProvider = { + create: () => Promise.resolve({}) + } as LanguageModel; + const languageModel = { + measureInputUsage: _i => Promise.resolve(123) + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + languageModel + ); + + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + + const countTokenRequest = { + contents: [{ role: 'user', parts: [{ text: inputText }] }] + } as GenerateContentRequest; + + try { + await adapter.countTokens(countTokenRequest); + } catch (e) { + // the call to countToken should be rejected with Error + expect((e as AIError).code).to.equal(AIErrorCode.REQUEST_ERROR); + expect((e as AIError).message).includes('not yet available'); + } + + // Asserts that no language model was initialized + expect(createStub).not.called; + }); + }); + describe('generateContentStream', () => { + it('generates content stream', async () => { + const languageModelProvider = { + create: () => Promise.resolve({}) + } as LanguageModel; + const languageModel = { + promptStreaming: _i => new ReadableStream() + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + languageModel + ); + const part = 'hi'; + const promptStub = stub(languageModel, 'promptStreaming').returns( + new ReadableStream({ + start(controller) { + controller.enqueue([part]); + controller.close(); + } + }) + ); + const createOptions = { + expectedInputs: [{ type: 'image' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { createOptions } + ); + const request = { + contents: [{ role: 'user', parts: [{ text: 'anything' }] }] + } as GenerateContentRequest; + const response = await adapter.generateContentStream(request); + expect(createStub).to.have.been.calledOnceWith(createOptions); + expect(promptStub).to.have.been.calledOnceWith([ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ]); + const actual = await toStringArray(response.body!); + expect(actual).to.deep.equal([ + `data: {"candidates":[{"content":{"role":"model","parts":[{"text":["${part}"]}]}}]}\n\n` + ]); + }); + it('generates content stream with image input', async () => { + const languageModelProvider = { + create: () => Promise.resolve({}) + } as LanguageModel; + const languageModel = { + promptStreaming: _i => new ReadableStream() + } as LanguageModel; + const createStub = stub(languageModelProvider, 'create').resolves( + languageModel + ); + const part = 'hi'; + const promptStub = stub(languageModel, 'promptStreaming').returns( + new ReadableStream({ + start(controller) { + controller.enqueue([part]); + controller.close(); + } + }) + ); + const createOptions = { + expectedInputs: [{ type: 'image' }] + } as LanguageModelCreateOptions; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { createOptions } + ); + const request = { + contents: [ + { + role: 'user', + parts: [ + { text: 'anything' }, + { + inlineData: { + data: sampleBase64EncodedImage, + mimeType: 'image/jpeg' + } + } + ] + } + ] + } as GenerateContentRequest; + const response = await adapter.generateContentStream(request); + expect(createStub).to.have.been.calledOnceWith(createOptions); + expect(promptStub).to.have.been.calledOnceWith([ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + }, + { + type: 'image', + value: match.instanceOf(ImageBitmap) + } + ] + } + ]); + const actual = await toStringArray(response.body!); + expect(actual).to.deep.equal([ + `data: {"candidates":[{"content":{"role":"model","parts":[{"text":["${part}"]}]}}]}\n\n` + ]); + }); + it('honors prompt options', async () => { + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + promptStreaming: p => new ReadableStream() + } as LanguageModel; + const languageModelProvider = { + create: () => Promise.resolve(languageModel) + } as LanguageModel; + const promptStub = stub(languageModel, 'promptStreaming').returns( + new ReadableStream() + ); + const promptOptions = { + responseConstraint: Schema.object({ + properties: {} + }) + }; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE, + { promptOptions } + ); + const request = { + contents: [{ role: 'user', parts: [{ text: 'anything' }] }] + } as GenerateContentRequest; + await adapter.generateContentStream(request); + expect(promptStub).to.have.been.calledOnceWith( + [ + { + role: request.contents[0].role, + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ], + promptOptions + ); + }); + it('normalizes roles', async () => { + const languageModel = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + promptStreaming: p => new ReadableStream() + } as LanguageModel; + const promptStub = stub(languageModel, 'promptStreaming').returns( + new ReadableStream() + ); + const languageModelProvider = { + create: () => Promise.resolve(languageModel) + } as LanguageModel; + const adapter = new ChromeAdapterImpl( + languageModelProvider, + InferenceMode.PREFER_ON_DEVICE + ); + const request = { + contents: [{ role: 'model', parts: [{ text: 'unused' }] }] + } as GenerateContentRequest; + await adapter.generateContentStream(request); + expect(promptStub).to.have.been.calledOnceWith([ + { + // Asserts Vertex's "model" role normalized to Chrome's "assistant" role. + role: 'assistant', + content: [ + { + type: 'text', + value: request.contents[0].parts[0].text + } + ] + } + ]); + }); + }); +}); + +describe('chromeAdapterFactory', () => { + it('creates a populated ChromeAdapterImpl', () => { + const fakeLanguageModel = {} as LanguageModel; + const adapter = chromeAdapterFactory( + InferenceMode.PREFER_ON_DEVICE, + { LanguageModel: fakeLanguageModel } as Window, + { createOptions: {} } + ); + expect(adapter?.languageModelProvider).to.equal(fakeLanguageModel); + expect(adapter?.mode).to.equal(InferenceMode.PREFER_ON_DEVICE); + expect(adapter?.onDeviceParams.createOptions).to.exist; + }); +}); + +// TODO: Move to using image from test-utils. +const sampleBase64EncodedImage = + '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'; diff --git a/packages/ai/src/methods/chrome-adapter.ts b/packages/ai/src/methods/chrome-adapter.ts new file mode 100644 index 00000000000..839276814bb --- /dev/null +++ b/packages/ai/src/methods/chrome-adapter.ts @@ -0,0 +1,408 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { logger } from '../logger'; +import { + CountTokensRequest, + GenerateContentRequest, + InferenceMode, + Part, + AIErrorCode, + OnDeviceParams, + Content, + Role +} from '../types'; +import { ChromeAdapter } from '../types/chrome-adapter'; +import { + Availability, + LanguageModel, + LanguageModelExpected, + LanguageModelMessage, + LanguageModelMessageContent, + LanguageModelMessageRole +} from '../types/language-model'; + +// Defaults to support image inputs for convenience. +const defaultExpectedInputs: LanguageModelExpected[] = [{ type: 'image' }]; + +/** + * Defines an inference "backend" that uses Chrome's on-device model, + * and encapsulates logic for detecting when on-device inference is + * possible. + */ +export class ChromeAdapterImpl implements ChromeAdapter { + // Visible for testing + static SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png']; + private isDownloading = false; + private downloadPromise: Promise | undefined; + private oldSession: LanguageModel | undefined; + onDeviceParams: OnDeviceParams = { + createOptions: { + expectedInputs: defaultExpectedInputs + } + }; + constructor( + public languageModelProvider: LanguageModel, + public mode: InferenceMode, + onDeviceParams?: OnDeviceParams + ) { + if (onDeviceParams) { + this.onDeviceParams = onDeviceParams; + if (!this.onDeviceParams.createOptions) { + this.onDeviceParams.createOptions = { + expectedInputs: defaultExpectedInputs + }; + } else if (!this.onDeviceParams.createOptions.expectedInputs) { + this.onDeviceParams.createOptions.expectedInputs = + defaultExpectedInputs; + } + } + } + + /** + * Checks if a given request can be made on-device. + * + * Encapsulates a few concerns: + * the mode + * API existence + * prompt formatting + * model availability, including triggering download if necessary + * + * + * Pros: callers needn't be concerned with details of on-device availability.

+ * Cons: this method spans a few concerns and splits request validation from usage. + * If instance variables weren't already part of the API, we could consider a better + * separation of concerns. + */ + async isAvailable(request: GenerateContentRequest): Promise { + if (!this.mode) { + logger.debug( + `On-device inference unavailable because mode is undefined.` + ); + return false; + } + if (this.mode === InferenceMode.ONLY_IN_CLOUD) { + logger.debug( + `On-device inference unavailable because mode is "only_in_cloud".` + ); + return false; + } + + // Triggers out-of-band download so model will eventually become available. + const availability = await this.downloadIfAvailable(); + + if (this.mode === InferenceMode.ONLY_ON_DEVICE) { + // If it will never be available due to API inavailability, throw. + if (availability === Availability.UNAVAILABLE) { + throw new AIError( + AIErrorCode.API_NOT_ENABLED, + 'Local LanguageModel API not available in this environment.' + ); + } else if ( + availability === Availability.DOWNLOADABLE || + availability === Availability.DOWNLOADING + ) { + // TODO(chholland): Better user experience during download - progress? + logger.debug(`Waiting for download of LanguageModel to complete.`); + await this.downloadPromise; + return true; + } + return true; + } + + // Applies prefer_on_device logic. + if (availability !== Availability.AVAILABLE) { + logger.debug( + `On-device inference unavailable because availability is "${availability}".` + ); + return false; + } + if (!ChromeAdapterImpl.isOnDeviceRequest(request)) { + logger.debug( + `On-device inference unavailable because request is incompatible.` + ); + return false; + } + + return true; + } + + /** + * Generates content on device. + * + * @remarks + * This is comparable to {@link GenerativeModel.generateContent} for generating content in + * Cloud. + * @param request - a standard Firebase AI {@link GenerateContentRequest} + * @returns {@link Response}, so we can reuse common response formatting. + */ + async generateContent(request: GenerateContentRequest): Promise { + const session = await this.createSession(); + const contents = await Promise.all( + request.contents.map(ChromeAdapterImpl.toLanguageModelMessage) + ); + const text = await session.prompt( + contents, + this.onDeviceParams.promptOptions + ); + return ChromeAdapterImpl.toResponse(text); + } + + /** + * Generates content stream on device. + * + * @remarks + * This is comparable to {@link GenerativeModel.generateContentStream} for generating content in + * Cloud. + * @param request - a standard Firebase AI {@link GenerateContentRequest} + * @returns {@link Response}, so we can reuse common response formatting. + */ + async generateContentStream( + request: GenerateContentRequest + ): Promise { + const session = await this.createSession(); + const contents = await Promise.all( + request.contents.map(ChromeAdapterImpl.toLanguageModelMessage) + ); + const stream = session.promptStreaming( + contents, + this.onDeviceParams.promptOptions + ); + return ChromeAdapterImpl.toStreamResponse(stream); + } + + async countTokens(_request: CountTokensRequest): Promise { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'Count Tokens is not yet available for on-device model.' + ); + } + + /** + * Asserts inference for the given request can be performed by an on-device model. + */ + private static isOnDeviceRequest(request: GenerateContentRequest): boolean { + // Returns false if the prompt is empty. + if (request.contents.length === 0) { + logger.debug('Empty prompt rejected for on-device inference.'); + return false; + } + + for (const content of request.contents) { + if (content.role === 'function') { + logger.debug(`"Function" role rejected for on-device inference.`); + return false; + } + + // Returns false if request contains an image with an unsupported mime type. + for (const part of content.parts) { + if ( + part.inlineData && + ChromeAdapterImpl.SUPPORTED_MIME_TYPES.indexOf( + part.inlineData.mimeType + ) === -1 + ) { + logger.debug( + `Unsupported mime type "${part.inlineData.mimeType}" rejected for on-device inference.` + ); + return false; + } + } + } + + return true; + } + + /** + * Encapsulates logic to get availability and download a model if one is downloadable. + */ + private async downloadIfAvailable(): Promise { + const availability = await this.languageModelProvider?.availability( + this.onDeviceParams.createOptions + ); + + if (availability === Availability.DOWNLOADABLE) { + this.download(); + } + + return availability; + } + + /** + * Triggers out-of-band download of an on-device model. + * + * Chrome only downloads models as needed. Chrome knows a model is needed when code calls + * LanguageModel.create. + * + * Since Chrome manages the download, the SDK can only avoid redundant download requests by + * tracking if a download has previously been requested. + */ + private download(): void { + if (this.isDownloading) { + return; + } + this.isDownloading = true; + this.downloadPromise = this.languageModelProvider + ?.create(this.onDeviceParams.createOptions) + .finally(() => { + this.isDownloading = false; + }); + } + + /** + * Converts Firebase AI {@link Content} object to a Chrome {@link LanguageModelMessage} object. + */ + private static async toLanguageModelMessage( + content: Content + ): Promise { + const languageModelMessageContents = await Promise.all( + content.parts.map(ChromeAdapterImpl.toLanguageModelMessageContent) + ); + return { + role: ChromeAdapterImpl.toLanguageModelMessageRole(content.role), + content: languageModelMessageContents + }; + } + + /** + * Converts a Firebase AI Part object to a Chrome LanguageModelMessageContent object. + */ + private static async toLanguageModelMessageContent( + part: Part + ): Promise { + if (part.text) { + return { + type: 'text', + value: part.text + }; + } else if (part.inlineData) { + const formattedImageContent = await fetch( + `data:${part.inlineData.mimeType};base64,${part.inlineData.data}` + ); + const imageBlob = await formattedImageContent.blob(); + const imageBitmap = await createImageBitmap(imageBlob); + return { + type: 'image', + value: imageBitmap + }; + } + throw new AIError( + AIErrorCode.REQUEST_ERROR, + `Processing of this Part type is not currently supported.` + ); + } + + /** + * Converts a Firebase AI {@link Role} string to a {@link LanguageModelMessageRole} string. + */ + private static toLanguageModelMessageRole( + role: Role + ): LanguageModelMessageRole { + // Assumes 'function' rule has been filtered by isOnDeviceRequest + return role === 'model' ? 'assistant' : 'user'; + } + + /** + * Abstracts Chrome session creation. + * + * Chrome uses a multi-turn session for all inference. Firebase AI uses single-turn for all + * inference. To map the Firebase AI API to Chrome's API, the SDK creates a new session for all + * inference. + * + * Chrome will remove a model from memory if it's no longer in use, so this method ensures a + * new session is created before an old session is destroyed. + */ + private async createSession(): Promise { + if (!this.languageModelProvider) { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'Chrome AI requested for unsupported browser version.' + ); + } + const newSession = await this.languageModelProvider.create( + this.onDeviceParams.createOptions + ); + if (this.oldSession) { + this.oldSession.destroy(); + } + // Holds session reference, so model isn't unloaded from memory. + this.oldSession = newSession; + return newSession; + } + + /** + * Formats string returned by Chrome as a {@link Response} returned by Firebase AI. + */ + private static toResponse(text: string): Response { + return { + json: async () => ({ + candidates: [ + { + content: { + parts: [{ text }] + } + } + ] + }) + } as Response; + } + + /** + * Formats string stream returned by Chrome as SSE returned by Firebase AI. + */ + private static toStreamResponse(stream: ReadableStream): Response { + const encoder = new TextEncoder(); + return { + body: stream.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + const json = JSON.stringify({ + candidates: [ + { + content: { + role: 'model', + parts: [{ text: chunk }] + } + } + ] + }); + controller.enqueue(encoder.encode(`data: ${json}\n\n`)); + } + }) + ) + } as Response; + } +} + +/** + * Creates a ChromeAdapterImpl on demand. + */ +export function chromeAdapterFactory( + mode: InferenceMode, + window?: Window, + params?: OnDeviceParams +): ChromeAdapterImpl | undefined { + // Do not initialize a ChromeAdapter if we are not in hybrid mode. + if (typeof window !== 'undefined' && mode) { + return new ChromeAdapterImpl( + (window as Window).LanguageModel as LanguageModel, + mode, + params + ); + } +} diff --git a/packages/ai/src/methods/count-tokens.test.ts b/packages/ai/src/methods/count-tokens.test.ts new file mode 100644 index 00000000000..84976d00ac9 --- /dev/null +++ b/packages/ai/src/methods/count-tokens.test.ts @@ -0,0 +1,204 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import Sinon, { match, restore, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { getMockResponse } from '../../test-utils/mock-response'; +import * as request from '../requests/request'; +import { countTokens } from './count-tokens'; +import { CountTokensRequest, InferenceMode } from '../types'; +import { ApiSettings } from '../types/internal'; +import { Task } from '../requests/request'; +import { mapCountTokensRequest } from '../googleai-mappers'; +import { GoogleAIBackend, VertexAIBackend } from '../backend'; +import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services'; + +use(sinonChai); +use(chaiAsPromised); + +const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + appId: 'my-appid', + location: 'us-central1', + backend: new VertexAIBackend() +}; + +const fakeGoogleAIApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + appId: 'my-appid', + location: '', + backend: new GoogleAIBackend() +}; + +const fakeRequestParams: CountTokensRequest = { + contents: [{ parts: [{ text: 'hello' }], role: 'user' }] +}; + +describe('countTokens()', () => { + afterEach(() => { + restore(); + }); + it('total tokens', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-total-tokens.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await countTokens( + fakeApiSettings, + 'model', + fakeRequestParams, + fakeChromeAdapter + ); + expect(result.totalTokens).to.equal(6); + expect(result.totalBillableCharacters).to.equal(16); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.COUNT_TOKENS, + fakeApiSettings, + false, + match((value: string) => { + return value.includes('contents'); + }), + undefined + ); + }); + it('total tokens with modality details', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-detailed-token-response.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await countTokens( + fakeApiSettings, + 'model', + fakeRequestParams, + fakeChromeAdapter + ); + expect(result.totalTokens).to.equal(1837); + expect(result.totalBillableCharacters).to.equal(117); + expect(result.promptTokensDetails?.[0].modality).to.equal('IMAGE'); + expect(result.promptTokensDetails?.[0].tokenCount).to.equal(1806); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.COUNT_TOKENS, + fakeApiSettings, + false, + match((value: string) => { + return value.includes('contents'); + }), + undefined + ); + }); + it('total tokens no billable characters', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-no-billable-characters.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await countTokens( + fakeApiSettings, + 'model', + fakeRequestParams, + fakeChromeAdapter + ); + expect(result.totalTokens).to.equal(258); + expect(result).to.not.have.property('totalBillableCharacters'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.COUNT_TOKENS, + fakeApiSettings, + false, + match((value: string) => { + return value.includes('contents'); + }), + undefined + ); + }); + it('model not found', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-model-not-found.json' + ); + const mockFetch = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 404, + json: mockResponse.json + } as Response); + await expect( + countTokens( + fakeApiSettings, + 'model', + fakeRequestParams, + fakeChromeAdapter + ) + ).to.be.rejectedWith(/404.*not found/); + expect(mockFetch).to.be.called; + }); + describe('googleAI', () => { + let makeRequestStub: Sinon.SinonStub; + + beforeEach(() => { + makeRequestStub = stub(request, 'makeRequest'); + }); + + afterEach(() => { + restore(); + }); + + it('maps request to GoogleAI format', async () => { + makeRequestStub.resolves({ ok: true, json: () => {} } as Response); // Unused + + await countTokens( + fakeGoogleAIApiSettings, + 'model', + fakeRequestParams, + fakeChromeAdapter + ); + + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.COUNT_TOKENS, + fakeGoogleAIApiSettings, + false, + JSON.stringify(mapCountTokensRequest(fakeRequestParams, 'model')), + undefined + ); + }); + }); + it('throws if mode is ONLY_ON_DEVICE', async () => { + const chromeAdapter = { + ...fakeChromeAdapter, + mode: InferenceMode.ONLY_ON_DEVICE + }; + await expect( + countTokens(fakeApiSettings, 'model', fakeRequestParams, chromeAdapter) + ).to.be.rejectedWith( + /countTokens\(\) is not supported for on-device models/ + ); + }); +}); diff --git a/packages/ai/src/methods/count-tokens.ts b/packages/ai/src/methods/count-tokens.ts new file mode 100644 index 00000000000..c6041a0bb99 --- /dev/null +++ b/packages/ai/src/methods/count-tokens.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { + CountTokensRequest, + CountTokensResponse, + InferenceMode, + RequestOptions, + AIErrorCode +} from '../types'; +import { Task, makeRequest } from '../requests/request'; +import { ApiSettings } from '../types/internal'; +import * as GoogleAIMapper from '../googleai-mappers'; +import { BackendType } from '../public-types'; +import { ChromeAdapter } from '../types/chrome-adapter'; + +export async function countTokensOnCloud( + apiSettings: ApiSettings, + model: string, + params: CountTokensRequest, + requestOptions?: RequestOptions +): Promise { + let body: string = ''; + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + const mappedParams = GoogleAIMapper.mapCountTokensRequest(params, model); + body = JSON.stringify(mappedParams); + } else { + body = JSON.stringify(params); + } + const response = await makeRequest( + model, + Task.COUNT_TOKENS, + apiSettings, + false, + body, + requestOptions + ); + return response.json(); +} + +export async function countTokens( + apiSettings: ApiSettings, + model: string, + params: CountTokensRequest, + chromeAdapter?: ChromeAdapter, + requestOptions?: RequestOptions +): Promise { + if (chromeAdapter?.mode === InferenceMode.ONLY_ON_DEVICE) { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'countTokens() is not supported for on-device models.' + ); + } + return countTokensOnCloud(apiSettings, model, params, requestOptions); +} diff --git a/packages/ai/src/methods/generate-content.test.ts b/packages/ai/src/methods/generate-content.test.ts new file mode 100644 index 00000000000..33a9ae5f5e3 --- /dev/null +++ b/packages/ai/src/methods/generate-content.test.ts @@ -0,0 +1,535 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import Sinon, { match, restore, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { getMockResponse } from '../../test-utils/mock-response'; +import * as request from '../requests/request'; +import { generateContent } from './generate-content'; +import { + AIErrorCode, + GenerateContentRequest, + HarmBlockMethod, + HarmBlockThreshold, + HarmCategory, + Language, + Outcome +} from '../types'; +import { ApiSettings } from '../types/internal'; +import { Task } from '../requests/request'; +import { AIError } from '../api'; +import { mapGenerateContentRequest } from '../googleai-mappers'; +import { GoogleAIBackend, VertexAIBackend } from '../backend'; +import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services'; + +use(sinonChai); +use(chaiAsPromised); + +const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + appId: 'my-appid', + location: 'us-central1', + backend: new VertexAIBackend() +}; + +const fakeGoogleAIApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + appId: 'my-appid', + location: 'us-central1', + backend: new GoogleAIBackend() +}; + +const fakeRequestParams: GenerateContentRequest = { + contents: [{ parts: [{ text: 'hello' }], role: 'user' }], + generationConfig: { + topK: 16 + }, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + method: HarmBlockMethod.SEVERITY + } + ] +}; + +const fakeGoogleAIRequestParams: GenerateContentRequest = { + contents: [{ parts: [{ text: 'hello' }], role: 'user' }], + generationConfig: { + topK: 16 + }, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE + } + ] +}; + +describe('generateContent()', () => { + afterEach(() => { + restore(); + }); + it('short response', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include('Mountain View, California'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + JSON.stringify(fakeRequestParams), + undefined + ); + }); + it('long response', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-long.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include('Use Freshly Ground Coffee'); + expect(result.response.text()).to.include('30 minutes of brewing'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('long response with token details', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-response-long-usage-metadata.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.usageMetadata?.totalTokenCount).to.equal(1913); + expect(result.response.usageMetadata?.candidatesTokenCount).to.equal(76); + expect( + result.response.usageMetadata?.promptTokensDetails?.[0].modality + ).to.equal('IMAGE'); + expect( + result.response.usageMetadata?.promptTokensDetails?.[0].tokenCount + ).to.equal(1806); + expect( + result.response.usageMetadata?.candidatesTokensDetails?.[0].modality + ).to.equal('TEXT'); + expect( + result.response.usageMetadata?.candidatesTokensDetails?.[0].tokenCount + ).to.equal(76); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('citations', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-citations.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include( + 'Some information cited from an external source' + ); + expect( + result.response.candidates?.[0].citationMetadata?.citations.length + ).to.equal(3); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('google search grounding', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-google-search-grounding.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include('The temperature is 67°F (19°C)'); + const groundingMetadata = result.response.candidates?.[0].groundingMetadata; + expect(groundingMetadata).to.not.be.undefined; + expect(groundingMetadata!.searchEntryPoint?.renderedContent).to.contain( + 'div' + ); + expect(groundingMetadata!.groundingChunks?.length).to.equal(2); + expect(groundingMetadata!.groundingChunks?.[0].web?.uri).to.contain( + 'https://vertexaisearch.cloud.google.com' + ); + expect(groundingMetadata!.groundingChunks?.[0].web?.title).to.equal( + 'accuweather.com' + ); + expect(groundingMetadata!.groundingSupports?.length).to.equal(3); + expect( + groundingMetadata!.groundingSupports?.[0].groundingChunkIndices + ).to.deep.equal([0]); + expect(groundingMetadata!.groundingSupports?.[0].segment).to.deep.equal({ + endIndex: 56, + text: 'The current weather in London, United Kingdom is cloudy.' + }); + expect(groundingMetadata!.groundingSupports?.[0].segment?.partIndex).to.be + .undefined; + expect(groundingMetadata!.groundingSupports?.[0].segment?.startIndex).to.be + .undefined; + + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + + it('url context', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-url-context.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include( + 'The temperature is 67°F (19°C)' + ); + const groundingMetadata = + result.response.candidates?.[0].groundingMetadata; + expect(groundingMetadata).to.not.be.undefined; + expect(groundingMetadata!.searchEntryPoint?.renderedContent).to.contain( + 'div' + ); + expect(groundingMetadata!.groundingChunks?.length).to.equal(2); + expect(groundingMetadata!.groundingChunks?.[0].web?.uri).to.contain( + 'https://vertexaisearch.cloud.google.com' + ); + expect(groundingMetadata!.groundingChunks?.[0].web?.title).to.equal( + 'accuweather.com' + ); + expect(groundingMetadata!.groundingSupports?.length).to.equal(3); + expect( + groundingMetadata!.groundingSupports?.[0].groundingChunkIndices + ).to.deep.equal([0]); + expect(groundingMetadata!.groundingSupports?.[0].segment).to.deep.equal({ + endIndex: 56, + text: 'The current weather in London, United Kingdom is cloudy.' + }); + expect(groundingMetadata!.groundingSupports?.[0].segment?.partIndex).to.be + .undefined; + expect(groundingMetadata!.groundingSupports?.[0].segment?.startIndex).to + .be.undefined; + + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + }); + it('codeExecution', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-code-execution.json' + ); + stub(request, 'makeRequest').resolves(mockResponse as Response); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + const parts = result.response.candidates?.[0].content.parts; + expect( + parts?.some(part => part.codeExecutionResult?.outcome === Outcome.OK) + ).to.be.true; + expect( + parts?.some(part => part.executableCode?.language === Language.PYTHON) + ).to.be.true; + }); + it('blocked prompt', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-prompt-blocked-safety.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text).to.throw('SAFETY'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('finishReason safety', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-finish-reason-safety.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text).to.throw('SAFETY'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('empty content', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-empty-content.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.equal(''); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('empty part', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-empty-part.json' + ); + stub(request, 'makeRequest').resolves(mockResponse as Response); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include( + 'I can certainly help you with that!' + ); + expect(result.response.inlineDataParts()?.length).to.equal(1); + }); + it('unknown enum - should ignore', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-unknown-enum-safety-ratings.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include('Some text'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('image rejected (400)', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-image-rejected.json' + ); + const mockFetch = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 400, + json: mockResponse.json + } as Response); + await expect( + generateContent(fakeApiSettings, 'model', fakeRequestParams) + ).to.be.rejectedWith(/400.*invalid argument/); + expect(mockFetch).to.be.called; + }); + it('api not enabled (403)', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-firebasevertexai-api-not-enabled.json' + ); + const mockFetch = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 403, + json: mockResponse.json + } as Response); + await expect( + generateContent(fakeApiSettings, 'model', fakeRequestParams) + ).to.be.rejectedWith( + /firebasevertexai\.googleapis[\s\S]*my-project[\s\S]*api-not-enabled/ + ); + expect(mockFetch).to.be.called; + }); + describe('googleAI', () => { + let makeRequestStub: Sinon.SinonStub; + + beforeEach(() => { + makeRequestStub = stub(request, 'makeRequest'); + }); + + afterEach(() => { + restore(); + }); + + it('throws error when method is defined', async () => { + const mockResponse = getMockResponse( + 'googleAI', + 'unary-success-basic-reply-short.txt' + ); + makeRequestStub.resolves(mockResponse as Response); + + const requestParamsWithMethod: GenerateContentRequest = { + contents: [{ parts: [{ text: 'hello' }], role: 'user' }], + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + method: HarmBlockMethod.SEVERITY // Unsupported in Google AI. + } + ] + }; + + // Expect generateContent to throw a AIError that method is not supported. + await expect( + generateContent( + fakeGoogleAIApiSettings, + 'model', + requestParamsWithMethod + ) + ).to.be.rejectedWith(AIError, AIErrorCode.UNSUPPORTED); + expect(makeRequestStub).to.not.be.called; + }); + it('maps request to GoogleAI format', async () => { + const mockResponse = getMockResponse( + 'googleAI', + 'unary-success-basic-reply-short.txt' + ); + makeRequestStub.resolves(mockResponse as Response); + + await generateContent( + fakeGoogleAIApiSettings, + 'model', + fakeGoogleAIRequestParams + ); + + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeGoogleAIApiSettings, + false, + JSON.stringify(mapGenerateContentRequest(fakeGoogleAIRequestParams)), + undefined + ); + }); + }); + // TODO: define a similar test for generateContentStream + it('on-device', async () => { + const chromeAdapter = fakeChromeAdapter; + const isAvailableStub = stub(chromeAdapter, 'isAvailable').resolves(true); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ); + const generateContentStub = stub(chromeAdapter, 'generateContent').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams, + chromeAdapter + ); + expect(result.response.text()).to.include('Mountain View, California'); + expect(isAvailableStub).to.be.called; + expect(generateContentStub).to.be.calledWith(fakeRequestParams); + }); +}); diff --git a/packages/ai/src/methods/generate-content.ts b/packages/ai/src/methods/generate-content.ts new file mode 100644 index 00000000000..a2fb29e20d1 --- /dev/null +++ b/packages/ai/src/methods/generate-content.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + GenerateContentRequest, + GenerateContentResponse, + GenerateContentResult, + GenerateContentStreamResult, + RequestOptions +} from '../types'; +import { Task, makeRequest } from '../requests/request'; +import { createEnhancedContentResponse } from '../requests/response-helpers'; +import { processStream } from '../requests/stream-reader'; +import { ApiSettings } from '../types/internal'; +import * as GoogleAIMapper from '../googleai-mappers'; +import { BackendType } from '../public-types'; +import { ChromeAdapter } from '../types/chrome-adapter'; +import { callCloudOrDevice } from '../requests/hybrid-helpers'; + +async function generateContentStreamOnCloud( + apiSettings: ApiSettings, + model: string, + params: GenerateContentRequest, + requestOptions?: RequestOptions +): Promise { + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + params = GoogleAIMapper.mapGenerateContentRequest(params); + } + return makeRequest( + model, + Task.STREAM_GENERATE_CONTENT, + apiSettings, + /* stream */ true, + JSON.stringify(params), + requestOptions + ); +} + +export async function generateContentStream( + apiSettings: ApiSettings, + model: string, + params: GenerateContentRequest, + chromeAdapter?: ChromeAdapter, + requestOptions?: RequestOptions +): Promise { + const callResult = await callCloudOrDevice( + params, + chromeAdapter, + () => chromeAdapter!.generateContentStream(params), + () => + generateContentStreamOnCloud(apiSettings, model, params, requestOptions) + ); + return processStream(callResult.response, apiSettings); // TODO: Map streaming responses +} + +async function generateContentOnCloud( + apiSettings: ApiSettings, + model: string, + params: GenerateContentRequest, + requestOptions?: RequestOptions +): Promise { + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + params = GoogleAIMapper.mapGenerateContentRequest(params); + } + return makeRequest( + model, + Task.GENERATE_CONTENT, + apiSettings, + /* stream */ false, + JSON.stringify(params), + requestOptions + ); +} + +export async function generateContent( + apiSettings: ApiSettings, + model: string, + params: GenerateContentRequest, + chromeAdapter?: ChromeAdapter, + requestOptions?: RequestOptions +): Promise { + const callResult = await callCloudOrDevice( + params, + chromeAdapter, + () => chromeAdapter!.generateContent(params), + () => generateContentOnCloud(apiSettings, model, params, requestOptions) + ); + const generateContentResponse = await processGenerateContentResponse( + callResult.response, + apiSettings + ); + const enhancedResponse = createEnhancedContentResponse( + generateContentResponse, + callResult.inferenceSource + ); + return { + response: enhancedResponse + }; +} + +async function processGenerateContentResponse( + response: Response, + apiSettings: ApiSettings +): Promise { + const responseJson = await response.json(); + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + return GoogleAIMapper.mapGenerateContentResponse(responseJson); + } else { + return responseJson; + } +} diff --git a/packages/ai/src/methods/live-session-helpers.test.ts b/packages/ai/src/methods/live-session-helpers.test.ts new file mode 100644 index 00000000000..a62315c701d --- /dev/null +++ b/packages/ai/src/methods/live-session-helpers.test.ts @@ -0,0 +1,366 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import sinon, { SinonFakeTimers, SinonStub, SinonStubbedInstance } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { AIError } from '../errors'; +import { startAudioConversation } from './live-session-helpers'; +import { + FunctionResponse, + LiveServerContent, + LiveServerToolCall +} from '../types'; +import { logger } from '../logger'; +import { isNode } from '@firebase/util'; + +use(sinonChai); +use(chaiAsPromised); + +// A mock message generator to simulate receiving messages from the server. +class MockMessageGenerator { + private resolvers: Array<(result: IteratorResult) => void> = []; + isDone = false; + + next(): Promise> { + return new Promise(resolve => this.resolvers.push(resolve)); + } + + simulateMessage(message: any): void { + const resolver = this.resolvers.shift(); + if (resolver) { + resolver({ value: message, done: false }); + } + } + + endStream(): void { + if (this.isDone) { + return; + } + this.isDone = true; + this.resolvers.forEach(resolve => + resolve({ value: undefined, done: true }) + ); + this.resolvers = []; + } +} + +// A mock LiveSession to intercept calls to the server. +class MockLiveSession { + isClosed = false; + inConversation = false; + send = sinon.stub(); + sendAudioRealtime = sinon.stub(); + sendFunctionResponses = sinon.stub(); + messageGenerator = new MockMessageGenerator(); + receive = (): MockMessageGenerator => this.messageGenerator; +} + +// Stubs and mocks for Web APIs used by the helpers. +let mockAudioContext: SinonStubbedInstance; +let mockMediaStream: SinonStubbedInstance; +let getUserMediaStub: SinonStub; +let mockWorkletNode: SinonStubbedInstance; +let mockSourceNode: SinonStubbedInstance; +let mockAudioBufferSource: any; + +function setupGlobalMocks(): void { + // Mock AudioWorkletNode + mockWorkletNode = { + port: { + postMessage: sinon.stub(), + onmessage: null + }, + connect: sinon.stub(), + disconnect: sinon.stub() + } as any; + sinon.stub(global, 'AudioWorkletNode').returns(mockWorkletNode); + + // Mock AudioContext + mockAudioBufferSource = { + connect: sinon.stub(), + start: sinon.stub(), + stop: sinon.stub(), + onended: null, + buffer: { duration: 0.5 } // Mock duration for scheduling + }; + mockSourceNode = { + connect: sinon.stub(), + disconnect: sinon.stub() + } as any; + mockAudioContext = { + resume: sinon.stub().resolves(), + close: sinon.stub().resolves(), + createBuffer: sinon.stub().returns({ + getChannelData: sinon.stub().returns(new Float32Array(1)) + } as any), + createBufferSource: sinon.stub().returns(mockAudioBufferSource), + createMediaStreamSource: sinon.stub().returns(mockSourceNode), + audioWorklet: { + addModule: sinon.stub().resolves() + }, + state: 'suspended' as AudioContextState, + currentTime: 0 + } as any; + sinon.stub(global, 'AudioContext').returns(mockAudioContext); + + // Mock other globals + sinon.stub(global, 'Blob').returns({} as Blob); + sinon.stub(URL, 'createObjectURL').returns('blob:http://localhost/fake-url'); + + // Mock getUserMedia + mockMediaStream = { + getTracks: sinon.stub().returns([{ stop: sinon.stub() } as any]) + } as any; + getUserMediaStub = sinon.stub().resolves(mockMediaStream); + if (typeof navigator === 'undefined') { + (global as any).navigator = { + mediaDevices: { getUserMedia: getUserMediaStub } + }; + } else { + if (!navigator.mediaDevices) { + (navigator as any).mediaDevices = {}; + } + sinon + .stub(navigator.mediaDevices, 'getUserMedia') + .callsFake(getUserMediaStub); + } +} + +describe('Audio Conversation Helpers', () => { + let clock: SinonFakeTimers; + + if (isNode()) { + return; + } + + beforeEach(() => { + clock = sinon.useFakeTimers(); + setupGlobalMocks(); + }); + + afterEach(() => { + sinon.restore(); + clock.restore(); + }); + + describe('startAudioConversation', () => { + let liveSession: MockLiveSession; + beforeEach(() => { + liveSession = new MockLiveSession(); + }); + + it('should throw if the session is closed.', async () => { + liveSession.isClosed = true; + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(AIError, /on a closed LiveSession/); + }); + + it('should throw if a conversation is in progress.', async () => { + liveSession.inConversation = true; + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(AIError, /is already in progress/); + }); + + it('should throw if APIs are not supported.', async () => { + (global as any).AudioWorkletNode = undefined; // Simulate lack of support + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(AIError, /not supported in this environment/); + }); + + it('should throw if microphone permissions are denied.', async () => { + getUserMediaStub.rejects( + new DOMException('Permission denied', 'NotAllowedError') + ); + await expect( + startAudioConversation(liveSession as any) + ).to.be.rejectedWith(DOMException, /Permission denied/); + }); + + it('should return a controller with a stop method on success.', async () => { + const controller = await startAudioConversation(liveSession as any); + expect(controller).to.have.property('stop').that.is.a('function'); + // Ensure it doesn't throw during cleanup + await expect(controller.stop()).to.be.fulfilled; + }); + }); + + describe('AudioConversationRunner', () => { + let liveSession: MockLiveSession; + let warnStub: SinonStub; + + beforeEach(() => { + liveSession = new MockLiveSession(); + warnStub = sinon.stub(logger, 'warn'); + }); + + afterEach(() => { + warnStub.restore(); + }); + + it('should send processed audio chunks received from the worklet.', async () => { + const controller = await startAudioConversation(liveSession as any); + expect(mockWorkletNode.port.onmessage).to.be.a('function'); + + // Simulate the worklet sending a message + const fakeAudioData = new Int16Array(128); + mockWorkletNode.port.onmessage!({ data: fakeAudioData } as MessageEvent); + + await clock.tickAsync(1); + + expect(liveSession.sendAudioRealtime).to.have.been.calledOnce; + const sentChunk = liveSession.sendAudioRealtime.getCall(0).args[0]; + expect(sentChunk.mimeType).to.equal('audio/pcm'); + expect(sentChunk.data).to.be.a('string'); + await controller.stop(); + }); + + it('should queue and play audio from a serverContent message.', async () => { + const controller = await startAudioConversation(liveSession as any); + const serverMessage: LiveServerContent = { + type: 'serverContent', + modelTurn: { + role: 'model', + parts: [ + { inlineData: { mimeType: 'audio/pcm', data: '1111222233334444' } } + ] // base64 for dummy data + } + }; + + liveSession.messageGenerator.simulateMessage(serverMessage); + await clock.tickAsync(1); // allow message processing + + expect(mockAudioContext.createBuffer).to.have.been.calledOnce; + expect(mockAudioBufferSource.start).to.have.been.calledOnce; + await controller.stop(); + }); + + it('should call function handler and send result on toolCall message.', async () => { + const functionResponse: FunctionResponse = { + id: '1', + name: 'get_weather', + response: { temp: '72F' } + }; + const handlerStub = sinon.stub().resolves(functionResponse); + const controller = await startAudioConversation(liveSession as any, { + functionCallingHandler: handlerStub + }); + + const toolCallMessage: LiveServerToolCall = { + type: 'toolCall', + functionCalls: [ + { id: '1', name: 'get_weather', args: { location: 'LA' } } + ] + }; + + liveSession.messageGenerator.simulateMessage(toolCallMessage); + await clock.tickAsync(1); + + expect(handlerStub).to.have.been.calledOnceWith( + toolCallMessage.functionCalls + ); + expect(liveSession.sendFunctionResponses).to.have.been.calledOnceWith([ + functionResponse + ]); + await controller.stop(); + }); + + it('should clear queue and stop sources on an interruption message.', async () => { + const controller = await startAudioConversation(liveSession as any); + + // 1. Enqueue some audio that is "playing" + const playingMessage: LiveServerContent = { + type: 'serverContent', + modelTurn: { + parts: [ + { inlineData: { mimeType: 'audio/pcm', data: '1111222233334444' } } + ], + role: 'model' + } + }; + liveSession.messageGenerator.simulateMessage(playingMessage); + await clock.tickAsync(1); + expect(mockAudioBufferSource.start).to.have.been.calledOnce; + + // 2. Enqueue another chunk that is now scheduled + liveSession.messageGenerator.simulateMessage(playingMessage); + await clock.tickAsync(1); + expect(mockAudioBufferSource.start).to.have.been.calledTwice; + + // 3. Send interruption message + const interruptionMessage: LiveServerContent = { + type: 'serverContent', + interrupted: true + }; + liveSession.messageGenerator.simulateMessage(interruptionMessage); + await clock.tickAsync(1); + + // Assert that all scheduled sources were stopped. + expect(mockAudioBufferSource.stop).to.have.been.calledTwice; + + // 4. Send new audio post-interruption + const newMessage: LiveServerContent = { + type: 'serverContent', + modelTurn: { + parts: [ + { inlineData: { mimeType: 'audio/pcm', data: '1111222233334444' } } + ], + role: 'model' + } + }; + liveSession.messageGenerator.simulateMessage(newMessage); + await clock.tickAsync(1); + + // Assert a new source was created and started (total of 3 starts) + expect(mockAudioBufferSource.start).to.have.been.calledThrice; + + await controller.stop(); + }); + + it('should warn if no function handler is provided for a toolCall message.', async () => { + const controller = await startAudioConversation(liveSession as any); + liveSession.messageGenerator.simulateMessage({ + type: 'toolCall', + functionCalls: [{ name: 'test' }] + }); + await clock.tickAsync(1); + + expect(warnStub).to.have.been.calledWithMatch( + /functionCallingHandler is undefined/ + ); + await controller.stop(); + }); + + it('stop() should call cleanup and release all resources.', async () => { + const controller = await startAudioConversation(liveSession as any); + + // Need to spy on the internal runner's cleanup method. This is a bit tricky. + // We can't do it directly. Instead, we'll just check the mock results. + await controller.stop(); + + expect(mockWorkletNode.disconnect).to.have.been.calledOnce; + expect(mockSourceNode.disconnect).to.have.been.calledOnce; + expect(mockMediaStream.getTracks()[0].stop).to.have.been.calledOnce; + expect(mockAudioContext.close).to.have.been.calledOnce; + expect(liveSession.inConversation).to.be.false; + }); + }); +}); diff --git a/packages/ai/src/methods/live-session-helpers.ts b/packages/ai/src/methods/live-session-helpers.ts new file mode 100644 index 00000000000..cb3be493f5d --- /dev/null +++ b/packages/ai/src/methods/live-session-helpers.ts @@ -0,0 +1,497 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { logger } from '../logger'; +import { + AIErrorCode, + FunctionCall, + FunctionResponse, + GenerativeContentBlob, + LiveServerContent +} from '../types'; +import { LiveSession } from './live-session'; +import { Deferred } from '@firebase/util'; + +const SERVER_INPUT_SAMPLE_RATE = 16_000; +const SERVER_OUTPUT_SAMPLE_RATE = 24_000; + +const AUDIO_PROCESSOR_NAME = 'audio-processor'; + +/** + * The JS for an `AudioWorkletProcessor`. + * This processor is responsible for taking raw audio from the microphone, + * converting it to the required 16-bit 16kHz PCM, and posting it back to the main thread. + * + * See: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor + * + * It is defined as a string here so that it can be converted into a `Blob` + * and loaded at runtime. + */ +const audioProcessorWorkletString = ` + class AudioProcessor extends AudioWorkletProcessor { + constructor(options) { + super(); + this.targetSampleRate = options.processorOptions.targetSampleRate; + // 'sampleRate' is a global variable available inside the AudioWorkletGlobalScope, + // representing the native sample rate of the AudioContext. + this.inputSampleRate = sampleRate; + } + + /** + * This method is called by the browser's audio engine for each block of audio data. + * Input is a single input, with a single channel (input[0][0]). + */ + process(inputs) { + const input = inputs[0]; + if (input && input.length > 0 && input[0].length > 0) { + const pcmData = input[0]; // Float32Array of raw audio samples. + + // Simple linear interpolation for resampling. + const resampled = new Float32Array(Math.round(pcmData.length * this.targetSampleRate / this.inputSampleRate)); + const ratio = pcmData.length / resampled.length; + for (let i = 0; i < resampled.length; i++) { + resampled[i] = pcmData[Math.floor(i * ratio)]; + } + + // Convert Float32 (-1, 1) samples to Int16 (-32768, 32767) + const resampledInt16 = new Int16Array(resampled.length); + for (let i = 0; i < resampled.length; i++) { + const sample = Math.max(-1, Math.min(1, resampled[i])); + if (sample < 0) { + resampledInt16[i] = sample * 32768; + } else { + resampledInt16[i] = sample * 32767; + } + } + + this.port.postMessage(resampledInt16); + } + // Return true to keep the processor alive and processing the next audio block. + return true; + } + } + + // Register the processor with a name that can be used to instantiate it from the main thread. + registerProcessor('${AUDIO_PROCESSOR_NAME}', AudioProcessor); +`; + +/** + * A controller for managing an active audio conversation. + * + * @beta + */ +export interface AudioConversationController { + /** + * Stops the audio conversation, closes the microphone connection, and + * cleans up resources. Returns a promise that resolves when cleanup is complete. + */ + stop: () => Promise; +} + +/** + * Options for {@link startAudioConversation}. + * + * @beta + */ +export interface StartAudioConversationOptions { + /** + * An async handler that is called when the model requests a function to be executed. + * The handler should perform the function call and return the result as a `Part`, + * which will then be sent back to the model. + */ + functionCallingHandler?: ( + functionCalls: FunctionCall[] + ) => Promise; +} + +/** + * Dependencies needed by the {@link AudioConversationRunner}. + * + * @internal + */ +interface RunnerDependencies { + audioContext: AudioContext; + mediaStream: MediaStream; + sourceNode: MediaStreamAudioSourceNode; + workletNode: AudioWorkletNode; +} + +/** + * Encapsulates the core logic of an audio conversation. + * + * @internal + */ +export class AudioConversationRunner { + /** A flag to indicate if the conversation has been stopped. */ + private isStopped = false; + /** A deferred that contains a promise that is resolved when stop() is called, to unblock the receive loop. */ + private readonly stopDeferred = new Deferred(); + /** A promise that tracks the lifecycle of the main `runReceiveLoop`. */ + private readonly receiveLoopPromise: Promise; + + /** A FIFO queue of 24kHz, 16-bit PCM audio chunks received from the server. */ + private readonly playbackQueue: ArrayBuffer[] = []; + /** Tracks scheduled audio sources. Used to cancel scheduled audio when the model is interrupted. */ + private scheduledSources: AudioBufferSourceNode[] = []; + /** A high-precision timeline pointer for scheduling gapless audio playback. */ + private nextStartTime = 0; + /** A mutex to prevent the playback processing loop from running multiple times concurrently. */ + private isPlaybackLoopRunning = false; + + constructor( + private readonly liveSession: LiveSession, + private readonly options: StartAudioConversationOptions, + private readonly deps: RunnerDependencies + ) { + this.liveSession.inConversation = true; + + // Start listening for messages from the server. + this.receiveLoopPromise = this.runReceiveLoop().finally(() => + this.cleanup() + ); + + // Set up the handler for receiving processed audio data from the worklet. + // Message data has been resampled to 16kHz 16-bit PCM. + this.deps.workletNode.port.onmessage = event => { + if (this.isStopped) { + return; + } + + const pcm16 = event.data as Int16Array; + const base64 = btoa( + String.fromCharCode.apply( + null, + Array.from(new Uint8Array(pcm16.buffer)) + ) + ); + + const chunk: GenerativeContentBlob = { + mimeType: 'audio/pcm', + data: base64 + }; + void this.liveSession.sendAudioRealtime(chunk); + }; + } + + /** + * Stops the conversation and unblocks the main receive loop. + */ + async stop(): Promise { + if (this.isStopped) { + return; + } + this.isStopped = true; + this.stopDeferred.resolve(); // Unblock the receive loop + await this.receiveLoopPromise; // Wait for the loop and cleanup to finish + } + + /** + * Cleans up all audio resources (nodes, stream tracks, context) and marks the + * session as no longer in a conversation. + */ + private cleanup(): void { + this.interruptPlayback(); // Ensure all audio is stopped on final cleanup. + this.deps.workletNode.port.onmessage = null; + this.deps.workletNode.disconnect(); + this.deps.sourceNode.disconnect(); + this.deps.mediaStream.getTracks().forEach(track => track.stop()); + if (this.deps.audioContext.state !== 'closed') { + void this.deps.audioContext.close(); + } + this.liveSession.inConversation = false; + } + + /** + * Adds audio data to the queue and ensures the playback loop is running. + */ + private enqueueAndPlay(audioData: ArrayBuffer): void { + this.playbackQueue.push(audioData); + // Will no-op if it's already running. + void this.processPlaybackQueue(); + } + + /** + * Stops all current and pending audio playback and clears the queue. This is + * called when the server indicates the model's speech was interrupted with + * `LiveServerContent.modelTurn.interrupted`. + */ + private interruptPlayback(): void { + // Stop all sources that have been scheduled. The onended event will fire for each, + // which will clean up the scheduledSources array. + [...this.scheduledSources].forEach(source => source.stop(0)); + + // Clear the internal buffer of unprocessed audio chunks. + this.playbackQueue.length = 0; + + // Reset the playback clock to start fresh. + this.nextStartTime = this.deps.audioContext.currentTime; + } + + /** + * Processes the playback queue in a loop, scheduling each chunk in a gapless sequence. + */ + private async processPlaybackQueue(): Promise { + if (this.isPlaybackLoopRunning) { + return; + } + this.isPlaybackLoopRunning = true; + + while (this.playbackQueue.length > 0 && !this.isStopped) { + const pcmRawBuffer = this.playbackQueue.shift()!; + try { + const pcm16 = new Int16Array(pcmRawBuffer); + const frameCount = pcm16.length; + + const audioBuffer = this.deps.audioContext.createBuffer( + 1, + frameCount, + SERVER_OUTPUT_SAMPLE_RATE + ); + + // Convert 16-bit PCM to 32-bit PCM, required by the Web Audio API. + const channelData = audioBuffer.getChannelData(0); + for (let i = 0; i < frameCount; i++) { + channelData[i] = pcm16[i] / 32768; // Normalize to Float32 range [-1.0, 1.0] + } + + const source = this.deps.audioContext.createBufferSource(); + source.buffer = audioBuffer; + source.connect(this.deps.audioContext.destination); + + // Track the source and set up a handler to remove it from tracking when it finishes. + this.scheduledSources.push(source); + source.onended = () => { + this.scheduledSources = this.scheduledSources.filter( + s => s !== source + ); + }; + + // To prevent gaps, schedule the next chunk to start either now (if we're catching up) + // or exactly when the previous chunk is scheduled to end. + this.nextStartTime = Math.max( + this.deps.audioContext.currentTime, + this.nextStartTime + ); + source.start(this.nextStartTime); + + // Update the schedule for the *next* chunk. + this.nextStartTime += audioBuffer.duration; + } catch (e) { + logger.error('Error playing audio:', e); + } + } + + this.isPlaybackLoopRunning = false; + } + + /** + * The main loop that listens for and processes messages from the server. + */ + private async runReceiveLoop(): Promise { + const messageGenerator = this.liveSession.receive(); + while (!this.isStopped) { + const result = await Promise.race([ + messageGenerator.next(), + this.stopDeferred.promise + ]); + + if (this.isStopped || !result || result.done) { + break; + } + + const message = result.value; + if (message.type === 'serverContent') { + const serverContent = message as LiveServerContent; + if (serverContent.interrupted) { + this.interruptPlayback(); + } + + const audioPart = serverContent.modelTurn?.parts.find(part => + part.inlineData?.mimeType.startsWith('audio/') + ); + if (audioPart?.inlineData) { + const audioData = Uint8Array.from( + atob(audioPart.inlineData.data), + c => c.charCodeAt(0) + ).buffer; + this.enqueueAndPlay(audioData); + } + } else if (message.type === 'toolCall') { + if (!this.options.functionCallingHandler) { + logger.warn( + 'Received tool call message, but StartAudioConversationOptions.functionCallingHandler is undefined. Ignoring tool call.' + ); + } else { + try { + const functionResponse = await this.options.functionCallingHandler( + message.functionCalls + ); + if (!this.isStopped) { + void this.liveSession.sendFunctionResponses([functionResponse]); + } + } catch (e) { + throw new AIError( + AIErrorCode.ERROR, + `Function calling handler failed: ${(e as Error).message}` + ); + } + } + } + } + } +} + +/** + * Starts a real-time, bidirectional audio conversation with the model. This helper function manages + * the complexities of microphone access, audio recording, playback, and interruptions. + * + * @remarks Important: This function must be called in response to a user gesture + * (for example, a button click) to comply with {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy | browser autoplay policies}. + * + * @example + * ```javascript + * const liveSession = await model.connect(); + * let conversationController; + * + * // This function must be called from within a click handler. + * async function startConversation() { + * try { + * conversationController = await startAudioConversation(liveSession); + * } catch (e) { + * // Handle AI-specific errors + * if (e instanceof AIError) { + * console.error("AI Error:", e.message); + * } + * // Handle microphone permission and hardware errors + * else if (e instanceof DOMException) { + * console.error("Microphone Error:", e.message); + * } + * // Handle other unexpected errors + * else { + * console.error("An unexpected error occurred:", e); + * } + * } + * } + * + * // Later, to stop the conversation: + * // if (conversationController) { + * // await conversationController.stop(); + * // } + * ``` + * + * @param liveSession - An active {@link LiveSession} instance. + * @param options - Configuration options for the audio conversation. + * @returns A `Promise` that resolves with an {@link AudioConversationController}. + * @throws `AIError` if the environment does not support required Web APIs (`UNSUPPORTED`), if a conversation is already active (`REQUEST_ERROR`), the session is closed (`SESSION_CLOSED`), or if an unexpected initialization error occurs (`ERROR`). + * @throws `DOMException` Thrown by `navigator.mediaDevices.getUserMedia()` if issues occur with microphone access, such as permissions being denied (`NotAllowedError`) or no compatible hardware being found (`NotFoundError`). See the {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#exceptions | MDN documentation} for a full list of exceptions. + * + * @beta + */ +export async function startAudioConversation( + liveSession: LiveSession, + options: StartAudioConversationOptions = {} +): Promise { + if (liveSession.isClosed) { + throw new AIError( + AIErrorCode.SESSION_CLOSED, + 'Cannot start audio conversation on a closed LiveSession.' + ); + } + + if (liveSession.inConversation) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'An audio conversation is already in progress for this session.' + ); + } + + // Check for necessary Web API support. + if ( + typeof AudioWorkletNode === 'undefined' || + typeof AudioContext === 'undefined' || + typeof navigator === 'undefined' || + !navigator.mediaDevices + ) { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'Audio conversation is not supported in this environment. It requires the Web Audio API and AudioWorklet support.' + ); + } + + let audioContext: AudioContext | undefined; + try { + // 1. Set up the audio context. This must be in response to a user gesture. + // See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy + audioContext = new AudioContext(); + if (audioContext.state === 'suspended') { + await audioContext.resume(); + } + + // 2. Prompt for microphone access and get the media stream. + // This can throw a variety of permission or hardware-related errors. + const mediaStream = await navigator.mediaDevices.getUserMedia({ + audio: true + }); + + // 3. Load the AudioWorklet processor. + // See: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet + const workletBlob = new Blob([audioProcessorWorkletString], { + type: 'application/javascript' + }); + const workletURL = URL.createObjectURL(workletBlob); + await audioContext.audioWorklet.addModule(workletURL); + + // 4. Create the audio graph: Microphone -> Source Node -> Worklet Node + const sourceNode = audioContext.createMediaStreamSource(mediaStream); + const workletNode = new AudioWorkletNode( + audioContext, + AUDIO_PROCESSOR_NAME, + { + processorOptions: { targetSampleRate: SERVER_INPUT_SAMPLE_RATE } + } + ); + sourceNode.connect(workletNode); + + // 5. Instantiate and return the runner which manages the conversation. + const runner = new AudioConversationRunner(liveSession, options, { + audioContext, + mediaStream, + sourceNode, + workletNode + }); + + return { stop: () => runner.stop() }; + } catch (e) { + // Ensure the audio context is closed on any setup error. + if (audioContext && audioContext.state !== 'closed') { + void audioContext.close(); + } + + // Re-throw specific, known error types directly. The user may want to handle `DOMException` + // errors differently (for example, if permission to access audio device was denied). + if (e instanceof AIError || e instanceof DOMException) { + throw e; + } + + // Wrap any other unexpected errors in a standard AIError. + throw new AIError( + AIErrorCode.ERROR, + `Failed to initialize audio recording: ${(e as Error).message}` + ); + } +} diff --git a/packages/ai/src/methods/live-session.test.ts b/packages/ai/src/methods/live-session.test.ts new file mode 100644 index 00000000000..428e92ec770 --- /dev/null +++ b/packages/ai/src/methods/live-session.test.ts @@ -0,0 +1,360 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { + FunctionResponse, + LiveResponseType, + LiveServerContent, + LiveServerToolCall, + LiveServerToolCallCancellation +} from '../types'; +import { LiveSession } from './live-session'; +import { WebSocketHandler } from '../websocket'; +import { AIError } from '../errors'; +import { logger } from '../logger'; + +use(sinonChai); +use(chaiAsPromised); + +class MockWebSocketHandler implements WebSocketHandler { + connect = stub().resolves(); + send = spy(); + close = stub().resolves(); + + private messageQueue: unknown[] = []; + private streamClosed = false; + private listenerPromiseResolver: (() => void) | null = null; + + async *listen(): AsyncGenerator { + while (!this.streamClosed) { + if (this.messageQueue.length > 0) { + yield this.messageQueue.shift(); + } else { + // Wait until a new message is pushed or the stream is ended. + await new Promise(resolve => { + this.listenerPromiseResolver = resolve; + }); + } + } + } + + simulateServerMessage(message: object): void { + this.messageQueue.push(message); + if (this.listenerPromiseResolver) { + // listener is waiting for our message + this.listenerPromiseResolver(); + this.listenerPromiseResolver = null; + } + } + + endStream(): void { + this.streamClosed = true; + if (this.listenerPromiseResolver) { + this.listenerPromiseResolver(); + this.listenerPromiseResolver = null; + } + } +} + +describe('LiveSession', () => { + let mockHandler: MockWebSocketHandler; + let session: LiveSession; + let serverMessagesGenerator: AsyncGenerator; + + beforeEach(() => { + mockHandler = new MockWebSocketHandler(); + serverMessagesGenerator = mockHandler.listen(); + session = new LiveSession(mockHandler, serverMessagesGenerator); + }); + + describe('send()', () => { + it('should format and send a valid text message', async () => { + await session.send('Hello there'); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + clientContent: { + turns: [{ role: 'user', parts: [{ text: 'Hello there' }] }], + turnComplete: true + } + }); + }); + + it('should format and send a message with an array of Parts', async () => { + const parts = [ + { text: 'Part 1' }, + { inlineData: { mimeType: 'image/png', data: 'base64==' } } + ]; + await session.send(parts); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData.clientContent.turns[0].parts).to.deep.equal(parts); + }); + }); + + describe('sendTextRealtime()', () => { + it('should send a correctly formatted realtimeInput message', async () => { + const text = 'foo'; + await session.sendTextRealtime(text); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + realtimeInput: { text } + }); + }); + }); + + describe('sendAudioRealtime()', () => { + it('should send a correctly formatted realtimeInput message', async () => { + const blob = { data: 'abcdef', mimeType: 'audio/pcm' }; + await session.sendAudioRealtime(blob); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + realtimeInput: { audio: blob } + }); + }); + }); + + describe('sendVideoRealtime()', () => { + it('should send a correctly formatted realtimeInput message', async () => { + const blob = { data: 'abcdef', mimeType: 'image/jpeg' }; + await session.sendVideoRealtime(blob); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + realtimeInput: { video: blob } + }); + }); + }); + + describe('sendMediaChunks()', () => { + it('should send a correctly formatted realtimeInput message', async () => { + const chunks = [{ data: 'base64', mimeType: 'audio/webm' }]; + await session.sendMediaChunks(chunks); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + realtimeInput: { mediaChunks: chunks } + }); + }); + }); + + describe('sendMediaStream()', () => { + it('should send multiple chunks from a stream', async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue({ data: 'chunk1', mimeType: 'audio/webm' }); + controller.enqueue({ data: 'chunk2', mimeType: 'audio/webm' }); + controller.close(); + } + }); + + await session.sendMediaStream(stream); + + expect(mockHandler.send).to.have.been.calledTwice; + const firstCall = JSON.parse(mockHandler.send.getCall(0).args[0]); + const secondCall = JSON.parse(mockHandler.send.getCall(1).args[0]); + expect(firstCall.realtimeInput.mediaChunks[0].data).to.equal('chunk1'); + expect(secondCall.realtimeInput.mediaChunks[0].data).to.equal('chunk2'); + }); + + it('should re-throw an AIError if the stream reader throws', async () => { + const errorStream = new ReadableStream({ + pull(controller) { + controller.error(new Error('Stream failed!')); + } + }); + await expect(session.sendMediaStream(errorStream)).to.be.rejectedWith( + AIError, + /Stream failed!/ + ); + }); + }); + + describe('sendFunctionResponses()', () => { + it('should send all function responses', async () => { + const functionResponses: FunctionResponse[] = [ + { + id: 'function-call-1', + name: 'function-name', + response: { + result: 'foo' + } + }, + { + id: 'function-call-2', + name: 'function-name-2', + response: { + result: 'bar' + } + } + ]; + await session.sendFunctionResponses(functionResponses); + expect(mockHandler.send).to.have.been.calledOnce; + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData).to.deep.equal({ + toolResponse: { + functionResponses + } + }); + }); + }); + + describe('receive()', () => { + it('should correctly parse and transform all server message types', async () => { + const receivePromise = (async () => { + const responses = []; + for await (const response of session.receive()) { + responses.push(response); + } + return responses; + })(); + + mockHandler.simulateServerMessage({ + serverContent: { modelTurn: { parts: [{ text: 'response 1' }] } } + }); + mockHandler.simulateServerMessage({ + toolCall: { functionCalls: [{ name: 'test_func' }] } + }); + mockHandler.simulateServerMessage({ + toolCallCancellation: { functionIds: ['123'] } + }); + mockHandler.simulateServerMessage({ + serverContent: { turnComplete: true } + }); + await new Promise(r => setTimeout(() => r(), 10)); // Wait for the listener to process messages + mockHandler.endStream(); + + const responses = await receivePromise; + expect(responses).to.have.lengthOf(4); + expect(responses[0]).to.deep.equal({ + type: LiveResponseType.SERVER_CONTENT, + modelTurn: { parts: [{ text: 'response 1' }] } + } as LiveServerContent); + expect(responses[1]).to.deep.equal({ + type: LiveResponseType.TOOL_CALL, + functionCalls: [{ name: 'test_func' }] + } as LiveServerToolCall); + expect(responses[2]).to.deep.equal({ + type: LiveResponseType.TOOL_CALL_CANCELLATION, + functionIds: ['123'] + } as LiveServerToolCallCancellation); + }); + + it('should log a warning and skip messages that are not objects', async () => { + const loggerStub = stub(logger, 'warn'); + const receivePromise = (async () => { + const responses = []; + for await (const response of session.receive()) { + responses.push(response); + } + return responses; + })(); + + mockHandler.simulateServerMessage(null as any); + mockHandler.simulateServerMessage('not an object' as any); + await new Promise(r => setTimeout(() => r(), 10)); // Wait for the listener to process messages + mockHandler.endStream(); + + const responses = await receivePromise; + expect(responses).to.be.empty; + expect(loggerStub).to.have.been.calledTwice; + expect(loggerStub).to.have.been.calledWithMatch( + /Received an invalid message/ + ); + + loggerStub.restore(); + }); + + it('should log a warning and skip objects of unknown type', async () => { + const loggerStub = stub(logger, 'warn'); + const receivePromise = (async () => { + const responses = []; + for await (const response of session.receive()) { + responses.push(response); + } + return responses; + })(); + + mockHandler.simulateServerMessage({ unknownType: { data: 'test' } }); + await new Promise(r => setTimeout(() => r(), 10)); // Wait for the listener to process messages + mockHandler.endStream(); + + const responses = await receivePromise; + expect(responses).to.be.empty; + expect(loggerStub).to.have.been.calledOnce; + expect(loggerStub).to.have.been.calledWithMatch( + /Received an unknown message type/ + ); + + loggerStub.restore(); + }); + }); + + describe('close()', () => { + it('should call the handler, set the isClosed flag, and be idempotent', async () => { + expect(session.isClosed).to.be.false; + await session.close(); + expect(mockHandler.close).to.have.been.calledOnce; + expect(session.isClosed).to.be.true; + + // Call again to test idempotency + await session.close(); + expect(mockHandler.close).to.have.been.calledOnce; // Should not be called again + }); + + it('should terminate an active receive() loop', async () => { + const received: unknown[] = []; + const receivePromise = (async () => { + for await (const msg of session.receive()) { + received.push(msg); + } + })(); + + mockHandler.simulateServerMessage({ + serverContent: { modelTurn: { parts: [{ text: 'one' }] } } + }); + // Allow the first message to be processed + await new Promise(r => setTimeout(r, 10)); + expect(received).to.have.lengthOf(1); + + await session.close(); + mockHandler.endStream(); // End the mock stream + + await receivePromise; // This should now resolve + + // No more messages should have been processed + expect(received).to.have.lengthOf(1); + }); + + it('methods should throw after session is closed', async () => { + await session.close(); + await expect(session.send('test')).to.be.rejectedWith(AIError, /closed/); + await expect(session.sendMediaChunks([])).to.be.rejectedWith( + AIError, + /closed/ + ); + const generator = session.receive(); + await expect(generator.next()).to.be.rejectedWith(AIError, /closed/); + }); + }); +}); diff --git a/packages/ai/src/methods/live-session.ts b/packages/ai/src/methods/live-session.ts new file mode 100644 index 00000000000..1db5e3d4dd4 --- /dev/null +++ b/packages/ai/src/methods/live-session.ts @@ -0,0 +1,362 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AIErrorCode, + FunctionResponse, + GenerativeContentBlob, + LiveResponseType, + LiveServerContent, + LiveServerToolCall, + LiveServerToolCallCancellation, + Part +} from '../public-types'; +import { formatNewContent } from '../requests/request-helpers'; +import { AIError } from '../errors'; +import { WebSocketHandler } from '../websocket'; +import { logger } from '../logger'; +import { + _LiveClientContent, + _LiveClientRealtimeInput, + _LiveClientToolResponse +} from '../types/live-responses'; + +/** + * Represents an active, real-time, bidirectional conversation with the model. + * + * This class should only be instantiated by calling {@link LiveGenerativeModel.connect}. + * + * @beta + */ +export class LiveSession { + /** + * Indicates whether this Live session is closed. + * + * @beta + */ + isClosed = false; + /** + * Indicates whether this Live session is being controlled by an `AudioConversationController`. + * + * @beta + */ + inConversation = false; + + /** + * @internal + */ + constructor( + private webSocketHandler: WebSocketHandler, + private serverMessages: AsyncGenerator + ) {} + + /** + * Sends content to the server. + * + * @param request - The message to send to the model. + * @param turnComplete - Indicates if the turn is complete. Defaults to false. + * @throws If this session has been closed. + * + * @beta + */ + async send( + request: string | Array, + turnComplete = true + ): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const newContent = formatNewContent(request); + + const message: _LiveClientContent = { + clientContent: { + turns: [newContent], + turnComplete + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + + /** + * Sends text to the server in realtime. + * + * @example + * ```javascript + * liveSession.sendTextRealtime("Hello, how are you?"); + * ``` + * + * @param text - The text data to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendTextRealtime(text: string): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const message: _LiveClientRealtimeInput = { + realtimeInput: { + text + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + + /** + * Sends audio data to the server in realtime. + * + * @remarks The server requires that the audio data is base64-encoded 16-bit PCM at 16kHz + * little-endian. + * + * @example + * ```javascript + * // const pcmData = ... base64-encoded 16-bit PCM at 16kHz little-endian. + * const blob = { mimeType: "audio/pcm", data: pcmData }; + * liveSession.sendAudioRealtime(blob); + * ``` + * + * @param blob - The base64-encoded PCM data to send to the server in realtime. + * @throws If this session has been closed. + * + * @beta + */ + async sendAudioRealtime(blob: GenerativeContentBlob): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const message: _LiveClientRealtimeInput = { + realtimeInput: { + audio: blob + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + + /** + * Sends video data to the server in realtime. + * + * @remarks The server requires that the video is sent as individual video frames at 1 FPS. It + * is recommended to set `mimeType` to `image/jpeg`. + * + * @example + * ```javascript + * // const videoFrame = ... base64-encoded JPEG data + * const blob = { mimeType: "image/jpeg", data: videoFrame }; + * liveSession.sendVideoRealtime(blob); + * ``` + * @param blob - The base64-encoded video data to send to the server in realtime. + * @throws If this session has been closed. + * + * @beta + */ + async sendVideoRealtime(blob: GenerativeContentBlob): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const message: _LiveClientRealtimeInput = { + realtimeInput: { + video: blob + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + + /** + * Sends function responses to the server. + * + * @param functionResponses - The function responses to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendFunctionResponses( + functionResponses: FunctionResponse[] + ): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const message: _LiveClientToolResponse = { + toolResponse: { + functionResponses + } + }; + this.webSocketHandler.send(JSON.stringify(message)); + } + + /** + * Yields messages received from the server. + * This can only be used by one consumer at a time. + * + * @returns An `AsyncGenerator` that yields server messages as they arrive. + * @throws If the session is already closed, or if we receive a response that we don't support. + * + * @beta + */ + async *receive(): AsyncGenerator< + LiveServerContent | LiveServerToolCall | LiveServerToolCallCancellation + > { + if (this.isClosed) { + throw new AIError( + AIErrorCode.SESSION_CLOSED, + 'Cannot read from a Live session that is closed. Try starting a new Live session.' + ); + } + for await (const message of this.serverMessages) { + if (message && typeof message === 'object') { + if (LiveResponseType.SERVER_CONTENT in message) { + yield { + type: 'serverContent', + ...(message as { serverContent: Omit }) + .serverContent + } as LiveServerContent; + } else if (LiveResponseType.TOOL_CALL in message) { + yield { + type: 'toolCall', + ...(message as { toolCall: Omit }) + .toolCall + } as LiveServerToolCall; + } else if (LiveResponseType.TOOL_CALL_CANCELLATION in message) { + yield { + type: 'toolCallCancellation', + ...( + message as { + toolCallCancellation: Omit< + LiveServerToolCallCancellation, + 'type' + >; + } + ).toolCallCancellation + } as LiveServerToolCallCancellation; + } else { + logger.warn( + `Received an unknown message type from the server: ${JSON.stringify( + message + )}` + ); + } + } else { + logger.warn( + `Received an invalid message from the server: ${JSON.stringify( + message + )}` + ); + } + } + } + + /** + * Closes this session. + * All methods on this session will throw an error once this resolves. + * + * @beta + */ + async close(): Promise { + if (!this.isClosed) { + this.isClosed = true; + await this.webSocketHandler.close(1000, 'Client closed session.'); + } + } + + /** + * Sends realtime input to the server. + * + * @deprecated Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead. + * + * @param mediaChunks - The media chunks to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendMediaChunks(mediaChunks: GenerativeContentBlob[]): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + // The backend does not support sending more than one mediaChunk in one message. + // Work around this limitation by sending mediaChunks in separate messages. + mediaChunks.forEach(mediaChunk => { + const message: _LiveClientRealtimeInput = { + realtimeInput: { mediaChunks: [mediaChunk] } + }; + this.webSocketHandler.send(JSON.stringify(message)); + }); + } + + /** + * @deprecated Use `sendTextRealtime()`, `sendAudioRealtime()`, and `sendVideoRealtime()` instead. + * + * Sends a stream of {@link GenerativeContentBlob}. + * + * @param mediaChunkStream - The stream of {@link GenerativeContentBlob} to send. + * @throws If this session has been closed. + * + * @beta + */ + async sendMediaStream( + mediaChunkStream: ReadableStream + ): Promise { + if (this.isClosed) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'This LiveSession has been closed and cannot be used.' + ); + } + + const reader = mediaChunkStream.getReader(); + while (true) { + try { + const { done, value } = await reader.read(); + + if (done) { + break; + } else if (!value) { + throw new Error('Missing chunk in reader, but reader is not done.'); + } + + await this.sendMediaChunks([value]); + } catch (e) { + // Re-throw any errors that occur during stream consumption or sending. + const message = + e instanceof Error ? e.message : 'Error processing media stream.'; + throw new AIError(AIErrorCode.REQUEST_ERROR, message); + } + } + } +} diff --git a/packages/ai/src/models/ai-model.test.ts b/packages/ai/src/models/ai-model.test.ts new file mode 100644 index 00000000000..2e8f8998c58 --- /dev/null +++ b/packages/ai/src/models/ai-model.test.ts @@ -0,0 +1,173 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { use, expect } from 'chai'; +import { AI, AIErrorCode } from '../public-types'; +import sinonChai from 'sinon-chai'; +import { stub } from 'sinon'; +import { AIModel } from './ai-model'; +import { AIError } from '../errors'; +import { VertexAIBackend } from '../backend'; +import { AIService } from '../service'; + +use(sinonChai); + +/** + * A class that extends AIModel that allows us to test the protected constructor. + */ +class TestModel extends AIModel { + /* eslint-disable @typescript-eslint/no-useless-constructor */ + constructor(ai: AI, modelName: string) { + super(ai, modelName); + } +} + +const fakeAI: AI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project', + appId: 'my-appid' + } + }, + backend: new VertexAIBackend('us-central1'), + location: 'us-central1' +}; + +describe('AIModel', () => { + it('handles plain model name', () => { + const testModel = new TestModel(fakeAI, 'my-model'); + expect(testModel.model).to.equal('publishers/google/models/my-model'); + }); + it('handles models/ prefixed model name', () => { + const testModel = new TestModel(fakeAI, 'models/my-model'); + expect(testModel.model).to.equal('publishers/google/models/my-model'); + }); + it('handles full model name', () => { + const testModel = new TestModel( + fakeAI, + 'publishers/google/models/my-model' + ); + expect(testModel.model).to.equal('publishers/google/models/my-model'); + }); + it('handles prefixed tuned model name', () => { + const testModel = new TestModel(fakeAI, 'tunedModels/my-model'); + expect(testModel.model).to.equal('tunedModels/my-model'); + }); + it('calls regular app check token when option is set', async () => { + const getTokenStub = stub().resolves(); + const getLimitedUseTokenStub = stub().resolves(); + const testModel = new TestModel( + //@ts-ignore + { + ...fakeAI, + options: { useLimitedUseAppCheckTokens: false }, + appCheck: { + getToken: getTokenStub, + getLimitedUseToken: getLimitedUseTokenStub + } + } as AIService, + 'models/my-model' + ); + if (testModel._apiSettings?.getAppCheckToken) { + await testModel._apiSettings.getAppCheckToken(); + } + expect(getTokenStub).to.be.called; + expect(getLimitedUseTokenStub).to.not.be.called; + getTokenStub.reset(); + getLimitedUseTokenStub.reset(); + }); + it('calls limited use token when option is set', async () => { + const getTokenStub = stub().resolves(); + const getLimitedUseTokenStub = stub().resolves(); + const testModel = new TestModel( + //@ts-ignore + { + ...fakeAI, + options: { useLimitedUseAppCheckTokens: true }, + appCheck: { + getToken: getTokenStub, + getLimitedUseToken: getLimitedUseTokenStub + } + } as AIService, + 'models/my-model' + ); + if (testModel._apiSettings?.getAppCheckToken) { + await testModel._apiSettings.getAppCheckToken(); + } + expect(getTokenStub).to.not.be.called; + expect(getLimitedUseTokenStub).to.be.called; + getTokenStub.reset(); + getLimitedUseTokenStub.reset(); + }); + it('throws if not passed an api key', () => { + const fakeAI: AI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + projectId: 'my-project' + } + }, + backend: new VertexAIBackend('us-central1'), + location: 'us-central1' + }; + try { + new TestModel(fakeAI, 'my-model'); + } catch (e) { + expect((e as AIError).code).to.equal(AIErrorCode.NO_API_KEY); + } + }); + it('throws if not passed a project ID', () => { + const fakeAI: AI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key' + } + }, + backend: new VertexAIBackend('us-central1'), + location: 'us-central1' + }; + try { + new TestModel(fakeAI, 'my-model'); + } catch (e) { + expect((e as AIError).code).to.equal(AIErrorCode.NO_PROJECT_ID); + } + }); + it('throws if not passed an app ID', () => { + const fakeAI: AI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project' + } + }, + backend: new VertexAIBackend('us-central1'), + location: 'us-central1' + }; + try { + new TestModel(fakeAI, 'my-model'); + } catch (e) { + expect((e as AIError).code).to.equal(AIErrorCode.NO_APP_ID); + } + }); +}); diff --git a/packages/ai/src/models/ai-model.ts b/packages/ai/src/models/ai-model.ts new file mode 100644 index 00000000000..3fe202d5eb2 --- /dev/null +++ b/packages/ai/src/models/ai-model.ts @@ -0,0 +1,160 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { AIErrorCode, AI, BackendType } from '../public-types'; +import { AIService } from '../service'; +import { ApiSettings } from '../types/internal'; +import { _isFirebaseServerApp } from '@firebase/app'; + +/** + * Base class for Firebase AI model APIs. + * + * Instances of this class are associated with a specific Firebase AI {@link Backend} + * and provide methods for interacting with the configured generative model. + * + * @public + */ +export abstract class AIModel { + /** + * The fully qualified model resource name to use for generating images + * (for example, `publishers/google/models/imagen-3.0-generate-002`). + */ + readonly model: string; + + /** + * @internal + */ + _apiSettings: ApiSettings; + + /** + * Constructs a new instance of the {@link AIModel} class. + * + * This constructor should only be called from subclasses that provide + * a model API. + * + * @param ai - an {@link AI} instance. + * @param modelName - The name of the model being used. It can be in one of the following formats: + * - `my-model` (short name, will resolve to `publishers/google/models/my-model`) + * - `models/my-model` (will resolve to `publishers/google/models/my-model`) + * - `publishers/my-publisher/models/my-model` (fully qualified model name) + * + * @throws If the `apiKey` or `projectId` fields are missing in your + * Firebase config. + * + * @internal + */ + protected constructor(ai: AI, modelName: string) { + if (!ai.app?.options?.apiKey) { + throw new AIError( + AIErrorCode.NO_API_KEY, + `The "apiKey" field is empty in the local Firebase config. Firebase AI requires this field to contain a valid API key.` + ); + } else if (!ai.app?.options?.projectId) { + throw new AIError( + AIErrorCode.NO_PROJECT_ID, + `The "projectId" field is empty in the local Firebase config. Firebase AI requires this field to contain a valid project ID.` + ); + } else if (!ai.app?.options?.appId) { + throw new AIError( + AIErrorCode.NO_APP_ID, + `The "appId" field is empty in the local Firebase config. Firebase AI requires this field to contain a valid app ID.` + ); + } else { + this._apiSettings = { + apiKey: ai.app.options.apiKey, + project: ai.app.options.projectId, + appId: ai.app.options.appId, + automaticDataCollectionEnabled: ai.app.automaticDataCollectionEnabled, + location: ai.location, + backend: ai.backend + }; + + if (_isFirebaseServerApp(ai.app) && ai.app.settings.appCheckToken) { + const token = ai.app.settings.appCheckToken; + this._apiSettings.getAppCheckToken = () => { + return Promise.resolve({ token }); + }; + } else if ((ai as AIService).appCheck) { + if (ai.options?.useLimitedUseAppCheckTokens) { + this._apiSettings.getAppCheckToken = () => + (ai as AIService).appCheck!.getLimitedUseToken(); + } else { + this._apiSettings.getAppCheckToken = () => + (ai as AIService).appCheck!.getToken(); + } + } + + if ((ai as AIService).auth) { + this._apiSettings.getAuthToken = () => + (ai as AIService).auth!.getToken(); + } + + this.model = AIModel.normalizeModelName( + modelName, + this._apiSettings.backend.backendType + ); + } + } + + /** + * Normalizes the given model name to a fully qualified model resource name. + * + * @param modelName - The model name to normalize. + * @returns The fully qualified model resource name. + * + * @internal + */ + static normalizeModelName( + modelName: string, + backendType: BackendType + ): string { + if (backendType === BackendType.GOOGLE_AI) { + return AIModel.normalizeGoogleAIModelName(modelName); + } else { + return AIModel.normalizeVertexAIModelName(modelName); + } + } + + /** + * @internal + */ + private static normalizeGoogleAIModelName(modelName: string): string { + return `models/${modelName}`; + } + + /** + * @internal + */ + private static normalizeVertexAIModelName(modelName: string): string { + let model: string; + if (modelName.includes('/')) { + if (modelName.startsWith('models/')) { + // Add 'publishers/google' if the user is only passing in 'models/model-name'. + model = `publishers/google/${modelName}`; + } else { + // Any other custom format (e.g. tuned models) must be passed in correctly. + model = modelName; + } + } else { + // If path is not included, assume it's a non-tuned model. + model = `publishers/google/models/${modelName}`; + } + + return model; + } +} diff --git a/packages/ai/src/models/generative-model.test.ts b/packages/ai/src/models/generative-model.test.ts new file mode 100644 index 00000000000..90399e6811b --- /dev/null +++ b/packages/ai/src/models/generative-model.test.ts @@ -0,0 +1,726 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { use, expect } from 'chai'; +import { GenerativeModel } from './generative-model'; +import { + FunctionCallingMode, + AI, + InferenceMode, + AIErrorCode, + ChromeAdapter +} from '../public-types'; +import * as request from '../requests/request'; +import { SinonStub, match, restore, stub } from 'sinon'; +import { + getMockResponse, + getMockResponseStreaming +} from '../../test-utils/mock-response'; +import sinonChai from 'sinon-chai'; +import { VertexAIBackend } from '../backend'; +import { AIError } from '../errors'; +import chaiAsPromised from 'chai-as-promised'; +import { fakeChromeAdapter } from '../../test-utils/get-fake-firebase-services'; + +use(sinonChai); +use(chaiAsPromised); + +const fakeAI: AI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project', + appId: 'my-appid' + } + }, + backend: new VertexAIBackend('us-central1'), + location: 'us-central1' +}; + +describe('GenerativeModel', () => { + it('passes params through to generateContent', async () => { + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + tools: [ + { + functionDeclarations: [ + { + name: 'myfunc', + description: 'mydesc' + } + ] + }, + { googleSearch: {} }, + { codeExecution: {} } + ], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingMode.NONE } + }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + }, + {}, + fakeChromeAdapter + ); + expect(genModel.tools?.length).to.equal(3); + expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( + FunctionCallingMode.NONE + ); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.generateContent('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return ( + value.includes('myfunc') && + value.includes('googleSearch') && + value.includes('codeExecution') && + value.includes(FunctionCallingMode.NONE) && + value.includes('be friendly') + ); + }), + {} + ); + restore(); + }); + it('passes text-only systemInstruction through to generateContent', async () => { + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + systemInstruction: 'be friendly' + }, + {}, + fakeChromeAdapter + ); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.generateContent('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return value.includes('be friendly'); + }), + {} + ); + restore(); + }); + it('generateContent overrides model values', async () => { + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + tools: [ + { + functionDeclarations: [ + { + name: 'myfunc', + description: 'mydesc' + } + ] + } + ], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingMode.NONE } + }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + }, + {}, + fakeChromeAdapter + ); + expect(genModel.tools?.length).to.equal(1); + expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( + FunctionCallingMode.NONE + ); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.generateContent({ + contents: [{ role: 'user', parts: [{ text: 'hello' }] }], + tools: [ + { + functionDeclarations: [ + { name: 'otherfunc', description: 'otherdesc' } + ] + }, + { googleSearch: {} }, + { codeExecution: {} } + ], + toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.AUTO } }, + systemInstruction: { role: 'system', parts: [{ text: 'be formal' }] } + }); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return ( + value.includes('otherfunc') && + value.includes('googleSearch') && + value.includes('codeExecution') && + value.includes(FunctionCallingMode.AUTO) && + value.includes('be formal') + ); + }), + {} + ); + restore(); + }); + it('passes base model params through to ChatSession when there are no startChatParams', async () => { + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + generationConfig: { + topK: 1 + } + }, + {}, + fakeChromeAdapter + ); + const chatSession = genModel.startChat(); + expect(chatSession.params?.generationConfig).to.deep.equal({ + topK: 1 + }); + restore(); + }); + it('overrides base model params with startChatParams', () => { + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + generationConfig: { + topK: 1 + } + }, + {}, + fakeChromeAdapter + ); + const chatSession = genModel.startChat({ + generationConfig: { + topK: 2 + } + }); + expect(chatSession.params?.generationConfig).to.deep.equal({ + topK: 2 + }); + }); + it('passes params through to chat.sendMessage', async () => { + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + tools: [ + { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] }, + { googleSearch: {} }, + { codeExecution: {} } + ], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingMode.NONE } + }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] }, + generationConfig: { + topK: 1 + } + }, + {}, + fakeChromeAdapter + ); + expect(genModel.tools?.length).to.equal(3); + expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( + FunctionCallingMode.NONE + ); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.startChat().sendMessage('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return ( + value.includes('myfunc') && + value.includes('googleSearch') && + value.includes('codeExecution') && + value.includes(FunctionCallingMode.NONE) && + value.includes('be friendly') && + value.includes('topK') + ); + }), + {} + ); + restore(); + }); + it('passes text-only systemInstruction through to chat.sendMessage', async () => { + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + systemInstruction: 'be friendly' + }, + {}, + fakeChromeAdapter + ); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.startChat().sendMessage('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return value.includes('be friendly'); + }), + {} + ); + restore(); + }); + it('startChat overrides model values', async () => { + const genModel = new GenerativeModel( + fakeAI, + { + model: 'my-model', + tools: [ + { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] } + ], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingMode.NONE } + }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] }, + generationConfig: { + responseMimeType: 'image/jpeg' + } + }, + {}, + fakeChromeAdapter + ); + expect(genModel.tools?.length).to.equal(1); + expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( + FunctionCallingMode.NONE + ); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel + .startChat({ + tools: [ + { + functionDeclarations: [ + { name: 'otherfunc', description: 'otherdesc' } + ] + }, + { googleSearch: {} }, + { codeExecution: {} } + ], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingMode.AUTO } + }, + systemInstruction: { role: 'system', parts: [{ text: 'be formal' }] }, + generationConfig: { + responseMimeType: 'image/png' + } + }) + .sendMessage('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return ( + value.includes('otherfunc') && + value.includes('googleSearch') && + value.includes('codeExecution') && + value.includes(FunctionCallingMode.AUTO) && + value.includes('be formal') && + value.includes('image/png') && + !value.includes('image/jpeg') + ); + }), + {} + ); + restore(); + }); + it('calls countTokens', async () => { + const genModel = new GenerativeModel( + fakeAI, + { model: 'my-model' }, + {}, + fakeChromeAdapter + ); + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-total-tokens.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.countTokens('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.COUNT_TOKENS, + match.any, + false, + match((value: string) => { + return value.includes('hello'); + }) + ); + restore(); + }); +}); + +describe('GenerativeModel dispatch logic', () => { + let makeRequestStub: SinonStub; + let mockChromeAdapter: ChromeAdapter; + + function stubMakeRequest(stream?: boolean): void { + if (stream) { + makeRequestStub = stub(request, 'makeRequest').resolves( + getMockResponseStreaming( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ) as Response + ); + } else { + makeRequestStub = stub(request, 'makeRequest').resolves( + getMockResponse( + 'vertexAI', + 'unary-success-basic-reply-short.json' + ) as Response + ); + } + } + + beforeEach(() => { + // @ts-ignore + mockChromeAdapter = { + isAvailable: stub(), + generateContent: stub().resolves(new Response(JSON.stringify({}))), + generateContentStream: stub().resolves( + new Response(new ReadableStream()) + ), + countTokens: stub().resolves(new Response(JSON.stringify({}))), + mode: InferenceMode.PREFER_ON_DEVICE + }; + }); + + afterEach(() => { + restore(); + }); + + describe('PREFER_ON_DEVICE', () => { + beforeEach(() => { + mockChromeAdapter.mode = InferenceMode.PREFER_ON_DEVICE; + }); + it('should use on-device for generateContent when available', async () => { + stubMakeRequest(); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(mockChromeAdapter.generateContent).to.have.been.calledOnce; + expect(makeRequestStub).to.not.have.been.called; + }); + it('should use cloud for generateContent when on-device is not available', async () => { + stubMakeRequest(); + (mockChromeAdapter.isAvailable as SinonStub).resolves(false); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(mockChromeAdapter.generateContent).to.not.have.been.called; + expect(makeRequestStub).to.have.been.calledOnce; + }); + it('should use on-device for generateContentStream when available', async () => { + stubMakeRequest(true); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(mockChromeAdapter.generateContentStream).to.have.been.calledOnce; + expect(makeRequestStub).to.not.have.been.called; + }); + it('should use cloud for generateContentStream when on-device is not available', async () => { + stubMakeRequest(true); + (mockChromeAdapter.isAvailable as SinonStub).resolves(false); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(mockChromeAdapter.generateContentStream).to.not.have.been.called; + expect(makeRequestStub).to.have.been.calledOnce; + }); + it('should use cloud for countTokens', async () => { + stubMakeRequest(); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.countTokens('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + }); + }); + + describe('ONLY_ON_DEVICE', () => { + beforeEach(() => { + mockChromeAdapter.mode = InferenceMode.ONLY_ON_DEVICE; + }); + it('should use on-device for generateContent when available', async () => { + stubMakeRequest(); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(mockChromeAdapter.generateContent).to.have.been.calledOnce; + expect(makeRequestStub).to.not.have.been.called; + }); + it('generateContent should throw when on-device is not available', async () => { + stubMakeRequest(); + (mockChromeAdapter.isAvailable as SinonStub).resolves(false); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await expect(model.generateContent('hello')).to.be.rejectedWith( + /on-device model is not available/ + ); + expect(mockChromeAdapter.generateContent).to.not.have.been.called; + expect(makeRequestStub).to.not.have.been.called; + }); + it('should use on-device for generateContentStream when available', async () => { + stubMakeRequest(true); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(mockChromeAdapter.generateContentStream).to.have.been.calledOnce; + expect(makeRequestStub).to.not.have.been.called; + }); + it('generateContentStream should throw when on-device is not available', async () => { + stubMakeRequest(true); + (mockChromeAdapter.isAvailable as SinonStub).resolves(false); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await expect(model.generateContentStream('hello')).to.be.rejectedWith( + /on-device model is not available/ + ); + expect(mockChromeAdapter.generateContent).to.not.have.been.called; + expect(makeRequestStub).to.not.have.been.called; + }); + it('should always throw for countTokens', async () => { + stubMakeRequest(); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await expect(model.countTokens('hello')).to.be.rejectedWith(AIError); + expect(makeRequestStub).to.not.have.been.called; + }); + }); + + describe('ONLY_IN_CLOUD', () => { + beforeEach(() => { + mockChromeAdapter.mode = InferenceMode.ONLY_IN_CLOUD; + }); + it('should use cloud for generateContent even when on-device is available', async () => { + stubMakeRequest(); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContent).to.not.have.been.called; + }); + it('should use cloud for generateContentStream even when on-device is available', async () => { + stubMakeRequest(true); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContentStream).to.not.have.been.called; + }); + it('should always use cloud for countTokens', async () => { + stubMakeRequest(); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.countTokens('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + }); + }); + + describe('PREFER_IN_CLOUD', () => { + beforeEach(() => { + mockChromeAdapter.mode = InferenceMode.PREFER_IN_CLOUD; + }); + it('should use cloud for generateContent when available', async () => { + stubMakeRequest(); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContent).to.not.have.been.called; + }); + it('should fall back to on-device for generateContent if cloud fails', async () => { + makeRequestStub.rejects( + new AIError(AIErrorCode.FETCH_ERROR, 'Network error') + ); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContent('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContent).to.have.been.calledOnce; + }); + it('should use cloud for generateContentStream when available', async () => { + stubMakeRequest(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContentStream).to.not.have.been.called; + }); + it('should fall back to on-device for generateContentStream if cloud fails', async () => { + makeRequestStub.rejects( + new AIError(AIErrorCode.FETCH_ERROR, 'Network error') + ); + (mockChromeAdapter.isAvailable as SinonStub).resolves(true); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.generateContentStream('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + expect(mockChromeAdapter.generateContentStream).to.have.been.calledOnce; + }); + it('should use cloud for countTokens', async () => { + stubMakeRequest(); + const model = new GenerativeModel( + fakeAI, + { model: 'model' }, + {}, + mockChromeAdapter + ); + await model.countTokens('hello'); + expect(makeRequestStub).to.have.been.calledOnce; + }); + }); +}); diff --git a/packages/ai/src/models/generative-model.ts b/packages/ai/src/models/generative-model.ts new file mode 100644 index 00000000000..ffce645eeb1 --- /dev/null +++ b/packages/ai/src/models/generative-model.ts @@ -0,0 +1,167 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + generateContent, + generateContentStream +} from '../methods/generate-content'; +import { + Content, + CountTokensRequest, + CountTokensResponse, + GenerateContentRequest, + GenerateContentResult, + GenerateContentStreamResult, + GenerationConfig, + ModelParams, + Part, + RequestOptions, + SafetySetting, + StartChatParams, + Tool, + ToolConfig +} from '../types'; +import { ChatSession } from '../methods/chat-session'; +import { countTokens } from '../methods/count-tokens'; +import { + formatGenerateContentInput, + formatSystemInstruction +} from '../requests/request-helpers'; +import { AI } from '../public-types'; +import { AIModel } from './ai-model'; +import { ChromeAdapter } from '../types/chrome-adapter'; + +/** + * Class for generative model APIs. + * @public + */ +export class GenerativeModel extends AIModel { + generationConfig: GenerationConfig; + safetySettings: SafetySetting[]; + requestOptions?: RequestOptions; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: Content; + + constructor( + ai: AI, + modelParams: ModelParams, + requestOptions?: RequestOptions, + private chromeAdapter?: ChromeAdapter + ) { + super(ai, modelParams.model); + this.generationConfig = modelParams.generationConfig || {}; + this.safetySettings = modelParams.safetySettings || []; + this.tools = modelParams.tools; + this.toolConfig = modelParams.toolConfig; + this.systemInstruction = formatSystemInstruction( + modelParams.systemInstruction + ); + this.requestOptions = requestOptions || {}; + } + + /** + * Makes a single non-streaming call to the model + * and returns an object containing a single {@link GenerateContentResponse}. + */ + async generateContent( + request: GenerateContentRequest | string | Array + ): Promise { + const formattedParams = formatGenerateContentInput(request); + return generateContent( + this._apiSettings, + this.model, + { + generationConfig: this.generationConfig, + safetySettings: this.safetySettings, + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + ...formattedParams + }, + this.chromeAdapter, + this.requestOptions + ); + } + + /** + * Makes a single streaming call to the model + * and returns an object containing an iterable stream that iterates + * over all chunks in the streaming response as well as + * a promise that returns the final aggregated response. + */ + async generateContentStream( + request: GenerateContentRequest | string | Array + ): Promise { + const formattedParams = formatGenerateContentInput(request); + return generateContentStream( + this._apiSettings, + this.model, + { + generationConfig: this.generationConfig, + safetySettings: this.safetySettings, + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + ...formattedParams + }, + this.chromeAdapter, + this.requestOptions + ); + } + + /** + * Gets a new {@link ChatSession} instance which can be used for + * multi-turn chats. + */ + startChat(startChatParams?: StartChatParams): ChatSession { + return new ChatSession( + this._apiSettings, + this.model, + this.chromeAdapter, + { + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + generationConfig: this.generationConfig, + safetySettings: this.safetySettings, + /** + * Overrides params inherited from GenerativeModel with those explicitly set in the + * StartChatParams. For example, if startChatParams.generationConfig is set, it'll override + * this.generationConfig. + */ + ...startChatParams + }, + this.requestOptions + ); + } + + /** + * Counts the tokens in the provided request. + */ + async countTokens( + request: CountTokensRequest | string | Array + ): Promise { + const formattedParams = formatGenerateContentInput(request); + return countTokens( + this._apiSettings, + this.model, + formattedParams, + this.chromeAdapter + ); + } +} diff --git a/packages/ai/src/models/imagen-model.test.ts b/packages/ai/src/models/imagen-model.test.ts new file mode 100644 index 00000000000..f4121e18f2d --- /dev/null +++ b/packages/ai/src/models/imagen-model.test.ts @@ -0,0 +1,166 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { use, expect } from 'chai'; +import { ImagenModel } from './imagen-model'; +import { + ImagenAspectRatio, + ImagenPersonFilterLevel, + ImagenSafetyFilterLevel, + AI, + AIErrorCode +} from '../public-types'; +import * as request from '../requests/request'; +import sinonChai from 'sinon-chai'; +import { AIError } from '../errors'; +import { getMockResponse } from '../../test-utils/mock-response'; +import { match, restore, stub } from 'sinon'; +import { VertexAIBackend } from '../backend'; + +use(sinonChai); + +const fakeAI: AI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project', + appId: 'my-appid' + } + }, + backend: new VertexAIBackend('us-central1'), + location: 'us-central1' +}; + +describe('ImagenModel', () => { + it('generateImages makes a request to predict with default parameters', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-generate-images-base64.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + + const imagenModel = new ImagenModel(fakeAI, { + model: 'my-model' + }); + const prompt = 'A photorealistic image of a toy boat at sea.'; + await imagenModel.generateImages(prompt); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.PREDICT, + match.any, + false, + match((value: string) => { + return ( + value.includes(`"prompt":"${prompt}"`) && + value.includes(`"sampleCount":1`) + ); + }), + undefined + ); + restore(); + }); + it('generateImages makes a request to predict with generation config and safety settings', async () => { + const imagenModel = new ImagenModel(fakeAI, { + model: 'my-model', + generationConfig: { + negativePrompt: 'do not hallucinate', + numberOfImages: 4, + aspectRatio: ImagenAspectRatio.LANDSCAPE_16x9, + imageFormat: { mimeType: 'image/jpeg', compressionQuality: 75 }, + addWatermark: true + }, + safetySettings: { + safetyFilterLevel: ImagenSafetyFilterLevel.BLOCK_ONLY_HIGH, + personFilterLevel: ImagenPersonFilterLevel.ALLOW_ADULT + } + }); + + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-generate-images-base64.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const prompt = 'A photorealistic image of a toy boat at sea.'; + await imagenModel.generateImages(prompt); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.PREDICT, + match.any, + false, + match((value: string) => { + return ( + value.includes( + `"negativePrompt":"${imagenModel.generationConfig?.negativePrompt}"` + ) && + value.includes( + `"sampleCount":${imagenModel.generationConfig?.numberOfImages}` + ) && + value.includes( + `"aspectRatio":"${imagenModel.generationConfig?.aspectRatio}"` + ) && + value.includes( + JSON.stringify(imagenModel.generationConfig?.imageFormat?.mimeType) + ) && + value.includes( + JSON.stringify(imagenModel.generationConfig?.addWatermark) + ) && + value.includes( + JSON.stringify(imagenModel.safetySettings?.safetyFilterLevel) + ) && + value.includes( + JSON.stringify(imagenModel.safetySettings?.personFilterLevel) + ) + ); + }), + undefined + ); + restore(); + }); + it('throws if prompt blocked', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-generate-images-prompt-blocked.json' + ); + + stub(globalThis, 'fetch').resolves({ + ok: false, + status: 400, + statusText: 'Bad Request', + json: mockResponse.json + } as Response); + + const imagenModel = new ImagenModel(fakeAI, { + model: 'my-model' + }); + try { + await imagenModel.generateImages('some inappropriate prompt.'); + } catch (e) { + expect((e as AIError).code).to.equal(AIErrorCode.FETCH_ERROR); + expect((e as AIError).message).to.include('400'); + expect((e as AIError).message).to.include( + "Image generation failed with the following error: The prompt could not be submitted. This prompt contains sensitive words that violate Google's Responsible AI practices. Try rephrasing the prompt. If you think this was an error, send feedback." + ); + } finally { + restore(); + } + }); +}); diff --git a/packages/ai/src/models/imagen-model.ts b/packages/ai/src/models/imagen-model.ts new file mode 100644 index 00000000000..a41a03f25cf --- /dev/null +++ b/packages/ai/src/models/imagen-model.ts @@ -0,0 +1,160 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AI } from '../public-types'; +import { Task, makeRequest } from '../requests/request'; +import { createPredictRequestBody } from '../requests/request-helpers'; +import { handlePredictResponse } from '../requests/response-helpers'; +import { + ImagenGCSImage, + ImagenGenerationConfig, + ImagenInlineImage, + RequestOptions, + ImagenModelParams, + ImagenGenerationResponse, + ImagenSafetySettings +} from '../types'; +import { AIModel } from './ai-model'; + +/** + * Class for Imagen model APIs. + * + * This class provides methods for generating images using the Imagen model. + * + * @example + * ```javascript + * const imagen = new ImagenModel( + * ai, + * { + * model: 'imagen-3.0-generate-002' + * } + * ); + * + * const response = await imagen.generateImages('A photo of a cat'); + * if (response.images.length > 0) { + * console.log(response.images[0].bytesBase64Encoded); + * } + * ``` + * + * @public + */ +export class ImagenModel extends AIModel { + /** + * The Imagen generation configuration. + */ + generationConfig?: ImagenGenerationConfig; + /** + * Safety settings for filtering inappropriate content. + */ + safetySettings?: ImagenSafetySettings; + + /** + * Constructs a new instance of the {@link ImagenModel} class. + * + * @param ai - an {@link AI} instance. + * @param modelParams - Parameters to use when making requests to Imagen. + * @param requestOptions - Additional options to use when making requests. + * + * @throws If the `apiKey` or `projectId` fields are missing in your + * Firebase config. + */ + constructor( + ai: AI, + modelParams: ImagenModelParams, + public requestOptions?: RequestOptions + ) { + const { model, generationConfig, safetySettings } = modelParams; + super(ai, model); + this.generationConfig = generationConfig; + this.safetySettings = safetySettings; + } + + /** + * Generates images using the Imagen model and returns them as + * base64-encoded strings. + * + * @param prompt - A text prompt describing the image(s) to generate. + * @returns A promise that resolves to an {@link ImagenGenerationResponse} + * object containing the generated images. + * + * @throws If the request to generate images fails. This happens if the + * prompt is blocked. + * + * @remarks + * If the prompt was not blocked, but one or more of the generated images were filtered, the + * returned object will have a `filteredReason` property. + * If all images are filtered, the `images` array will be empty. + * + * @public + */ + async generateImages( + prompt: string + ): Promise> { + const body = createPredictRequestBody(prompt, { + ...this.generationConfig, + ...this.safetySettings + }); + const response = await makeRequest( + this.model, + Task.PREDICT, + this._apiSettings, + /* stream */ false, + JSON.stringify(body), + this.requestOptions + ); + return handlePredictResponse(response); + } + + /** + * Generates images to Cloud Storage for Firebase using the Imagen model. + * + * @internal This method is temporarily internal. + * + * @param prompt - A text prompt describing the image(s) to generate. + * @param gcsURI - The URI of file stored in a Cloud Storage for Firebase bucket. + * This should be a directory. For example, `gs://my-bucket/my-directory/`. + * @returns A promise that resolves to an {@link ImagenGenerationResponse} + * object containing the URLs of the generated images. + * + * @throws If the request fails to generate images fails. This happens if + * the prompt is blocked. + * + * @remarks + * If the prompt was not blocked, but one or more of the generated images were filtered, the + * returned object will have a `filteredReason` property. + * If all images are filtered, the `images` array will be empty. + */ + async generateImagesGCS( + prompt: string, + gcsURI: string + ): Promise> { + const body = createPredictRequestBody(prompt, { + gcsURI, + ...this.generationConfig, + ...this.safetySettings + }); + const response = await makeRequest( + this.model, + Task.PREDICT, + this._apiSettings, + /* stream */ false, + JSON.stringify(body), + this.requestOptions + ); + return handlePredictResponse(response); + } +} diff --git a/packages/ai/src/models/index.ts b/packages/ai/src/models/index.ts new file mode 100644 index 00000000000..5d2492d6784 --- /dev/null +++ b/packages/ai/src/models/index.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './ai-model'; +export * from './generative-model'; +export * from './live-generative-model'; +export * from './imagen-model'; diff --git a/packages/ai/src/models/live-generative-model.test.ts b/packages/ai/src/models/live-generative-model.test.ts new file mode 100644 index 00000000000..a899e0b39fa --- /dev/null +++ b/packages/ai/src/models/live-generative-model.test.ts @@ -0,0 +1,202 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { use, expect } from 'chai'; +import sinon, { SinonFakeTimers, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { AI } from '../public-types'; +import { LiveSession } from '../methods/live-session'; +import { WebSocketHandler } from '../websocket'; +import { GoogleAIBackend } from '../backend'; +import { LiveGenerativeModel } from './live-generative-model'; +import { AIError } from '../errors'; + +use(sinonChai); +use(chaiAsPromised); + +// A controllable mock for the WebSocketHandler interface +class MockWebSocketHandler implements WebSocketHandler { + connect = stub().resolves(); + send = stub(); + close = stub().resolves(); + + private serverMessages: unknown[] = []; + private generatorController: { + resolve: () => void; + promise: Promise; + } | null = null; + + async *listen(): AsyncGenerator { + while (true) { + if (this.serverMessages.length > 0) { + yield this.serverMessages.shift(); + } else { + const promise = new Promise(resolve => { + this.generatorController = { resolve, promise: null! }; + }); + await promise; + } + } + } + + // Test method to simulate a message from the server + simulateServerMessage(message: object): void { + this.serverMessages.push(message); + if (this.generatorController) { + this.generatorController.resolve(); + this.generatorController = null; + } + } +} + +const fakeAI: AI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project', + appId: 'my-appid' + } + }, + backend: new GoogleAIBackend(), + location: 'us-central1' +}; + +describe('LiveGenerativeModel', () => { + let mockHandler: MockWebSocketHandler; + let clock: SinonFakeTimers; + + beforeEach(() => { + mockHandler = new MockWebSocketHandler(); + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + sinon.restore(); + clock.restore(); + }); + + it('connect() should call handler.connect and send setup message', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { model: 'my-model' }, + mockHandler + ); + const connectPromise = model.connect(); + + // Ensure connect was called before simulating server response + expect(mockHandler.connect).to.have.been.calledOnce; + + // Wait for the setup message to be sent + await clock.runAllAsync(); + + expect(mockHandler.send).to.have.been.calledOnce; + const setupMessage = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(setupMessage.setup.model).to.include('my-model'); + + // Simulate successful handshake and resolve the promise + mockHandler.simulateServerMessage({ setupComplete: true }); + const session = await connectPromise; + expect(session).to.be.an.instanceOf(LiveSession); + await session.close(); + }); + + it('connect() should throw if handshake fails', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { model: 'my-model' }, + mockHandler + ); + const connectPromise = model.connect(); + + // Wait for setup message + await clock.runAllAsync(); + + // Simulate a failed handshake + mockHandler.simulateServerMessage({ error: 'handshake failed' }); + await expect(connectPromise).to.be.rejectedWith( + AIError, + /Server connection handshake failed/ + ); + }); + + it('connect() should pass through connection errors', async () => { + mockHandler.connect.rejects(new Error('Connection refused')); + const model = new LiveGenerativeModel( + fakeAI, + { model: 'my-model' }, + mockHandler + ); + await expect(model.connect()).to.be.rejectedWith('Connection refused'); + }); + + it('connect() should pass through setup parameters correctly', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { + model: 'gemini-pro', + generationConfig: { temperature: 0.8 }, + systemInstruction: { role: 'system', parts: [{ text: 'Be a pirate' }] } + }, + mockHandler + ); + const connectPromise = model.connect(); + + // Wait for setup message + await clock.runAllAsync(); + + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + expect(sentData.setup.generationConfig).to.deep.equal({ temperature: 0.8 }); + expect(sentData.setup.systemInstruction.parts[0].text).to.equal( + 'Be a pirate' + ); + mockHandler.simulateServerMessage({ setupComplete: true }); + await connectPromise; + }); + it('connect() should deconstruct generationConfig to send transcription configs in top level setup', async () => { + const model = new LiveGenerativeModel( + fakeAI, + { + model: 'gemini-pro', + generationConfig: { + temperature: 0.8, + inputAudioTranscription: {}, + outputAudioTranscription: {} + }, + systemInstruction: { role: 'system', parts: [{ text: 'Be a pirate' }] } + }, + mockHandler + ); + const connectPromise = model.connect(); + + // Wait for setup message + await clock.runAllAsync(); + + const sentData = JSON.parse(mockHandler.send.getCall(0).args[0]); + // inputAudioTranscription and outputAudioTranscription should be at the top-level setup message, + // rather than in the generationConfig. + expect(sentData.setup.generationConfig).to.deep.equal({ temperature: 0.8 }); + expect(sentData.setup.inputAudioTranscription).to.deep.equal({}); + expect(sentData.setup.outputAudioTranscription).to.deep.equal({}); + expect(sentData.setup.systemInstruction.parts[0].text).to.equal( + 'Be a pirate' + ); + mockHandler.simulateServerMessage({ setupComplete: true }); + await connectPromise; + }); +}); diff --git a/packages/ai/src/models/live-generative-model.ts b/packages/ai/src/models/live-generative-model.ts new file mode 100644 index 00000000000..a89921070e3 --- /dev/null +++ b/packages/ai/src/models/live-generative-model.ts @@ -0,0 +1,135 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIModel } from './ai-model'; +import { LiveSession } from '../methods/live-session'; +import { AIError } from '../errors'; +import { + AI, + AIErrorCode, + BackendType, + Content, + LiveGenerationConfig, + LiveModelParams, + Tool, + ToolConfig +} from '../public-types'; +import { WebSocketHandler } from '../websocket'; +import { WebSocketUrl } from '../requests/request'; +import { formatSystemInstruction } from '../requests/request-helpers'; +import { _LiveClientSetup } from '../types/live-responses'; + +/** + * Class for Live generative model APIs. The Live API enables low-latency, two-way multimodal + * interactions with Gemini. + * + * This class should only be instantiated with {@link getLiveGenerativeModel}. + * + * @beta + */ +export class LiveGenerativeModel extends AIModel { + generationConfig: LiveGenerationConfig; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: Content; + + /** + * @internal + */ + constructor( + ai: AI, + modelParams: LiveModelParams, + /** + * @internal + */ + private _webSocketHandler: WebSocketHandler + ) { + super(ai, modelParams.model); + this.generationConfig = modelParams.generationConfig || {}; + this.tools = modelParams.tools; + this.toolConfig = modelParams.toolConfig; + this.systemInstruction = formatSystemInstruction( + modelParams.systemInstruction + ); + } + + /** + * Starts a {@link LiveSession}. + * + * @returns A {@link LiveSession}. + * @throws If the connection failed to be established with the server. + * + * @beta + */ + async connect(): Promise { + const url = new WebSocketUrl(this._apiSettings); + await this._webSocketHandler.connect(url.toString()); + + let fullModelPath: string; + if (this._apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + fullModelPath = `projects/${this._apiSettings.project}/${this.model}`; + } else { + fullModelPath = `projects/${this._apiSettings.project}/locations/${this._apiSettings.location}/${this.model}`; + } + + // inputAudioTranscription and outputAudioTranscription are on the generation config in the public API, + // but the backend expects them to be in the `setup` message. + const { + inputAudioTranscription, + outputAudioTranscription, + ...generationConfig + } = this.generationConfig; + + const setupMessage: _LiveClientSetup = { + setup: { + model: fullModelPath, + generationConfig, + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + inputAudioTranscription, + outputAudioTranscription + } + }; + + try { + // Begin listening for server messages, and begin the handshake by sending the 'setupMessage' + const serverMessages = this._webSocketHandler.listen(); + this._webSocketHandler.send(JSON.stringify(setupMessage)); + + // Verify we received the handshake response 'setupComplete' + const firstMessage = (await serverMessages.next()).value; + if ( + !firstMessage || + !(typeof firstMessage === 'object') || + !('setupComplete' in firstMessage) + ) { + await this._webSocketHandler.close(1011, 'Handshake failure'); + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + 'Server connection handshake failed. The server did not respond with a setupComplete message.' + ); + } + + return new LiveSession(this._webSocketHandler, serverMessages); + } catch (e) { + // Ensure connection is closed on any setup error + await this._webSocketHandler.close(); + throw e; + } + } +} diff --git a/packages/ai/src/public-types.ts b/packages/ai/src/public-types.ts new file mode 100644 index 00000000000..fff41251a01 --- /dev/null +++ b/packages/ai/src/public-types.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseApp } from '@firebase/app'; +import { Backend } from './backend'; + +export * from './types'; + +/** + * An instance of the Firebase AI SDK. + * + * Do not create this instance directly. Instead, use {@link getAI | getAI()}. + * + * @public + */ +export interface AI { + /** + * The {@link @firebase/app#FirebaseApp} this {@link AI} instance is associated with. + */ + app: FirebaseApp; + /** + * A {@link Backend} instance that specifies the configuration for the target backend, + * either the Gemini Developer API (using {@link GoogleAIBackend}) or the + * Vertex AI Gemini API (using {@link VertexAIBackend}). + */ + backend: Backend; + /** + * Options applied to this {@link AI} instance. + */ + options?: AIOptions; + /** + * @deprecated use `AI.backend.location` instead. + * + * The location configured for this AI service instance, relevant for Vertex AI backends. + */ + location: string; +} + +/** + * An enum-like object containing constants that represent the supported backends + * for the Firebase AI SDK. + * This determines which backend service (Vertex AI Gemini API or Gemini Developer API) + * the SDK will communicate with. + * + * These values are assigned to the `backendType` property within the specific backend + * configuration objects ({@link GoogleAIBackend} or {@link VertexAIBackend}) to identify + * which service to target. + * + * @public + */ +export const BackendType = { + /** + * Identifies the backend service for the Vertex AI Gemini API provided through Google Cloud. + * Use this constant when creating a {@link VertexAIBackend} configuration. + */ + VERTEX_AI: 'VERTEX_AI', + + /** + * Identifies the backend service for the Gemini Developer API ({@link https://ai.google/ | Google AI}). + * Use this constant when creating a {@link GoogleAIBackend} configuration. + */ + GOOGLE_AI: 'GOOGLE_AI' +} as const; // Using 'as const' makes the string values literal types + +/** + * Type alias representing valid backend types. + * It can be either `'VERTEX_AI'` or `'GOOGLE_AI'`. + * + * @public + */ +export type BackendType = (typeof BackendType)[keyof typeof BackendType]; + +/** + * Options for initializing the AI service using {@link getAI | getAI()}. + * This allows specifying which backend to use (Vertex AI Gemini API or Gemini Developer API) + * and configuring its specific options (like location for Vertex AI). + * + * @public + */ +export interface AIOptions { + /** + * The backend configuration to use for the AI service instance. + * Defaults to the Gemini Developer API backend ({@link GoogleAIBackend}). + */ + backend?: Backend; + /** + * Whether to use App Check limited use tokens. Defaults to false. + */ + useLimitedUseAppCheckTokens?: boolean; +} diff --git a/packages/ai/src/requests/hybrid-helpers.test.ts b/packages/ai/src/requests/hybrid-helpers.test.ts new file mode 100644 index 00000000000..d5d13691316 --- /dev/null +++ b/packages/ai/src/requests/hybrid-helpers.test.ts @@ -0,0 +1,199 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { use, expect } from 'chai'; +import { SinonStub, SinonStubbedInstance, restore, stub } from 'sinon'; +import { callCloudOrDevice } from './hybrid-helpers'; +import { + GenerateContentRequest, + InferenceMode, + AIErrorCode, + ChromeAdapter, + InferenceSource +} from '../types'; +import { AIError } from '../errors'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; + +use(sinonChai); +use(chaiAsPromised); + +describe('callCloudOrDevice', () => { + let chromeAdapter: SinonStubbedInstance; + let onDeviceCall: SinonStub; + let inCloudCall: SinonStub; + let request: GenerateContentRequest; + + beforeEach(() => { + // @ts-ignore + chromeAdapter = { + mode: InferenceMode.PREFER_ON_DEVICE, + isAvailable: stub(), + generateContent: stub(), + generateContentStream: stub(), + countTokens: stub() + }; + onDeviceCall = stub().resolves('on-device-response'); + inCloudCall = stub().resolves('in-cloud-response'); + request = { contents: [] }; + }); + + afterEach(() => { + restore(); + }); + + it('should call inCloudCall if chromeAdapter is undefined', async () => { + const result = await callCloudOrDevice( + request, + undefined, + onDeviceCall, + inCloudCall + ); + expect(result.response).to.equal('in-cloud-response'); + expect(result.inferenceSource).to.equal(InferenceSource.IN_CLOUD); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.not.have.been.called; + }); + + describe('PREFER_ON_DEVICE mode', () => { + beforeEach(() => { + chromeAdapter.mode = InferenceMode.PREFER_ON_DEVICE; + }); + + it('should call onDeviceCall if available', async () => { + chromeAdapter.isAvailable.resolves(true); + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result.response).to.equal('on-device-response'); + expect(result.inferenceSource).to.equal(InferenceSource.ON_DEVICE); + expect(onDeviceCall).to.have.been.calledOnce; + expect(inCloudCall).to.not.have.been.called; + }); + + it('should call inCloudCall if not available', async () => { + chromeAdapter.isAvailable.resolves(false); + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result.response).to.equal('in-cloud-response'); + expect(result.inferenceSource).to.equal(InferenceSource.IN_CLOUD); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.not.have.been.called; + }); + }); + + describe('ONLY_ON_DEVICE mode', () => { + beforeEach(() => { + chromeAdapter.mode = InferenceMode.ONLY_ON_DEVICE; + }); + + it('should call onDeviceCall if available', async () => { + chromeAdapter.isAvailable.resolves(true); + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result.response).to.equal('on-device-response'); + expect(result.inferenceSource).to.equal(InferenceSource.ON_DEVICE); + expect(onDeviceCall).to.have.been.calledOnce; + expect(inCloudCall).to.not.have.been.called; + }); + + it('should throw if not available', async () => { + chromeAdapter.isAvailable.resolves(false); + await expect( + callCloudOrDevice(request, chromeAdapter, onDeviceCall, inCloudCall) + ).to.be.rejectedWith(/on-device model is not available/); + expect(inCloudCall).to.not.have.been.called; + expect(onDeviceCall).to.not.have.been.called; + }); + }); + + describe('ONLY_IN_CLOUD mode', () => { + beforeEach(() => { + chromeAdapter.mode = InferenceMode.ONLY_IN_CLOUD; + }); + + it('should call inCloudCall even if on-device is available', async () => { + chromeAdapter.isAvailable.resolves(true); + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result.response).to.equal('in-cloud-response'); + expect(result.inferenceSource).to.equal(InferenceSource.IN_CLOUD); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.not.have.been.called; + }); + }); + + describe('PREFER_IN_CLOUD mode', () => { + beforeEach(() => { + chromeAdapter.mode = InferenceMode.PREFER_IN_CLOUD; + }); + + it('should call inCloudCall first', async () => { + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result.response).to.equal('in-cloud-response'); + expect(result.inferenceSource).to.equal(InferenceSource.IN_CLOUD); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.not.have.been.called; + }); + + it('should fall back to onDeviceCall if inCloudCall fails with AIErrorCode.FETCH_ERROR', async () => { + inCloudCall.rejects( + new AIError(AIErrorCode.FETCH_ERROR, 'Network error') + ); + const result = await callCloudOrDevice( + request, + chromeAdapter, + onDeviceCall, + inCloudCall + ); + expect(result.response).to.equal('on-device-response'); + expect(result.inferenceSource).to.equal(InferenceSource.ON_DEVICE); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.have.been.calledOnce; + }); + + it('should re-throw other errors from inCloudCall', async () => { + const error = new AIError(AIErrorCode.RESPONSE_ERROR, 'safety problem'); + inCloudCall.rejects(error); + await expect( + callCloudOrDevice(request, chromeAdapter, onDeviceCall, inCloudCall) + ).to.be.rejectedWith(error); + expect(inCloudCall).to.have.been.calledOnce; + expect(onDeviceCall).to.not.have.been.called; + }); + }); +}); diff --git a/packages/ai/src/requests/hybrid-helpers.ts b/packages/ai/src/requests/hybrid-helpers.ts new file mode 100644 index 00000000000..2697216cd8a --- /dev/null +++ b/packages/ai/src/requests/hybrid-helpers.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { + GenerateContentRequest, + InferenceMode, + AIErrorCode, + ChromeAdapter, + InferenceSource +} from '../types'; + +const errorsCausingFallback: AIErrorCode[] = [ + // most network errors + AIErrorCode.FETCH_ERROR, + // fallback code for all other errors in makeRequest + AIErrorCode.ERROR, + // error due to API not being enabled in project + AIErrorCode.API_NOT_ENABLED +]; + +interface CallResult { + response: Response; + inferenceSource: InferenceSource; +} + +/** + * Dispatches a request to the appropriate backend (on-device or in-cloud) + * based on the inference mode. + * + * @param request - The request to be sent. + * @param chromeAdapter - The on-device model adapter. + * @param onDeviceCall - The function to call for on-device inference. + * @param inCloudCall - The function to call for in-cloud inference. + * @returns The response from the backend. + */ +export async function callCloudOrDevice( + request: GenerateContentRequest, + chromeAdapter: ChromeAdapter | undefined, + onDeviceCall: () => Promise, + inCloudCall: () => Promise +): Promise> { + if (!chromeAdapter) { + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; + } + switch (chromeAdapter.mode) { + case InferenceMode.ONLY_ON_DEVICE: + if (await chromeAdapter.isAvailable(request)) { + return { + response: await onDeviceCall(), + inferenceSource: InferenceSource.ON_DEVICE + }; + } + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'Inference mode is ONLY_ON_DEVICE, but an on-device model is not available.' + ); + case InferenceMode.ONLY_IN_CLOUD: + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; + case InferenceMode.PREFER_IN_CLOUD: + try { + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; + } catch (e) { + if (e instanceof AIError && errorsCausingFallback.includes(e.code)) { + return { + response: await onDeviceCall(), + inferenceSource: InferenceSource.ON_DEVICE + }; + } + throw e; + } + case InferenceMode.PREFER_ON_DEVICE: + if (await chromeAdapter.isAvailable(request)) { + return { + response: await onDeviceCall(), + inferenceSource: InferenceSource.ON_DEVICE + }; + } + return { + response: await inCloudCall(), + inferenceSource: InferenceSource.IN_CLOUD + }; + default: + throw new AIError( + AIErrorCode.ERROR, + `Unexpected infererence mode: ${chromeAdapter.mode}` + ); + } +} diff --git a/packages/ai/src/requests/imagen-image-format.ts b/packages/ai/src/requests/imagen-image-format.ts new file mode 100644 index 00000000000..e07d4cec818 --- /dev/null +++ b/packages/ai/src/requests/imagen-image-format.ts @@ -0,0 +1,81 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { logger } from '../logger'; + +/** + * Defines the image format for images generated by Imagen. + * + * Use this class to specify the desired format (JPEG or PNG) and compression quality + * for images generated by Imagen. This is typically included as part of + * {@link ImagenModelParams}. + * + * @example + * ```javascript + * const imagenModelParams = { + * // ... other ImagenModelParams + * imageFormat: ImagenImageFormat.jpeg(75) // JPEG with a compression level of 75. + * } + * ``` + * + * @public + */ +export class ImagenImageFormat { + /** + * The MIME type. + */ + mimeType: string; + /** + * The level of compression (a number between 0 and 100). + */ + compressionQuality?: number; + + private constructor() { + this.mimeType = 'image/png'; + } + + /** + * Creates an {@link ImagenImageFormat} for a JPEG image. + * + * @param compressionQuality - The level of compression (a number between 0 and 100). + * @returns An {@link ImagenImageFormat} object for a JPEG image. + * + * @public + */ + static jpeg(compressionQuality?: number): ImagenImageFormat { + if ( + compressionQuality && + (compressionQuality < 0 || compressionQuality > 100) + ) { + logger.warn( + `Invalid JPEG compression quality of ${compressionQuality} specified; the supported range is [0, 100].` + ); + } + return { mimeType: 'image/jpeg', compressionQuality }; + } + + /** + * Creates an {@link ImagenImageFormat} for a PNG image. + * + * @returns An {@link ImagenImageFormat} object for a PNG image. + * + * @public + */ + static png(): ImagenImageFormat { + return { mimeType: 'image/png' }; + } +} diff --git a/packages/ai/src/requests/request-helpers.test.ts b/packages/ai/src/requests/request-helpers.test.ts new file mode 100644 index 00000000000..993b0f1d3ae --- /dev/null +++ b/packages/ai/src/requests/request-helpers.test.ts @@ -0,0 +1,276 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import sinonChai from 'sinon-chai'; +import { + Content, + ImagenAspectRatio, + ImagenPersonFilterLevel, + ImagenSafetyFilterLevel +} from '../types'; +import { + createPredictRequestBody, + formatGenerateContentInput +} from './request-helpers'; + +use(sinonChai); + +describe('request formatting methods', () => { + describe('formatGenerateContentInput', () => { + it('formats a text string into a request', () => { + const result = formatGenerateContentInput('some text content'); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'some text content' }] + } + ] + }); + }); + it('formats an array of strings into a request', () => { + const result = formatGenerateContentInput(['txt1', 'txt2']); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txt1' }, { text: 'txt2' }] + } + ] + }); + }); + it('formats an array of Parts into a request', () => { + const result = formatGenerateContentInput([ + { text: 'txt1' }, + { text: 'txtB' } + ]); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txt1' }, { text: 'txtB' }] + } + ] + }); + }); + it('formats a mixed array into a request', () => { + const result = formatGenerateContentInput(['txtA', { text: 'txtB' }]); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }, { text: 'txtB' }] + } + ] + }); + }); + it('preserves other properties of request', () => { + const result = formatGenerateContentInput({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + generationConfig: { topK: 100 } + }); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + generationConfig: { topK: 100 } + }); + }); + it('formats systemInstructions if provided as text', () => { + const result = formatGenerateContentInput({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: 'be excited' + }); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + }); + }); + it('formats systemInstructions if provided as Part', () => { + const result = formatGenerateContentInput({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { text: 'be excited' } + }); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + }); + }); + it('formats systemInstructions if provided as Content (no role)', () => { + const result = formatGenerateContentInput({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { parts: [{ text: 'be excited' }] } as Content + }); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + }); + }); + it('passes thru systemInstructions if provided as Content', () => { + const result = formatGenerateContentInput({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + }); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + }); + }), + it('formats fileData as part if provided as part', () => { + const result = formatGenerateContentInput([ + 'What is this?', + { + fileData: { + mimeType: 'image/jpeg', + fileUri: 'gs://sample.appspot.com/image.jpeg' + } + } + ]); + expect(result).to.be.deep.equal({ + contents: [ + { + role: 'user', + parts: [ + { text: 'What is this?' }, + { + fileData: { + mimeType: 'image/jpeg', + fileUri: 'gs://sample.appspot.com/image.jpeg' + } + } + ] + } + ] + }); + }); + }); + describe('createPredictRequestBody', () => { + it('creates body with default request parameters', () => { + const prompt = 'A photorealistic image of a toy boat at sea.'; + const body = createPredictRequestBody(prompt, {}); + expect(body.instances[0].prompt).to.equal(prompt); + expect(body.parameters.sampleCount).to.equal(1); + expect(body.parameters.includeRaiReason).to.be.true; + expect(body.parameters.includeSafetyAttributes).to.be.true; + + // Parameters without default values should be undefined + expect(body.parameters.storageUri).to.be.undefined; + expect(body.parameters.storageUri).to.be.undefined; + expect(body.parameters.outputOptions).to.be.undefined; + expect(body.parameters.negativePrompt).to.be.undefined; + expect(body.parameters.aspectRatio).to.be.undefined; + expect(body.parameters.addWatermark).to.be.undefined; + expect(body.parameters.safetyFilterLevel).to.be.undefined; + expect(body.parameters.personGeneration).to.be.undefined; + }); + }); + it('creates body with non-default request paramaters', () => { + const prompt = 'A photorealistic image of a toy boat at sea.'; + const imageFormat = { mimeType: 'image/jpeg', compressionQuality: 75 }; + const safetySettings = { + safetyFilterLevel: ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE, + personFilterLevel: ImagenPersonFilterLevel.ALLOW_ADULT + }; + const addWatermark = true; + const numberOfImages = 4; + const negativePrompt = 'do not hallucinate'; + const aspectRatio = ImagenAspectRatio.LANDSCAPE_16x9; + const body = createPredictRequestBody(prompt, { + numberOfImages, + imageFormat, + addWatermark, + negativePrompt, + aspectRatio, + ...safetySettings + }); + expect(body.instances[0].prompt).to.equal(prompt); + expect(body.parameters).deep.equal({ + sampleCount: numberOfImages, + outputOptions: { + mimeType: imageFormat.mimeType, + compressionQuality: imageFormat.compressionQuality + }, + addWatermark, + negativePrompt, + safetyFilterLevel: safetySettings.safetyFilterLevel, + personGeneration: safetySettings.personFilterLevel, + aspectRatio, + includeRaiReason: true, + includeSafetyAttributes: true, + storageUri: undefined + }); + }); + it('creates body with GCS URI', () => { + const prompt = 'A photorealistic image of a toy boat at sea.'; + const gcsURI = 'gcs-uri'; + const body = createPredictRequestBody(prompt, { + gcsURI + }); + + expect(body.instances[0].prompt).to.equal(prompt); + expect(body.parameters.storageUri).to.equal(gcsURI); + }); +}); diff --git a/packages/ai/src/requests/request-helpers.ts b/packages/ai/src/requests/request-helpers.ts new file mode 100644 index 00000000000..ee80142481b --- /dev/null +++ b/packages/ai/src/requests/request-helpers.ts @@ -0,0 +1,164 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Content, GenerateContentRequest, Part, AIErrorCode } from '../types'; +import { AIError } from '../errors'; +import { ImagenGenerationParams, PredictRequestBody } from '../types/internal'; + +export function formatSystemInstruction( + input?: string | Part | Content +): Content | undefined { + // null or undefined + if (input == null) { + return undefined; + } else if (typeof input === 'string') { + return { role: 'system', parts: [{ text: input }] } as Content; + } else if ((input as Part).text) { + return { role: 'system', parts: [input as Part] }; + } else if ((input as Content).parts) { + if (!(input as Content).role) { + return { role: 'system', parts: (input as Content).parts }; + } else { + return input as Content; + } + } +} + +export function formatNewContent( + request: string | Array +): Content { + let newParts: Part[] = []; + if (typeof request === 'string') { + newParts = [{ text: request }]; + } else { + for (const partOrString of request) { + if (typeof partOrString === 'string') { + newParts.push({ text: partOrString }); + } else { + newParts.push(partOrString); + } + } + } + return assignRoleToPartsAndValidateSendMessageRequest(newParts); +} + +/** + * When multiple Part types (i.e. FunctionResponsePart and TextPart) are + * passed in a single Part array, we may need to assign different roles to each + * part. Currently only FunctionResponsePart requires a role other than 'user'. + * @private + * @param parts Array of parts to pass to the model + * @returns Array of content items + */ +function assignRoleToPartsAndValidateSendMessageRequest( + parts: Part[] +): Content { + const userContent: Content = { role: 'user', parts: [] }; + const functionContent: Content = { role: 'function', parts: [] }; + let hasUserContent = false; + let hasFunctionContent = false; + for (const part of parts) { + if ('functionResponse' in part) { + functionContent.parts.push(part); + hasFunctionContent = true; + } else { + userContent.parts.push(part); + hasUserContent = true; + } + } + + if (hasUserContent && hasFunctionContent) { + throw new AIError( + AIErrorCode.INVALID_CONTENT, + 'Within a single message, FunctionResponse cannot be mixed with other type of Part in the request for sending chat message.' + ); + } + + if (!hasUserContent && !hasFunctionContent) { + throw new AIError( + AIErrorCode.INVALID_CONTENT, + 'No Content is provided for sending chat message.' + ); + } + + if (hasUserContent) { + return userContent; + } + + return functionContent; +} + +export function formatGenerateContentInput( + params: GenerateContentRequest | string | Array +): GenerateContentRequest { + let formattedRequest: GenerateContentRequest; + if ((params as GenerateContentRequest).contents) { + formattedRequest = params as GenerateContentRequest; + } else { + // Array or string + const content = formatNewContent(params as string | Array); + formattedRequest = { contents: [content] }; + } + if ((params as GenerateContentRequest).systemInstruction) { + formattedRequest.systemInstruction = formatSystemInstruction( + (params as GenerateContentRequest).systemInstruction + ); + } + return formattedRequest; +} + +/** + * Convert the user-defined parameters in {@link ImagenGenerationParams} to the format + * that is expected from the REST API. + * + * @internal + */ +export function createPredictRequestBody( + prompt: string, + { + gcsURI, + imageFormat, + addWatermark, + numberOfImages = 1, + negativePrompt, + aspectRatio, + safetyFilterLevel, + personFilterLevel + }: ImagenGenerationParams +): PredictRequestBody { + // Properties that are undefined will be omitted from the JSON string that is sent in the request. + const body: PredictRequestBody = { + instances: [ + { + prompt + } + ], + parameters: { + storageUri: gcsURI, + negativePrompt, + sampleCount: numberOfImages, + aspectRatio, + outputOptions: imageFormat, + addWatermark, + safetyFilterLevel, + personGeneration: personFilterLevel, + includeRaiReason: true, + includeSafetyAttributes: true + } + }; + return body; +} diff --git a/packages/ai/src/requests/request.test.ts b/packages/ai/src/requests/request.test.ts new file mode 100644 index 00000000000..0d162906fdc --- /dev/null +++ b/packages/ai/src/requests/request.test.ts @@ -0,0 +1,436 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import { match, restore, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { RequestUrl, Task, getHeaders, makeRequest } from './request'; +import { ApiSettings } from '../types/internal'; +import { DEFAULT_API_VERSION } from '../constants'; +import { AIErrorCode } from '../types'; +import { AIError } from '../errors'; +import { getMockResponse } from '../../test-utils/mock-response'; +import { VertexAIBackend } from '../backend'; + +use(sinonChai); +use(chaiAsPromised); + +const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + appId: 'my-appid', + location: 'us-central1', + backend: new VertexAIBackend() +}; + +describe('request methods', () => { + afterEach(() => { + restore(); + }); + describe('RequestUrl', () => { + it('stream', async () => { + const url = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + true, + {} + ); + expect(url.toString()).to.include('models/model-name:generateContent'); + expect(url.toString()).to.not.include(fakeApiSettings); + expect(url.toString()).to.include('alt=sse'); + }); + it('non-stream', async () => { + const url = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + {} + ); + expect(url.toString()).to.include('models/model-name:generateContent'); + expect(url.toString()).to.not.include(fakeApiSettings); + expect(url.toString()).to.not.include('alt=sse'); + }); + it('default apiVersion', async () => { + const url = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + {} + ); + expect(url.toString()).to.include(DEFAULT_API_VERSION); + }); + it('custom baseUrl', async () => { + const url = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + { baseUrl: 'https://my.special.endpoint' } + ); + expect(url.toString()).to.include('https://my.special.endpoint'); + }); + it('non-stream - tunedModels/', async () => { + const url = new RequestUrl( + 'tunedModels/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + {} + ); + expect(url.toString()).to.include( + 'tunedModels/model-name:generateContent' + ); + expect(url.toString()).to.not.include(fakeApiSettings); + expect(url.toString()).to.not.include('alt=sse'); + }); + }); + describe('getHeaders', () => { + const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'myproject', + appId: 'my-appid', + location: 'moon', + backend: new VertexAIBackend(), + getAuthToken: () => Promise.resolve({ accessToken: 'authtoken' }), + getAppCheckToken: () => Promise.resolve({ token: 'appchecktoken' }) + }; + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + true, + {} + ); + it('adds client headers', async () => { + const headers = await getHeaders(fakeUrl); + expect(headers.get('x-goog-api-client')).to.match( + /gl-js\/[0-9\.]+ fire\/[0-9\.]+/ + ); + }); + it('adds api key', async () => { + const headers = await getHeaders(fakeUrl); + expect(headers.get('x-goog-api-key')).to.equal('key'); + }); + it('adds app id if automatedDataCollectionEnabled is true', async () => { + const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'myproject', + appId: 'my-appid', + location: 'moon', + backend: new VertexAIBackend(), + automaticDataCollectionEnabled: true, + getAuthToken: () => Promise.resolve({ accessToken: 'authtoken' }), + getAppCheckToken: () => Promise.resolve({ token: 'appchecktoken' }) + }; + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.get('X-Firebase-Appid')).to.equal('my-appid'); + }); + it('does not add app id if automatedDataCollectionEnabled is undefined', async () => { + const headers = await getHeaders(fakeUrl); + expect(headers.get('X-Firebase-Appid')).to.be.null; + }); + it('does not add app id if automatedDataCollectionEnabled is false', async () => { + const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'myproject', + appId: 'my-appid', + location: 'moon', + backend: new VertexAIBackend(), + automaticDataCollectionEnabled: false, + getAuthToken: () => Promise.resolve({ accessToken: 'authtoken' }), + getAppCheckToken: () => Promise.resolve({ token: 'appchecktoken' }) + }; + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.get('X-Firebase-Appid')).to.be.null; + }); + it('adds app check token if it exists', async () => { + const headers = await getHeaders(fakeUrl); + expect(headers.get('X-Firebase-AppCheck')).to.equal('appchecktoken'); + }); + it('ignores app check token header if no appcheck service', async () => { + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + { + apiKey: 'key', + project: 'myproject', + appId: 'my-appid', + location: 'moon', + backend: new VertexAIBackend() + }, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.has('X-Firebase-AppCheck')).to.be.false; + }); + it('ignores app check token header if returned token was undefined', async () => { + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + { + apiKey: 'key', + project: 'myproject', + location: 'moon', + //@ts-ignore + getAppCheckToken: () => Promise.resolve() + }, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.has('X-Firebase-AppCheck')).to.be.false; + }); + it('ignores app check token header if returned token had error', async () => { + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + { + apiKey: 'key', + project: 'myproject', + appId: 'my-appid', + location: 'moon', + backend: new VertexAIBackend(), + getAppCheckToken: () => + Promise.resolve({ token: 'dummytoken', error: Error('oops') }) + }, + true, + {} + ); + const warnStub = stub(console, 'warn'); + const headers = await getHeaders(fakeUrl); + expect(headers.get('X-Firebase-AppCheck')).to.equal('dummytoken'); + expect(warnStub).to.be.calledWith( + match(/vertexai/), + match(/App Check.*oops/) + ); + }); + it('adds auth token if it exists', async () => { + const headers = await getHeaders(fakeUrl); + expect(headers.get('Authorization')).to.equal('Firebase authtoken'); + }); + it('ignores auth token header if no auth service', async () => { + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + { + apiKey: 'key', + project: 'myproject', + appId: 'my-appid', + location: 'moon', + backend: new VertexAIBackend() + }, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.has('Authorization')).to.be.false; + }); + it('ignores auth token header if returned token was undefined', async () => { + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + { + apiKey: 'key', + project: 'myproject', + location: 'moon', + //@ts-ignore + getAppCheckToken: () => Promise.resolve() + }, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.has('Authorization')).to.be.false; + }); + }); + describe('makeRequest', () => { + it('no error', async () => { + const fetchStub = stub(globalThis, 'fetch').resolves({ + ok: true + } as Response); + const response = await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + expect(fetchStub).to.be.calledOnce; + expect(response.ok).to.be.true; + }); + it('error with timeout', async () => { + const fetchStub = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 500, + statusText: 'AbortError' + } as Response); + + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '', + { + timeout: 180000 + } + ); + } catch (e) { + expect((e as AIError).code).to.equal(AIErrorCode.FETCH_ERROR); + expect((e as AIError).customErrorData?.status).to.equal(500); + expect((e as AIError).customErrorData?.statusText).to.equal( + 'AbortError' + ); + expect((e as AIError).message).to.include('500 AbortError'); + } + + expect(fetchStub).to.be.calledOnce; + }); + it('Network error, no response.json()', async () => { + const fetchStub = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 500, + statusText: 'Server Error' + } as Response); + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + } catch (e) { + expect((e as AIError).code).to.equal(AIErrorCode.FETCH_ERROR); + expect((e as AIError).customErrorData?.status).to.equal(500); + expect((e as AIError).customErrorData?.statusText).to.equal( + 'Server Error' + ); + expect((e as AIError).message).to.include('500 Server Error'); + } + expect(fetchStub).to.be.calledOnce; + }); + it('Network error, includes response.json()', async () => { + const fetchStub = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 500, + statusText: 'Server Error', + json: () => Promise.resolve({ error: { message: 'extra info' } }) + } as Response); + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + } catch (e) { + expect((e as AIError).code).to.equal(AIErrorCode.FETCH_ERROR); + expect((e as AIError).customErrorData?.status).to.equal(500); + expect((e as AIError).customErrorData?.statusText).to.equal( + 'Server Error' + ); + expect((e as AIError).message).to.include('500 Server Error'); + expect((e as AIError).message).to.include('extra info'); + } + expect(fetchStub).to.be.calledOnce; + }); + it('Network error, includes response.json() and details', async () => { + const fetchStub = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 500, + statusText: 'Server Error', + json: () => + Promise.resolve({ + error: { + message: 'extra info', + details: [ + { + '@type': 'type.googleapis.com/google.rpc.DebugInfo', + detail: + '[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short' + } + ] + } + }) + } as Response); + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + } catch (e) { + expect((e as AIError).code).to.equal(AIErrorCode.FETCH_ERROR); + expect((e as AIError).customErrorData?.status).to.equal(500); + expect((e as AIError).customErrorData?.statusText).to.equal( + 'Server Error' + ); + expect((e as AIError).message).to.include('500 Server Error'); + expect((e as AIError).message).to.include('extra info'); + expect((e as AIError).message).to.include('generic::invalid_argument'); + } + expect(fetchStub).to.be.calledOnce; + }); + }); + it('Network error, API not enabled', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-firebasevertexai-api-not-enabled.json' + ); + const fetchStub = stub(globalThis, 'fetch').resolves( + mockResponse as Response + ); + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + } catch (e) { + expect((e as AIError).code).to.equal(AIErrorCode.API_NOT_ENABLED); + expect((e as AIError).message).to.include('my-project'); + expect((e as AIError).message).to.include('googleapis.com'); + } + expect(fetchStub).to.be.calledOnce; + }); +}); diff --git a/packages/ai/src/requests/request.ts b/packages/ai/src/requests/request.ts new file mode 100644 index 00000000000..90195b4b788 --- /dev/null +++ b/packages/ai/src/requests/request.ts @@ -0,0 +1,270 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ErrorDetails, RequestOptions, AIErrorCode } from '../types'; +import { AIError } from '../errors'; +import { ApiSettings } from '../types/internal'; +import { + DEFAULT_API_VERSION, + DEFAULT_DOMAIN, + DEFAULT_FETCH_TIMEOUT_MS, + LANGUAGE_TAG, + PACKAGE_VERSION +} from '../constants'; +import { logger } from '../logger'; +import { GoogleAIBackend, VertexAIBackend } from '../backend'; +import { BackendType } from '../public-types'; + +export enum Task { + GENERATE_CONTENT = 'generateContent', + STREAM_GENERATE_CONTENT = 'streamGenerateContent', + COUNT_TOKENS = 'countTokens', + PREDICT = 'predict' +} + +export class RequestUrl { + constructor( + public model: string, + public task: Task, + public apiSettings: ApiSettings, + public stream: boolean, + public requestOptions?: RequestOptions + ) {} + toString(): string { + const url = new URL(this.baseUrl); // Throws if the URL is invalid + url.pathname = `/${this.apiVersion}/${this.modelPath}:${this.task}`; + url.search = this.queryParams.toString(); + return url.toString(); + } + + private get baseUrl(): string { + return this.requestOptions?.baseUrl || `https://${DEFAULT_DOMAIN}`; + } + + private get apiVersion(): string { + return DEFAULT_API_VERSION; // TODO: allow user-set options if that feature becomes available + } + + private get modelPath(): string { + if (this.apiSettings.backend instanceof GoogleAIBackend) { + return `projects/${this.apiSettings.project}/${this.model}`; + } else if (this.apiSettings.backend instanceof VertexAIBackend) { + return `projects/${this.apiSettings.project}/locations/${this.apiSettings.backend.location}/${this.model}`; + } else { + throw new AIError( + AIErrorCode.ERROR, + `Invalid backend: ${JSON.stringify(this.apiSettings.backend)}` + ); + } + } + + private get queryParams(): URLSearchParams { + const params = new URLSearchParams(); + if (this.stream) { + params.set('alt', 'sse'); + } + + return params; + } +} + +export class WebSocketUrl { + constructor(public apiSettings: ApiSettings) {} + toString(): string { + const url = new URL(`wss://${DEFAULT_DOMAIN}`); + url.pathname = this.pathname; + + const queryParams = new URLSearchParams(); + queryParams.set('key', this.apiSettings.apiKey); + url.search = queryParams.toString(); + + return url.toString(); + } + + private get pathname(): string { + if (this.apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + return 'ws/google.firebase.vertexai.v1beta.GenerativeService/BidiGenerateContent'; + } else { + return `ws/google.firebase.vertexai.v1beta.LlmBidiService/BidiGenerateContent/locations/${this.apiSettings.location}`; + } + } +} + +/** + * Log language and "fire/version" to x-goog-api-client + */ +function getClientHeaders(): string { + const loggingTags = []; + loggingTags.push(`${LANGUAGE_TAG}/${PACKAGE_VERSION}`); + loggingTags.push(`fire/${PACKAGE_VERSION}`); + return loggingTags.join(' '); +} + +export async function getHeaders(url: RequestUrl): Promise { + const headers = new Headers(); + headers.append('Content-Type', 'application/json'); + headers.append('x-goog-api-client', getClientHeaders()); + headers.append('x-goog-api-key', url.apiSettings.apiKey); + if (url.apiSettings.automaticDataCollectionEnabled) { + headers.append('X-Firebase-Appid', url.apiSettings.appId); + } + if (url.apiSettings.getAppCheckToken) { + const appCheckToken = await url.apiSettings.getAppCheckToken(); + if (appCheckToken) { + headers.append('X-Firebase-AppCheck', appCheckToken.token); + if (appCheckToken.error) { + logger.warn( + `Unable to obtain a valid App Check token: ${appCheckToken.error.message}` + ); + } + } + } + + if (url.apiSettings.getAuthToken) { + const authToken = await url.apiSettings.getAuthToken(); + if (authToken) { + headers.append('Authorization', `Firebase ${authToken.accessToken}`); + } + } + + return headers; +} + +export async function constructRequest( + model: string, + task: Task, + apiSettings: ApiSettings, + stream: boolean, + body: string, + requestOptions?: RequestOptions +): Promise<{ url: string; fetchOptions: RequestInit }> { + const url = new RequestUrl(model, task, apiSettings, stream, requestOptions); + return { + url: url.toString(), + fetchOptions: { + method: 'POST', + headers: await getHeaders(url), + body + } + }; +} + +export async function makeRequest( + model: string, + task: Task, + apiSettings: ApiSettings, + stream: boolean, + body: string, + requestOptions?: RequestOptions +): Promise { + const url = new RequestUrl(model, task, apiSettings, stream, requestOptions); + let response; + let fetchTimeoutId: string | number | NodeJS.Timeout | undefined; + try { + const request = await constructRequest( + model, + task, + apiSettings, + stream, + body, + requestOptions + ); + // Timeout is 180s by default + const timeoutMillis = + requestOptions?.timeout != null && requestOptions.timeout >= 0 + ? requestOptions.timeout + : DEFAULT_FETCH_TIMEOUT_MS; + const abortController = new AbortController(); + fetchTimeoutId = setTimeout(() => abortController.abort(), timeoutMillis); + request.fetchOptions.signal = abortController.signal; + + response = await fetch(request.url, request.fetchOptions); + if (!response.ok) { + let message = ''; + let errorDetails; + try { + const json = await response.json(); + message = json.error.message; + if (json.error.details) { + message += ` ${JSON.stringify(json.error.details)}`; + errorDetails = json.error.details; + } + } catch (e) { + // ignored + } + if ( + response.status === 403 && + errorDetails && + errorDetails.some( + (detail: ErrorDetails) => detail.reason === 'SERVICE_DISABLED' + ) && + errorDetails.some((detail: ErrorDetails) => + ( + detail.links as Array> + )?.[0]?.description.includes( + 'Google developers console API activation' + ) + ) + ) { + throw new AIError( + AIErrorCode.API_NOT_ENABLED, + `The Firebase AI SDK requires the Firebase AI ` + + `API ('firebasevertexai.googleapis.com') to be enabled in your ` + + `Firebase project. Enable this API by visiting the Firebase Console ` + + `at https://console.firebase.google.com/project/${url.apiSettings.project}/genai/ ` + + `and clicking "Get started". If you enabled this API recently, ` + + `wait a few minutes for the action to propagate to our systems and ` + + `then retry.`, + { + status: response.status, + statusText: response.statusText, + errorDetails + } + ); + } + throw new AIError( + AIErrorCode.FETCH_ERROR, + `Error fetching from ${url}: [${response.status} ${response.statusText}] ${message}`, + { + status: response.status, + statusText: response.statusText, + errorDetails + } + ); + } + } catch (e) { + let err = e as Error; + if ( + (e as AIError).code !== AIErrorCode.FETCH_ERROR && + (e as AIError).code !== AIErrorCode.API_NOT_ENABLED && + e instanceof Error + ) { + err = new AIError( + AIErrorCode.ERROR, + `Error fetching from ${url.toString()}: ${e.message}` + ); + err.stack = e.stack; + } + + throw err; + } finally { + if (fetchTimeoutId) { + clearTimeout(fetchTimeoutId); + } + } + return response; +} diff --git a/packages/ai/src/requests/response-helpers.test.ts b/packages/ai/src/requests/response-helpers.test.ts new file mode 100644 index 00000000000..8583ca9a733 --- /dev/null +++ b/packages/ai/src/requests/response-helpers.test.ts @@ -0,0 +1,423 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + addHelpers, + formatBlockErrorMessage, + handlePredictResponse +} from './response-helpers'; +import { expect, use } from 'chai'; +import { restore } from 'sinon'; +import sinonChai from 'sinon-chai'; +import { + BlockReason, + Content, + FinishReason, + GenerateContentResponse, + ImagenGCSImage, + InlineDataPart, + ImagenInlineImage +} from '../types'; +import { getMockResponse } from '../../test-utils/mock-response'; + +use(sinonChai); + +const fakeResponseText: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [{ text: 'Some text' }, { text: ' and some more text' }] + } + } + ] +}; + +const fakeResponseThoughts: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [ + { text: 'Some text' }, + { text: 'and some thoughts', thought: true } + ] + } + } + ] +}; + +const functionCallPart1 = { + functionCall: { + name: 'find_theaters', + args: { + location: 'Mountain View, CA', + movie: 'Barbie' + } + } +}; + +const functionCallPart2 = { + functionCall: { + name: 'find_times', + args: { + location: 'Mountain View, CA', + movie: 'Barbie', + time: '20:00' + } + } +}; + +const fakeResponseFunctionCall: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [functionCallPart1] + } + } + ] +}; + +const fakeResponseFunctionCalls: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [functionCallPart1, functionCallPart2] + } + } + ] +}; + +const fakeResponseMixed1: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [{ text: 'some text' }, functionCallPart2] + } + } + ] +}; + +const fakeResponseMixed2: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [functionCallPart1, { text: 'some text' }] + } + } + ] +}; + +const fakeResponseMixed3: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [ + { text: 'some text' }, + functionCallPart1, + { text: ' and more text' } + ] + } + } + ] +}; + +const inlineDataPart1: InlineDataPart = { + inlineData: { + mimeType: 'image/png', + data: 'base64encoded...' + } +}; + +const inlineDataPart2: InlineDataPart = { + inlineData: { + mimeType: 'image/jpeg', + data: 'anotherbase64...' + } +}; + +const fakeResponseInlineData: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [inlineDataPart1, inlineDataPart2] + } + } + ] +}; + +const fakeResponseTextAndInlineData: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [{ text: 'Describe this:' }, inlineDataPart1] + } + } + ] +}; + +const badFakeResponse: GenerateContentResponse = { + promptFeedback: { + blockReason: BlockReason.SAFETY, + safetyRatings: [] + } +}; + +describe('response-helpers methods', () => { + afterEach(() => { + restore(); + }); + describe('addHelpers', () => { + it('good response text', async () => { + const enhancedResponse = addHelpers(fakeResponseText); + expect(enhancedResponse.text()).to.equal('Some text and some more text'); + expect(enhancedResponse.functionCalls()).to.be.undefined; + expect(enhancedResponse.inlineDataParts()).to.be.undefined; + expect(enhancedResponse.thoughtSummary()).to.be.undefined; + }); + it('good response functionCall', async () => { + const enhancedResponse = addHelpers(fakeResponseFunctionCall); + expect(enhancedResponse.text()).to.equal(''); + expect(enhancedResponse.functionCalls()).to.deep.equal([ + functionCallPart1.functionCall + ]); + expect(enhancedResponse.inlineDataParts()).to.be.undefined; + expect(enhancedResponse.thoughtSummary()).to.be.undefined; + }); + it('good response functionCalls', async () => { + const enhancedResponse = addHelpers(fakeResponseFunctionCalls); + expect(enhancedResponse.text()).to.equal(''); + expect(enhancedResponse.functionCalls()).to.deep.equal([ + functionCallPart1.functionCall, + functionCallPart2.functionCall + ]); + expect(enhancedResponse.inlineDataParts()).to.be.undefined; + expect(enhancedResponse.thoughtSummary()).to.be.undefined; + }); + it('good response text/functionCall', async () => { + const enhancedResponse = addHelpers(fakeResponseMixed1); + expect(enhancedResponse.functionCalls()).to.deep.equal([ + functionCallPart2.functionCall + ]); + expect(enhancedResponse.text()).to.equal('some text'); + expect(enhancedResponse.inlineDataParts()).to.be.undefined; + expect(enhancedResponse.thoughtSummary()).to.be.undefined; + }); + it('good response functionCall/text', async () => { + const enhancedResponse = addHelpers(fakeResponseMixed2); + expect(enhancedResponse.functionCalls()).to.deep.equal([ + functionCallPart1.functionCall + ]); + expect(enhancedResponse.text()).to.equal('some text'); + expect(enhancedResponse.inlineDataParts()).to.be.undefined; + expect(enhancedResponse.thoughtSummary()).to.be.undefined; + }); + it('good response text/functionCall/text', async () => { + const enhancedResponse = addHelpers(fakeResponseMixed3); + expect(enhancedResponse.functionCalls()).to.deep.equal([ + functionCallPart1.functionCall + ]); + expect(enhancedResponse.text()).to.equal('some text and more text'); + expect(enhancedResponse.thoughtSummary()).to.be.undefined; + expect(enhancedResponse.inlineDataParts()).to.be.undefined; + }); + it('bad response safety', async () => { + const enhancedResponse = addHelpers(badFakeResponse); + expect(enhancedResponse.text).to.throw('SAFETY'); + expect(enhancedResponse.thoughtSummary).to.throw('SAFETY'); + expect(enhancedResponse.functionCalls).to.throw('SAFETY'); + expect(enhancedResponse.inlineDataParts).to.throw('SAFETY'); + }); + it('good response inlineData', async () => { + const enhancedResponse = addHelpers(fakeResponseInlineData); + expect(enhancedResponse.text()).to.equal(''); + expect(enhancedResponse.thoughtSummary()).to.be.undefined; + expect(enhancedResponse.functionCalls()).to.be.undefined; + expect(enhancedResponse.inlineDataParts()).to.deep.equal([ + inlineDataPart1, + inlineDataPart2 + ]); + }); + it('good response text/inlineData', async () => { + const enhancedResponse = addHelpers(fakeResponseTextAndInlineData); + expect(enhancedResponse.text()).to.equal('Describe this:'); + expect(enhancedResponse.thoughtSummary()).to.be.undefined; + expect(enhancedResponse.functionCalls()).to.be.undefined; + expect(enhancedResponse.inlineDataParts()).to.deep.equal([ + inlineDataPart1 + ]); + }); + it('good response text/thought', async () => { + const enhancedResponse = addHelpers(fakeResponseThoughts); + expect(enhancedResponse.text()).to.equal('Some text'); + expect(enhancedResponse.thoughtSummary()).to.equal('and some thoughts'); + expect(enhancedResponse.functionCalls()).to.be.undefined; + expect(enhancedResponse.inlineDataParts()).to.be.undefined; + }); + }); + describe('getBlockString', () => { + it('has no promptFeedback or bad finishReason', async () => { + const message = formatBlockErrorMessage({ + candidates: [ + { + index: 0, + finishReason: FinishReason.STOP, + finishMessage: 'this was fine', + content: {} as Content + } + ] + }); + expect(message).to.equal(''); + }); + it('has promptFeedback and blockReason only', async () => { + const message = formatBlockErrorMessage({ + promptFeedback: { + blockReason: BlockReason.SAFETY, + safetyRatings: [] + } + }); + expect(message).to.include('Response was blocked due to SAFETY'); + }); + it('has promptFeedback with blockReason and blockMessage', async () => { + const message = formatBlockErrorMessage({ + promptFeedback: { + blockReason: BlockReason.SAFETY, + blockReasonMessage: 'safety reasons', + safetyRatings: [] + } + }); + expect(message).to.include( + 'Response was blocked due to SAFETY: safety reasons' + ); + }); + it('has bad finishReason only', async () => { + const message = formatBlockErrorMessage({ + candidates: [ + { + index: 0, + finishReason: FinishReason.SAFETY, + content: {} as Content + } + ] + }); + expect(message).to.include('Candidate was blocked due to SAFETY'); + }); + it('has finishReason and finishMessage', async () => { + const message = formatBlockErrorMessage({ + candidates: [ + { + index: 0, + finishReason: FinishReason.SAFETY, + finishMessage: 'unsafe candidate', + content: {} as Content + } + ] + }); + expect(message).to.include( + 'Candidate was blocked due to SAFETY: unsafe candidate' + ); + }); + }); + + describe('handlePredictResponse', () => { + it('returns base64 images', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-generate-images-base64.json' + ) as Response; + const res = await handlePredictResponse(mockResponse); + expect(res.filteredReason).to.be.undefined; + expect(res.images.length).to.equal(4); + res.images.forEach(image => { + expect(image.mimeType).to.equal('image/png'); + expect(image.bytesBase64Encoded.length).to.be.greaterThan(0); + }); + }); + }); + it('returns GCS images', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-success-generate-images-gcs.json' + ) as Response; + const res = await handlePredictResponse(mockResponse); + expect(res.filteredReason).to.be.undefined; + expect(res.images.length).to.equal(4); + res.images.forEach((image, i) => { + expect(image.mimeType).to.equal('image/jpeg'); + expect(image.gcsURI).to.equal( + `gs://test-project-id-1234.firebasestorage.app/images/1234567890123/sample_${i}.jpg` + ); + }); + }); + it('has filtered reason and no images if all images were filtered', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-generate-images-all-filtered.json' + ) as Response; + const res = await handlePredictResponse(mockResponse); + expect(res.filteredReason).to.equal( + "Unable to show generated images. All images were filtered out because they violated Vertex AI's usage guidelines. You will not be charged for blocked images. Try rephrasing the prompt. If you think this was an error, send feedback. Support codes: 39322892, 29310472" + ); + expect(res.images.length).to.equal(0); + }); + it('has filtered reason and no images if all base64 images were filtered', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-generate-images-base64-some-filtered.json' + ) as Response; + const res = await handlePredictResponse(mockResponse); + expect(res.filteredReason).to.equal( + 'Your current safety filter threshold filtered out 2 generated images. You will not be charged for blocked images. Try rephrasing the prompt. If you think this was an error, send feedback.' + ); + expect(res.images.length).to.equal(2); + res.images.forEach(image => { + expect(image.mimeType).to.equal('image/png'); + expect(image.bytesBase64Encoded).to.have.length.greaterThan(0); + }); + }); + it('has filtered reason and no images if all GCS images were filtered', async () => { + const mockResponse = getMockResponse( + 'vertexAI', + 'unary-failure-generate-images-gcs-some-filtered.json' + ) as Response; + const res = await handlePredictResponse(mockResponse); + expect(res.filteredReason).to.equal( + 'Your current safety filter threshold filtered out 2 generated images. You will not be charged for blocked images. Try rephrasing the prompt. If you think this was an error, send feedback.' + ); + expect(res.images.length).to.equal(2); + res.images.forEach(image => { + expect(image.mimeType).to.equal('image/jpeg'); + expect(image.gcsURI).to.have.length.greaterThan(0); + }); + }); +}); diff --git a/packages/ai/src/requests/response-helpers.ts b/packages/ai/src/requests/response-helpers.ts new file mode 100644 index 00000000000..bb3748f6bc9 --- /dev/null +++ b/packages/ai/src/requests/response-helpers.ts @@ -0,0 +1,314 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EnhancedGenerateContentResponse, + FinishReason, + FunctionCall, + GenerateContentCandidate, + GenerateContentResponse, + ImagenGCSImage, + ImagenInlineImage, + AIErrorCode, + InlineDataPart, + Part, + InferenceSource +} from '../types'; +import { AIError } from '../errors'; +import { logger } from '../logger'; +import { ImagenResponseInternal } from '../types/internal'; + +/** + * Check that at least one candidate exists and does not have a bad + * finish reason. Warns if multiple candidates exist. + */ +function hasValidCandidates(response: GenerateContentResponse): boolean { + if (response.candidates && response.candidates.length > 0) { + if (response.candidates.length > 1) { + logger.warn( + `This response had ${response.candidates.length} ` + + `candidates. Returning text from the first candidate only. ` + + `Access response.candidates directly to use the other candidates.` + ); + } + if (hadBadFinishReason(response.candidates[0])) { + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + `Response error: ${formatBlockErrorMessage( + response + )}. Response body stored in error.response`, + { + response + } + ); + } + return true; + } else { + return false; + } +} + +/** + * Creates an EnhancedGenerateContentResponse object that has helper functions and + * other modifications that improve usability. + */ +export function createEnhancedContentResponse( + response: GenerateContentResponse, + inferenceSource: InferenceSource = InferenceSource.IN_CLOUD +): EnhancedGenerateContentResponse { + /** + * The Vertex AI backend omits default values. + * This causes the `index` property to be omitted from the first candidate in the + * response, since it has index 0, and 0 is a default value. + * See: https://github.com/firebase/firebase-js-sdk/issues/8566 + */ + if (response.candidates && !response.candidates[0].hasOwnProperty('index')) { + response.candidates[0].index = 0; + } + + const responseWithHelpers = addHelpers(response); + responseWithHelpers.inferenceSource = inferenceSource; + return responseWithHelpers; +} + +/** + * Adds convenience helper methods to a response object, including stream + * chunks (as long as each chunk is a complete GenerateContentResponse JSON). + */ +export function addHelpers( + response: GenerateContentResponse +): EnhancedGenerateContentResponse { + (response as EnhancedGenerateContentResponse).text = () => { + if (hasValidCandidates(response)) { + return getText(response, part => !part.thought); + } else if (response.promptFeedback) { + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + `Text not available. ${formatBlockErrorMessage(response)}`, + { + response + } + ); + } + return ''; + }; + (response as EnhancedGenerateContentResponse).thoughtSummary = () => { + if (hasValidCandidates(response)) { + const result = getText(response, part => !!part.thought); + return result === '' ? undefined : result; + } else if (response.promptFeedback) { + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + `Thought summary not available. ${formatBlockErrorMessage(response)}`, + { + response + } + ); + } + return undefined; + }; + (response as EnhancedGenerateContentResponse).inlineDataParts = (): + | InlineDataPart[] + | undefined => { + if (hasValidCandidates(response)) { + return getInlineDataParts(response); + } else if (response.promptFeedback) { + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + `Data not available. ${formatBlockErrorMessage(response)}`, + { + response + } + ); + } + return undefined; + }; + (response as EnhancedGenerateContentResponse).functionCalls = () => { + if (hasValidCandidates(response)) { + return getFunctionCalls(response); + } else if (response.promptFeedback) { + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + `Function call not available. ${formatBlockErrorMessage(response)}`, + { + response + } + ); + } + return undefined; + }; + return response as EnhancedGenerateContentResponse; +} + +/** + * Returns all text from the first candidate's parts, filtering by whether + * `partFilter()` returns true. + * + * @param response - The `GenerateContentResponse` from which to extract text. + * @param partFilter - Only return `Part`s for which this returns true + */ +export function getText( + response: GenerateContentResponse, + partFilter: (part: Part) => boolean +): string { + const textStrings = []; + if (response.candidates?.[0].content?.parts) { + for (const part of response.candidates?.[0].content?.parts) { + if (part.text && partFilter(part)) { + textStrings.push(part.text); + } + } + } + if (textStrings.length > 0) { + return textStrings.join(''); + } else { + return ''; + } +} + +/** + * Returns every {@link FunctionCall} associated with first candidate. + */ +export function getFunctionCalls( + response: GenerateContentResponse +): FunctionCall[] | undefined { + const functionCalls: FunctionCall[] = []; + if (response.candidates?.[0].content?.parts) { + for (const part of response.candidates?.[0].content?.parts) { + if (part.functionCall) { + functionCalls.push(part.functionCall); + } + } + } + if (functionCalls.length > 0) { + return functionCalls; + } else { + return undefined; + } +} + +/** + * Returns every {@link InlineDataPart} in the first candidate if present. + * + * @internal + */ +export function getInlineDataParts( + response: GenerateContentResponse +): InlineDataPart[] | undefined { + const data: InlineDataPart[] = []; + + if (response.candidates?.[0].content?.parts) { + for (const part of response.candidates?.[0].content?.parts) { + if (part.inlineData) { + data.push(part); + } + } + } + + if (data.length > 0) { + return data; + } else { + return undefined; + } +} + +const badFinishReasons = [FinishReason.RECITATION, FinishReason.SAFETY]; + +function hadBadFinishReason(candidate: GenerateContentCandidate): boolean { + return ( + !!candidate.finishReason && + badFinishReasons.some(reason => reason === candidate.finishReason) + ); +} + +export function formatBlockErrorMessage( + response: GenerateContentResponse +): string { + let message = ''; + if ( + (!response.candidates || response.candidates.length === 0) && + response.promptFeedback + ) { + message += 'Response was blocked'; + if (response.promptFeedback?.blockReason) { + message += ` due to ${response.promptFeedback.blockReason}`; + } + if (response.promptFeedback?.blockReasonMessage) { + message += `: ${response.promptFeedback.blockReasonMessage}`; + } + } else if (response.candidates?.[0]) { + const firstCandidate = response.candidates[0]; + if (hadBadFinishReason(firstCandidate)) { + message += `Candidate was blocked due to ${firstCandidate.finishReason}`; + if (firstCandidate.finishMessage) { + message += `: ${firstCandidate.finishMessage}`; + } + } + } + return message; +} + +/** + * Convert a generic successful fetch response body to an Imagen response object + * that can be returned to the user. This converts the REST APIs response format to our + * APIs representation of a response. + * + * @internal + */ +export async function handlePredictResponse< + T extends ImagenInlineImage | ImagenGCSImage +>(response: Response): Promise<{ images: T[]; filteredReason?: string }> { + const responseJson: ImagenResponseInternal = await response.json(); + + const images: T[] = []; + let filteredReason: string | undefined = undefined; + + // The backend should always send a non-empty array of predictions if the response was successful. + if (!responseJson.predictions || responseJson.predictions?.length === 0) { + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + 'No predictions or filtered reason received from Vertex AI. Please report this issue with the full error details at https://github.com/firebase/firebase-js-sdk/issues.' + ); + } + + for (const prediction of responseJson.predictions) { + if (prediction.raiFilteredReason) { + filteredReason = prediction.raiFilteredReason; + } else if (prediction.mimeType && prediction.bytesBase64Encoded) { + images.push({ + mimeType: prediction.mimeType, + bytesBase64Encoded: prediction.bytesBase64Encoded + } as T); + } else if (prediction.mimeType && prediction.gcsUri) { + images.push({ + mimeType: prediction.mimeType, + gcsURI: prediction.gcsUri + } as T); + } else if (prediction.safetyAttributes) { + // Ignore safetyAttributes "prediction" to avoid throwing an error below. + } else { + throw new AIError( + AIErrorCode.RESPONSE_ERROR, + `Unexpected element in 'predictions' array in response: '${JSON.stringify( + prediction + )}'` + ); + } + } + + return { images, filteredReason }; +} diff --git a/packages/ai/src/requests/schema-builder.test.ts b/packages/ai/src/requests/schema-builder.test.ts new file mode 100644 index 00000000000..e4d000a4c13 --- /dev/null +++ b/packages/ai/src/requests/schema-builder.test.ts @@ -0,0 +1,647 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { expect, use } from 'chai'; +import sinonChai from 'sinon-chai'; +import { + AnyOfSchema, + NumberSchema, + Schema, + StringSchema +} from './schema-builder'; +import { AIErrorCode, SchemaType } from '../types'; + +use(sinonChai); + +describe('Schema builder', () => { + it('builds integer schema', () => { + const schema = Schema.integer(); + expect(schema.toJSON()).to.eql({ + type: 'integer', + nullable: false + }); + }); + it('builds integer schema with options and overrides', () => { + const schema = Schema.integer({ + nullable: true, + format: 'int32', + title: 'Age', + minimum: 0, + maximum: 120 + }); + expect(schema.toJSON()).to.eql({ + type: 'integer', + format: 'int32', + nullable: true, + title: 'Age', + minimum: 0, + maximum: 120 + }); + }); + it('builds number schema', () => { + const schema = Schema.number(); + expect(schema.toJSON()).to.eql({ + type: 'number', + nullable: false + }); + }); + it('builds number schema with options and unknown options', () => { + const schema = Schema.number({ + format: 'float', + futureOption: 'test', + title: 'Price', + minimum: 0.01, + maximum: 1000.99 + }); + expect(schema.toJSON()).to.eql({ + type: 'number', + format: 'float', + futureOption: 'test', + nullable: false, + title: 'Price', + minimum: 0.01, + maximum: 1000.99 + }); + }); + it('builds boolean schema', () => { + const schema = Schema.boolean({ title: 'Is Active' }); + expect(schema.toJSON()).to.eql({ + type: 'boolean', + nullable: false, + title: 'Is Active' + }); + }); + it('builds string schema', () => { + const schema = Schema.string({ description: 'hey', title: 'Greeting' }); + expect(schema.toJSON()).to.eql({ + type: 'string', + description: 'hey', + nullable: false, + title: 'Greeting' + }); + }); + it('builds enumString schema', () => { + const schema = Schema.enumString({ + example: 'east', + enum: ['east', 'west'], + title: 'Direction' + }); + expect(schema.toJSON()).to.eql({ + type: 'string', + example: 'east', + enum: ['east', 'west'], + nullable: false, + title: 'Direction' + }); + }); + describe('Schema.array', () => { + it('builds an array schema with basic items', () => { + const schema = Schema.array({ + items: Schema.string() + }); + expect(schema.toJSON()).to.eql({ + type: 'array', + nullable: false, + items: { + type: 'string', + nullable: false + } + }); + }); + + it('builds an array schema with items and minItems', () => { + const schema = Schema.array({ + items: Schema.number(), + minItems: 1 + }); + expect(schema.toJSON()).to.eql({ + type: 'array', + nullable: false, + items: { + type: 'number', + nullable: false + }, + minItems: 1 + }); + }); + + it('builds an array schema with items and maxItems', () => { + const schema = Schema.array({ + items: Schema.boolean(), + maxItems: 10 + }); + expect(schema.toJSON()).to.eql({ + type: 'array', + nullable: false, + items: { + type: 'boolean', + nullable: false + }, + maxItems: 10 + }); + }); + + it('builds an array schema with items, minItems, and maxItems', () => { + const schema = Schema.array({ + items: Schema.integer(), + minItems: 0, + maxItems: 5 + }); + expect(schema.toJSON()).to.eql({ + type: 'array', + nullable: false, + items: { + type: 'integer', + nullable: false + }, + minItems: 0, + maxItems: 5 + }); + }); + + it('builds an array schema with items, minItems, maxItems, and other options', () => { + const schema = Schema.array({ + items: Schema.string({ description: 'A list of names' }), + minItems: 1, + maxItems: 3, + nullable: true, + description: 'An array of strings' + }); + expect(schema.toJSON()).to.eql({ + type: 'array', + nullable: true, + description: 'An array of strings', + items: { + type: 'string', + description: 'A list of names', + nullable: false + }, + minItems: 1, + maxItems: 3 + }); + }); + }); + it('builds an object schema', () => { + const schema = Schema.object({ + properties: { + 'someInput': Schema.string() + }, + title: 'Input Object' + }); + expect(schema.toJSON()).to.eql({ + type: 'object', + nullable: false, + properties: { + 'someInput': { + type: 'string', + nullable: false + } + }, + required: ['someInput'], + title: 'Input Object' + }); + }); + it('builds an object schema with optional properties and propertyOrdering', () => { + const schema = Schema.object({ + properties: { + 'someInput': Schema.string(), + 'someBool': Schema.boolean(), + 'anotherInput': Schema.integer() + }, + optionalProperties: ['someBool'], + propertyOrdering: ['someInput', 'anotherInput', 'someBool'], + title: 'Ordered Object' + }); + expect(schema.toJSON()).to.eql({ + type: 'object', + nullable: false, + properties: { + 'someInput': { + type: 'string', + nullable: false + }, + 'someBool': { + type: 'boolean', + nullable: false + }, + 'anotherInput': { + type: 'integer', + nullable: false + } + }, + required: ['someInput', 'anotherInput'], + propertyOrdering: ['someInput', 'anotherInput', 'someBool'], + title: 'Ordered Object' + }); + }); + it('builds layered schema - partially filled out', () => { + const schema = Schema.array({ + items: Schema.object({ + properties: { + country: Schema.string({ + description: 'A country name', + title: 'Country Name' + }), + population: Schema.integer({ title: 'Population Count', minimum: 0 }), + coordinates: Schema.object({ + title: 'Geographical Coordinates', + properties: { + latitude: Schema.number({ format: 'float', title: 'Latitude' }), + longitude: Schema.number({ format: 'double', title: 'Longitude' }) + } + }), + hemisphere: Schema.object({ + title: 'Hemisphere Information', + properties: { + latitudinal: Schema.enumString({ + enum: ['N', 'S'], + title: 'Latitudinal Hemisphere' + }), + longitudinal: Schema.enumString({ + enum: ['E', 'W'], + title: 'Longitudinal Hemisphere' + }) + } + }), + isCapital: Schema.boolean({ title: 'Is Capital City' }) + } + }), + title: 'List of Countries' + }); + const jsonSchema = schema.toJSON(); + expect(jsonSchema.title).to.equal('List of Countries'); + expect(jsonSchema.items?.properties?.country.title).to.equal( + 'Country Name' + ); + expect(jsonSchema.items?.properties?.population.title).to.equal( + 'Population Count' + ); + expect(jsonSchema.items?.properties?.population.minimum).to.equal(0); + expect(jsonSchema.items?.properties?.coordinates.title).to.equal( + 'Geographical Coordinates' + ); + expect(jsonSchema.items?.properties?.hemisphere.title).to.equal( + 'Hemisphere Information' + ); + expect(jsonSchema.items?.properties?.isCapital.title).to.equal( + 'Is Capital City' + ); + }); + it('builds layered schema - fully filled out with new properties', () => { + const schema = Schema.array({ + title: 'Detailed Country Profiles', + items: Schema.object({ + description: 'A country profile', + nullable: false, + title: 'Country Profile', + propertyOrdering: [ + 'country', + 'population', + 'isCapital', + 'elevation', + 'coordinates', + 'hemisphere' + ], + properties: { + country: Schema.string({ + nullable: false, + description: 'Country name', + format: undefined, + title: 'Official Country Name' + }), + population: Schema.integer({ + nullable: false, + description: 'Number of people in country', + format: 'int64', + title: 'Total Population', + minimum: 1 + }), + coordinates: Schema.object({ + nullable: false, + description: 'Latitude and longitude', + title: 'Capital Coordinates', + properties: { + latitude: Schema.number({ + nullable: false, + description: 'Latitude of capital', + format: 'float', + title: 'Latitude Value', + minimum: -90, + maximum: 90 + }), + longitude: Schema.number({ + nullable: false, + description: 'Longitude of capital', + format: 'double', + title: 'Longitude Value', + minimum: -180, + maximum: 180 + }) + } + }), + hemisphere: Schema.object({ + nullable: false, + description: 'Hemisphere(s) country is in', + title: 'Geographical Hemispheres', + properties: { + latitudinal: Schema.enumString({ + enum: ['N', 'S'], + title: 'Latitudinal' + }), + longitudinal: Schema.enumString({ + enum: ['E', 'W'], + title: 'Longitudinal' + }) + } + }), + isCapital: Schema.boolean({ + nullable: false, + description: "This doesn't make a lot of sense but it's a demo", + title: 'Is it a capital?' + }), + elevation: Schema.integer({ + nullable: false, + description: 'Average elevation in meters', + format: 'int32', + title: 'Average Elevation (m)', + minimum: -500, + maximum: 9000 + }) + }, + optionalProperties: [] + }) + }); + + const jsonResult = schema.toJSON(); + expect(jsonResult.title).to.equal('Detailed Country Profiles'); + expect(jsonResult.items?.title).to.equal('Country Profile'); + expect(jsonResult.items?.propertyOrdering).to.deep.equal([ + 'country', + 'population', + 'isCapital', + 'elevation', + 'coordinates', + 'hemisphere' + ]); + expect(jsonResult.items?.properties?.population.title).to.equal( + 'Total Population' + ); + expect(jsonResult.items?.properties?.population.minimum).to.equal(1); + expect( + jsonResult.items?.properties?.coordinates.properties?.latitude.maximum + ).to.equal(90); + expect(jsonResult.items?.properties?.elevation.title).to.equal( + 'Average Elevation (m)' + ); + expect(jsonResult.items?.properties?.elevation.minimum).to.equal(-500); + expect(jsonResult.items?.properties?.elevation.maximum).to.equal(9000); + }); + it('can override "nullable" and set optional properties', () => { + const schema = Schema.object({ + properties: { + country: Schema.string(), + elevation: Schema.number(), + population: Schema.integer({ nullable: true }) + }, + optionalProperties: ['elevation'] + }); + expect(schema.toJSON()).to.eql({ + 'type': 'object', + 'nullable': false, + 'properties': { + 'country': { + 'type': 'string', + 'nullable': false + }, + 'elevation': { + 'type': 'number', + 'nullable': false + }, + 'population': { + 'type': 'integer', + 'nullable': true + } + }, + 'required': ['country', 'population'] + }); + }); + it('throws if an optionalProperties item does not exist', () => { + const schema = Schema.object({ + properties: { + country: Schema.string(), + elevation: Schema.number(), + population: Schema.integer({ nullable: true }) + }, + optionalProperties: ['cat'] + }) as any; // Cast to any to bypass TypedSchema check for testing purposes + expect(() => schema.toJSON()).to.throw( + AIError, + /Property "cat" specified in "optionalProperties" does not exist./ + ); + // Check the error code as well + expect(() => schema.toJSON()).to.throw(AIErrorCode.INVALID_SCHEMA); + }); + + describe('AnyOfSchema', () => { + it('builds an anyOf schema with basic types using Schema.anyOf()', () => { + const schema: AnyOfSchema = Schema.anyOf({ + anyOf: [Schema.string(), Schema.number()] + }); + + expect(schema).to.be.instanceOf(AnyOfSchema); + expect(schema.type).to.be.undefined; + expect(schema.nullable).to.be.false; // Default from SchemaParams + expect(schema.anyOf).to.be.an('array').with.lengthOf(2); + expect(schema.anyOf[0]).to.be.instanceOf(StringSchema); + expect(schema.anyOf[1]).to.be.instanceOf(NumberSchema); + + expect(schema.toJSON()).to.eql({ + type: undefined, + anyOf: [ + { type: 'string', nullable: false }, + { type: 'number', nullable: false } + ], + nullable: false + }); + }); + + it('builds an anyOf schema with complex types and options', () => { + const schema = Schema.anyOf({ + description: 'Can be a string or a detailed object', + nullable: true, + anyOf: [ + Schema.string({ description: 'A simple string' }), + Schema.object({ + properties: { + id: Schema.integer(), + name: Schema.string() + }, + description: 'A detailed object', + nullable: false // Explicitly set for the object schema itself + }) + ] + }); + + expect(schema.description).to.equal( + 'Can be a string or a detailed object' + ); + expect(schema.nullable).to.be.true; + expect(schema.anyOf).to.be.an('array').with.lengthOf(2); + + expect(schema.toJSON()).to.eql({ + type: undefined, + description: 'Can be a string or a detailed object', + nullable: true, + anyOf: [ + { type: 'string', description: 'A simple string', nullable: false }, + { + type: 'object', + description: 'A detailed object', + properties: { + id: { type: 'integer', nullable: false }, + name: { type: 'string', nullable: false } + }, + required: ['id', 'name'], + nullable: false + } + ] + }); + }); + + it('correctly overrides type to undefined even if type is passed in params', () => { + const schema = Schema.anyOf({ + type: SchemaType.STRING, + anyOf: [Schema.string(), Schema.number()] + }); + expect(schema.toJSON().type).to.be.undefined; + expect(schema.toJSON()).to.eql({ + type: undefined, // Explicitly undefined for anyOf + anyOf: [ + { type: 'string', nullable: false }, + { type: 'number', nullable: false } + ], + nullable: false // Default from SchemaParams + }); + }); + + it('toJSON() correctly serializes nested complex schemas within anyOf', () => { + const schema = Schema.anyOf({ + anyOf: [ + Schema.object({ + properties: { name: Schema.string() }, + optionalProperties: ['name'] + }), + Schema.array({ items: Schema.integer() }) + ] + }); + expect(schema.toJSON()).to.eql({ + type: undefined, + anyOf: [ + { + type: 'object', + properties: { name: { type: 'string', nullable: false } }, + nullable: false + }, + { + type: 'array', + items: { type: 'integer', nullable: false }, + nullable: false + } + ], + nullable: false + }); + }); + + it('throws an error if the anyOf array is empty', () => { + expect(() => Schema.anyOf({ anyOf: [] })).to.throw( + AIErrorCode.INVALID_SCHEMA + ); + }); + }); + + describe('ObjectSchema toJSON() optionalProperties edge cases', () => { + it('handles empty optionalProperties array (all properties required)', () => { + const schema = Schema.object({ + properties: { a: Schema.string(), b: Schema.integer() }, + optionalProperties: [] + }); + expect(schema.toJSON().required).to.deep.equal(['a', 'b']); + }); + + it('handles all properties being optional (empty required array)', () => { + const schema = Schema.object({ + properties: { a: Schema.string(), b: Schema.integer() }, + optionalProperties: ['a', 'b'] + }); + expect(schema.toJSON().required).to.be.undefined; // or empty array, depending on implementation + }); + it('builds schema with minimum and maximum for integer', () => { + const schema = Schema.integer({ + minimum: 5, + maximum: 10, + title: 'Rating' + }); + expect(schema.toJSON()).to.eql({ + type: 'integer', + nullable: false, + minimum: 5, + maximum: 10, + title: 'Rating' + }); + }); + + it('builds schema with minimum and maximum for number', () => { + const schema = Schema.number({ + minimum: 1.5, + maximum: 9.9, + title: 'Measurement' + }); + expect(schema.toJSON()).to.eql({ + type: 'number', + nullable: false, + minimum: 1.5, + maximum: 9.9, + title: 'Measurement' + }); + }); + + it('builds object schema with propertyOrdering', () => { + const schema = Schema.object({ + title: 'User Data', + properties: { + name: Schema.string(), + age: Schema.integer(), + email: Schema.string() + }, + propertyOrdering: ['name', 'email', 'age'] + }); + expect(schema.toJSON()).to.eql({ + type: 'object', + nullable: false, + title: 'User Data', + properties: { + name: { type: 'string', nullable: false }, + age: { type: 'integer', nullable: false }, + email: { type: 'string', nullable: false } + }, + required: ['name', 'age', 'email'], + propertyOrdering: ['name', 'email', 'age'] + }); + }); + }); +}); diff --git a/packages/ai/src/requests/schema-builder.ts b/packages/ai/src/requests/schema-builder.ts new file mode 100644 index 00000000000..c3b7d29a820 --- /dev/null +++ b/packages/ai/src/requests/schema-builder.ts @@ -0,0 +1,349 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from '../errors'; +import { AIErrorCode } from '../types'; +import { + SchemaInterface, + SchemaType, + SchemaParams, + SchemaRequest +} from '../types/schema'; + +/** + * Parent class encompassing all Schema types, with static methods that + * allow building specific Schema types. This class can be converted with + * `JSON.stringify()` into a JSON string accepted by Vertex AI REST endpoints. + * (This string conversion is automatically done when calling SDK methods.) + * @public + */ +export abstract class Schema implements SchemaInterface { + /** + * Optional. The type of the property. + * This can only be undefined when using `anyOf` schemas, which do not have an + * explicit type in the {@link https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type | OpenAPI specification}. + */ + type?: SchemaType; + /** Optional. The format of the property. + * Supported formats:
+ *
    + *
  • for NUMBER type: "float", "double"
  • + *
  • for INTEGER type: "int32", "int64"
  • + *
  • for STRING type: "email", "byte", etc
  • + *
+ */ + format?: string; + /** Optional. The description of the property. */ + description?: string; + /** Optional. The items of the property. */ + items?: SchemaInterface; + /** The minimum number of items (elements) in a schema of {@link (SchemaType:type)} `array`. */ + minItems?: number; + /** The maximum number of items (elements) in a schema of {@link (SchemaType:type)} `array`. */ + maxItems?: number; + /** Optional. Whether the property is nullable. Defaults to false. */ + nullable: boolean; + /** Optional. The example of the property. */ + example?: unknown; + /** + * Allows user to add other schema properties that have not yet + * been officially added to the SDK. + */ + [key: string]: unknown; + + constructor(schemaParams: SchemaInterface) { + // TODO(dlarocque): Enforce this with union types + if (!schemaParams.type && !schemaParams.anyOf) { + throw new AIError( + AIErrorCode.INVALID_SCHEMA, + "A schema must have either a 'type' or an 'anyOf' array of sub-schemas." + ); + } + // eslint-disable-next-line guard-for-in + for (const paramKey in schemaParams) { + this[paramKey] = schemaParams[paramKey]; + } + // Ensure these are explicitly set to avoid TS errors. + this.type = schemaParams.type; + this.format = schemaParams.hasOwnProperty('format') + ? schemaParams.format + : undefined; + this.nullable = schemaParams.hasOwnProperty('nullable') + ? !!schemaParams.nullable + : false; + } + + /** + * Defines how this Schema should be serialized as JSON. + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior + * @internal + */ + toJSON(): SchemaRequest { + const obj: { type?: SchemaType; [key: string]: unknown } = { + type: this.type + }; + for (const prop in this) { + if (this.hasOwnProperty(prop) && this[prop] !== undefined) { + if (prop !== 'required' || this.type === SchemaType.OBJECT) { + obj[prop] = this[prop]; + } + } + } + return obj as SchemaRequest; + } + + static array(arrayParams: SchemaParams & { items: Schema }): ArraySchema { + return new ArraySchema(arrayParams, arrayParams.items); + } + + static object( + objectParams: SchemaParams & { + properties: { + [k: string]: Schema; + }; + optionalProperties?: string[]; + } + ): ObjectSchema { + return new ObjectSchema( + objectParams, + objectParams.properties, + objectParams.optionalProperties + ); + } + + // eslint-disable-next-line id-blacklist + static string(stringParams?: SchemaParams): StringSchema { + return new StringSchema(stringParams); + } + + static enumString( + stringParams: SchemaParams & { enum: string[] } + ): StringSchema { + return new StringSchema(stringParams, stringParams.enum); + } + + static integer(integerParams?: SchemaParams): IntegerSchema { + return new IntegerSchema(integerParams); + } + + // eslint-disable-next-line id-blacklist + static number(numberParams?: SchemaParams): NumberSchema { + return new NumberSchema(numberParams); + } + + // eslint-disable-next-line id-blacklist + static boolean(booleanParams?: SchemaParams): BooleanSchema { + return new BooleanSchema(booleanParams); + } + + static anyOf( + anyOfParams: SchemaParams & { anyOf: TypedSchema[] } + ): AnyOfSchema { + return new AnyOfSchema(anyOfParams); + } +} + +/** + * A type that includes all specific Schema types. + * @public + */ +export type TypedSchema = + | IntegerSchema + | NumberSchema + | StringSchema + | BooleanSchema + | ObjectSchema + | ArraySchema + | AnyOfSchema; + +/** + * Schema class for "integer" types. + * @public + */ +export class IntegerSchema extends Schema { + constructor(schemaParams?: SchemaParams) { + super({ + type: SchemaType.INTEGER, + ...schemaParams + }); + } +} + +/** + * Schema class for "number" types. + * @public + */ +export class NumberSchema extends Schema { + constructor(schemaParams?: SchemaParams) { + super({ + type: SchemaType.NUMBER, + ...schemaParams + }); + } +} + +/** + * Schema class for "boolean" types. + * @public + */ +export class BooleanSchema extends Schema { + constructor(schemaParams?: SchemaParams) { + super({ + type: SchemaType.BOOLEAN, + ...schemaParams + }); + } +} + +/** + * Schema class for "string" types. Can be used with or without + * enum values. + * @public + */ +export class StringSchema extends Schema { + enum?: string[]; + constructor(schemaParams?: SchemaParams, enumValues?: string[]) { + super({ + type: SchemaType.STRING, + ...schemaParams + }); + this.enum = enumValues; + } + + /** + * @internal + */ + toJSON(): SchemaRequest { + const obj = super.toJSON(); + if (this.enum) { + obj['enum'] = this.enum; + } + return obj as SchemaRequest; + } +} + +/** + * Schema class for "array" types. + * The `items` param should refer to the type of item that can be a member + * of the array. + * @public + */ +export class ArraySchema extends Schema { + constructor(schemaParams: SchemaParams, public items: TypedSchema) { + super({ + type: SchemaType.ARRAY, + ...schemaParams + }); + } + + /** + * @internal + */ + toJSON(): SchemaRequest { + const obj = super.toJSON(); + obj.items = this.items.toJSON(); + return obj; + } +} + +/** + * Schema class for "object" types. + * The `properties` param must be a map of `Schema` objects. + * @public + */ +export class ObjectSchema extends Schema { + constructor( + schemaParams: SchemaParams, + public properties: { + [k: string]: TypedSchema; + }, + public optionalProperties: string[] = [] + ) { + super({ + type: SchemaType.OBJECT, + ...schemaParams + }); + } + + /** + * @internal + */ + toJSON(): SchemaRequest { + const obj = super.toJSON(); + obj.properties = { ...this.properties }; + const required = []; + if (this.optionalProperties) { + for (const propertyKey of this.optionalProperties) { + if (!this.properties.hasOwnProperty(propertyKey)) { + throw new AIError( + AIErrorCode.INVALID_SCHEMA, + `Property "${propertyKey}" specified in "optionalProperties" does not exist.` + ); + } + } + } + for (const propertyKey in this.properties) { + if (this.properties.hasOwnProperty(propertyKey)) { + obj.properties[propertyKey] = this.properties[ + propertyKey + ].toJSON() as SchemaRequest; + if (!this.optionalProperties.includes(propertyKey)) { + required.push(propertyKey); + } + } + } + if (required.length > 0) { + obj.required = required; + } + delete obj.optionalProperties; + return obj as SchemaRequest; + } +} + +/** + * Schema class representing a value that can conform to any of the provided sub-schemas. This is + * useful when a field can accept multiple distinct types or structures. + * @public + */ +export class AnyOfSchema extends Schema { + anyOf: TypedSchema[]; // Re-define field to narrow to required type + constructor(schemaParams: SchemaParams & { anyOf: TypedSchema[] }) { + if (schemaParams.anyOf.length === 0) { + throw new AIError( + AIErrorCode.INVALID_SCHEMA, + "The 'anyOf' array must not be empty." + ); + } + super({ + ...schemaParams, + type: undefined // anyOf schemas do not have an explicit type + }); + this.anyOf = schemaParams.anyOf; + } + + /** + * @internal + */ + toJSON(): SchemaRequest { + const obj = super.toJSON(); + // Ensure the 'anyOf' property contains serialized SchemaRequest objects. + if (this.anyOf && Array.isArray(this.anyOf)) { + obj.anyOf = (this.anyOf as TypedSchema[]).map(s => s.toJSON()); + } + return obj; + } +} diff --git a/packages/ai/src/requests/stream-reader.test.ts b/packages/ai/src/requests/stream-reader.test.ts new file mode 100644 index 00000000000..ca3c2cdcfe2 --- /dev/null +++ b/packages/ai/src/requests/stream-reader.test.ts @@ -0,0 +1,532 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + aggregateResponses, + getResponseStream, + processStream +} from './stream-reader'; +import { expect, use } from 'chai'; +import { restore } from 'sinon'; +import sinonChai from 'sinon-chai'; +import { + getChunkedStream, + getMockResponseStreaming +} from '../../test-utils/mock-response'; +import { + BlockReason, + FinishReason, + GenerateContentResponse, + HarmCategory, + HarmProbability, + SafetyRating, + AIErrorCode, + InferenceSource +} from '../types'; +import { AIError } from '../errors'; +import { ApiSettings } from '../types/internal'; +import { VertexAIBackend } from '../backend'; + +const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + appId: 'my-appid', + location: 'us-central1', + backend: new VertexAIBackend() +}; + +use(sinonChai); + +describe('getResponseStream', () => { + afterEach(() => { + restore(); + }); + it('two lines', async () => { + const src = [{ text: 'A' }, { text: 'B' }]; + const inputStream = getChunkedStream( + src + .map(v => JSON.stringify(v)) + .map(v => 'data: ' + v + '\r\n\r\n') + .join('') + // @ts-ignore + ).pipeThrough(new TextDecoderStream('utf8', { fatal: true })); + const responseStream = getResponseStream<{ text: string }>(inputStream); + const reader = responseStream.getReader(); + const responses: Array<{ text: string }> = []; + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + responses.push(value); + } + expect(responses).to.deep.equal(src); + }); +}); + +describe('processStream', () => { + afterEach(() => { + restore(); + }); + it('streaming response - short', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-basic-reply-short.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + for await (const response of result.stream) { + expect(response.text()).to.not.be.empty; + expect(response.inferenceSource).to.equal(InferenceSource.IN_CLOUD); + } + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.include('Cheyenne'); + expect(aggregatedResponse.inferenceSource).to.equal( + InferenceSource.IN_CLOUD + ); + }); + it('streaming response - short - on-device', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-basic-reply-short.txt' + ); + const result = processStream( + fakeResponse as Response, + fakeApiSettings, + InferenceSource.ON_DEVICE + ); + for await (const response of result.stream) { + expect(response.text()).to.not.be.empty; + expect(response.inferenceSource).to.equal(InferenceSource.ON_DEVICE); + } + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.include('Cheyenne'); + expect(aggregatedResponse.inferenceSource).to.equal( + InferenceSource.ON_DEVICE + ); + }); + it('streaming response - long', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-basic-reply-long.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + for await (const response of result.stream) { + expect(response.text()).to.not.be.empty; + } + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.include('Okay'); + expect(aggregatedResponse.text()).to.include('brewing delicious coffee'); + }); + it('streaming response - long - big chunk', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-basic-reply-long.txt', + 1e6 + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + for await (const response of result.stream) { + expect(response.text()).to.not.be.empty; + } + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.include('Okay'); + expect(aggregatedResponse.text()).to.include('brewing delicious coffee'); + }); + it('streaming response - utf8', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-utf8.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + for await (const response of result.stream) { + expect(response.text()).to.not.be.empty; + } + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.include('秋风瑟瑟,叶落纷纷'); + expect(aggregatedResponse.text()).to.include('家人围坐在一起'); + }); + it('streaming response - functioncall', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-function-call-short.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + for await (const response of result.stream) { + expect(response.text()).to.be.empty; + expect(response.functionCalls()).to.be.deep.equal([ + { + name: 'getTemperature', + args: { city: 'San Jose' } + } + ]); + } + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.be.empty; + expect(aggregatedResponse.functionCalls()).to.be.deep.equal([ + { + name: 'getTemperature', + args: { city: 'San Jose' } + } + ]); + }); + it('candidate had finishReason', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-failure-finish-reason-safety.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + const aggregatedResponse = await result.response; + expect(aggregatedResponse.candidates?.[0].finishReason).to.equal('SAFETY'); + expect(aggregatedResponse.text).to.throw('SAFETY'); + for await (const response of result.stream) { + expect(response.text).to.throw('SAFETY'); + } + }); + it('prompt was blocked', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-failure-prompt-blocked-safety.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text).to.throw('SAFETY'); + expect(aggregatedResponse.promptFeedback?.blockReason).to.equal('SAFETY'); + for await (const response of result.stream) { + expect(response.text).to.throw('SAFETY'); + } + }); + it('empty content', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-failure-empty-content.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.equal(''); + for await (const response of result.stream) { + expect(response.text()).to.equal(''); + } + }); + it('handles empty parts', async () => { + const fakeResponse = getMockResponseStreaming( + 'googleAI', + 'streaming-success-empty-parts.txt' + ); + + const result = processStream(fakeResponse as Response, fakeApiSettings); + for await (const response of result.stream) { + expect(response.candidates?.[0].content.parts.length).to.be.at.least(1); + } + + const aggregatedResponse = await result.response; + expect(aggregatedResponse.candidates?.[0].content.parts.length).to.equal(6); + }); + it('unknown enum - should ignore', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-unknown-safety-enum.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.include('Cats'); + for await (const response of result.stream) { + expect(response.text()).to.not.be.empty; + } + }); + it('recitation ending with a missing content field', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-failure-recitation-no-content.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text).to.throw('RECITATION'); + expect(aggregatedResponse.candidates?.[0].content.parts[0].text).to.include( + 'Copyrighted text goes here' + ); + for await (const response of result.stream) { + if (response.candidates?.[0].finishReason !== FinishReason.RECITATION) { + expect(response.text()).to.not.be.empty; + } else { + expect(response.text).to.throw('RECITATION'); + } + } + }); + it('handles citations', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-citations.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.include('Quantum mechanics is'); + expect( + aggregatedResponse.candidates?.[0].citationMetadata?.citations.length + ).to.equal(3); + let foundCitationMetadata = false; + for await (const response of result.stream) { + expect(response.text()).to.not.be.empty; + if (response.candidates?.[0].citationMetadata) { + foundCitationMetadata = true; + } + } + expect(foundCitationMetadata).to.be.true; + }); + it('removes empty text parts', async () => { + const fakeResponse = getMockResponseStreaming( + 'vertexAI', + 'streaming-success-empty-text-part.txt' + ); + const result = processStream(fakeResponse as Response, fakeApiSettings); + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).to.equal('1'); + expect(aggregatedResponse.candidates?.length).to.equal(1); + expect(aggregatedResponse.candidates?.[0].content.parts.length).to.equal(1); + + // The chunk with the empty text part will still go through the stream + let numChunks = 0; + for await (const _ of result.stream) { + numChunks++; + } + expect(numChunks).to.equal(2); + }); +}); + +describe('aggregateResponses', () => { + it('handles no candidates, and promptFeedback', () => { + const responsesToAggregate: GenerateContentResponse[] = [ + { + promptFeedback: { + blockReason: BlockReason.SAFETY, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + probability: HarmProbability.LOW + } as SafetyRating + ] + } + } + ]; + const response = aggregateResponses(responsesToAggregate); + expect(response.candidates).to.not.exist; + expect(response.promptFeedback?.blockReason).to.equal(BlockReason.SAFETY); + }); + describe('multiple responses, has candidates', () => { + let response: GenerateContentResponse; + before(() => { + const responsesToAggregate: GenerateContentResponse[] = [ + { + candidates: [ + { + index: 0, + content: { + role: 'user', + parts: [{ text: 'hello.' }] + }, + finishReason: FinishReason.STOP, + finishMessage: 'something', + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + probability: HarmProbability.NEGLIGIBLE + } as SafetyRating + ] + } + ], + promptFeedback: { + blockReason: BlockReason.SAFETY, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + probability: HarmProbability.LOW + } as SafetyRating + ] + } + }, + { + candidates: [ + { + index: 0, + content: { + role: 'user', + parts: [{ text: 'angry stuff' }] + }, + finishReason: FinishReason.STOP, + finishMessage: 'something', + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + probability: HarmProbability.NEGLIGIBLE + } as SafetyRating + ], + citationMetadata: { + citations: [ + { + startIndex: 0, + endIndex: 20, + uri: 'sourceurl', + license: '' + } + ] + } + } + ], + promptFeedback: { + blockReason: BlockReason.OTHER, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + probability: HarmProbability.HIGH + } as SafetyRating + ] + } + }, + { + candidates: [ + { + index: 0, + content: { + role: 'user', + parts: [{ text: '...more stuff' }] + }, + finishReason: FinishReason.MAX_TOKENS, + finishMessage: 'too many tokens', + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + probability: HarmProbability.MEDIUM + } as SafetyRating + ], + citationMetadata: { + citations: [ + { + startIndex: 0, + endIndex: 20, + uri: 'sourceurl', + license: '' + }, + { + startIndex: 150, + endIndex: 155, + uri: 'sourceurl', + license: '' + } + ] + } + } + ], + promptFeedback: { + blockReason: BlockReason.OTHER, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + probability: HarmProbability.HIGH + } as SafetyRating + ] + } + } + ]; + response = aggregateResponses(responsesToAggregate); + }); + + it('aggregates text across responses', () => { + expect(response.candidates?.length).to.equal(1); + expect( + response.candidates?.[0].content.parts.map(({ text }) => text) + ).to.deep.equal(['hello.', 'angry stuff', '...more stuff']); + }); + + it("takes the last response's promptFeedback", () => { + expect(response.promptFeedback?.blockReason).to.equal(BlockReason.OTHER); + }); + + it("takes the last response's finishReason", () => { + expect(response.candidates?.[0].finishReason).to.equal( + FinishReason.MAX_TOKENS + ); + }); + + it("takes the last response's finishMessage", () => { + expect(response.candidates?.[0].finishMessage).to.equal( + 'too many tokens' + ); + }); + + it("takes the last response's candidate safetyRatings", () => { + expect(response.candidates?.[0].safetyRatings?.[0].category).to.equal( + HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT + ); + expect(response.candidates?.[0].safetyRatings?.[0].probability).to.equal( + HarmProbability.MEDIUM + ); + }); + + it('collects all citations into one array', () => { + expect( + response.candidates?.[0].citationMetadata?.citations.length + ).to.equal(2); + expect( + response.candidates?.[0].citationMetadata?.citations[0].startIndex + ).to.equal(0); + expect( + response.candidates?.[0].citationMetadata?.citations[1].startIndex + ).to.equal(150); + }); + }); + + it('throws if a part has no properties', () => { + const responsesToAggregate: GenerateContentResponse[] = [ + { + candidates: [ + { + index: 0, + content: { + role: 'user', + parts: [{} as any] // Empty + }, + finishReason: FinishReason.STOP, + finishMessage: 'something', + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + probability: HarmProbability.NEGLIGIBLE + } as SafetyRating + ] + } + ], + promptFeedback: { + blockReason: BlockReason.SAFETY, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + probability: HarmProbability.LOW + } as SafetyRating + ] + } + } + ]; + + try { + aggregateResponses(responsesToAggregate); + } catch (e) { + expect((e as AIError).code).includes(AIErrorCode.INVALID_CONTENT); + expect((e as AIError).message).to.include( + 'Part should have at least one property, but there are none. This is likely caused ' + + 'by a malformed response from the backend.' + ); + } + }); +}); diff --git a/packages/ai/src/requests/stream-reader.ts b/packages/ai/src/requests/stream-reader.ts new file mode 100644 index 00000000000..b4968969be7 --- /dev/null +++ b/packages/ai/src/requests/stream-reader.ts @@ -0,0 +1,266 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EnhancedGenerateContentResponse, + GenerateContentCandidate, + GenerateContentResponse, + GenerateContentStreamResult, + Part, + AIErrorCode +} from '../types'; +import { AIError } from '../errors'; +import { createEnhancedContentResponse } from './response-helpers'; +import * as GoogleAIMapper from '../googleai-mappers'; +import { GoogleAIGenerateContentResponse } from '../types/googleai'; +import { ApiSettings } from '../types/internal'; +import { + BackendType, + InferenceSource, + URLContextMetadata +} from '../public-types'; + +const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/; + +/** + * Process a response.body stream from the backend and return an + * iterator that provides one complete GenerateContentResponse at a time + * and a promise that resolves with a single aggregated + * GenerateContentResponse. + * + * @param response - Response from a fetch call + */ +export function processStream( + response: Response, + apiSettings: ApiSettings, + inferenceSource?: InferenceSource +): GenerateContentStreamResult { + const inputStream = response.body!.pipeThrough( + new TextDecoderStream('utf8', { fatal: true }) + ); + const responseStream = + getResponseStream(inputStream); + const [stream1, stream2] = responseStream.tee(); + return { + stream: generateResponseSequence(stream1, apiSettings, inferenceSource), + response: getResponsePromise(stream2, apiSettings, inferenceSource) + }; +} + +async function getResponsePromise( + stream: ReadableStream, + apiSettings: ApiSettings, + inferenceSource?: InferenceSource +): Promise { + const allResponses: GenerateContentResponse[] = []; + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + let generateContentResponse = aggregateResponses(allResponses); + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + generateContentResponse = GoogleAIMapper.mapGenerateContentResponse( + generateContentResponse as GoogleAIGenerateContentResponse + ); + } + return createEnhancedContentResponse( + generateContentResponse, + inferenceSource + ); + } + + allResponses.push(value); + } +} + +async function* generateResponseSequence( + stream: ReadableStream, + apiSettings: ApiSettings, + inferenceSource?: InferenceSource +): AsyncGenerator { + const reader = stream.getReader(); + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + + let enhancedResponse: EnhancedGenerateContentResponse; + if (apiSettings.backend.backendType === BackendType.GOOGLE_AI) { + enhancedResponse = createEnhancedContentResponse( + GoogleAIMapper.mapGenerateContentResponse( + value as GoogleAIGenerateContentResponse + ), + inferenceSource + ); + } else { + enhancedResponse = createEnhancedContentResponse(value, inferenceSource); + } + + const firstCandidate = enhancedResponse.candidates?.[0]; + // Don't yield a response with no useful data for the developer. + if ( + !firstCandidate?.content?.parts && + !firstCandidate?.finishReason && + !firstCandidate?.citationMetadata && + !firstCandidate?.urlContextMetadata + ) { + continue; + } + + yield enhancedResponse; + } +} + +/** + * Reads a raw stream from the fetch response and join incomplete + * chunks, returning a new stream that provides a single complete + * GenerateContentResponse in each iteration. + */ +export function getResponseStream( + inputStream: ReadableStream +): ReadableStream { + const reader = inputStream.getReader(); + const stream = new ReadableStream({ + start(controller) { + let currentText = ''; + return pump(); + function pump(): Promise<(() => Promise) | undefined> { + return reader.read().then(({ value, done }) => { + if (done) { + if (currentText.trim()) { + controller.error( + new AIError(AIErrorCode.PARSE_FAILED, 'Failed to parse stream') + ); + return; + } + controller.close(); + return; + } + + currentText += value; + let match = currentText.match(responseLineRE); + let parsedResponse: T; + while (match) { + try { + parsedResponse = JSON.parse(match[1]); + } catch (e) { + controller.error( + new AIError( + AIErrorCode.PARSE_FAILED, + `Error parsing JSON response: "${match[1]}` + ) + ); + return; + } + controller.enqueue(parsedResponse); + currentText = currentText.substring(match[0].length); + match = currentText.match(responseLineRE); + } + return pump(); + }); + } + } + }); + return stream; +} + +/** + * Aggregates an array of `GenerateContentResponse`s into a single + * GenerateContentResponse. + */ +export function aggregateResponses( + responses: GenerateContentResponse[] +): GenerateContentResponse { + const lastResponse = responses[responses.length - 1]; + const aggregatedResponse: GenerateContentResponse = { + promptFeedback: lastResponse?.promptFeedback + }; + for (const response of responses) { + if (response.candidates) { + for (const candidate of response.candidates) { + // Index will be undefined if it's the first index (0), so we should use 0 if it's undefined. + // See: https://github.com/firebase/firebase-js-sdk/issues/8566 + const i = candidate.index || 0; + if (!aggregatedResponse.candidates) { + aggregatedResponse.candidates = []; + } + if (!aggregatedResponse.candidates[i]) { + aggregatedResponse.candidates[i] = { + index: candidate.index + } as GenerateContentCandidate; + } + // Keep overwriting, the last one will be final + aggregatedResponse.candidates[i].citationMetadata = + candidate.citationMetadata; + aggregatedResponse.candidates[i].finishReason = candidate.finishReason; + aggregatedResponse.candidates[i].finishMessage = + candidate.finishMessage; + aggregatedResponse.candidates[i].safetyRatings = + candidate.safetyRatings; + aggregatedResponse.candidates[i].groundingMetadata = + candidate.groundingMetadata; + + // The urlContextMetadata object is defined in the first chunk of the response stream. + // In all subsequent chunks, the urlContextMetadata object will be undefined. We need to + // make sure that we don't overwrite the first value urlContextMetadata object with undefined. + // FIXME: What happens if we receive a second, valid urlContextMetadata object? + const urlContextMetadata = candidate.urlContextMetadata as unknown; + if ( + typeof urlContextMetadata === 'object' && + urlContextMetadata !== null && + Object.keys(urlContextMetadata).length > 0 + ) { + aggregatedResponse.candidates[i].urlContextMetadata = + urlContextMetadata as URLContextMetadata; + } + + /** + * Candidates should always have content and parts, but this handles + * possible malformed responses. + */ + if (candidate.content) { + // Skip a candidate without parts. + if (!candidate.content.parts) { + continue; + } + if (!aggregatedResponse.candidates[i].content) { + aggregatedResponse.candidates[i].content = { + role: candidate.content.role || 'user', + parts: [] + }; + } + for (const part of candidate.content.parts) { + const newPart: Part = { ...part }; + // The backend can send empty text parts. If these are sent back + // (e.g. in chat history), the backend will respond with an error. + // To prevent this, ignore empty text parts. + if (part.text === '') { + continue; + } + if (Object.keys(newPart).length > 0) { + aggregatedResponse.candidates[i].content.parts.push( + newPart as Part + ); + } + } + } + } + } + } + return aggregatedResponse; +} diff --git a/packages/ai/src/service.test.ts b/packages/ai/src/service.test.ts new file mode 100644 index 00000000000..ba4c736e810 --- /dev/null +++ b/packages/ai/src/service.test.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { VertexAIBackend } from './backend'; +import { DEFAULT_LOCATION } from './constants'; +import { AIService } from './service'; +import { expect } from 'chai'; + +const fakeApp = { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project' + } +}; + +describe('AIService', () => { + // TODO (dlarocque): move some of these tests to helpers.test.ts + it('uses default location if not specified', () => { + const ai = new AIService(fakeApp, new VertexAIBackend()); + expect(ai.location).to.equal(DEFAULT_LOCATION); + }); + it('uses custom location if specified', () => { + const ai = new AIService( + fakeApp, + new VertexAIBackend('somewhere'), + /* authProvider */ undefined, + /* appCheckProvider */ undefined + ); + expect(ai.location).to.equal('somewhere'); + }); +}); diff --git a/packages/ai/src/service.ts b/packages/ai/src/service.ts new file mode 100644 index 00000000000..a862f19422e --- /dev/null +++ b/packages/ai/src/service.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseApp, _FirebaseService } from '@firebase/app'; +import { + AI, + AIOptions, + ChromeAdapter, + InferenceMode, + OnDeviceParams +} from './public-types'; +import { + AppCheckInternalComponentName, + FirebaseAppCheckInternal +} from '@firebase/app-check-interop-types'; +import { Provider } from '@firebase/component'; +import { + FirebaseAuthInternal, + FirebaseAuthInternalName +} from '@firebase/auth-interop-types'; +import { Backend, VertexAIBackend } from './backend'; + +export class AIService implements AI, _FirebaseService { + auth: FirebaseAuthInternal | null; + appCheck: FirebaseAppCheckInternal | null; + _options?: Omit; + location: string; // This is here for backwards-compatibility + + constructor( + public app: FirebaseApp, + public backend: Backend, + authProvider?: Provider, + appCheckProvider?: Provider, + public chromeAdapterFactory?: ( + mode: InferenceMode, + window?: Window, + params?: OnDeviceParams + ) => ChromeAdapter | undefined + ) { + const appCheck = appCheckProvider?.getImmediate({ optional: true }); + const auth = authProvider?.getImmediate({ optional: true }); + this.auth = auth || null; + this.appCheck = appCheck || null; + + if (backend instanceof VertexAIBackend) { + this.location = backend.location; + } else { + this.location = ''; + } + } + + _delete(): Promise { + return Promise.resolve(); + } + + set options(optionsToSet: AIOptions) { + this._options = optionsToSet; + } + + get options(): AIOptions | undefined { + return this._options; + } +} diff --git a/packages/ai/src/types/chrome-adapter.ts b/packages/ai/src/types/chrome-adapter.ts new file mode 100644 index 00000000000..a026b8a1a40 --- /dev/null +++ b/packages/ai/src/types/chrome-adapter.ts @@ -0,0 +1,66 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InferenceMode } from './enums'; +import { CountTokensRequest, GenerateContentRequest } from './requests'; + +/** + * Defines an inference "backend" that uses Chrome's on-device model, + * and encapsulates logic for detecting when on-device inference is + * possible. + * + * These methods should not be called directly by the user. + * + * @beta + */ +export interface ChromeAdapter { + /** + * @internal + */ + mode: InferenceMode; + /** + * Checks if the on-device model is capable of handling a given + * request. + * @param request - A potential request to be passed to the model. + */ + isAvailable(request: GenerateContentRequest): Promise; + + /** + * Generates content using on-device inference. + * + * @remarks + * This is comparable to {@link GenerativeModel.generateContent} for generating + * content using in-cloud inference. + * @param request - a standard Firebase AI {@link GenerateContentRequest} + */ + generateContent(request: GenerateContentRequest): Promise; + + /** + * Generates a content stream using on-device inference. + * + * @remarks + * This is comparable to {@link GenerativeModel.generateContentStream} for generating + * a content stream using in-cloud inference. + * @param request - a standard Firebase AI {@link GenerateContentRequest} + */ + generateContentStream(request: GenerateContentRequest): Promise; + + /** + * @internal + */ + countTokens(request: CountTokensRequest): Promise; +} diff --git a/packages/ai/src/types/content.ts b/packages/ai/src/types/content.ts new file mode 100644 index 00000000000..401a8cfb1a8 --- /dev/null +++ b/packages/ai/src/types/content.ts @@ -0,0 +1,289 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Language, Outcome, Role } from './enums'; + +/** + * Content type for both prompts and response candidates. + * @public + */ +export interface Content { + role: Role; + parts: Part[]; +} + +/** + * Content part - includes text, image/video, or function call/response + * part types. + * @public + */ +export type Part = + | TextPart + | InlineDataPart + | FunctionCallPart + | FunctionResponsePart + | FileDataPart + | ExecutableCodePart + | CodeExecutionResultPart; + +/** + * Content part interface if the part represents a text string. + * @public + */ +export interface TextPart { + text: string; + inlineData?: never; + functionCall?: never; + functionResponse?: never; + thought?: boolean; + /** + * @internal + */ + thoughtSignature?: string; + executableCode?: never; + codeExecutionResult?: never; +} + +/** + * Content part interface if the part represents an image. + * @public + */ +export interface InlineDataPart { + text?: never; + inlineData: GenerativeContentBlob; + functionCall?: never; + functionResponse?: never; + /** + * Applicable if `inlineData` is a video. + */ + videoMetadata?: VideoMetadata; + thought?: boolean; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: never; + codeExecutionResult?: never; +} + +/** + * Describes the input video content. + * @public + */ +export interface VideoMetadata { + /** + * The start offset of the video in + * protobuf {@link https://cloud.google.com/ruby/docs/reference/google-cloud-workflows-v1/latest/Google-Protobuf-Duration#json-mapping | Duration} format. + */ + startOffset: string; + /** + * The end offset of the video in + * protobuf {@link https://cloud.google.com/ruby/docs/reference/google-cloud-workflows-v1/latest/Google-Protobuf-Duration#json-mapping | Duration} format. + */ + endOffset: string; +} + +/** + * Content part interface if the part represents a {@link FunctionCall}. + * @public + */ +export interface FunctionCallPart { + text?: never; + inlineData?: never; + functionCall: FunctionCall; + functionResponse?: never; + thought?: boolean; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: never; + codeExecutionResult?: never; +} + +/** + * Content part interface if the part represents {@link FunctionResponse}. + * @public + */ +export interface FunctionResponsePart { + text?: never; + inlineData?: never; + functionCall?: never; + functionResponse: FunctionResponse; + thought?: boolean; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: never; + codeExecutionResult?: never; +} + +/** + * Content part interface if the part represents {@link FileData} + * @public + */ +export interface FileDataPart { + text?: never; + inlineData?: never; + functionCall?: never; + functionResponse?: never; + fileData: FileData; + thought?: boolean; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: never; + codeExecutionResult?: never; +} + +/** + * Represents the code that is executed by the model. + * + * @beta + */ +export interface ExecutableCodePart { + text?: never; + inlineData?: never; + functionCall?: never; + functionResponse?: never; + fileData: never; + thought?: never; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: ExecutableCode; + codeExecutionResult?: never; +} + +/** + * Represents the code execution result from the model. + * + * @beta + */ +export interface CodeExecutionResultPart { + text?: never; + inlineData?: never; + functionCall?: never; + functionResponse?: never; + fileData: never; + thought?: never; + /** + * @internal + */ + thoughtSignature?: never; + executableCode?: never; + codeExecutionResult?: CodeExecutionResult; +} + +/** + * An interface for executable code returned by the model. + * + * @beta + */ +export interface ExecutableCode { + /** + * The programming language of the code. + */ + language?: Language; + /** + * The source code to be executed. + */ + code?: string; +} + +/** + * The results of code execution run by the model. + * + * @beta + */ +export interface CodeExecutionResult { + /** + * The result of the code execution. + */ + outcome?: Outcome; + /** + * The output from the code execution, or an error message + * if it failed. + */ + output?: string; +} + +/** + * A predicted {@link FunctionCall} returned from the model + * that contains a string representing the {@link FunctionDeclaration.name} + * and a structured JSON object containing the parameters and their values. + * @public + */ +export interface FunctionCall { + /** + * The id of the function call. This must be sent back in the associated {@link FunctionResponse}. + * + * + * @remarks This property is only supported in the Gemini Developer API ({@link GoogleAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property will be + * `undefined`. + */ + id?: string; + name: string; + args: object; +} + +/** + * The result output from a {@link FunctionCall} that contains a string + * representing the {@link FunctionDeclaration.name} + * and a structured JSON object containing any output + * from the function is used as context to the model. + * This should contain the result of a {@link FunctionCall} + * made based on model prediction. + * @public + */ +export interface FunctionResponse { + /** + * The id of the {@link FunctionCall}. + * + * @remarks This property is only supported in the Gemini Developer API ({@link GoogleAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property will be + * `undefined`. + */ + id?: string; + name: string; + response: object; +} + +/** + * Interface for sending an image. + * @public + */ +export interface GenerativeContentBlob { + mimeType: string; + /** + * Image as a base64 string. + */ + data: string; +} + +/** + * Data pointing to a file uploaded on Google Cloud Storage. + * @public + */ +export interface FileData { + mimeType: string; + fileUri: string; +} diff --git a/packages/ai/src/types/enums.ts b/packages/ai/src/types/enums.ts new file mode 100644 index 00000000000..f7c55d5e4c3 --- /dev/null +++ b/packages/ai/src/types/enums.ts @@ -0,0 +1,434 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Role is the producer of the content. + * @public + */ +export type Role = (typeof POSSIBLE_ROLES)[number]; + +/** + * Possible roles. + * @public + */ +export const POSSIBLE_ROLES = ['user', 'model', 'function', 'system'] as const; + +/** + * Harm categories that would cause prompts or candidates to be blocked. + * @public + */ +export const HarmCategory = { + HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH', + HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', + HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT', + HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT' +} as const; + +/** + * Harm categories that would cause prompts or candidates to be blocked. + * @public + */ +export type HarmCategory = (typeof HarmCategory)[keyof typeof HarmCategory]; + +/** + * Threshold above which a prompt or candidate will be blocked. + * @public + */ +export const HarmBlockThreshold = { + /** + * Content with `NEGLIGIBLE` will be allowed. + */ + BLOCK_LOW_AND_ABOVE: 'BLOCK_LOW_AND_ABOVE', + /** + * Content with `NEGLIGIBLE` and `LOW` will be allowed. + */ + BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE', + /** + * Content with `NEGLIGIBLE`, `LOW`, and `MEDIUM` will be allowed. + */ + BLOCK_ONLY_HIGH: 'BLOCK_ONLY_HIGH', + /** + * All content will be allowed. + */ + BLOCK_NONE: 'BLOCK_NONE', + /** + * All content will be allowed. This is the same as `BLOCK_NONE`, but the metadata corresponding + * to the {@link (HarmCategory:type)} will not be present in the response. + */ + OFF: 'OFF' +} as const; + +/** + * Threshold above which a prompt or candidate will be blocked. + * @public + */ +export type HarmBlockThreshold = + (typeof HarmBlockThreshold)[keyof typeof HarmBlockThreshold]; + +/** + * This property is not supported in the Gemini Developer API ({@link GoogleAIBackend}). + * + * @public + */ +export const HarmBlockMethod = { + /** + * The harm block method uses both probability and severity scores. + */ + SEVERITY: 'SEVERITY', + /** + * The harm block method uses the probability score. + */ + PROBABILITY: 'PROBABILITY' +} as const; + +/** + * This property is not supported in the Gemini Developer API ({@link GoogleAIBackend}). + * + * @public + */ +export type HarmBlockMethod = + (typeof HarmBlockMethod)[keyof typeof HarmBlockMethod]; + +/** + * Probability that a prompt or candidate matches a harm category. + * @public + */ +export const HarmProbability = { + /** + * Content has a negligible chance of being unsafe. + */ + NEGLIGIBLE: 'NEGLIGIBLE', + /** + * Content has a low chance of being unsafe. + */ + LOW: 'LOW', + /** + * Content has a medium chance of being unsafe. + */ + MEDIUM: 'MEDIUM', + /** + * Content has a high chance of being unsafe. + */ + HIGH: 'HIGH' +} as const; + +/** + * Probability that a prompt or candidate matches a harm category. + * @public + */ +export type HarmProbability = + (typeof HarmProbability)[keyof typeof HarmProbability]; + +/** + * Harm severity levels. + * @public + */ +export const HarmSeverity = { + /** + * Negligible level of harm severity. + */ + HARM_SEVERITY_NEGLIGIBLE: 'HARM_SEVERITY_NEGLIGIBLE', + /** + * Low level of harm severity. + */ + HARM_SEVERITY_LOW: 'HARM_SEVERITY_LOW', + /** + * Medium level of harm severity. + */ + HARM_SEVERITY_MEDIUM: 'HARM_SEVERITY_MEDIUM', + /** + * High level of harm severity. + */ + HARM_SEVERITY_HIGH: 'HARM_SEVERITY_HIGH', + /** + * Harm severity is not supported. + * + * @remarks + * The GoogleAI backend does not support `HarmSeverity`, so this value is used as a fallback. + */ + HARM_SEVERITY_UNSUPPORTED: 'HARM_SEVERITY_UNSUPPORTED' +} as const; + +/** + * Harm severity levels. + * @public + */ +export type HarmSeverity = (typeof HarmSeverity)[keyof typeof HarmSeverity]; + +/** + * Reason that a prompt was blocked. + * @public + */ +export const BlockReason = { + /** + * Content was blocked by safety settings. + */ + SAFETY: 'SAFETY', + /** + * Content was blocked, but the reason is uncategorized. + */ + OTHER: 'OTHER', + /** + * Content was blocked because it contained terms from the terminology blocklist. + */ + BLOCKLIST: 'BLOCKLIST', + /** + * Content was blocked due to prohibited content. + */ + PROHIBITED_CONTENT: 'PROHIBITED_CONTENT' +} as const; + +/** + * Reason that a prompt was blocked. + * @public + */ +export type BlockReason = (typeof BlockReason)[keyof typeof BlockReason]; + +/** + * Reason that a candidate finished. + * @public + */ +export const FinishReason = { + /** + * Natural stop point of the model or provided stop sequence. + */ + STOP: 'STOP', + /** + * The maximum number of tokens as specified in the request was reached. + */ + MAX_TOKENS: 'MAX_TOKENS', + /** + * The candidate content was flagged for safety reasons. + */ + SAFETY: 'SAFETY', + /** + * The candidate content was flagged for recitation reasons. + */ + RECITATION: 'RECITATION', + /** + * Unknown reason. + */ + OTHER: 'OTHER', + /** + * The candidate content contained forbidden terms. + */ + BLOCKLIST: 'BLOCKLIST', + /** + * The candidate content potentially contained prohibited content. + */ + PROHIBITED_CONTENT: 'PROHIBITED_CONTENT', + /** + * The candidate content potentially contained Sensitive Personally Identifiable Information (SPII). + */ + SPII: 'SPII', + /** + * The function call generated by the model was invalid. + */ + MALFORMED_FUNCTION_CALL: 'MALFORMED_FUNCTION_CALL' +} as const; + +/** + * Reason that a candidate finished. + * @public + */ +export type FinishReason = (typeof FinishReason)[keyof typeof FinishReason]; + +/** + * @public + */ +export const FunctionCallingMode = { + /** + * Default model behavior; model decides to predict either a function call + * or a natural language response. + */ + AUTO: 'AUTO', + /** + * Model is constrained to always predicting a function call only. + * If `allowed_function_names` is set, the predicted function call will be + * limited to any one of `allowed_function_names`, else the predicted + * function call will be any one of the provided `function_declarations`. + */ + ANY: 'ANY', + /** + * Model will not predict any function call. Model behavior is same as when + * not passing any function declarations. + */ + NONE: 'NONE' +} as const; + +/** + * @public + */ +export type FunctionCallingMode = + (typeof FunctionCallingMode)[keyof typeof FunctionCallingMode]; + +/** + * Content part modality. + * @public + */ +export const Modality = { + /** + * Unspecified modality. + */ + MODALITY_UNSPECIFIED: 'MODALITY_UNSPECIFIED', + /** + * Plain text. + */ + TEXT: 'TEXT', + /** + * Image. + */ + IMAGE: 'IMAGE', + /** + * Video. + */ + VIDEO: 'VIDEO', + /** + * Audio. + */ + AUDIO: 'AUDIO', + /** + * Document (for example, PDF). + */ + DOCUMENT: 'DOCUMENT' +} as const; + +/** + * Content part modality. + * @public + */ +export type Modality = (typeof Modality)[keyof typeof Modality]; + +/** + * Generation modalities to be returned in generation responses. + * + * @beta + */ +export const ResponseModality = { + /** + * Text. + * @beta + */ + TEXT: 'TEXT', + /** + * Image. + * @beta + */ + IMAGE: 'IMAGE', + /** + * Audio. + * @beta + */ + AUDIO: 'AUDIO' +} as const; + +/** + * Generation modalities to be returned in generation responses. + * + * @beta + */ +export type ResponseModality = + (typeof ResponseModality)[keyof typeof ResponseModality]; + +/** + * Determines whether inference happens on-device or in-cloud. + * + * @remarks + * PREFER_ON_DEVICE: Attempt to make inference calls using an + * on-device model. If on-device inference is not available, the SDK + * will fall back to using a cloud-hosted model. + *
+ * ONLY_ON_DEVICE: Only attempt to make inference calls using an + * on-device model. The SDK will not fall back to a cloud-hosted model. + * If on-device inference is not available, inference methods will throw. + *
+ * ONLY_IN_CLOUD: Only attempt to make inference calls using a + * cloud-hosted model. The SDK will not fall back to an on-device model. + *
+ * PREFER_IN_CLOUD: Attempt to make inference calls to a + * cloud-hosted model. If not available, the SDK will fall back to an + * on-device model. + * + * @beta + */ +export const InferenceMode = { + 'PREFER_ON_DEVICE': 'prefer_on_device', + 'ONLY_ON_DEVICE': 'only_on_device', + 'ONLY_IN_CLOUD': 'only_in_cloud', + 'PREFER_IN_CLOUD': 'prefer_in_cloud' +} as const; + +/** + * Determines whether inference happens on-device or in-cloud. + * + * @beta + */ +export type InferenceMode = (typeof InferenceMode)[keyof typeof InferenceMode]; + +/** + * Indicates whether inference happened on-device or in-cloud. + * + * @beta + */ +export const InferenceSource = { + 'ON_DEVICE': 'on_device', + 'IN_CLOUD': 'in_cloud' +} as const; + +/** + * Indicates whether inference happened on-device or in-cloud. + * + * @beta + */ +export type InferenceSource = + (typeof InferenceSource)[keyof typeof InferenceSource]; + +/** + * Represents the result of the code execution. + * + * @beta + */ +export const Outcome = { + UNSPECIFIED: 'OUTCOME_UNSPECIFIED', + OK: 'OUTCOME_OK', + FAILED: 'OUTCOME_FAILED', + DEADLINE_EXCEEDED: 'OUTCOME_DEADLINE_EXCEEDED' +}; + +/** + * Represents the result of the code execution. + * + * @beta + */ +export type Outcome = (typeof Outcome)[keyof typeof Outcome]; + +/** + * The programming language of the code. + * + * @beta + */ +export const Language = { + UNSPECIFIED: 'LANGUAGE_UNSPECIFIED', + PYTHON: 'PYTHON' +}; + +/** + * The programming language of the code. + * + * @beta + */ +export type Language = (typeof Language)[keyof typeof Language]; diff --git a/packages/ai/src/types/error.ts b/packages/ai/src/types/error.ts new file mode 100644 index 00000000000..a230f683f37 --- /dev/null +++ b/packages/ai/src/types/error.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GenerateContentResponse } from './responses'; + +/** + * Details object that may be included in an error response. + * + * @public + */ +export interface ErrorDetails { + '@type'?: string; + + /** The reason for the error. */ + reason?: string; + + /** The domain where the error occurred. */ + domain?: string; + + /** Additional metadata about the error. */ + metadata?: Record; + + /** Any other relevant information about the error. */ + [key: string]: unknown; +} + +/** + * Details object that contains data originating from a bad HTTP response. + * + * @public + */ +export interface CustomErrorData { + /** HTTP status code of the error response. */ + status?: number; + + /** HTTP status text of the error response. */ + statusText?: string; + + /** Response from a {@link GenerateContentRequest} */ + response?: GenerateContentResponse; + + /** Optional additional details about the error. */ + errorDetails?: ErrorDetails[]; +} + +/** + * Standardized error codes that {@link AIError} can have. + * + * @public + */ +export const AIErrorCode = { + /** A generic error occurred. */ + ERROR: 'error', + + /** An error occurred in a request. */ + REQUEST_ERROR: 'request-error', + + /** An error occurred in a response. */ + RESPONSE_ERROR: 'response-error', + + /** An error occurred while performing a fetch. */ + FETCH_ERROR: 'fetch-error', + + /** An error occurred because an operation was attempted on a closed session. */ + SESSION_CLOSED: 'session-closed', + + /** An error associated with a Content object. */ + INVALID_CONTENT: 'invalid-content', + + /** An error due to the Firebase API not being enabled in the Console. */ + API_NOT_ENABLED: 'api-not-enabled', + + /** An error due to invalid Schema input. */ + INVALID_SCHEMA: 'invalid-schema', + + /** An error occurred due to a missing Firebase API key. */ + NO_API_KEY: 'no-api-key', + + /** An error occurred due to a missing Firebase app ID. */ + NO_APP_ID: 'no-app-id', + + /** An error occurred due to a model name not being specified during initialization. */ + NO_MODEL: 'no-model', + + /** An error occurred due to a missing project ID. */ + NO_PROJECT_ID: 'no-project-id', + + /** An error occurred while parsing. */ + PARSE_FAILED: 'parse-failed', + + /** An error occurred due an attempt to use an unsupported feature. */ + UNSUPPORTED: 'unsupported' +} as const; + +/** + * Standardized error codes that {@link AIError} can have. + * + * @public + */ +export type AIErrorCode = (typeof AIErrorCode)[keyof typeof AIErrorCode]; diff --git a/packages/ai/src/types/googleai.ts b/packages/ai/src/types/googleai.ts new file mode 100644 index 00000000000..eb282b094fc --- /dev/null +++ b/packages/ai/src/types/googleai.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Tool, + GenerationConfig, + Citation, + FinishReason, + GroundingMetadata, + PromptFeedback, + SafetyRating, + UsageMetadata, + URLContextMetadata +} from '../public-types'; +import { Content, Part } from './content'; + +/** + * @internal + */ +export interface GoogleAICountTokensRequest { + generateContentRequest: { + model: string; // 'models/model-name' + contents: Content[]; + systemInstruction?: string | Part | Content; + tools?: Tool[]; + generationConfig?: GenerationConfig; + }; +} + +/** + * @internal + */ +export interface GoogleAIGenerateContentResponse { + candidates?: GoogleAIGenerateContentCandidate[]; + promptFeedback?: PromptFeedback; + usageMetadata?: UsageMetadata; +} + +/** + * @internal + */ +export interface GoogleAIGenerateContentCandidate { + index: number; + content: Content; + finishReason?: FinishReason; + finishMessage?: string; + safetyRatings?: SafetyRating[]; + citationMetadata?: GoogleAICitationMetadata; + groundingMetadata?: GroundingMetadata; + urlContextMetadata?: URLContextMetadata; +} + +/** + * @internal + */ +export interface GoogleAICitationMetadata { + citationSources: Citation[]; // Maps to `citations` +} diff --git a/packages/ai/src/types/imagen/index.ts b/packages/ai/src/types/imagen/index.ts new file mode 100644 index 00000000000..546c64f13b1 --- /dev/null +++ b/packages/ai/src/types/imagen/index.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './requests'; +export * from './responses'; diff --git a/packages/ai/src/types/imagen/internal.ts b/packages/ai/src/types/imagen/internal.ts new file mode 100644 index 00000000000..1a34eb18f56 --- /dev/null +++ b/packages/ai/src/types/imagen/internal.ts @@ -0,0 +1,139 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ImagenGenerationConfig, ImagenSafetySettings } from './requests'; + +/** + * A response from the REST API is expected to look like this in the success case: + * { + * "predictions": [ + * { + * "mimeType": "image/png", + * "bytesBase64Encoded": "iVBORw0KG..." + * }, + * { + * "mimeType": "image/png", + * "bytesBase64Encoded": "i4BOtw0KG..." + * } + * ] + * } + * + * And like this in the failure case: + * { + * "predictions": [ + * { + * "raiFilteredReason": "..." + * } + * ] + * } + * + * @internal + */ +export interface ImagenResponseInternal { + predictions?: Array<{ + /** + * The MIME type of the generated image. + */ + mimeType?: string; + /** + * The image data encoded as a base64 string. + */ + bytesBase64Encoded?: string; + /** + * The GCS URI where the image was stored. + */ + gcsUri?: string; + /** + * The reason why the image was filtered. + */ + raiFilteredReason?: string; + /** + * The safety attributes. + * + * This type is currently unused in the SDK. It is sent back because our requests set + * `includeSafetyAttributes`. This property is currently only used to avoid throwing an error + * when encountering this unsupported prediction type. + */ + safetyAttributes?: unknown; + }>; +} + +/** + * The parameters to be sent in the request body of the HTTP call + * to the Vertex AI backend. + * + * We need a seperate internal-only interface for this because the REST + * API expects different parameter names than what we show to our users. + * + * Sample request body JSON: + * { + * "instances": [ + * { + * "prompt": "Portrait of a golden retriever on a beach." + * } + * ], + * "parameters": { + * "mimeType": "image/png", + * "safetyFilterLevel": "block_low_and_above", + * "personGeneration": "allow_all", + * "sampleCount": 2, + * "includeRaiReason": true, + * "includeSafetyAttributes": true, + * "aspectRatio": "9:16" + * } + * } + * + * See the Google Cloud docs: https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/imagen-api#-drest + * + * @internal + */ +export interface PredictRequestBody { + instances: [ + { + prompt: string; + } + ]; + parameters: { + sampleCount: number; // Maps to numberOfImages + aspectRatio?: string; + outputOptions?: { + mimeType: string; + compressionQuality?: number; + }; + negativePrompt?: string; + storageUri?: string; // Maps to gcsURI + addWatermark?: boolean; + safetyFilterLevel?: string; + personGeneration?: string; // Maps to personFilterLevel + includeRaiReason: boolean; + includeSafetyAttributes: boolean; + }; +} + +/** + * Contains all possible REST API paramaters that are provided by the caller. + * + * @internal + */ +export type ImagenGenerationParams = { + /** + * The Cloud Storage for Firebase bucket URI where the images should be stored + * (for GCS requests only). + */ + gcsURI?: string; +} & ImagenGenerationConfig & + ImagenSafetySettings; diff --git a/packages/ai/src/types/imagen/requests.ts b/packages/ai/src/types/imagen/requests.ts new file mode 100644 index 00000000000..4cd59342948 --- /dev/null +++ b/packages/ai/src/types/imagen/requests.ts @@ -0,0 +1,258 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ImagenImageFormat } from '../../requests/imagen-image-format'; + +/** + * Parameters for configuring an {@link ImagenModel}. + * + * @public + */ +export interface ImagenModelParams { + /** + * The Imagen model to use for generating images. + * For example: `imagen-3.0-generate-002`. + * + * Only Imagen 3 models (named `imagen-3.0-*`) are supported. + * + * See {@link https://firebase.google.com/docs/vertex-ai/models | model versions} + * for a full list of supported Imagen 3 models. + */ + model: string; + /** + * Configuration options for generating images with Imagen. + */ + generationConfig?: ImagenGenerationConfig; + /** + * Safety settings for filtering potentially inappropriate content. + */ + safetySettings?: ImagenSafetySettings; +} + +/** + * Configuration options for generating images with Imagen. + * + * See the {@link http://firebase.google.com/docs/vertex-ai/generate-images-imagen | documentation} for + * more details. + * + * @public + */ +export interface ImagenGenerationConfig { + /** + * A description of what should be omitted from the generated images. + * + * Support for negative prompts depends on the Imagen model. + * + * See the {@link http://firebase.google.com/docs/vertex-ai/model-parameters#imagen | documentation} for more details. + * + * This is no longer supported in the Gemini Developer API ({@link GoogleAIBackend}) in versions + * greater than `imagen-3.0-generate-002`. + */ + negativePrompt?: string; + /** + * The number of images to generate. The default value is 1. + * + * The number of sample images that may be generated in each request depends on the model + * (typically up to 4); see the
sampleCount + * documentation for more details. + */ + numberOfImages?: number; + /** + * The aspect ratio of the generated images. The default value is square 1:1. + * Supported aspect ratios depend on the Imagen model, see {@link (ImagenAspectRatio:type)} + * for more details. + */ + aspectRatio?: ImagenAspectRatio; + /** + * The image format of the generated images. The default is PNG. + * + * See {@link ImagenImageFormat} for more details. + */ + imageFormat?: ImagenImageFormat; + /** + * Whether to add an invisible watermark to generated images. + * + * If set to `true`, an invisible SynthID watermark is embedded in generated images to indicate + * that they are AI generated. If set to `false`, watermarking will be disabled. + * + * For Imagen 3 models, the default value is `true`; see the addWatermark + * documentation for more details. + * + * When using the Gemini Developer API ({@link GoogleAIBackend}), this will default to true, + * and cannot be turned off. + */ + addWatermark?: boolean; +} + +/** + * A filter level controlling how aggressively to filter sensitive content. + * + * Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI + * are assessed against a list of safety filters, which include 'harmful categories' (for example, + * `violence`, `sexual`, `derogatory`, and `toxic`). This filter level controls how aggressively to + * filter out potentially harmful content from responses. See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } + * and the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters | Responsible AI and usage guidelines} + * for more details. + * + * @public + */ +export const ImagenSafetyFilterLevel = { + /** + * The most aggressive filtering level; most strict blocking. + */ + BLOCK_LOW_AND_ABOVE: 'block_low_and_above', + /** + * Blocks some sensitive prompts and responses. + */ + BLOCK_MEDIUM_AND_ABOVE: 'block_medium_and_above', + /** + * Blocks few sensitive prompts and responses. + */ + BLOCK_ONLY_HIGH: 'block_only_high', + /** + * The least aggressive filtering level; blocks very few sensitive prompts and responses. + * + * Access to this feature is restricted and may require your case to be reviewed and approved by + * Cloud support. + */ + BLOCK_NONE: 'block_none' +} as const; + +/** + * A filter level controlling how aggressively to filter sensitive content. + * + * Text prompts provided as inputs and images (generated or uploaded) through Imagen on Vertex AI + * are assessed against a list of safety filters, which include 'harmful categories' (for example, + * `violence`, `sexual`, `derogatory`, and `toxic`). This filter level controls how aggressively to + * filter out potentially harmful content from responses. See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } + * and the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters | Responsible AI and usage guidelines} + * for more details. + * + * @public + */ +export type ImagenSafetyFilterLevel = + (typeof ImagenSafetyFilterLevel)[keyof typeof ImagenSafetyFilterLevel]; + +/** + * A filter level controlling whether generation of images containing people or faces is allowed. + * + * See the personGeneration + * documentation for more details. + * + * @public + */ +export const ImagenPersonFilterLevel = { + /** + * Disallow generation of images containing people or faces; images of people are filtered out. + */ + BLOCK_ALL: 'dont_allow', + /** + * Allow generation of images containing adults only; images of children are filtered out. + * + * Generation of images containing people or faces may require your use case to be + * reviewed and approved by Cloud support; see the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen | Responsible AI and usage guidelines} + * for more details. + */ + ALLOW_ADULT: 'allow_adult', + /** + * Allow generation of images containing adults only; images of children are filtered out. + * + * Generation of images containing people or faces may require your use case to be + * reviewed and approved by Cloud support; see the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#person-face-gen | Responsible AI and usage guidelines} + * for more details. + */ + ALLOW_ALL: 'allow_all' +} as const; + +/** + * A filter level controlling whether generation of images containing people or faces is allowed. + * + * See the personGeneration + * documentation for more details. + * + * @public + */ +export type ImagenPersonFilterLevel = + (typeof ImagenPersonFilterLevel)[keyof typeof ImagenPersonFilterLevel]; + +/** + * Settings for controlling the aggressiveness of filtering out sensitive content. + * + * See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } + * for more details. + * + * @public + */ +export interface ImagenSafetySettings { + /** + * A filter level controlling how aggressive to filter out sensitive content from generated + * images. + */ + safetyFilterLevel?: ImagenSafetyFilterLevel; + /** + * A filter level controlling whether generation of images containing people or faces is allowed. + */ + personFilterLevel?: ImagenPersonFilterLevel; +} + +/** + * Aspect ratios for Imagen images. + * + * To specify an aspect ratio for generated images, set the `aspectRatio` property in your + * {@link ImagenGenerationConfig}. + * + * See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } + * for more details and examples of the supported aspect ratios. + * + * @public + */ +export const ImagenAspectRatio = { + /** + * Square (1:1) aspect ratio. + */ + 'SQUARE': '1:1', + /** + * Landscape (3:4) aspect ratio. + */ + 'LANDSCAPE_3x4': '3:4', + /** + * Portrait (4:3) aspect ratio. + */ + 'PORTRAIT_4x3': '4:3', + /** + * Landscape (16:9) aspect ratio. + */ + 'LANDSCAPE_16x9': '16:9', + /** + * Portrait (9:16) aspect ratio. + */ + 'PORTRAIT_9x16': '9:16' +} as const; + +/** + * Aspect ratios for Imagen images. + * + * To specify an aspect ratio for generated images, set the `aspectRatio` property in your + * {@link ImagenGenerationConfig}. + * + * See the {@link http://firebase.google.com/docs/vertex-ai/generate-images | documentation } + * for more details and examples of the supported aspect ratios. + * + * @public + */ +export type ImagenAspectRatio = + (typeof ImagenAspectRatio)[keyof typeof ImagenAspectRatio]; diff --git a/packages/ai/src/types/imagen/responses.ts b/packages/ai/src/types/imagen/responses.ts new file mode 100644 index 00000000000..b0985ea6043 --- /dev/null +++ b/packages/ai/src/types/imagen/responses.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * An image generated by Imagen, represented as inline data. + * + * @public + */ +export interface ImagenInlineImage { + /** + * The MIME type of the image; either `"image/png"` or `"image/jpeg"`. + * + * To request a different format, set the `imageFormat` property in your {@link ImagenGenerationConfig}. + */ + mimeType: string; + /** + * The base64-encoded image data. + */ + bytesBase64Encoded: string; +} + +/** + * An image generated by Imagen, stored in a Cloud Storage for Firebase bucket. + * + * This feature is not available yet. + * @public + */ +export interface ImagenGCSImage { + /** + * The MIME type of the image; either `"image/png"` or `"image/jpeg"`. + * + * To request a different format, set the `imageFormat` property in your {@link ImagenGenerationConfig}. + */ + mimeType: string; + /** + * The URI of the file stored in a Cloud Storage for Firebase bucket. + * + * @example `"gs://bucket-name/path/sample_0.jpg"`. + */ + gcsURI: string; +} + +/** + * The response from a request to generate images with Imagen. + * + * @public + */ +export interface ImagenGenerationResponse< + T extends ImagenInlineImage | ImagenGCSImage +> { + /** + * The images generated by Imagen. + * + * The number of images generated may be fewer than the number requested if one or more were + * filtered out; see `filteredReason`. + */ + images: T[]; + /** + * The reason that images were filtered out. This property will only be defined if one + * or more images were filtered. + * + * Images may be filtered out due to the {@link (ImagenSafetyFilterLevel:type)}, + * {@link (ImagenPersonFilterLevel:type)}, or filtering included in the model. + * The filter levels may be adjusted in your {@link ImagenSafetySettings}. + * + * See the {@link https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen | Responsible AI and usage guidelines for Imagen} + * for more details. + */ + filteredReason?: string; +} diff --git a/packages/ai/src/types/index.ts b/packages/ai/src/types/index.ts new file mode 100644 index 00000000000..2dfe73040ae --- /dev/null +++ b/packages/ai/src/types/index.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './content'; +export * from './enums'; +export * from './requests'; +export * from './responses'; +export * from './error'; +export * from './schema'; +export * from './imagen'; +export * from './googleai'; +export { + LanguageModelCreateOptions, + LanguageModelCreateCoreOptions, + LanguageModelExpected, + LanguageModelMessage, + LanguageModelMessageContent, + LanguageModelMessageContentValue, + LanguageModelMessageRole, + LanguageModelMessageType, + LanguageModelPromptOptions +} from './language-model'; +export * from './chrome-adapter'; diff --git a/packages/ai/src/types/internal.ts b/packages/ai/src/types/internal.ts new file mode 100644 index 00000000000..a41ec5652d3 --- /dev/null +++ b/packages/ai/src/types/internal.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppCheckTokenResult } from '@firebase/app-check-interop-types'; +import { FirebaseAuthTokenData } from '@firebase/auth-interop-types'; +import { Backend } from '../backend'; + +export * from './imagen/internal'; + +export interface ApiSettings { + apiKey: string; + project: string; + appId: string; + automaticDataCollectionEnabled?: boolean; + /** + * @deprecated Use `backend.location` instead. + */ + location: string; + backend: Backend; + getAuthToken?: () => Promise; + getAppCheckToken?: () => Promise; +} diff --git a/packages/ai/src/types/language-model.ts b/packages/ai/src/types/language-model.ts new file mode 100644 index 00000000000..9ac4c7202e1 --- /dev/null +++ b/packages/ai/src/types/language-model.ts @@ -0,0 +1,133 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * The subset of the Prompt API + * (see {@link https://github.com/webmachinelearning/prompt-api#full-api-surface-in-web-idl } + * required for hybrid functionality. + * + * @internal + */ +export interface LanguageModel extends EventTarget { + create(options?: LanguageModelCreateOptions): Promise; + availability(options?: LanguageModelCreateCoreOptions): Promise; + prompt( + input: LanguageModelPrompt, + options?: LanguageModelPromptOptions + ): Promise; + promptStreaming( + input: LanguageModelPrompt, + options?: LanguageModelPromptOptions + ): ReadableStream; + measureInputUsage( + input: LanguageModelPrompt, + options?: LanguageModelPromptOptions + ): Promise; + destroy(): undefined; +} + +/** + * @internal + */ +export enum Availability { + 'UNAVAILABLE' = 'unavailable', + 'DOWNLOADABLE' = 'downloadable', + 'DOWNLOADING' = 'downloading', + 'AVAILABLE' = 'available' +} + +/** + * Configures the creation of an on-device language model session. + * @beta + */ +export interface LanguageModelCreateCoreOptions { + topK?: number; + temperature?: number; + expectedInputs?: LanguageModelExpected[]; +} + +/** + * Configures the creation of an on-device language model session. + * @beta + */ +export interface LanguageModelCreateOptions + extends LanguageModelCreateCoreOptions { + signal?: AbortSignal; + initialPrompts?: LanguageModelMessage[]; +} + +/** + * Options for an on-device language model prompt. + * @beta + */ +export interface LanguageModelPromptOptions { + responseConstraint?: object; + // TODO: Restore AbortSignal once the API is defined. +} + +/** + * Options for the expected inputs for an on-device language model. + * @beta + */ export interface LanguageModelExpected { + type: LanguageModelMessageType; + languages?: string[]; +} + +/** + * An on-device language model prompt. + * @beta + */ +export type LanguageModelPrompt = LanguageModelMessage[]; + +/** + * An on-device language model message. + * @beta + */ +export interface LanguageModelMessage { + role: LanguageModelMessageRole; + content: LanguageModelMessageContent[]; +} + +/** + * An on-device language model content object. + * @beta + */ +export interface LanguageModelMessageContent { + type: LanguageModelMessageType; + value: LanguageModelMessageContentValue; +} + +/** + * Allowable roles for on-device language model usage. + * @beta + */ +export type LanguageModelMessageRole = 'system' | 'user' | 'assistant'; + +/** + * Allowable types for on-device language model messages. + * @beta + */ +export type LanguageModelMessageType = 'text' | 'image' | 'audio'; + +/** + * Content formats that can be provided as on-device message content. + * @beta + */ +export type LanguageModelMessageContentValue = + | ImageBitmapSource + | AudioBuffer + | BufferSource + | string; diff --git a/packages/ai/src/types/live-responses.ts b/packages/ai/src/types/live-responses.ts new file mode 100644 index 00000000000..3bdb32c1269 --- /dev/null +++ b/packages/ai/src/types/live-responses.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Content, + FunctionResponse, + GenerativeContentBlob, + Part +} from './content'; +import { + AudioTranscriptionConfig, + LiveGenerationConfig, + Tool, + ToolConfig +} from './requests'; +import { Transcription } from './responses'; + +/** + * User input that is sent to the model. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientContent { + clientContent: { + turns: [Content]; + turnComplete: boolean; + inputTranscription?: Transcription; + outputTranscription?: Transcription; + }; +} + +/** + * User input that is sent to the model in real time. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientRealtimeInput { + realtimeInput: { + text?: string; + audio?: GenerativeContentBlob; + video?: GenerativeContentBlob; + + /** + * @deprecated Use `text`, `audio`, and `video` instead. + */ + mediaChunks?: GenerativeContentBlob[]; + }; +} + +/** + * Function responses that are sent to the model in real time. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientToolResponse { + toolResponse: { + functionResponses: FunctionResponse[]; + }; +} + +/** + * The first message in a Live session, used to configure generation options. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _LiveClientSetup { + setup: { + model: string; + generationConfig?: _LiveGenerationConfig; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; + inputAudioTranscription?: AudioTranscriptionConfig; + outputAudioTranscription?: AudioTranscriptionConfig; + }; +} + +/** + * The Live Generation Config. + * + * The public API ({@link LiveGenerationConfig}) has `inputAudioTranscription` and `outputAudioTranscription`, + * but the server expects these fields to be in the top-level `setup` message. This was a conscious API decision. + */ +export type _LiveGenerationConfig = Omit< + LiveGenerationConfig, + 'inputAudioTranscription' | 'outputAudioTranscription' +>; diff --git a/packages/ai/src/types/requests.ts b/packages/ai/src/types/requests.ts new file mode 100644 index 00000000000..6e5d2147686 --- /dev/null +++ b/packages/ai/src/types/requests.ts @@ -0,0 +1,503 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ObjectSchema, TypedSchema } from '../requests/schema-builder'; +import { Content, Part } from './content'; +import { + LanguageModelCreateOptions, + LanguageModelPromptOptions +} from './language-model'; +import { + FunctionCallingMode, + HarmBlockMethod, + HarmBlockThreshold, + HarmCategory, + InferenceMode, + ResponseModality +} from './enums'; +import { ObjectSchemaRequest, SchemaRequest } from './schema'; + +/** + * Base parameters for a number of methods. + * @public + */ +export interface BaseParams { + safetySettings?: SafetySetting[]; + generationConfig?: GenerationConfig; +} + +/** + * Params passed to {@link getGenerativeModel}. + * @public + */ +export interface ModelParams extends BaseParams { + model: string; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; +} + +/** + * Params passed to {@link getLiveGenerativeModel}. + * @beta + */ +export interface LiveModelParams { + model: string; + generationConfig?: LiveGenerationConfig; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; +} +/** + * Request sent through {@link GenerativeModel.generateContent} + * @public + */ +export interface GenerateContentRequest extends BaseParams { + contents: Content[]; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; +} + +/** + * Safety setting that can be sent as part of request parameters. + * @public + */ +export interface SafetySetting { + category: HarmCategory; + threshold: HarmBlockThreshold; + /** + * The harm block method. + * + * This property is only supported in the Vertex AI Gemini API ({@link VertexAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), an {@link AIError} will be + * thrown if this property is defined. + */ + method?: HarmBlockMethod; +} + +/** + * Config options for content-related requests + * @public + */ +export interface GenerationConfig { + candidateCount?: number; + stopSequences?: string[]; + maxOutputTokens?: number; + temperature?: number; + topP?: number; + topK?: number; + presencePenalty?: number; + frequencyPenalty?: number; + /** + * Output response MIME type of the generated candidate text. + * Supported MIME types are `text/plain` (default, text output), + * `application/json` (JSON response in the candidates), and + * `text/x.enum`. + */ + responseMimeType?: string; + /** + * Output response schema of the generated candidate text. This + * value can be a class generated with a {@link Schema} static method + * like `Schema.string()` or `Schema.object()` or it can be a plain + * JS object matching the {@link SchemaRequest} interface. + *
Note: This only applies when the specified `responseMimeType` supports a schema; currently + * this is limited to `application/json` and `text/x.enum`. + */ + responseSchema?: TypedSchema | SchemaRequest; + /** + * Generation modalities to be returned in generation responses. + * + * @remarks + * - Multimodal response generation is only supported by some Gemini models and versions; see {@link https://firebase.google.com/docs/vertex-ai/models | model versions}. + * - Only image generation (`ResponseModality.IMAGE`) is supported. + * + * @beta + */ + responseModalities?: ResponseModality[]; + /** + * Configuration for "thinking" behavior of compatible Gemini models. + */ + thinkingConfig?: ThinkingConfig; +} + +/** + * Configuration parameters used by {@link LiveGenerativeModel} to control live content generation. + * + * @beta + */ +export interface LiveGenerationConfig { + /** + * Configuration for speech synthesis. + */ + speechConfig?: SpeechConfig; + /** + * Specifies the maximum number of tokens that can be generated in the response. The number of + * tokens per word varies depending on the language outputted. Is unbounded by default. + */ + maxOutputTokens?: number; + /** + * Controls the degree of randomness in token selection. A `temperature` value of 0 means that the highest + * probability tokens are always selected. In this case, responses for a given prompt are mostly + * deterministic, but a small amount of variation is still possible. + */ + temperature?: number; + /** + * Changes how the model selects tokens for output. Tokens are + * selected from the most to least probable until the sum of their probabilities equals the `topP` + * value. For example, if tokens A, B, and C have probabilities of 0.3, 0.2, and 0.1 respectively + * and the `topP` value is 0.5, then the model will select either A or B as the next token by using + * the `temperature` and exclude C as a candidate. Defaults to 0.95 if unset. + */ + topP?: number; + /** + * Changes how the model selects token for output. A `topK` value of 1 means the select token is + * the most probable among all tokens in the model's vocabulary, while a `topK` value 3 means that + * the next token is selected from among the 3 most probably using probabilities sampled. Tokens + * are then further filtered with the highest selected `temperature` sampling. Defaults to 40 + * if unspecified. + */ + topK?: number; + /** + * Positive penalties. + */ + presencePenalty?: number; + /** + * Frequency penalties. + */ + frequencyPenalty?: number; + /** + * The modalities of the response. + */ + responseModalities?: ResponseModality[]; + /** + * Enables transcription of audio input. + * + * When enabled, the model will respond with transcriptions of your audio input in the `inputTranscriptions` property + * in {@link LiveServerContent} messages. Note that the transcriptions are broken up across + * messages, so you may only receive small amounts of text per message. For example, if you ask the model + * "How are you today?", the model may transcribe that input across three messages, broken up as "How a", "re yo", "u today?". + */ + inputAudioTranscription?: AudioTranscriptionConfig; + /** + * Enables transcription of audio input. + * + * When enabled, the model will respond with transcriptions of its audio output in the `outputTranscription` property + * in {@link LiveServerContent} messages. Note that the transcriptions are broken up across + * messages, so you may only receive small amounts of text per message. For example, if the model says + * "How are you today?", the model may transcribe that output across three messages, broken up as "How a", "re yo", "u today?". + */ + outputAudioTranscription?: AudioTranscriptionConfig; +} + +/** + * Params for {@link GenerativeModel.startChat}. + * @public + */ +export interface StartChatParams extends BaseParams { + history?: Content[]; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; +} + +/** + * Params for calling {@link GenerativeModel.countTokens} + * @public + */ +export interface CountTokensRequest { + contents: Content[]; + /** + * Instructions that direct the model to behave a certain way. + */ + systemInstruction?: string | Part | Content; + /** + * {@link Tool} configuration. + */ + tools?: Tool[]; + /** + * Configuration options that control how the model generates a response. + */ + generationConfig?: GenerationConfig; +} + +/** + * Params passed to {@link getGenerativeModel}. + * @public + */ +export interface RequestOptions { + /** + * Request timeout in milliseconds. Defaults to 180 seconds (180000ms). + */ + timeout?: number; + /** + * Base url for endpoint. Defaults to + * https://firebasevertexai.googleapis.com, which is the + * {@link https://console.cloud.google.com/apis/library/firebasevertexai.googleapis.com?project=_ | Firebase AI Logic API} + * (used regardless of your chosen Gemini API provider). + */ + baseUrl?: string; +} + +/** + * Defines a tool that model can call to access external knowledge. + * @public + */ +export type Tool = + | FunctionDeclarationsTool + | GoogleSearchTool + | CodeExecutionTool + | URLContextTool; + +/** + * Structured representation of a function declaration as defined by the + * {@link https://spec.openapis.org/oas/v3.0.3 | OpenAPI 3.0 specification}. + * Included + * in this declaration are the function name and parameters. This + * `FunctionDeclaration` is a representation of a block of code that can be used + * as a Tool by the model and executed by the client. + * @public + */ +export interface FunctionDeclaration { + /** + * The name of the function to call. Must start with a letter or an + * underscore. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with + * a max length of 64. + */ + name: string; + /** + * Description and purpose of the function. Model uses it to decide + * how and whether to call the function. + */ + description: string; + /** + * Optional. Describes the parameters to this function in JSON Schema Object + * format. Reflects the Open API 3.03 Parameter Object. Parameter names are + * case-sensitive. For a function with no parameters, this can be left unset. + */ + parameters?: ObjectSchema | ObjectSchemaRequest; +} + +/** + * A tool that allows a Gemini model to connect to Google Search to access and incorporate + * up-to-date information from the web into its responses. + * + * Important: If using Grounding with Google Search, you are required to comply with the + * "Grounding with Google Search" usage requirements for your chosen API provider: {@link https://ai.google.dev/gemini-api/terms#grounding-with-google-search | Gemini Developer API} + * or Vertex AI Gemini API (see {@link https://cloud.google.com/terms/service-terms | Service Terms} + * section within the Service Specific Terms). + * + * @public + */ +export interface GoogleSearchTool { + /** + * Specifies the Google Search configuration. + * Currently, this is an empty object, but it's reserved for future configuration options. + * + * When using this feature, you are required to comply with the "Grounding with Google Search" + * usage requirements for your chosen API provider: {@link https://ai.google.dev/gemini-api/terms#grounding-with-google-search | Gemini Developer API} + * or Vertex AI Gemini API (see {@link https://cloud.google.com/terms/service-terms | Service Terms} + * section within the Service Specific Terms). + */ + googleSearch: GoogleSearch; +} + +/** + * A tool that enables the model to use code execution. + * + * @beta + */ +export interface CodeExecutionTool { + /** + * Specifies the Google Search configuration. + * Currently, this is an empty object, but it's reserved for future configuration options. + */ + codeExecution: {}; +} + +/** + * Specifies the Google Search configuration. + * + * @remarks Currently, this is an empty object, but it's reserved for future configuration options. + * + * @public + */ +export interface GoogleSearch {} + +/** + * A tool that allows you to provide additional context to the models in the form of public web + * URLs. By including URLs in your request, the Gemini model will access the content from those + * pages to inform and enhance its response. + * + * @beta + */ +export interface URLContextTool { + /** + * Specifies the URL Context configuration. + */ + urlContext: URLContext; +} + +/** + * Specifies the URL Context configuration. + * + * @beta + */ +export interface URLContext {} + +/** + * A `FunctionDeclarationsTool` is a piece of code that enables the system to + * interact with external systems to perform an action, or set of actions, + * outside of knowledge and scope of the model. + * @public + */ +export interface FunctionDeclarationsTool { + /** + * Optional. One or more function declarations + * to be passed to the model along with the current user query. Model may + * decide to call a subset of these functions by populating + * {@link FunctionCall} in the response. User should + * provide a {@link FunctionResponse} for each + * function call in the next turn. Based on the function responses, the model will + * generate the final response back to the user. Maximum 64 function + * declarations can be provided. + */ + functionDeclarations?: FunctionDeclaration[]; +} + +/** + * Tool config. This config is shared for all tools provided in the request. + * @public + */ +export interface ToolConfig { + functionCallingConfig?: FunctionCallingConfig; +} + +/** + * @public + */ +export interface FunctionCallingConfig { + mode?: FunctionCallingMode; + allowedFunctionNames?: string[]; +} + +/** + * Encapsulates configuration for on-device inference. + * + * @beta + */ +export interface OnDeviceParams { + createOptions?: LanguageModelCreateOptions; + promptOptions?: LanguageModelPromptOptions; +} + +/** + * Configures hybrid inference. + * @beta + */ +export interface HybridParams { + /** + * Specifies on-device or in-cloud inference. Defaults to prefer on-device. + */ + mode: InferenceMode; + /** + * Optional. Specifies advanced params for on-device inference. + */ + onDeviceParams?: OnDeviceParams; + /** + * Optional. Specifies advanced params for in-cloud inference. + */ + inCloudParams?: ModelParams; +} + +/** + * Configuration for "thinking" behavior of compatible Gemini models. + * + * Certain models utilize a thinking process before generating a response. This allows them to + * reason through complex problems and plan a more coherent and accurate answer. + * + * @public + */ +export interface ThinkingConfig { + /** + * The thinking budget, in tokens. + * + * This parameter sets an upper limit on the number of tokens the model can use for its internal + * "thinking" process. A higher budget may result in higher quality responses for complex tasks + * but can also increase latency and cost. + * + * If you don't specify a budget, the model will determine the appropriate amount + * of thinking based on the complexity of the prompt. + * + * An error will be thrown if you set a thinking budget for a model that does not support this + * feature or if the specified budget is not within the model's supported range. + */ + thinkingBudget?: number; + + /** + * Whether to include "thought summaries" in the model's response. + * + * @remarks + * Thought summaries provide a brief overview of the model's internal thinking process, + * offering insight into how it arrived at the final answer. This can be useful for + * debugging, understanding the model's reasoning, and verifying its accuracy. + */ + includeThoughts?: boolean; +} + +/** + * Configuration for a pre-built voice. + * + * @beta + */ +export interface PrebuiltVoiceConfig { + /** + * The voice name to use for speech synthesis. + * + * For a full list of names and demos of what each voice sounds like, see {@link https://cloud.google.com/text-to-speech/docs/chirp3-hd | Chirp 3: HD Voices}. + */ + voiceName?: string; +} + +/** + * Configuration for the voice to used in speech synthesis. + * + * @beta + */ +export interface VoiceConfig { + /** + * Configures the voice using a pre-built voice configuration. + */ + prebuiltVoiceConfig?: PrebuiltVoiceConfig; +} + +/** + * Configures speech synthesis. + * + * @beta + */ +export interface SpeechConfig { + /** + * Configures the voice to be used in speech synthesis. + */ + voiceConfig?: VoiceConfig; +} + +/** + * The audio transcription configuration. + */ +export interface AudioTranscriptionConfig {} diff --git a/packages/ai/src/types/responses.ts b/packages/ai/src/types/responses.ts new file mode 100644 index 00000000000..be56d0d2baa --- /dev/null +++ b/packages/ai/src/types/responses.ts @@ -0,0 +1,626 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Content, FunctionCall, InlineDataPart } from './content'; +import { + BlockReason, + FinishReason, + HarmCategory, + HarmProbability, + HarmSeverity, + InferenceSource, + Modality +} from './enums'; + +/** + * Result object returned from {@link GenerativeModel.generateContent} call. + * + * @public + */ +export interface GenerateContentResult { + response: EnhancedGenerateContentResponse; +} + +/** + * Result object returned from {@link GenerativeModel.generateContentStream} call. + * Iterate over `stream` to get chunks as they come in and/or + * use the `response` promise to get the aggregated response when + * the stream is done. + * + * @public + */ +export interface GenerateContentStreamResult { + stream: AsyncGenerator; + response: Promise; +} + +/** + * Response object wrapped with helper methods. + * + * @public + */ +export interface EnhancedGenerateContentResponse + extends GenerateContentResponse { + /** + * Returns the text string from the response, if available. + * Throws if the prompt or candidate was blocked. + */ + text: () => string; + /** + * Aggregates and returns every {@link InlineDataPart} from the first candidate of + * {@link GenerateContentResponse}. + * + * @throws If the prompt or candidate was blocked. + */ + inlineDataParts: () => InlineDataPart[] | undefined; + /** + * Aggregates and returns every {@link FunctionCall} from the first candidate of + * {@link GenerateContentResponse}. + * + * @throws If the prompt or candidate was blocked. + */ + functionCalls: () => FunctionCall[] | undefined; + /** + * Aggregates and returns every {@link TextPart} with their `thought` property set + * to `true` from the first candidate of {@link GenerateContentResponse}. + * + * @throws If the prompt or candidate was blocked. + * + * @remarks + * Thought summaries provide a brief overview of the model's internal thinking process, + * offering insight into how it arrived at the final answer. This can be useful for + * debugging, understanding the model's reasoning, and verifying its accuracy. + * + * Thoughts will only be included if {@link ThinkingConfig.includeThoughts} is + * set to `true`. + */ + thoughtSummary: () => string | undefined; + /** + * Indicates whether inference happened on-device or in-cloud. + * + * @beta + */ + inferenceSource?: InferenceSource; +} + +/** + * Individual response from {@link GenerativeModel.generateContent} and + * {@link GenerativeModel.generateContentStream}. + * `generateContentStream()` will return one in each chunk until + * the stream is done. + * @public + */ +export interface GenerateContentResponse { + candidates?: GenerateContentCandidate[]; + promptFeedback?: PromptFeedback; + usageMetadata?: UsageMetadata; +} + +/** + * Usage metadata about a {@link GenerateContentResponse}. + * + * @public + */ +export interface UsageMetadata { + promptTokenCount: number; + candidatesTokenCount: number; + /** + * The number of tokens used by the model's internal "thinking" process. + */ + thoughtsTokenCount?: number; + totalTokenCount: number; + /** + * The number of tokens used by tools. + */ + toolUsePromptTokenCount?: number; + promptTokensDetails?: ModalityTokenCount[]; + candidatesTokensDetails?: ModalityTokenCount[]; + /** + * A list of tokens used by tools, broken down by modality. + */ + toolUsePromptTokensDetails?: ModalityTokenCount[]; +} + +/** + * Represents token counting info for a single modality. + * + * @public + */ +export interface ModalityTokenCount { + /** The modality associated with this token count. */ + modality: Modality; + /** The number of tokens counted. */ + tokenCount: number; +} + +/** + * If the prompt was blocked, this will be populated with `blockReason` and + * the relevant `safetyRatings`. + * @public + */ +export interface PromptFeedback { + blockReason?: BlockReason; + safetyRatings: SafetyRating[]; + /** + * A human-readable description of the `blockReason`. + * + * This property is only supported in the Vertex AI Gemini API ({@link VertexAIBackend}). + */ + blockReasonMessage?: string; +} + +/** + * A candidate returned as part of a {@link GenerateContentResponse}. + * @public + */ +export interface GenerateContentCandidate { + index: number; + content: Content; + finishReason?: FinishReason; + finishMessage?: string; + safetyRatings?: SafetyRating[]; + citationMetadata?: CitationMetadata; + groundingMetadata?: GroundingMetadata; + urlContextMetadata?: URLContextMetadata; +} + +/** + * Citation metadata that may be found on a {@link GenerateContentCandidate}. + * @public + */ +export interface CitationMetadata { + citations: Citation[]; +} + +/** + * A single citation. + * @public + */ +export interface Citation { + startIndex?: number; + endIndex?: number; + uri?: string; + license?: string; + /** + * The title of the cited source, if available. + * + * This property is only supported in the Vertex AI Gemini API ({@link VertexAIBackend}). + */ + title?: string; + /** + * The publication date of the cited source, if available. + * + * This property is only supported in the Vertex AI Gemini API ({@link VertexAIBackend}). + */ + publicationDate?: Date; +} + +/** + * Metadata returned when grounding is enabled. + * + * Currently, only Grounding with Google Search is supported (see {@link GoogleSearchTool}). + * + * Important: If using Grounding with Google Search, you are required to comply with the + * "Grounding with Google Search" usage requirements for your chosen API provider: {@link https://ai.google.dev/gemini-api/terms#grounding-with-google-search | Gemini Developer API} + * or Vertex AI Gemini API (see {@link https://cloud.google.com/terms/service-terms | Service Terms} + * section within the Service Specific Terms). + * + * @public + */ +export interface GroundingMetadata { + /** + * Google Search entry point for web searches. This contains an HTML/CSS snippet that must be + * embedded in an app to display a Google Search entry point for follow-up web searches related to + * a model's "Grounded Response". + */ + searchEntryPoint?: SearchEntrypoint; + /** + * A list of {@link GroundingChunk} objects. Each chunk represents a piece of retrieved content + * (for example, from a web page). that the model used to ground its response. + */ + groundingChunks?: GroundingChunk[]; + /** + * A list of {@link GroundingSupport} objects. Each object details how specific segments of the + * model's response are supported by the `groundingChunks`. + */ + groundingSupports?: GroundingSupport[]; + /** + * A list of web search queries that the model performed to gather the grounding information. + * These can be used to allow users to explore the search results themselves. + */ + webSearchQueries?: string[]; + /** + * @deprecated Use {@link GroundingSupport} instead. + */ + retrievalQueries?: string[]; +} + +/** + * Google search entry point. + * + * @public + */ +export interface SearchEntrypoint { + /** + * HTML/CSS snippet that must be embedded in a web page. The snippet is designed to avoid + * undesired interaction with the rest of the page's CSS. + * + * To ensure proper rendering and prevent CSS conflicts, it is recommended + * to encapsulate this `renderedContent` within a shadow DOM when embedding it + * into a webpage. See {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM | MDN: Using shadow DOM}. + * + * @example + * ```javascript + * const container = document.createElement('div'); + * document.body.appendChild(container); + * container.attachShadow({ mode: 'open' }).innerHTML = renderedContent; + * ``` + */ + renderedContent?: string; +} + +/** + * Represents a chunk of retrieved data that supports a claim in the model's response. This is part + * of the grounding information provided when grounding is enabled. + * + * @public + */ +export interface GroundingChunk { + /** + * Contains details if the grounding chunk is from a web source. + */ + web?: WebGroundingChunk; +} + +/** + * A grounding chunk from the web. + * + * Important: If using Grounding with Google Search, you are required to comply with the + * {@link https://cloud.google.com/terms/service-terms | Service Specific Terms} for "Grounding with Google Search". + * + * @public + */ +export interface WebGroundingChunk { + /** + * The URI of the retrieved web page. + */ + uri?: string; + /** + * The title of the retrieved web page. + */ + title?: string; + /** + * The domain of the original URI from which the content was retrieved. + * + * This property is only supported in the Vertex AI Gemini API ({@link VertexAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property will be + * `undefined`. + */ + domain?: string; +} + +/** + * Provides information about how a specific segment of the model's response is supported by the + * retrieved grounding chunks. + * + * @public + */ +export interface GroundingSupport { + /** + * Specifies the segment of the model's response content that this grounding support pertains to. + */ + segment?: Segment; + /** + * A list of indices that refer to specific {@link GroundingChunk} objects within the + * {@link GroundingMetadata.groundingChunks} array. These referenced chunks + * are the sources that support the claim made in the associated `segment` of the response. + * For example, an array `[1, 3, 4]` means that `groundingChunks[1]`, `groundingChunks[3]`, + * and `groundingChunks[4]` are the retrieved content supporting this part of the response. + */ + groundingChunkIndices?: number[]; +} + +/** + * Represents a specific segment within a {@link Content} object, often used to + * pinpoint the exact location of text or data that grounding information refers to. + * + * @public + */ +export interface Segment { + /** + * The zero-based index of the {@link Part} object within the `parts` array + * of its parent {@link Content} object. This identifies which part of the + * content the segment belongs to. + */ + partIndex: number; + /** + * The zero-based start index of the segment within the specified `Part`, + * measured in UTF-8 bytes. This offset is inclusive, starting from 0 at the + * beginning of the part's content (e.g., `Part.text`). + */ + startIndex: number; + /** + * The zero-based end index of the segment within the specified `Part`, + * measured in UTF-8 bytes. This offset is exclusive, meaning the character + * at this index is not included in the segment. + */ + endIndex: number; + /** + * The text corresponding to the segment from the response. + */ + text: string; +} + +/** + * Metadata related to {@link URLContextTool}. + * + * @beta + */ +export interface URLContextMetadata { + /** + * List of URL metadata used to provide context to the Gemini model. + */ + urlMetadata: URLMetadata[]; +} + +/** + * Metadata for a single URL retrieved by the {@link URLContextTool} tool. + * + * @beta + */ +export interface URLMetadata { + /** + * The retrieved URL. + */ + retrievedUrl?: string; + /** + * The status of the URL retrieval. + */ + urlRetrievalStatus?: URLRetrievalStatus; +} + +/** + * The status of a URL retrieval. + * + * @remarks + * URL_RETRIEVAL_STATUS_UNSPECIFIED: Unspecified retrieval status. + *
+ * URL_RETRIEVAL_STATUS_SUCCESS: The URL retrieval was successful. + *
+ * URL_RETRIEVAL_STATUS_ERROR: The URL retrieval failed. + *
+ * URL_RETRIEVAL_STATUS_PAYWALL: The URL retrieval failed because the content is behind a paywall. + *
+ * URL_RETRIEVAL_STATUS_UNSAFE: The URL retrieval failed because the content is unsafe. + *
+ * + * @beta + */ +export const URLRetrievalStatus = { + /** + * Unspecified retrieval status. + */ + URL_RETRIEVAL_STATUS_UNSPECIFIED: 'URL_RETRIEVAL_STATUS_UNSPECIFIED', + /** + * The URL retrieval was successful. + */ + URL_RETRIEVAL_STATUS_SUCCESS: 'URL_RETRIEVAL_STATUS_SUCCESS', + /** + * The URL retrieval failed. + */ + URL_RETRIEVAL_STATUS_ERROR: 'URL_RETRIEVAL_STATUS_ERROR', + /** + * The URL retrieval failed because the content is behind a paywall. + */ + URL_RETRIEVAL_STATUS_PAYWALL: 'URL_RETRIEVAL_STATUS_PAYWALL', + /** + * The URL retrieval failed because the content is unsafe. + */ + URL_RETRIEVAL_STATUS_UNSAFE: 'URL_RETRIEVAL_STATUS_UNSAFE' +}; + +/** + * The status of a URL retrieval. + * + * @remarks + * URL_RETRIEVAL_STATUS_UNSPECIFIED: Unspecified retrieval status. + *
+ * URL_RETRIEVAL_STATUS_SUCCESS: The URL retrieval was successful. + *
+ * URL_RETRIEVAL_STATUS_ERROR: The URL retrieval failed. + *
+ * URL_RETRIEVAL_STATUS_PAYWALL: The URL retrieval failed because the content is behind a paywall. + *
+ * URL_RETRIEVAL_STATUS_UNSAFE: The URL retrieval failed because the content is unsafe. + *
+ * + * @beta + */ +export type URLRetrievalStatus = + (typeof URLRetrievalStatus)[keyof typeof URLRetrievalStatus]; + +/** + * @public + */ +export interface WebAttribution { + uri: string; + title: string; +} + +/** + * @public + */ +export interface RetrievedContextAttribution { + uri: string; + title: string; +} + +/** + * Protobuf google.type.Date + * @public + */ +export interface Date { + year: number; + month: number; + day: number; +} + +/** + * A safety rating associated with a {@link GenerateContentCandidate} + * @public + */ +export interface SafetyRating { + category: HarmCategory; + probability: HarmProbability; + /** + * The harm severity level. + * + * This property is only supported when using the Vertex AI Gemini API ({@link VertexAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property is not supported and will default to `HarmSeverity.UNSUPPORTED`. + */ + severity: HarmSeverity; + /** + * The probability score of the harm category. + * + * This property is only supported when using the Vertex AI Gemini API ({@link VertexAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property is not supported and will default to 0. + */ + probabilityScore: number; + /** + * The severity score of the harm category. + * + * This property is only supported when using the Vertex AI Gemini API ({@link VertexAIBackend}). + * When using the Gemini Developer API ({@link GoogleAIBackend}), this property is not supported and will default to 0. + */ + severityScore: number; + blocked: boolean; +} + +/** + * Response from calling {@link GenerativeModel.countTokens}. + * @public + */ +export interface CountTokensResponse { + /** + * The total number of tokens counted across all instances from the request. + */ + totalTokens: number; + /** + * @deprecated Use `totalTokens` instead. This property is undefined when using models greater than `gemini-1.5-*`. + * + * The total number of billable characters counted across all instances + * from the request. + */ + totalBillableCharacters?: number; + /** + * The breakdown, by modality, of how many tokens are consumed by the prompt. + */ + promptTokensDetails?: ModalityTokenCount[]; +} + +/** + * An incremental content update from the model. + * + * @beta + */ +export interface LiveServerContent { + type: 'serverContent'; + /** + * The content that the model has generated as part of the current conversation with the user. + */ + modelTurn?: Content; + /** + * Indicates whether the turn is complete. This is `undefined` if the turn is not complete. + */ + turnComplete?: boolean; + /** + * Indicates whether the model was interrupted by the client. An interruption occurs when + * the client sends a message before the model finishes it's turn. This is `undefined` if the + * model was not interrupted. + */ + interrupted?: boolean; + /** + * Transcription of the audio that was input to the model. + */ + inputTranscription?: Transcription; + /** + * Transcription of the audio output from the model. + */ + outputTranscription?: Transcription; +} + +/** + * Transcription of audio. This can be returned from a {@link LiveGenerativeModel} if transcription + * is enabled with the `inputAudioTranscription` or `outputAudioTranscription` properties on + * the {@link LiveGenerationConfig}. + * + * @beta + */ + +export interface Transcription { + /** + * The text transcription of the audio. + */ + text?: string; +} + +/** + * A request from the model for the client to execute one or more functions. + * + * @beta + */ +export interface LiveServerToolCall { + type: 'toolCall'; + /** + * An array of function calls to run. + */ + functionCalls: FunctionCall[]; +} + +/** + * Notification to cancel a previous function call triggered by {@link LiveServerToolCall}. + * + * @beta + */ +export interface LiveServerToolCallCancellation { + type: 'toolCallCancellation'; + /** + * IDs of function calls that were cancelled. These refer to the `id` property of a {@link FunctionCall}. + */ + functionIds: string[]; +} + +/** + * The types of responses that can be returned by {@link LiveSession.receive}. + * + * @beta + */ +export const LiveResponseType = { + SERVER_CONTENT: 'serverContent', + TOOL_CALL: 'toolCall', + TOOL_CALL_CANCELLATION: 'toolCallCancellation' +}; + +/** + * The types of responses that can be returned by {@link LiveSession.receive}. + * This is a property on all messages that can be used for type narrowing. This property is not + * returned by the server, it is assigned to a server message object once it's parsed. + * + * @beta + */ +export type LiveResponseType = + (typeof LiveResponseType)[keyof typeof LiveResponseType]; diff --git a/packages/ai/src/types/schema.ts b/packages/ai/src/types/schema.ts new file mode 100644 index 00000000000..8068ce62a91 --- /dev/null +++ b/packages/ai/src/types/schema.ts @@ -0,0 +1,145 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Contains the list of OpenAPI data types + * as defined by the + * {@link https://swagger.io/docs/specification/data-models/data-types/ | OpenAPI specification} + * @public + */ +export const SchemaType = { + /** String type. */ + STRING: 'string', + /** Number type. */ + NUMBER: 'number', + /** Integer type. */ + INTEGER: 'integer', + /** Boolean type. */ + BOOLEAN: 'boolean', + /** Array type. */ + ARRAY: 'array', + /** Object type. */ + OBJECT: 'object' +} as const; + +/** + * Contains the list of OpenAPI data types + * as defined by the + * {@link https://swagger.io/docs/specification/data-models/data-types/ | OpenAPI specification} + * @public + */ +export type SchemaType = (typeof SchemaType)[keyof typeof SchemaType]; + +/** + * Basic {@link Schema} properties shared across several Schema-related + * types. + * @public + */ +export interface SchemaShared { + /** + * An array of {@link Schema}. The generated data must be valid against any of the schemas + * listed in this array. This allows specifying multiple possible structures or types for a + * single field. + */ + anyOf?: T[]; + /** Optional. The format of the property. + * When using the Gemini Developer API ({@link GoogleAIBackend}), this must be either `'enum'` or + * `'date-time'`, otherwise requests will fail. + */ + format?: string; + /** Optional. The description of the property. */ + description?: string; + /** + * The title of the property. This helps document the schema's purpose but does not typically + * constrain the generated value. It can subtly guide the model by clarifying the intent of a + * field. + */ + title?: string; + /** Optional. The items of the property. */ + items?: T; + /** The minimum number of items (elements) in a schema of {@link (SchemaType:type)} `array`. */ + minItems?: number; + /** The maximum number of items (elements) in a schema of {@link (SchemaType:type)} `array`. */ + maxItems?: number; + /** Optional. Map of `Schema` objects. */ + properties?: { + [k: string]: T; + }; + /** A hint suggesting the order in which the keys should appear in the generated JSON string. */ + propertyOrdering?: string[]; + /** Optional. The enum of the property. */ + enum?: string[]; + /** Optional. The example of the property. */ + example?: unknown; + /** Optional. Whether the property is nullable. */ + nullable?: boolean; + /** The minimum value of a numeric type. */ + minimum?: number; + /** The maximum value of a numeric type. */ + maximum?: number; + [key: string]: unknown; +} + +/** + * Params passed to {@link Schema} static methods to create specific + * {@link Schema} classes. + * @public + */ +export interface SchemaParams extends SchemaShared {} + +/** + * Final format for {@link Schema} params passed to backend requests. + * @public + */ +export interface SchemaRequest extends SchemaShared { + /** + * The type of the property. this can only be undefined when using `anyOf` schemas, + * which do not have an explicit type in the {@link https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type | OpenAPI specification }. + */ + type?: SchemaType; + /** Optional. Array of required property. */ + required?: string[]; +} + +/** + * Interface for {@link Schema} class. + * @public + */ +export interface SchemaInterface extends SchemaShared { + /** + * The type of the property. this can only be undefined when using `anyof` schemas, + * which do not have an explicit type in the {@link https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type | OpenAPI Specification}. + */ + type?: SchemaType; +} + +/** + * Interface for JSON parameters in a schema of {@link (SchemaType:type)} + * "object" when not using the `Schema.object()` helper. + * @public + */ +export interface ObjectSchemaRequest extends SchemaRequest { + type: 'object'; + /** + * This is not a property accepted in the final request to the backend, but is + * a client-side convenience property that is only usable by constructing + * a schema through the `Schema.object()` helper method. Populating this + * property will cause response errors if the object is not wrapped with + * `Schema.object()`. + */ + optionalProperties?: never; +} diff --git a/packages/ai/src/websocket.test.ts b/packages/ai/src/websocket.test.ts new file mode 100644 index 00000000000..6d8f08282e7 --- /dev/null +++ b/packages/ai/src/websocket.test.ts @@ -0,0 +1,269 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import sinon, { SinonFakeTimers, SinonStub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { WebSocketHandlerImpl } from './websocket'; +import { AIError } from './errors'; + +use(sinonChai); +use(chaiAsPromised); + +class MockWebSocket { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + readyState: number = MockWebSocket.CONNECTING; + sentMessages: Array = []; + url: string; + private listeners: Map> = new Map(); + + constructor(url: string) { + this.url = url; + } + + send(data: string | ArrayBuffer): void { + if (this.readyState !== MockWebSocket.OPEN) { + throw new Error('WebSocket is not in OPEN state'); + } + this.sentMessages.push(data); + } + + close(): void { + if ( + this.readyState === MockWebSocket.CLOSED || + this.readyState === MockWebSocket.CLOSING + ) { + return; + } + this.readyState = MockWebSocket.CLOSING; + setTimeout(() => { + this.readyState = MockWebSocket.CLOSED; + this.dispatchEvent(new Event('close')); + }, 10); + } + + addEventListener(type: string, listener: EventListener): void { + if (!this.listeners.has(type)) { + this.listeners.set(type, new Set()); + } + this.listeners.get(type)!.add(listener); + } + + removeEventListener(type: string, listener: EventListener): void { + this.listeners.get(type)?.delete(listener); + } + + dispatchEvent(event: Event): void { + this.listeners.get(event.type)?.forEach(listener => listener(event)); + } + + triggerOpen(): void { + this.readyState = MockWebSocket.OPEN; + this.dispatchEvent(new Event('open')); + } + + triggerMessage(data: any): void { + this.dispatchEvent(new MessageEvent('message', { data })); + } + + triggerError(): void { + this.dispatchEvent(new Event('error')); + } +} + +describe('WebSocketHandlerImpl', () => { + let handler: WebSocketHandlerImpl; + let mockWebSocket: MockWebSocket; + let clock: SinonFakeTimers; + let webSocketStub: SinonStub; + + beforeEach(() => { + webSocketStub = sinon + .stub(globalThis, 'WebSocket') + .callsFake((url: string) => { + mockWebSocket = new MockWebSocket(url); + return mockWebSocket as any; + }); + clock = sinon.useFakeTimers(); + handler = new WebSocketHandlerImpl(); + }); + + afterEach(() => { + sinon.restore(); + clock.restore(); + }); + + describe('connect()', () => { + it('should resolve on open event', async () => { + const connectPromise = handler.connect('ws://test-url'); + expect(webSocketStub).to.have.been.calledWith('ws://test-url'); + + await clock.tickAsync(1); + mockWebSocket.triggerOpen(); + + await expect(connectPromise).to.be.fulfilled; + }); + + it('should reject on error event', async () => { + const connectPromise = handler.connect('ws://test-url'); + await clock.tickAsync(1); + mockWebSocket.triggerError(); + + await expect(connectPromise).to.be.rejectedWith( + AIError, + /Error event raised on WebSocket/ + ); + }); + }); + + describe('listen()', () => { + beforeEach(async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + }); + + it('should yield multiple messages as they arrive', async () => { + const generator = handler.listen(); + + const received: unknown[] = []; + const listenPromise = (async () => { + for await (const msg of generator) { + received.push(msg); + } + })(); + + // Use tickAsync to allow the consumer to start listening + await clock.tickAsync(1); + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 1 })])); + + await clock.tickAsync(10); + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 2 })])); + + await clock.tickAsync(5); + mockWebSocket.close(); + await clock.runAllAsync(); // Let timers finish + + await listenPromise; // Wait for the consumer to finish + + expect(received).to.deep.equal([ + { + foo: 1 + }, + { + foo: 2 + } + ]); + }); + + it('should buffer messages that arrive before the consumer calls .next()', async () => { + const generator = handler.listen(); + + // Create a promise that will consume the generator in a separate async context + const received: unknown[] = []; + const consumptionPromise = (async () => { + for await (const message of generator) { + received.push(message); + } + })(); + + await clock.tickAsync(1); + + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 1 })])); + mockWebSocket.triggerMessage(new Blob([JSON.stringify({ foo: 2 })])); + + await clock.tickAsync(1); + mockWebSocket.close(); + await clock.runAllAsync(); + + await consumptionPromise; + + expect(received).to.deep.equal([ + { + foo: 1 + }, + { + foo: 2 + } + ]); + }); + }); + + describe('close()', () => { + it('should be idempotent and not throw if called multiple times', async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + + const closePromise1 = handler.close(); + await clock.runAllAsync(); + await closePromise1; + + await expect(handler.close()).to.be.fulfilled; + }); + + it('should wait for the onclose event before resolving', async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + + let closed = false; + const closePromise = handler.close().then(() => { + closed = true; + }); + + // The promise should not have resolved yet + await clock.tickAsync(5); + expect(closed).to.be.false; + + // Now, let the mock's setTimeout for closing run, which triggers onclose + await clock.tickAsync(10); + + await expect(closePromise).to.be.fulfilled; + expect(closed).to.be.true; + }); + }); + + describe('Interaction between listen() and close()', () => { + it('should allow close() to take precedence and resolve correctly, while also terminating the listener', async () => { + const connectPromise = handler.connect('ws://test'); + mockWebSocket.triggerOpen(); + await connectPromise; + + const generator = handler.listen(); + const listenPromise = (async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _ of generator) { + } + })(); + + const closePromise = handler.close(); + + await clock.runAllAsync(); + + await expect(closePromise).to.be.fulfilled; + await expect(listenPromise).to.be.fulfilled; + + expect(mockWebSocket.readyState).to.equal(MockWebSocket.CLOSED); + }); + }); +}); diff --git a/packages/ai/src/websocket.ts b/packages/ai/src/websocket.ts new file mode 100644 index 00000000000..fa34f2d48c3 --- /dev/null +++ b/packages/ai/src/websocket.ts @@ -0,0 +1,241 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AIError } from './errors'; +import { logger } from './logger'; +import { AIErrorCode } from './types'; + +/** + * A standardized interface for interacting with a WebSocket connection. + * This abstraction allows the SDK to use the appropriate WebSocket implementation + * for the current JS environment (Browser vs. Node) without + * changing the core logic of the `LiveSession`. + * @internal + */ + +export interface WebSocketHandler { + /** + * Establishes a connection to the given URL. + * + * @param url The WebSocket URL (e.g., wss://...). + * @returns A promise that resolves on successful connection or rejects on failure. + */ + connect(url: string): Promise; + + /** + * Sends data over the WebSocket. + * + * @param data The string or binary data to send. + */ + send(data: string | ArrayBuffer): void; + + /** + * Returns an async generator that yields parsed JSON objects from the server. + * The yielded type is `unknown` because the handler cannot guarantee the shape of the data. + * The consumer is responsible for type validation. + * The generator terminates when the connection is closed. + * + * @returns A generator that allows consumers to pull messages using a `for await...of` loop. + */ + listen(): AsyncGenerator; + + /** + * Closes the WebSocket connection. + * + * @param code - A numeric status code explaining why the connection is closing. + * @param reason - A human-readable string explaining why the connection is closing. + */ + close(code?: number, reason?: string): Promise; +} + +/** + * A wrapper for the native `WebSocket` available in both Browsers and Node >= 22. + * + * @internal + */ +export class WebSocketHandlerImpl implements WebSocketHandler { + private ws?: WebSocket; + + constructor() { + if (typeof WebSocket === 'undefined') { + throw new AIError( + AIErrorCode.UNSUPPORTED, + 'The WebSocket API is not available in this environment. ' + + 'The "Live" feature is not supported here. It is supported in ' + + 'modern browser windows, Web Workers with WebSocket support, and Node >= 22.' + ); + } + } + + connect(url: string): Promise { + return new Promise((resolve, reject) => { + this.ws = new WebSocket(url); + this.ws.binaryType = 'blob'; // Only important to set in Node + this.ws.addEventListener('open', () => resolve(), { once: true }); + this.ws.addEventListener( + 'error', + () => + reject( + new AIError( + AIErrorCode.FETCH_ERROR, + `Error event raised on WebSocket` + ) + ), + { once: true } + ); + this.ws!.addEventListener('close', (closeEvent: CloseEvent) => { + if (closeEvent.reason) { + logger.warn( + `WebSocket connection closed by server. Reason: '${closeEvent.reason}'` + ); + } + }); + }); + } + + send(data: string | ArrayBuffer): void { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + throw new AIError(AIErrorCode.REQUEST_ERROR, 'WebSocket is not open.'); + } + this.ws.send(data); + } + + async *listen(): AsyncGenerator { + if (!this.ws) { + throw new AIError( + AIErrorCode.REQUEST_ERROR, + 'WebSocket is not connected.' + ); + } + + const messageQueue: unknown[] = []; + const errorQueue: Error[] = []; + let resolvePromise: (() => void) | null = null; + let isClosed = false; + + const messageListener = async (event: MessageEvent): Promise => { + let data: string; + if (event.data instanceof Blob) { + data = await event.data.text(); + } else if (typeof event.data === 'string') { + data = event.data; + } else { + errorQueue.push( + new AIError( + AIErrorCode.PARSE_FAILED, + `Failed to parse WebSocket response. Expected data to be a Blob or string, but was ${typeof event.data}.` + ) + ); + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + return; + } + + try { + const obj = JSON.parse(data) as unknown; + messageQueue.push(obj); + } catch (e) { + const err = e as Error; + errorQueue.push( + new AIError( + AIErrorCode.PARSE_FAILED, + `Error parsing WebSocket message to JSON: ${err.message}` + ) + ); + } + + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + }; + + const errorListener = (): void => { + errorQueue.push( + new AIError(AIErrorCode.FETCH_ERROR, 'WebSocket connection error.') + ); + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + }; + + const closeListener = (event: CloseEvent): void => { + if (event.reason) { + logger.warn( + `WebSocket connection closed by the server with reason: ${event.reason}` + ); + } + isClosed = true; + if (resolvePromise) { + resolvePromise(); + resolvePromise = null; + } + // Clean up listeners to prevent memory leaks + this.ws?.removeEventListener('message', messageListener); + this.ws?.removeEventListener('close', closeListener); + this.ws?.removeEventListener('error', errorListener); + }; + + this.ws.addEventListener('message', messageListener); + this.ws.addEventListener('close', closeListener); + this.ws.addEventListener('error', errorListener); + + while (!isClosed) { + if (errorQueue.length > 0) { + const error = errorQueue.shift()!; + throw error; + } + if (messageQueue.length > 0) { + yield messageQueue.shift()!; + } else { + await new Promise(resolve => { + resolvePromise = resolve; + }); + } + } + + // If the loop terminated because isClosed is true, check for any final errors + if (errorQueue.length > 0) { + const error = errorQueue.shift()!; + throw error; + } + } + + close(code?: number, reason?: string): Promise { + return new Promise(resolve => { + if (!this.ws) { + return resolve(); + } + + this.ws.addEventListener('close', () => resolve(), { once: true }); + // Calling 'close' during these states results in an error. + if ( + this.ws.readyState === WebSocket.CLOSED || + this.ws.readyState === WebSocket.CONNECTING + ) { + return resolve(); + } + + if (this.ws.readyState !== WebSocket.CLOSING) { + this.ws.close(code, reason); + } + }); + } +} diff --git a/packages/ai/test-utils/base64cat.ts b/packages/ai/test-utils/base64cat.ts new file mode 100644 index 00000000000..45325a1bf55 --- /dev/null +++ b/packages/ai/test-utils/base64cat.ts @@ -0,0 +1,19 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const base64Cat = + 'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAABJWlDQ1BrQ0dDb2xvclNwYWNlQWRvYmVSR0IxOTk4AAAokWNgYFJILCjIYRJgYMjNKykKcndSiIiMUmB/zsDNwAnE2gwGicnFBY4BAT4MQACjUcG3awyMIPqyLsgsTHm8gCsltTgZSP8B4uzkgqISBgbGDCBbubykAMTuAbJFkrLB7AUgdhHQgUD2FhA7HcI+AVYDYd8BqwkJcgayPwDZfElgNhPILr50CFsAxIbaCwKCjin5SakKIN9rGFpaWmiS6AeCoCS1ogREO+cXVBZlpmeUKDgCQypVwTMvWU9HwcjAyJiBARTuENWfA8HhySh2BiGGAAixORIMDP5LGRhY/iDETHoZGBboMDDwT0WIqRkyMAjoMzDsm5NcWlQGNYaRCWgnIT4AXxVKdgMmGHwAAAFQZVhJZk1NACoAAAAIAAkBDgACAAAARwAAAHoBEgADAAAAAQABAAABGgAFAAAAAQAAAMIBGwAFAAAAAQAAAMoBKAADAAAAAQACAAABMQACAAAACwAAANIBMgACAAAAFAAAAN6CmAACAAAAEwAAAPKHaQAEAAAAAQAAAQYAAAAAUGhvdG9ncmFwaCBmcm9tIFdhbHRlciBDaGFuZG9oYTogVGhlIENhdCBQaG90b2dyYXBoZXIgKEFwZXJ0dXJlLCAyMDE1KQAAAAABLAAAAAEAAAEsAAAAAVBob3RvU2NhcGUAADIwMTU6MDY6MTggMTE6MTM6NTMAwqkgV2FsdGVyIENoYW5kb2hhAAAABJAAAAcAAAAEMDIyMZAEAAIAAAAUAAABPKACAAQAAAABAAABAKADAAQAAAABAAABAAAAAAAyMDE1OjA0OjAxIDE1OjE3OjAzALUWG8IAAAAJcEhZcwAALiMAAC4jAXilP3YAADtdaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBSaWdodHM9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9yaWdodHMvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgICAgICAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgICAgICAgICB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIKICAgICAgICAgICAgeG1sbnM6eHdudj0iaHR0cDovL25zLnhpbmV0LmNvbS9ucy94aW5ldHNjaGVtYSMiCiAgICAgICAgICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyI+CiAgICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2UvdGlmZjwvZGM6Zm9ybWF0PgogICAgICAgICA8ZGM6ZGVzY3JpcHRpb24+CiAgICAgICAgICAgIDxyZGY6QWx0PgogICAgICAgICAgICAgICA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiPlBob3RvZ3JhcGggZnJvbSBXYWx0ZXIgQ2hhbmRvaGE6IFRoZSBDYXQgUGhvdG9ncmFwaGVyIChBcGVydHVyZSwgMjAxNSk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOmRlc2NyaXB0aW9uPgogICAgICAgICA8ZGM6cmlnaHRzPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij7CqSBXYWx0ZXIgQ2hhbmRvaGE8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnJpZ2h0cz4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6Q29tcHJlc3Npb24+MTwvdGlmZjpDb21wcmVzc2lvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+MzAwPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj4zMDA8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMDU3PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6RXhpZlZlcnNpb24+MDIyMTwvZXhpZjpFeGlmVmVyc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT42NTUzNTwvZXhpZjpDb2xvclNwYWNlPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+MjE3MzwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDx4bXBSaWdodHM6TWFya2VkPlRydWU8L3htcFJpZ2h0czpNYXJrZWQ+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGhvdG9TY2FwZTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOlJhdGluZz4xPC94bXA6UmF0aW5nPgogICAgICAgICA8eG1wOk1ldGFkYXRhRGF0ZT4yMDE1LTA2LTE4VDExOjEzOjUzLTA0OjAwPC94bXA6TWV0YWRhdGFEYXRlPgogICAgICAgICA8eG1wOkNyZWF0ZURhdGU+MjAxNS0wNC0wMVQxNToxNzowMy0wNDowMDwveG1wOkNyZWF0ZURhdGU+CiAgICAgICAgIDx4bXA6TGFiZWw+QXBwcm92ZWQ8L3htcDpMYWJlbD4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMTUtMDYtMThUMTE6MTM6NTMtMDQ6MDA8L3htcDpNb2RpZnlEYXRlPgogICAgICAgICA8eG1wTU06SGlzdG9yeT4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTAxVDE1OjE3OjAzLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOkQ2NDkzMDk2MzgyNzY4MTE4NzFGRDg0Mjk3MDE2Mjk3PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPmNyZWF0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgKE1hY2ludG9zaCk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTUtMDQtMDFUMTg6NTg6MDEtMDQ6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6YTI2NDlkMWMtM2YzNy00YzVlLWFmNGMtN2MxMDg1ZTc4Yjc5PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jb252ZXJ0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+ZnJvbSBpbWFnZS90aWZmIHRvIGltYWdlL2Vwc2Y8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5kZXJpdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpwYXJhbWV0ZXJzPmNvbnZlcnRlZCBmcm9tIGltYWdlL3RpZmYgdG8gaW1hZ2UvZXBzZjwvc3RFdnQ6cGFyYW1ldGVycz4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgKE1hY2ludG9zaCk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTUtMDQtMDFUMTg6NTg6MDEtMDQ6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6OGEwOTVhMjMtNTBlMS00ZDA3LWI0YjctNGNhNDA4YzVlODcyPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTAxVDE5OjQ0OjQ2LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjlkYzRhZDAyLTgwMzItNDMyZS05YjhlLTk1YThlMTQ1YzJkMDwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+Y29udmVydGVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpwYXJhbWV0ZXJzPmZyb20gaW1hZ2UvZXBzZiB0byBpbWFnZS90aWZmPC9zdEV2dDpwYXJhbWV0ZXJzPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+ZGVyaXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6cGFyYW1ldGVycz5jb252ZXJ0ZWQgZnJvbSBpbWFnZS9lcHNmIHRvIGltYWdlL3RpZmY8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTAxVDE5OjQ0OjQ2LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjRkZTAwN2U3LTk5MWUtNDc5MS1hOTZkLTE5ZmVhMDllNWI4NTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNC0wMVQyMDo0Njo1Ni0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDowMjMyZmIxZC1jYjQ0LTQwOGYtYWE2MC04N2U3MDYyMGNhYmM8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93czwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNC0xOVQwMDoxODozMy0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDpCOUM3OENGNDQ2RTZFNDExOTRFQzkzQjJERjdGRjg2NTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+Y29udmVydGVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpwYXJhbWV0ZXJzPmZyb20gaW1hZ2UvdGlmZiB0byBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wPC9zdEV2dDpwYXJhbWV0ZXJzPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+ZGVyaXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6cGFyYW1ldGVycz5jb252ZXJ0ZWQgZnJvbSBpbWFnZS90aWZmIHRvIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3A8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENTNS4xIFdpbmRvd3M8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTUtMDQtMTlUMDA6MTg6MzMtMDQ6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6QkFDNzhDRjQ0NkU2RTQxMTk0RUM5M0IyREY3RkY4NjU8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93czwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNC0xOVQxMjo0Mjo1OC0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDowNUZDQzI0NUIxRTZFNDExQjVCMEU3QTI0NTYyMUZGNjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDUzUuMSBXaW5kb3dzPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTE5VDEyOjQzOjMwLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjA4RkNDMjQ1QjFFNkU0MTFCNUIwRTdBMjQ1NjIxRkY2PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jb252ZXJ0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+ZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3RpZmY8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5kZXJpdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpwYXJhbWV0ZXJzPmNvbnZlcnRlZCBmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvdGlmZjwvc3RFdnQ6cGFyYW1ldGVycz4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93czwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNC0xOVQxMjo0MzozMC0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDowOUZDQzI0NUIxRTZFNDExQjVCMEU3QTI0NTYyMUZGNjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIxVDEwOjQ4OjM1LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmUzODZlNjQ4LWZmNjYtNDA5NC04NWEyLWY2NjgxZTRiM2I4Mjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIEJyaWRnZSBDQyAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+L21ldGFkYXRhPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIzVDIxOjEwOjQ2LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmIwZmJjMzZkLThhMjgtNDU4NC05NTlkLTk5NjFmZDEyMDA5Mjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIzVDIyOjIyOjIzLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmM1MzNhNjRlLTk0OTktNDMzOS05MjM4LThhOGY0Nzc3NzQ3YTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIzVDIyOjIzOjQyLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmRiMjlmZTVlLWQ2ZDAtNDBjZi04Y2RkLTBkYTU2NDgwMTU2YTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDYW1lcmEgUmF3IDguODwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+L21ldGFkYXRhPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIzVDIyOjMyOjMwLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmRkMzFmNTVlLTUyMzctNDA5ZC1hMTk2LTdhM2Q1ZTIwMTM0Mjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDYW1lcmEgUmF3IDguOCAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+L21ldGFkYXRhPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIzVDIyOjMzOjM3LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmUzNjExZWQ3LTI4YmYtNGE5MS1hZDA5LWVhNjc3M2U4Y2YyOTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNC0yOFQxMDowMTo1MS0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDo1MDViZDMzYS03ZDJiLTRiNzQtOTU1My0xNjIzYjE4MzExMmM8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNi0xOFQxMDowMjo1OS0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDpFMzBDRTVCOUMyMTVFNTExQkVDQkU3RTMxNDVCODA4NTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDYW1lcmEgUmF3IDguMjwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+L21ldGFkYXRhPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA2LTE4VDEwOjA0OjQ1LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOkE4MUY0NUYxQzIxNUU1MTE4OTE4RkI0OTg3NjBDNzI5PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENhbWVyYSBSYXcgOC4yIChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+L21ldGFkYXRhPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA2LTE4VDEwOjA1OjU1LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjMzNTk0ZDkyLWNlMmYtNGE0Ny1iNTFmLTRmOTUzNjVmNWJkNTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA2LTE4VDExOjEyLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjA3QUE3MDVFQ0MxNUU1MTE5NjEwQjRCN0U1NjM5OEUwPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTUtMDYtMThUMTE6MTM6NTMtMDQ6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6MTFBQTcwNUVDQzE1RTUxMTk2MTBCNEI3RTU2Mzk4RTA8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC94bXBNTTpIaXN0b3J5PgogICAgICAgICA8eG1wTU06T3JpZ2luYWxEb2N1bWVudElEPnhtcC5kaWQ6RDY0OTMwOTYzODI3NjgxMTg3MUZEODQyOTcwMTYyOTc8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkRlcml2ZWRGcm9tIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgPHN0UmVmOm9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOkQ2NDkzMDk2MzgyNzY4MTE4NzFGRDg0Mjk3MDE2Mjk3PC9zdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgICAgIDxzdFJlZjppbnN0YW5jZUlEPnhtcC5paWQ6MDhGQ0MyNDVCMUU2RTQxMUI1QjBFN0EyNDU2MjFGRjY8L3N0UmVmOmluc3RhbmNlSUQ+CiAgICAgICAgICAgIDxzdFJlZjpkb2N1bWVudElEPnhtcC5kaWQ6RDY0OTMwOTYzODI3NjgxMTg3MUZEODQyOTcwMTYyOTc8L3N0UmVmOmRvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAg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'; diff --git a/packages/ai/test-utils/cat.jpeg b/packages/ai/test-utils/cat.jpeg new file mode 100644 index 00000000000..2c21763bad2 Binary files /dev/null and b/packages/ai/test-utils/cat.jpeg differ diff --git a/packages/ai/test-utils/cat.png b/packages/ai/test-utils/cat.png new file mode 100644 index 00000000000..edcddfacc92 Binary files /dev/null and b/packages/ai/test-utils/cat.png differ diff --git a/packages/ai/test-utils/convert-mocks.ts b/packages/ai/test-utils/convert-mocks.ts new file mode 100644 index 00000000000..34233a73ace --- /dev/null +++ b/packages/ai/test-utils/convert-mocks.ts @@ -0,0 +1,124 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable @typescript-eslint/no-require-imports */ +const { readdirSync, readFileSync, writeFileSync } = require('node:fs'); +const { join } = require('node:path'); + +type BackendName = import('./types').BackendName; // Import type without triggering ES module detection + +const MOCK_RESPONSES_DIR_PATH = join( + __dirname, + 'vertexai-sdk-test-data', + 'mock-responses' +); +const MOCK_LOOKUP_OUTPUT_PATH = join(__dirname, 'mocks-lookup.ts'); + +const mockDirs: Record = { + vertexAI: join(MOCK_RESPONSES_DIR_PATH, 'vertexai'), + googleAI: join(MOCK_RESPONSES_DIR_PATH, 'googleai') +}; + +/** + * Generates a JS file that exports maps from filenames to JSON mock responses (as strings) + * for each backend. + * + * This allows tests that run in a browser to access the mock responses without having to + * read from local disk and requiring 'fs'. + */ +function generateMockLookupFile(): void { + console.log('Generating mock lookup file...'); + const vertexAIMockLookupText = generateMockLookup('vertexAI'); + const googleAIMockLookupText = generateMockLookup('googleAI'); + + const fileText = ` +/** + * DO NOT EDIT - This file was generated by the packages/ai/test-utils/convert-mocks.ts script. + * + * These objects map mock response filenames to their JSON contents. + * + * Mock response files are pulled from https://github.com/FirebaseExtended/vertexai-sdk-test-data. + */ + +// Automatically generated at: ${new Date().toISOString()} + +${vertexAIMockLookupText} + +${googleAIMockLookupText} +`; + try { + writeFileSync(MOCK_LOOKUP_OUTPUT_PATH, fileText, 'utf-8'); + console.log( + `Successfully generated mock lookup file at: ${MOCK_LOOKUP_OUTPUT_PATH}` + ); + } catch (err) { + console.error( + `Error writing mock lookup file to ${MOCK_LOOKUP_OUTPUT_PATH}:`, + err + ); + process.exit(1); + } +} + +/** + * Given a directory that contains mock response files, reads through all the files, + * maps file names to file contents, and returns a string of typescript code + * that exports that map as an object. + */ +function generateMockLookup(backendName: BackendName): string { + const lookup: Record = {}; + const mockDir = mockDirs[backendName]; + let mockFilenames: string[]; + + console.log( + `Processing mocks for "${backendName}" from directory: ${mockDir}` + ); + + try { + mockFilenames = readdirSync(mockDir); + } catch (err) { + console.error( + `Error reading directory ${mockDir} for ${backendName}:`, + err + ); + return `export const ${backendName}MocksLookup: Record = {};`; + } + + if (mockFilenames.length === 0) { + console.warn(`No .json files found in ${mockDir} for ${backendName}.`); + } + + for (const mockFilename of mockFilenames) { + const mockFilepath = `${mockDir}/${mockFilename}`; + try { + const fullText = readFileSync(mockFilepath, 'utf-8'); + lookup[mockFilename] = fullText; + } catch (err) { + console.error( + `Error reading mock file ${mockFilepath} for ${backendName}:`, + err + ); + } + } + + // Use JSON.stringify with indentation for readable output in the generated file + const lookupJsonString = JSON.stringify(lookup, null, 2); + + return `export const ${backendName}MocksLookup: Record = ${lookupJsonString};`; +} + +generateMockLookupFile(); diff --git a/packages/ai/test-utils/get-fake-firebase-services.ts b/packages/ai/test-utils/get-fake-firebase-services.ts new file mode 100644 index 00000000000..f639d919ed0 --- /dev/null +++ b/packages/ai/test-utils/get-fake-firebase-services.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + FirebaseApp, + initializeApp, + _registerComponent, + _addOrOverwriteComponent +} from '@firebase/app'; +import { Component, ComponentType, InstanceFactory } from '@firebase/component'; +import { FirebaseAppCheckInternal } from '@firebase/app-check-interop-types'; +import { AI_TYPE } from '../src/constants'; +import { factory as factoryNode } from '../src/factory-node'; +import { ChromeAdapter, InferenceMode } from '../src/types'; + +const fakeConfig = { + projectId: 'projectId', + authDomain: 'authDomain', + messagingSenderId: 'messagingSenderId', + databaseURL: 'databaseUrl', + storageBucket: 'storageBucket' +}; + +export function getFullApp( + fakeAppParams?: { + appId?: string; + apiKey?: string; + }, + factory: InstanceFactory<'AI'> = factoryNode +): FirebaseApp { + _registerComponent( + new Component(AI_TYPE, factory, ComponentType.PUBLIC).setMultipleInstances( + true + ) + ); + _registerComponent( + new Component( + 'app-check-internal', + () => { + return {} as FirebaseAppCheckInternal; + }, + ComponentType.PUBLIC + ) + ); + const app = initializeApp({ ...fakeConfig, ...fakeAppParams }); + _addOrOverwriteComponent( + app, + //@ts-ignore + new Component( + 'heartbeat', + // @ts-ignore + () => { + return { + triggerHeartbeat: () => {} + }; + }, + ComponentType.PUBLIC + ) + ); + return app; +} + +export const fakeChromeAdapter: ChromeAdapter = { + mode: InferenceMode.PREFER_ON_DEVICE, + // Individual tests may stub this to resolve true as needed. + isAvailable: () => Promise.resolve(false), + generateContent: () => Promise.resolve({} as Response), + generateContentStream: () => Promise.resolve({} as Response), + countTokens: () => Promise.resolve({} as Response) +}; diff --git a/packages/ai/test-utils/mock-response.ts b/packages/ai/test-utils/mock-response.ts new file mode 100644 index 00000000000..4963bcbb193 --- /dev/null +++ b/packages/ai/test-utils/mock-response.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BackendName } from './types'; +import { vertexAIMocksLookup, googleAIMocksLookup } from './mocks-lookup'; + +const mockSetMaps: Record> = { + 'vertexAI': vertexAIMocksLookup, + 'googleAI': googleAIMocksLookup +}; + +/** + * Mock native Response.body + * Streams contents of json file in 20 character chunks + */ +export function getChunkedStream( + input: string, + chunkLength = 20 +): ReadableStream { + const encoder = new TextEncoder(); + let currentChunkStart = 0; + + const stream = new ReadableStream({ + start(controller) { + while (currentChunkStart < input.length) { + const substring = input.slice( + currentChunkStart, + currentChunkStart + chunkLength + ); + currentChunkStart += chunkLength; + const chunk = encoder.encode(substring); + controller.enqueue(chunk); + } + controller.close(); + } + }); + + return stream; +} + +export function getMockResponseStreaming( + backendName: BackendName, + filename: string, + chunkLength: number = 20 +): Partial { + const mocksMap = mockSetMaps[backendName]; + if (!(filename in mocksMap)) { + throw Error(`${backendName} mock response file '${filename}' not found.`); + } + const fullText = mocksMap[filename]; + + return { + body: getChunkedStream(fullText, chunkLength) + }; +} + +export function getMockResponse( + backendName: BackendName, + filename: string +): Partial { + const mocksLookup = mockSetMaps[backendName]; + if (!(filename in mocksLookup)) { + throw Error(`${backendName} mock response file '${filename}' not found.`); + } + const fullText = mocksLookup[filename]; + + return { + ok: true, + json: () => Promise.resolve(JSON.parse(fullText)) + }; +} diff --git a/packages/ai/test-utils/types.ts b/packages/ai/test-utils/types.ts new file mode 100644 index 00000000000..00b99eef55a --- /dev/null +++ b/packages/ai/test-utils/types.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type BackendName = 'vertexAI' | 'googleAI'; diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json new file mode 100644 index 00000000000..4e0ae05eebc --- /dev/null +++ b/packages/ai/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "exclude": ["dist/**/*"] +} diff --git a/packages/analytics-compat/.eslintrc.js b/packages/analytics-compat/.eslintrc.js new file mode 100644 index 00000000000..85388055d9d --- /dev/null +++ b/packages/analytics-compat/.eslintrc.js @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const path = require('path'); + +module.exports = { + 'extends': '../../config/.eslintrc.js', + 'parserOptions': { + 'project': 'tsconfig.json', + 'tsconfigRootDir': __dirname + }, + rules: { + 'import/no-extraneous-dependencies': [ + 'error', + { + 'packageDir': [path.resolve(__dirname, '../../'), __dirname] + } + ] + } +}; diff --git a/packages/analytics-compat/CHANGELOG.md b/packages/analytics-compat/CHANGELOG.md new file mode 100644 index 00000000000..a175e1a29eb --- /dev/null +++ b/packages/analytics-compat/CHANGELOG.md @@ -0,0 +1,412 @@ +# @firebase/analytics-compat + +## 0.2.25 + +### Patch Changes + +- Updated dependencies [[`1bcf83d`](https://github.com/firebase/firebase-js-sdk/commit/1bcf83d7f0640dff67c20939fb9af7bae6a941e0)]: + - @firebase/analytics@0.10.19 + +## 0.2.24 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/analytics@0.10.18 + - @firebase/component@0.7.0 + - @firebase/util@1.13.0 + +## 0.2.23 + +### Patch Changes + +- Updated dependencies [[`13e6cce`](https://github.com/firebase/firebase-js-sdk/commit/13e6cce882d687e06c8d9bfb56895f8a77fc57b5), [`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/analytics@0.10.17 + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + +## 0.2.22 + +### Patch Changes + +- Updated dependencies [[`8a03143`](https://github.com/firebase/firebase-js-sdk/commit/8a03143b9217effdd86d68bdf195493c0979aa27)]: + - @firebase/util@1.12.0 + - @firebase/analytics@0.10.16 + - @firebase/component@0.6.17 + +## 0.2.21 + +### Patch Changes + +- Updated dependencies [[`9bcd1ea`](https://github.com/firebase/firebase-js-sdk/commit/9bcd1ea9b8cc5b55692765d40df000da8ddef02b)]: + - @firebase/util@1.11.3 + - @firebase/analytics@0.10.15 + - @firebase/component@0.6.16 + +## 0.2.20 + +### Patch Changes + +- Updated dependencies [[`8593fa0`](https://github.com/firebase/firebase-js-sdk/commit/8593fa05bd884c2f1f6f3b4ae062efa48af93d24)]: + - @firebase/util@1.11.2 + - @firebase/analytics@0.10.14 + - @firebase/component@0.6.15 + +## 0.2.19 + +### Patch Changes + +- Updated dependencies [[`ea1f913`](https://github.com/firebase/firebase-js-sdk/commit/ea1f9139e6baec0269fbb91233fd3f7f4b0d5875), [`0e12766`](https://github.com/firebase/firebase-js-sdk/commit/0e127664946ba324c6566a02b393dafd23fc1ddb)]: + - @firebase/util@1.11.1 + - @firebase/analytics@0.10.13 + - @firebase/component@0.6.14 + +## 0.2.18 + +### Patch Changes + +- Updated dependencies [[`777f465`](https://github.com/firebase/firebase-js-sdk/commit/777f465ff37495ff933a29583769ce8a6a2b59b5)]: + - @firebase/util@1.11.0 + - @firebase/analytics@0.10.12 + - @firebase/component@0.6.13 + +## 0.2.17 + +### Patch Changes + +- Updated dependencies [[`25a6204c1`](https://github.com/firebase/firebase-js-sdk/commit/25a6204c1531b6c772e5368d12b2411ae1d21bbc)]: + - @firebase/util@1.10.3 + - @firebase/analytics@0.10.11 + - @firebase/component@0.6.12 + +## 0.2.16 + +### Patch Changes + +- [`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1) [#8604](https://github.com/firebase/firebase-js-sdk/pull/8604) - Upgrade to TypeScript 5.5.4 + +- Updated dependencies [[`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1)]: + - @firebase/analytics@0.10.10 + - @firebase/analytics-types@0.8.3 + - @firebase/component@0.6.11 + - @firebase/util@1.10.2 + +## 0.2.15 + +### Patch Changes + +- [`479226bf3`](https://github.com/firebase/firebase-js-sdk/commit/479226bf3ebd99017bb12fa21440c75715658702) [#8475](https://github.com/firebase/firebase-js-sdk/pull/8475) - Remove ES5 bundles. The minimum required ES version is now ES2017. + +- Updated dependencies [[`479226bf3`](https://github.com/firebase/firebase-js-sdk/commit/479226bf3ebd99017bb12fa21440c75715658702)]: + - @firebase/analytics@0.10.9 + - @firebase/component@0.6.10 + - @firebase/util@1.10.1 + +## 0.2.14 + +### Patch Changes + +- Updated dependencies [[`16d62d4fa`](https://github.com/firebase/firebase-js-sdk/commit/16d62d4fa16faddb8cb676c0af3f29b8a5824741)]: + - @firebase/util@1.10.0 + - @firebase/analytics@0.10.8 + - @firebase/component@0.6.9 + +## 0.2.13 + +### Patch Changes + +- Updated dependencies [[`a9f844066`](https://github.com/firebase/firebase-js-sdk/commit/a9f844066045d8567ae143bae77d184ac227690d)]: + - @firebase/analytics@0.10.7 + +## 0.2.12 + +### Patch Changes + +- Updated dependencies [[`f58d48cd4`](https://github.com/firebase/firebase-js-sdk/commit/f58d48cd42c9f09eab4d8b2a606af360528917f8)]: + - @firebase/analytics@0.10.6 + +## 0.2.11 + +### Patch Changes + +- Updated dependencies [[`192561b15`](https://github.com/firebase/firebase-js-sdk/commit/192561b1552a08840d8e341f30f3dbe275465558)]: + - @firebase/util@1.9.7 + - @firebase/analytics@0.10.5 + - @firebase/component@0.6.8 + +## 0.2.10 + +### Patch Changes + +- Updated dependencies [[`f66769cca`](https://github.com/firebase/firebase-js-sdk/commit/f66769cca243019354f88ac9dc8de07caf9de56e)]: + - @firebase/analytics@0.10.4 + +## 0.2.9 + +### Patch Changes + +- [`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7) [#8237](https://github.com/firebase/firebase-js-sdk/pull/8237) - Bump all packages so staging works. + +- Updated dependencies [[`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7)]: + - @firebase/analytics@0.10.3 + - @firebase/analytics-types@0.8.2 + - @firebase/component@0.6.7 + - @firebase/util@1.9.6 + +## 0.2.8 + +### Patch Changes + +- [`0c5150106`](https://github.com/firebase/firebase-js-sdk/commit/0c515010607bf2223b468acb94c672b1279ed1a0) [#8079](https://github.com/firebase/firebase-js-sdk/pull/8079) - Update `repository.url` field in all `package.json` files to NPM's preferred format. + +- Updated dependencies [[`0c5150106`](https://github.com/firebase/firebase-js-sdk/commit/0c515010607bf2223b468acb94c672b1279ed1a0)]: + - @firebase/analytics-types@0.8.1 + - @firebase/analytics@0.10.2 + - @firebase/component@0.6.6 + - @firebase/util@1.9.5 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies [[`bf59c0aed`](https://github.com/firebase/firebase-js-sdk/commit/bf59c0aedefabae9bff4d777e1591fe496259293), [`434f8418c`](https://github.com/firebase/firebase-js-sdk/commit/434f8418c3db3ae98489a8461c437c248c039070)]: + - @firebase/analytics@0.10.1 + - @firebase/util@1.9.4 + - @firebase/component@0.6.5 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [[`0a27d2fbf`](https://github.com/firebase/firebase-js-sdk/commit/0a27d2fbf268f07099d4fa5ecab7fbf35a579780)]: + - @firebase/analytics@0.10.0 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies [[`3435ba945`](https://github.com/firebase/firebase-js-sdk/commit/3435ba945a9febf5a0aece05517a5656f58b246f)]: + - @firebase/analytics@0.9.5 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies [[`c59f537b1`](https://github.com/firebase/firebase-js-sdk/commit/c59f537b1262b5d7997291b8c1e9324d378effb6)]: + - @firebase/util@1.9.3 + - @firebase/analytics@0.9.4 + - @firebase/component@0.6.4 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [[`d071bd1ac`](https://github.com/firebase/firebase-js-sdk/commit/d071bd1acaa0583b4dd3454387fc58eafddb5c30)]: + - @firebase/util@1.9.2 + - @firebase/analytics@0.9.3 + - @firebase/component@0.6.3 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies [[`0bab0b7a7`](https://github.com/firebase/firebase-js-sdk/commit/0bab0b7a786d1563bf665904c7097d1fe06efce5)]: + - @firebase/util@1.9.1 + - @firebase/analytics@0.9.2 + - @firebase/component@0.6.2 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [[`d4114a4f7`](https://github.com/firebase/firebase-js-sdk/commit/d4114a4f7da3f469c0c900416ac8beee58885ec3), [`06dc1364d`](https://github.com/firebase/firebase-js-sdk/commit/06dc1364d7560f4c563e1ccc89af9cad4cd91df8)]: + - @firebase/util@1.9.0 + - @firebase/analytics@0.9.1 + - @firebase/component@0.6.1 + +## 0.2.0 + +### Minor Changes + +- [`1625f7a95`](https://github.com/firebase/firebase-js-sdk/commit/1625f7a95cc3ffb666845db0a8044329be74b5be) [#6799](https://github.com/firebase/firebase-js-sdk/pull/6799) - Update TypeScript version to 4.7.4. + +### Patch Changes + +- Updated dependencies [[`c20633ed3`](https://github.com/firebase/firebase-js-sdk/commit/c20633ed35056cbadc9d65d9ceddf4e28d1ea666), [`1625f7a95`](https://github.com/firebase/firebase-js-sdk/commit/1625f7a95cc3ffb666845db0a8044329be74b5be)]: + - @firebase/util@1.8.0 + - @firebase/analytics@0.9.0 + - @firebase/analytics-types@0.8.0 + - @firebase/component@0.6.0 + +## 0.1.17 + +### Patch Changes + +- [`4af28c1a4`](https://github.com/firebase/firebase-js-sdk/commit/4af28c1a42bd25ce2353f694ca1724c6101cbce5) [#6682](https://github.com/firebase/firebase-js-sdk/pull/6682) - Upgrade TypeScript to 4.7.4. + +- Updated dependencies [[`4af28c1a4`](https://github.com/firebase/firebase-js-sdk/commit/4af28c1a42bd25ce2353f694ca1724c6101cbce5)]: + - @firebase/analytics@0.8.4 + - @firebase/analytics-types@0.7.1 + - @firebase/component@0.5.21 + - @firebase/util@1.7.3 + +## 0.1.16 + +### Patch Changes + +- Updated dependencies [[`807f06aa2`](https://github.com/firebase/firebase-js-sdk/commit/807f06aa26438a91aaea08fd38efb6c706bb8a5d), [`03d1fabcb`](https://github.com/firebase/firebase-js-sdk/commit/03d1fabcb652b3af61631d1e1100ed13efa6fc87)]: + - @firebase/util@1.7.2 + - @firebase/analytics@0.8.3 + - @firebase/component@0.5.20 + +## 0.1.15 + +### Patch Changes + +- Updated dependencies [[`171b78b76`](https://github.com/firebase/firebase-js-sdk/commit/171b78b762826a640d267dd4dd172ad9459c4561), [`1fbc4c4b7`](https://github.com/firebase/firebase-js-sdk/commit/1fbc4c4b7f893ac1f973ccc29205771adec536ca), [`29d034072`](https://github.com/firebase/firebase-js-sdk/commit/29d034072c20af394ce384e42aa10a37d5dfcb18)]: + - @firebase/util@1.7.1 + - @firebase/analytics@0.8.2 + - @firebase/component@0.5.19 + +## 0.1.14 + +### Patch Changes + +- Updated dependencies [[`fdd4ab464`](https://github.com/firebase/firebase-js-sdk/commit/fdd4ab464b59a107bdcc195df3f01e32efd89ed4)]: + - @firebase/util@1.7.0 + - @firebase/analytics@0.8.1 + - @firebase/component@0.5.18 + +## 0.1.13 + +### Patch Changes + +- Updated dependencies [[`b12af44a5`](https://github.com/firebase/firebase-js-sdk/commit/b12af44a5c7500e1192d6cc1a4afc4d77efadbaf), [`1d3a34d7d`](https://github.com/firebase/firebase-js-sdk/commit/1d3a34d7da5bf3c267d014efb587e03c46ff3064), [`69e2ee064`](https://github.com/firebase/firebase-js-sdk/commit/69e2ee064e0729d8da823f1e60f6fb7f3bbe5700)]: + - @firebase/util@1.6.3 + - @firebase/analytics@0.8.0 + - @firebase/component@0.5.17 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies [[`835f1d46a`](https://github.com/firebase/firebase-js-sdk/commit/835f1d46a6780535bc660ef7dc23293350d5fe43), [`efe2000fc`](https://github.com/firebase/firebase-js-sdk/commit/efe2000fc499e2c85c4e5e0fef6741ff3bad2eb0)]: + - @firebase/analytics@0.7.11 + - @firebase/util@1.6.2 + - @firebase/component@0.5.16 + +## 0.1.11 + +### Patch Changes + +- [`2cd1cc76f`](https://github.com/firebase/firebase-js-sdk/commit/2cd1cc76f2a308135cd60f424fe09084a34b5cb5) [#6307](https://github.com/firebase/firebase-js-sdk/pull/6307) (fixes [#6300](https://github.com/firebase/firebase-js-sdk/issues/6300)) - fix: add type declarations to exports field + +- Updated dependencies [[`2cd1cc76f`](https://github.com/firebase/firebase-js-sdk/commit/2cd1cc76f2a308135cd60f424fe09084a34b5cb5)]: + - @firebase/analytics@0.7.10 + - @firebase/component@0.5.15 + - @firebase/util@1.6.1 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies [[`9c5c9c36d`](https://github.com/firebase/firebase-js-sdk/commit/9c5c9c36da80b98b73cfd60ef2e2965087e9f801)]: + - @firebase/util@1.6.0 + - @firebase/analytics@0.7.9 + - @firebase/component@0.5.14 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies [[`e9e5f6b3c`](https://github.com/firebase/firebase-js-sdk/commit/e9e5f6b3ca9d61323b22f87986d9959f5297ec59)]: + - @firebase/util@1.5.2 + - @firebase/analytics@0.7.8 + - @firebase/component@0.5.13 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies [[`3198d58dc`](https://github.com/firebase/firebase-js-sdk/commit/3198d58dcedbf7583914dbcc76984f6f7df8d2ef)]: + - @firebase/util@1.5.1 + - @firebase/analytics@0.7.7 + - @firebase/component@0.5.12 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies [[`2d672cead`](https://github.com/firebase/firebase-js-sdk/commit/2d672cead167187cb714cd89b638c0884ba58f03)]: + - @firebase/util@1.5.0 + - @firebase/analytics@0.7.6 + - @firebase/component@0.5.11 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies [[`3b481f572`](https://github.com/firebase/firebase-js-sdk/commit/3b481f572456e1eab3435bfc25717770d95a8c49)]: + - @firebase/util@1.4.3 + - @firebase/analytics@0.7.5 + - @firebase/component@0.5.10 + +## 0.1.5 + +### Patch Changes + +- [`3281315fa`](https://github.com/firebase/firebase-js-sdk/commit/3281315fae9c6f535f9d5052ee17d60861ea569a) [#5708](https://github.com/firebase/firebase-js-sdk/pull/5708) (fixes [#1487](https://github.com/firebase/firebase-js-sdk/issues/1487)) - Update build scripts to work with the exports field + +- Updated dependencies [[`3281315fa`](https://github.com/firebase/firebase-js-sdk/commit/3281315fae9c6f535f9d5052ee17d60861ea569a)]: + - @firebase/analytics@0.7.4 + - @firebase/component@0.5.9 + - @firebase/util@1.4.2 + +## 0.1.4 + +### Patch Changes + +- [`2322b6023`](https://github.com/firebase/firebase-js-sdk/commit/2322b6023c628cd9f4f4172767c17d215dd91684) [#5693](https://github.com/firebase/firebase-js-sdk/pull/5693) - Add exports field to all packages + +- Updated dependencies [[`2322b6023`](https://github.com/firebase/firebase-js-sdk/commit/2322b6023c628cd9f4f4172767c17d215dd91684)]: + - @firebase/analytics@0.7.3 + - @firebase/component@0.5.8 + - @firebase/util@1.4.1 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [[`93795c780`](https://github.com/firebase/firebase-js-sdk/commit/93795c7801d6b28ccbbe5855fd2f3fc377b1db5f)]: + - @firebase/analytics@0.7.2 + +## 0.1.2 + +### Patch Changes + +- [`b835b4cba`](https://github.com/firebase/firebase-js-sdk/commit/b835b4cbabc4b7b180ae38b908c49205ce31a422) [#5506](https://github.com/firebase/firebase-js-sdk/pull/5506) - checking isSupported led to runtime errors in certain environments + +- Updated dependencies [[`a99943fe3`](https://github.com/firebase/firebase-js-sdk/commit/a99943fe3bd5279761aa29d138ec91272b06df39), [`b835b4cba`](https://github.com/firebase/firebase-js-sdk/commit/b835b4cbabc4b7b180ae38b908c49205ce31a422), [`b835b4cba`](https://github.com/firebase/firebase-js-sdk/commit/b835b4cbabc4b7b180ae38b908c49205ce31a422)]: + - @firebase/util@1.4.0 + - @firebase/analytics@0.7.1 + - @firebase/component@0.5.7 + +## 0.1.1 + +### Patch Changes + +- [`cd15df0d1`](https://github.com/firebase/firebase-js-sdk/commit/cd15df0d1f51110f448e4284244b06be8d37f1c3) [#5400](https://github.com/firebase/firebase-js-sdk/pull/5400) (fixes [#2903](https://github.com/firebase/firebase-js-sdk/issues/2903)) - Fix cjs builds by removing the named export from app-compat + +## 0.1.0 + +### Minor Changes + +- [`cdada6c68`](https://github.com/firebase/firebase-js-sdk/commit/cdada6c68f9740d13dd6674bcb658e28e68253b6) [#5345](https://github.com/firebase/firebase-js-sdk/pull/5345) (fixes [#5015](https://github.com/firebase/firebase-js-sdk/issues/5015)) - Release modularized SDKs + +### Patch Changes + +- Updated dependencies [[`cdada6c68`](https://github.com/firebase/firebase-js-sdk/commit/cdada6c68f9740d13dd6674bcb658e28e68253b6)]: + - @firebase/analytics@0.7.0 + - @firebase/analytics-types@0.7.0 diff --git a/packages/analytics-compat/README.md b/packages/analytics-compat/README.md new file mode 100644 index 00000000000..cff70fc650f --- /dev/null +++ b/packages/analytics-compat/README.md @@ -0,0 +1,5 @@ +# @firebase/analytics-compat + +This is the compatibility layer for the Firebase Analytics component of the Firebase JS SDK. + +**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages/analytics-compat/karma.conf.js b/packages/analytics-compat/karma.conf.js new file mode 100644 index 00000000000..c6488ea06bd --- /dev/null +++ b/packages/analytics-compat/karma.conf.js @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const karmaBase = require('../../config/karma.base'); + +const files = [`**/*.test.ts`]; + +module.exports = function (config) { + config.set({ + ...karmaBase, + files, + preprocessors: { '**/*.ts': ['webpack', 'sourcemap'] }, + frameworks: ['mocha'] + }); +}; + +module.exports.files = files; diff --git a/packages/analytics-compat/package.json b/packages/analytics-compat/package.json new file mode 100644 index 00000000000..f06c82a5541 --- /dev/null +++ b/packages/analytics-compat/package.json @@ -0,0 +1,67 @@ +{ + "name": "@firebase/analytics-compat", + "version": "0.2.25", + "description": "", + "author": "Firebase (https://firebase.google.com/)", + "main": "dist/index.cjs.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "require": "./dist/index.cjs.js", + "default": "./dist/esm/index.esm.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-compat": "0.x" + }, + "devDependencies": { + "@firebase/app-compat": "0.5.5", + "rollup": "2.79.2", + "@rollup/plugin-json": "6.1.0", + "rollup-plugin-typescript2": "0.36.0", + "typescript": "5.5.4" + }, + "repository": { + "directory": "packages/analytics-compat", + "type": "git", + "url": "git+https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "scripts": { + "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "build": "rollup -c", + "build:deps": "lerna run --scope @firebase/analytics-compat --include-dependencies build", + "build:release": "yarn build && yarn add-compat-overloads", + "dev": "rollup -c -w", + "test": "run-p --npm-path npm lint test:browser", + "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:browser", + "test:browser": "karma start", + "test:browser:debug": "karma start --browsers=Chrome --auto-watch", + "trusted-type-check": "tsec -p tsconfig.json --noEmit", + "add-compat-overloads": "ts-node-script ../../scripts/build/create-overloads.ts -i ../analytics/dist/analytics-public.d.ts -o dist/src/index.d.ts -a -r Analytics:FirebaseAnalytics -r FirebaseApp:FirebaseAppCompat --moduleToEnhance @firebase/analytics" + }, + "typings": "dist/src/index.d.ts", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/analytics": "0.10.19", + "@firebase/analytics-types": "0.8.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "nyc": { + "extension": [ + ".ts" + ], + "reportDir": "./coverage/node" + } +} diff --git a/packages/analytics-compat/rollup.config.js b/packages/analytics-compat/rollup.config.js new file mode 100644 index 00000000000..49b8ac0d21e --- /dev/null +++ b/packages/analytics-compat/rollup.config.js @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import json from '@rollup/plugin-json'; +import typescriptPlugin from 'rollup-plugin-typescript2'; +import typescript from 'typescript'; +import { emitModulePackageFile } from '../../scripts/build/rollup_emit_module_package_file'; +import pkg from './package.json'; + +const deps = Object.keys( + Object.assign({}, pkg.peerDependencies, pkg.dependencies) +); + +const buildPlugins = [ + typescriptPlugin({ + typescript + }), + json({ preferConst: true }) +]; + +const esmBuild = { + input: 'src/index.ts', + output: { + file: pkg.browser, + format: 'es', + sourcemap: true + }, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + plugins: [...buildPlugins, emitModulePackageFile()] +}; + +const cjsBuild = { + input: 'src/index.ts', + output: { + file: pkg.main, + format: 'cjs', + sourcemap: true + }, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + plugins: buildPlugins +}; + +export default [esmBuild, cjsBuild]; diff --git a/packages/analytics-compat/src/constants.ts b/packages/analytics-compat/src/constants.ts new file mode 100644 index 00000000000..89944e29afe --- /dev/null +++ b/packages/analytics-compat/src/constants.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Officially recommended event names for gtag.js + * Any other string is also allowed. + */ +export enum EventName { + ADD_SHIPPING_INFO = 'add_shipping_info', + ADD_PAYMENT_INFO = 'add_payment_info', + ADD_TO_CART = 'add_to_cart', + ADD_TO_WISHLIST = 'add_to_wishlist', + BEGIN_CHECKOUT = 'begin_checkout', + /** + * @deprecated + * This event name is deprecated and is unsupported in updated + * Enhanced Ecommerce reports. + */ + CHECKOUT_PROGRESS = 'checkout_progress', + EXCEPTION = 'exception', + GENERATE_LEAD = 'generate_lead', + LOGIN = 'login', + PAGE_VIEW = 'page_view', + PURCHASE = 'purchase', + REFUND = 'refund', + REMOVE_FROM_CART = 'remove_from_cart', + SCREEN_VIEW = 'screen_view', + SEARCH = 'search', + SELECT_CONTENT = 'select_content', + SELECT_ITEM = 'select_item', + SELECT_PROMOTION = 'select_promotion', + /** @deprecated */ + SET_CHECKOUT_OPTION = 'set_checkout_option', + SHARE = 'share', + SIGN_UP = 'sign_up', + TIMING_COMPLETE = 'timing_complete', + VIEW_CART = 'view_cart', + VIEW_ITEM = 'view_item', + VIEW_ITEM_LIST = 'view_item_list', + VIEW_PROMOTION = 'view_promotion', + VIEW_SEARCH_RESULTS = 'view_search_results' +} diff --git a/packages/analytics-compat/src/index.ts b/packages/analytics-compat/src/index.ts new file mode 100644 index 00000000000..6f559c983af --- /dev/null +++ b/packages/analytics-compat/src/index.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import firebase, { + _FirebaseNamespace, + FirebaseApp +} from '@firebase/app-compat'; +import { FirebaseAnalytics } from '@firebase/analytics-types'; +import { name, version } from '../package.json'; +import { AnalyticsService } from './service'; +import { + Component, + ComponentContainer, + ComponentType, + InstanceFactory +} from '@firebase/component'; +import { + settings as settingsExp, + isSupported as isSupportedExp +} from '@firebase/analytics'; +import { EventName } from './constants'; + +const factory: InstanceFactory<'analytics-compat'> = ( + container: ComponentContainer +) => { + // Dependencies + const app = container.getProvider('app-compat').getImmediate(); + const analyticsServiceExp = container.getProvider('analytics').getImmediate(); + + return new AnalyticsService(app as FirebaseApp, analyticsServiceExp); +}; + +export function registerAnalytics(): void { + const namespaceExports = { + Analytics: AnalyticsService, + settings: settingsExp, + isSupported: isSupportedExp, + // We removed this enum in exp so need to re-create it here for compat. + EventName + }; + (firebase as _FirebaseNamespace).INTERNAL.registerComponent( + new Component('analytics-compat', factory, ComponentType.PUBLIC) + .setServiceProps(namespaceExports) + .setMultipleInstances(true) + ); +} + +registerAnalytics(); +firebase.registerVersion(name, version); + +/** + * Define extension behavior of `registerAnalytics` + */ +declare module '@firebase/app-compat' { + interface FirebaseNamespace { + analytics(app?: FirebaseApp): FirebaseAnalytics; + } + interface FirebaseApp { + analytics(): FirebaseAnalytics; + } +} diff --git a/packages/analytics-compat/src/service.test.ts b/packages/analytics-compat/src/service.test.ts new file mode 100644 index 00000000000..f369fdc5326 --- /dev/null +++ b/packages/analytics-compat/src/service.test.ts @@ -0,0 +1,162 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, use } from 'chai'; +import { AnalyticsService } from './service'; +import firebase, { FirebaseApp } from '@firebase/app-compat'; +import * as analyticsExp from '@firebase/analytics'; +import { stub, match, SinonStub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +use(sinonChai); + +function createTestService(app: FirebaseApp): AnalyticsService { + return new AnalyticsService(app, analyticsExp.getAnalytics(app)); +} + +describe('Firebase Analytics > Service', () => { + let app: FirebaseApp; + let service: AnalyticsService; + let logEventStub: SinonStub = stub(); + let setUserIdStub: SinonStub = stub(); + let setCurrentScreenStub: SinonStub = stub(); + let setUserPropertiesStub: SinonStub = stub(); + let setAnalyticsCollectionEnabledStub: SinonStub = stub(); + + before(() => { + logEventStub = stub(analyticsExp, 'logEvent'); + setUserIdStub = stub(analyticsExp, 'setUserId'); + setCurrentScreenStub = stub(analyticsExp, 'setCurrentScreen'); + setUserPropertiesStub = stub(analyticsExp, 'setUserProperties'); + setAnalyticsCollectionEnabledStub = stub( + analyticsExp, + 'setAnalyticsCollectionEnabled' + ); + }); + + beforeEach(() => { + app = firebase.initializeApp({ + apiKey: '456_LETTERS_AND_1234NUMBERS', + appId: '123lettersand:numbers', + projectId: 'my-project', + messagingSenderId: 'messaging-sender-id' + }); + }); + + afterEach(async () => { + await app.delete(); + }); + + after(() => { + logEventStub.restore(); + setUserIdStub.restore(); + }); + + it('logEvent() calls modular logEvent() with only event name', () => { + service = createTestService(app); + service.logEvent('begin_checkout'); + expect(logEventStub).to.be.calledWith(match.any, 'begin_checkout'); + logEventStub.resetHistory(); + }); + + it('logEvent() calls modular logEvent() with 2 args', () => { + service = createTestService(app); + service.logEvent('begin_checkout', { 'currency': 'USD' }); + expect(logEventStub).to.be.calledWith(match.any, 'begin_checkout', { + 'currency': 'USD' + }); + logEventStub.resetHistory(); + }); + + it('logEvent() calls modular logEvent() with all args', () => { + service = createTestService(app); + service.logEvent('begin_checkout', { 'currency': 'USD' }, { global: true }); + expect(logEventStub).to.be.calledWith( + match.any, + 'begin_checkout', + { 'currency': 'USD' }, + { global: true } + ); + logEventStub.resetHistory(); + }); + + it('setUserId() calls modular setUserId()', () => { + service = createTestService(app); + service.setUserId('user123'); + expect(setUserIdStub).to.be.calledWith(match.any, 'user123'); + setUserIdStub.resetHistory(); + }); + + it('setUserId() calls modular setUserId() with options if provided', () => { + service = createTestService(app); + service.setUserId('user123', { global: true }); + expect(setUserIdStub).to.be.calledWith(match.any, 'user123', { + global: true + }); + setUserIdStub.resetHistory(); + }); + + it('setCurrentScreen() (deprecated) calls modular setCurrentScreen() (deprecated)', () => { + service = createTestService(app); + service.setCurrentScreen('some_screen'); + expect(setCurrentScreenStub).to.be.calledWith(match.any, 'some_screen'); + setCurrentScreenStub.resetHistory(); + }); + + it('setCurrentScreen() (deprecated) calls modular setCurrentScreen() (deprecated) with options if provided', () => { + service = createTestService(app); + service.setCurrentScreen('some_screen', { global: true }); + expect(setCurrentScreenStub).to.be.calledWith(match.any, 'some_screen', { + global: true + }); + setCurrentScreenStub.resetHistory(); + }); + + it('setUserProperties() calls modular setUserProperties()', () => { + service = createTestService(app); + service.setUserProperties({ 'my_custom_property': 'abc' }); + expect(setUserPropertiesStub).to.be.calledWith(match.any, { + 'my_custom_property': 'abc' + }); + setUserPropertiesStub.resetHistory(); + }); + + it('setUserProperties() calls modular setUserProperties() with options if provided', () => { + service = createTestService(app); + service.setUserProperties( + { 'my_custom_property': 'abc' }, + { global: true } + ); + expect(setUserPropertiesStub).to.be.calledWith( + match.any, + { 'my_custom_property': 'abc' }, + { + global: true + } + ); + setCurrentScreenStub.resetHistory(); + }); + + it('setAnalyticsCollectionEnabled() calls modular setAnalyticsCollectionEnabled()', () => { + service = createTestService(app); + service.setAnalyticsCollectionEnabled(false); + expect(setAnalyticsCollectionEnabledStub).to.be.calledWith( + match.any, + false + ); + setAnalyticsCollectionEnabledStub.resetHistory(); + }); +}); diff --git a/packages/analytics-compat/src/service.ts b/packages/analytics-compat/src/service.ts new file mode 100644 index 00000000000..f3b6ec2e8fb --- /dev/null +++ b/packages/analytics-compat/src/service.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AnalyticsCallOptions, + CustomParams, + EventParams, + FirebaseAnalytics +} from '@firebase/analytics-types'; +import { + Analytics as AnalyticsServiceExp, + logEvent as logEventExp, + setAnalyticsCollectionEnabled as setAnalyticsCollectionEnabledExp, + setCurrentScreen as setCurrentScreenExp, + setUserId as setUserIdExp, + setUserProperties as setUserPropertiesExp +} from '@firebase/analytics'; +import { _FirebaseService, FirebaseApp } from '@firebase/app-compat'; + +export class AnalyticsService implements FirebaseAnalytics, _FirebaseService { + constructor( + public app: FirebaseApp, + readonly _delegate: AnalyticsServiceExp + ) {} + + logEvent( + eventName: string, + eventParams?: EventParams | CustomParams, + options?: AnalyticsCallOptions + ): void { + logEventExp(this._delegate, eventName as '', eventParams, options); + } + + /** + * @deprecated Use {@link logEvent} with `eventName` as 'screen_view' and add relevant `eventParams`. + * See {@link https://firebase.google.com/docs/analytics/screenviews | Track Screenviews}. + */ + setCurrentScreen(screenName: string, options?: AnalyticsCallOptions): void { + setCurrentScreenExp(this._delegate, screenName, options); + } + + setUserId(id: string, options?: AnalyticsCallOptions): void { + setUserIdExp(this._delegate, id, options); + } + + setUserProperties( + properties: CustomParams, + options?: AnalyticsCallOptions + ): void { + setUserPropertiesExp(this._delegate, properties, options); + } + + setAnalyticsCollectionEnabled(enabled: boolean): void { + setAnalyticsCollectionEnabledExp(this._delegate, enabled); + } +} diff --git a/packages/analytics-compat/tsconfig.json b/packages/analytics-compat/tsconfig.json new file mode 100644 index 00000000000..4e0ae05eebc --- /dev/null +++ b/packages/analytics-compat/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "exclude": ["dist/**/*"] +} diff --git a/packages/analytics-interop-types/CHANGELOG.md b/packages/analytics-interop-types/CHANGELOG.md new file mode 100644 index 00000000000..aeb3fe36a8d --- /dev/null +++ b/packages/analytics-interop-types/CHANGELOG.md @@ -0,0 +1,43 @@ +# @firebase/analytics-interop-types + +## 0.3.4 + +### Patch Changes + +- [`1bcf83d`](https://github.com/firebase/firebase-js-sdk/commit/1bcf83d7f0640dff67c20939fb9af7bae6a941e0) [#9263](https://github.com/firebase/firebase-js-sdk/pull/9263) - Expose `setUserProperties` on internal Analytics instance. + +## 0.3.3 + +### Patch Changes + +- [`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1) [#8604](https://github.com/firebase/firebase-js-sdk/pull/8604) - Upgrade to TypeScript 5.5.4 + +## 0.3.2 + +### Patch Changes + +- [`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7) [#8237](https://github.com/firebase/firebase-js-sdk/pull/8237) - Bump all packages so staging works. + +## 0.3.1 + +### Patch Changes + +- [`0c5150106`](https://github.com/firebase/firebase-js-sdk/commit/0c515010607bf2223b468acb94c672b1279ed1a0) [#8079](https://github.com/firebase/firebase-js-sdk/pull/8079) - Update `repository.url` field in all `package.json` files to NPM's preferred format. + +## 0.3.0 + +### Minor Changes + +- [`1625f7a95`](https://github.com/firebase/firebase-js-sdk/commit/1625f7a95cc3ffb666845db0a8044329be74b5be) [#6799](https://github.com/firebase/firebase-js-sdk/pull/6799) - Update TypeScript version to 4.7.4. + +## 0.2.1 + +### Patch Changes + +- [`4af28c1a4`](https://github.com/firebase/firebase-js-sdk/commit/4af28c1a42bd25ce2353f694ca1724c6101cbce5) [#6682](https://github.com/firebase/firebase-js-sdk/pull/6682) - Upgrade TypeScript to 4.7.4. + +## 0.2.0 + +### Minor Changes + +- [`bd50d8310`](https://github.com/firebase/firebase-js-sdk/commit/bd50d83107be3d87064f72800c608abc94ae3456) [#5206](https://github.com/firebase/firebase-js-sdk/pull/5206) - Fix formatting of links in comments and update some event typings to correctly match GA4 specs. diff --git a/packages/analytics-interop-types/index.d.ts b/packages/analytics-interop-types/index.d.ts index a6e205dc2f5..6cb6936147b 100644 --- a/packages/analytics-interop-types/index.d.ts +++ b/packages/analytics-interop-types/index.d.ts @@ -21,14 +21,18 @@ export interface FirebaseAnalyticsInternal { * automatically associates this logged event with this Firebase web * app instance on this device. * List of official event parameters can be found in - * {@link https://developers.google.com/gtagjs/reference/event - * the gtag.js reference documentation}. + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * the GA4 reference documentation}. */ logEvent( eventName: string, eventParams?: { [key: string]: unknown }, options?: AnalyticsCallOptions ): void; + setUserProperties: ( + properties: { [key: string]: unknown }, + options?: AnalyticsCallOptions + ) => void; } export interface AnalyticsCallOptions { diff --git a/packages/analytics-interop-types/package.json b/packages/analytics-interop-types/package.json index 51bcf67e7b3..d06f0c36771 100644 --- a/packages/analytics-interop-types/package.json +++ b/packages/analytics-interop-types/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/analytics-interop-types", - "version": "0.1.5", + "version": "0.3.4", "description": "@firebase/analytics Types", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -14,12 +14,12 @@ "repository": { "directory": "packages/analytics-interop-types", "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" + "url": "git+https://github.com/firebase/firebase-js-sdk.git" }, "bugs": { "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.5" + "typescript": "5.5.4" } } diff --git a/packages/analytics-interop-types/tsconfig.json b/packages/analytics-interop-types/tsconfig.json index 9a785433d90..ad532c5f58b 100644 --- a/packages/analytics-interop-types/tsconfig.json +++ b/packages/analytics-interop-types/tsconfig.json @@ -3,7 +3,5 @@ "compilerOptions": { "noEmit": true }, - "exclude": [ - "dist/**/*" - ] + "exclude": ["dist/**/*"] } diff --git a/packages/analytics-types/CHANGELOG.md b/packages/analytics-types/CHANGELOG.md index c74993447d8..02d1ad9828e 100644 --- a/packages/analytics-types/CHANGELOG.md +++ b/packages/analytics-types/CHANGELOG.md @@ -1,10 +1,57 @@ # @firebase/analytics-types -## 0.4.0 +## 0.8.3 + +### Patch Changes + +- [`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1) [#8604](https://github.com/firebase/firebase-js-sdk/pull/8604) - Upgrade to TypeScript 5.5.4 + +## 0.8.2 + +### Patch Changes + +- [`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7) [#8237](https://github.com/firebase/firebase-js-sdk/pull/8237) - Bump all packages so staging works. + +## 0.8.1 + +### Patch Changes + +- [`0c5150106`](https://github.com/firebase/firebase-js-sdk/commit/0c515010607bf2223b468acb94c672b1279ed1a0) [#8079](https://github.com/firebase/firebase-js-sdk/pull/8079) - Update `repository.url` field in all `package.json` files to NPM's preferred format. + +## 0.8.0 + ### Minor Changes +- [`1625f7a95`](https://github.com/firebase/firebase-js-sdk/commit/1625f7a95cc3ffb666845db0a8044329be74b5be) [#6799](https://github.com/firebase/firebase-js-sdk/pull/6799) - Update TypeScript version to 4.7.4. + +## 0.7.1 + +### Patch Changes + +- [`4af28c1a4`](https://github.com/firebase/firebase-js-sdk/commit/4af28c1a42bd25ce2353f694ca1724c6101cbce5) [#6682](https://github.com/firebase/firebase-js-sdk/pull/6682) - Upgrade TypeScript to 4.7.4. + +## 0.7.0 +### Minor Changes + +- [`cdada6c68`](https://github.com/firebase/firebase-js-sdk/commit/cdada6c68f9740d13dd6674bcb658e28e68253b6) [#5345](https://github.com/firebase/firebase-js-sdk/pull/5345) (fixes [#5015](https://github.com/firebase/firebase-js-sdk/issues/5015)) - Release modularized SDKs + +## 0.6.0 + +### Minor Changes + +- [`bd50d8310`](https://github.com/firebase/firebase-js-sdk/commit/bd50d83107be3d87064f72800c608abc94ae3456) [#5206](https://github.com/firebase/firebase-js-sdk/pull/5206) - Fix formatting of links in comments and update some event typings to correctly match GA4 specs. + +## 0.5.0 + +### Minor Changes + +- [`02586c975`](https://github.com/firebase/firebase-js-sdk/commit/02586c9754318b01a0051561d2c7c4906059b5af) [#5070](https://github.com/firebase/firebase-js-sdk/pull/5070) - Add `firebase_screen` and `firebase_screen_class` to `logEvent()` overload for `screen_view` events. + +## 0.4.0 + +### Minor Changes -- [`fb3b095e4`](https://github.com/firebase/firebase-js-sdk/commit/fb3b095e4b7c8f57fdb3172bc039c84576abf290) [#2800](https://github.com/firebase/firebase-js-sdk/pull/2800) - Analytics now dynamically fetches the app's Measurement ID from the Dynamic Config backend +- [`fb3b095e4`](https://github.com/firebase/firebase-js-sdk/commit/fb3b095e4b7c8f57fdb3172bc039c84576abf290) [#2800](https://github.com/firebase/firebase-js-sdk/pull/2800) - Analytics now dynamically fetches the app's Measurement ID from the Dynamic Config backend instead of depending on the local Firebase config. It will fall back to any `measurementId` value found in the local config if the Dynamic Config fetch fails. diff --git a/packages/analytics-types/index.d.ts b/packages/analytics-types/index.d.ts index 09708902516..9a9f27499ae 100644 --- a/packages/analytics-types/index.d.ts +++ b/packages/analytics-types/index.d.ts @@ -23,7 +23,7 @@ export type DataLayer = Array; /** * Additional options that can be passed to Firebase Analytics method - * calls such as `logEvent`, `setCurrentScreen`, etc. + * calls such as `logEvent`, etc. */ export interface AnalyticsCallOptions { /** @@ -40,18 +40,422 @@ export interface FirebaseAnalytics { * Sends analytics event with given `eventParams`. This method * automatically associates this logged event with this Firebase web * app instance on this device. - * List of official event parameters can be found in - * {@link https://developers.google.com/gtagjs/reference/event - * the gtag.js reference documentation}. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. */ logEvent( - eventName: EventNameString, - eventParams?: EventParams, + eventName: 'add_payment_info', + eventParams?: { + coupon?: EventParams['coupon']; + currency?: EventParams['currency']; + items?: EventParams['items']; + payment_type?: EventParams['payment_type']; + value?: EventParams['value']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'add_shipping_info', + eventParams?: { + coupon?: EventParams['coupon']; + currency?: EventParams['currency']; + items?: EventParams['items']; + shipping_tier?: EventParams['shipping_tier']; + value?: EventParams['value']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'add_to_cart' | 'add_to_wishlist' | 'remove_from_cart', + eventParams?: { + currency?: EventParams['currency']; + value?: EventParams['value']; + items?: EventParams['items']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'begin_checkout', + eventParams?: { + currency?: EventParams['currency']; + coupon?: EventParams['coupon']; + value?: EventParams['value']; + items?: EventParams['items']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'checkout_progress', + eventParams?: { + currency?: EventParams['currency']; + coupon?: EventParams['coupon']; + value?: EventParams['value']; + items?: EventParams['items']; + checkout_step?: EventParams['checkout_step']; + checkout_option?: EventParams['checkout_option']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * See + * {@link https://developers.google.com/analytics/devguides/collection/ga4/exceptions + * | Measure exceptions}. + */ + logEvent( + eventName: 'exception', + eventParams?: { + description?: EventParams['description']; + fatal?: EventParams['fatal']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'generate_lead', + eventParams?: { + value?: EventParams['value']; + currency?: EventParams['currency']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'login', + eventParams?: { + method?: EventParams['method']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * See + * {@link https://developers.google.com/analytics/devguides/collection/ga4/views + * | Page views}. + */ + logEvent( + eventName: 'page_view', + eventParams?: { + page_title?: string; + page_location?: string; + page_path?: string; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'purchase' | 'refund', + eventParams?: { + value?: EventParams['value']; + currency?: EventParams['currency']; + transaction_id: EventParams['transaction_id']; + tax?: EventParams['tax']; + shipping?: EventParams['shipping']; + items?: EventParams['items']; + coupon?: EventParams['coupon']; + affiliation?: EventParams['affiliation']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * See {@link https://firebase.google.com/docs/analytics/screenviews + * | Track Screenviews}. + */ + logEvent( + eventName: 'screen_view', + eventParams?: { + firebase_screen: EventParams['firebase_screen']; + firebase_screen_class: EventParams['firebase_screen_class']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'search' | 'view_search_results', + eventParams?: { + search_term?: EventParams['search_term']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'select_content', + eventParams?: { + content_type?: EventParams['content_type']; + item_id?: EventParams['item_id']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'select_item', + eventParams?: { + items?: EventParams['items']; + item_list_name?: EventParams['item_list_name']; + item_list_id?: EventParams['item_list_id']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'select_promotion' | 'view_promotion', + eventParams?: { + items?: EventParams['items']; + promotion_id?: EventParams['promotion_id']; + promotion_name?: EventParams['promotion_name']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'set_checkout_option', + eventParams?: { + checkout_step?: EventParams['checkout_step']; + checkout_option?: EventParams['checkout_option']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'share', + eventParams?: { + method?: EventParams['method']; + content_type?: EventParams['content_type']; + item_id?: EventParams['item_id']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'sign_up', + eventParams?: { + method?: EventParams['method']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'timing_complete', + eventParams?: { + name: string; + value: number; + event_category?: string; + event_label?: string; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'view_cart' | 'view_item', + eventParams?: { + currency?: EventParams['currency']; + items?: EventParams['items']; + value?: EventParams['value']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: 'view_item_list', + eventParams?: { + items?: EventParams['items']; + item_list_name?: EventParams['item_list_name']; + item_list_id?: EventParams['item_list_id']; + [key: string]: any; + }, + options?: AnalyticsCallOptions + ): void; + + /** + * Sends analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ + logEvent( + eventName: CustomEventName, + eventParams?: { [key: string]: any }, options?: AnalyticsCallOptions ): void; /** * Use gtag 'config' command to set 'screen_name'. + * + * @deprecated Use {@link logEvent} with `eventName` as 'screen_view' and add relevant `eventParams`. + * See {@link https://firebase.google.com/docs/analytics/screenviews | Track Screenviews}. */ setCurrentScreen(screenName: string, options?: AnalyticsCallOptions): void; @@ -75,6 +479,8 @@ export interface FirebaseAnalytics { setAnalyticsCollectionEnabled(enabled: boolean): void; } +export type CustomEventName = T extends EventNameString ? never : T; + /** * Specifies custom options for your Firebase Analytics instance. * You must set these before initializing `firebase.analytics()`. @@ -125,7 +531,7 @@ export interface ControlParams { export interface EventParams { checkout_option?: string; checkout_step?: number; - content_id?: string; + item_id?: string; content_type?: string; coupon?: string; currency?: string; @@ -136,6 +542,14 @@ export interface EventParams { number?: string; promotions?: Promotion[]; screen_name?: string; + /** + * Firebase-specific. Use to log a `screen_name` to Firebase Analytics. + */ + firebase_screen?: string; + /** + * Firebase-specific. Use to log a `screen_class` to Firebase Analytics. + */ + firebase_screen_class?: string; search_term?: string; shipping?: Currency; tax?: Currency; @@ -237,7 +651,7 @@ export interface Promotion { /** * Dynamic configuration fetched from server. - * See https://firebase.google.com/docs/projects/api/reference/rest/v1beta1/projects.webApps/getConfig + * See https://firebase.google.com/docs/reference/firebase-management/rest/v1beta1/projects.webApps/getConfig */ interface DynamicConfig { projectId: string; @@ -268,6 +682,6 @@ export interface ThrottleMetadata { declare module '@firebase/component' { interface NameServiceMapping { - 'analytics': FirebaseAnalytics; + 'analytics-compat': FirebaseAnalytics; } } diff --git a/packages/analytics-types/package.json b/packages/analytics-types/package.json index 977acdf4360..2798c23b3fe 100644 --- a/packages/analytics-types/package.json +++ b/packages/analytics-types/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/analytics-types", - "version": "0.4.0", + "version": "0.8.3", "description": "@firebase/analytics Types", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -14,12 +14,12 @@ "repository": { "directory": "packages/analytics-types", "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" + "url": "git+https://github.com/firebase/firebase-js-sdk.git" }, "bugs": { "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.5" + "typescript": "5.5.4" } } diff --git a/packages/analytics-types/tsconfig.json b/packages/analytics-types/tsconfig.json index 9a785433d90..ad532c5f58b 100644 --- a/packages/analytics-types/tsconfig.json +++ b/packages/analytics-types/tsconfig.json @@ -3,7 +3,5 @@ "compilerOptions": { "noEmit": true }, - "exclude": [ - "dist/**/*" - ] + "exclude": ["dist/**/*"] } diff --git a/packages/analytics/CHANGELOG.md b/packages/analytics/CHANGELOG.md index c88b5b8c751..1e82d5d173a 100644 --- a/packages/analytics/CHANGELOG.md +++ b/packages/analytics/CHANGELOG.md @@ -1,5 +1,557 @@ # @firebase/analytics +## 0.10.19 + +### Patch Changes + +- [`1bcf83d`](https://github.com/firebase/firebase-js-sdk/commit/1bcf83d7f0640dff67c20939fb9af7bae6a941e0) [#9263](https://github.com/firebase/firebase-js-sdk/pull/9263) - Expose `setUserProperties` on internal Analytics instance. + +## 0.10.18 + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/installations@0.6.19 + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 0.10.17 + +### Patch Changes + +- [`13e6cce`](https://github.com/firebase/firebase-js-sdk/commit/13e6cce882d687e06c8d9bfb56895f8a77fc57b5) [#9085](https://github.com/firebase/firebase-js-sdk/pull/9085) - Add rollup config to generate modular typings for google3 + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + - @firebase/installations@0.6.18 + +## 0.10.16 + +### Patch Changes + +- Updated dependencies [[`8a03143`](https://github.com/firebase/firebase-js-sdk/commit/8a03143b9217effdd86d68bdf195493c0979aa27)]: + - @firebase/util@1.12.0 + - @firebase/component@0.6.17 + - @firebase/installations@0.6.17 + +## 0.10.15 + +### Patch Changes + +- Updated dependencies [[`9bcd1ea`](https://github.com/firebase/firebase-js-sdk/commit/9bcd1ea9b8cc5b55692765d40df000da8ddef02b)]: + - @firebase/util@1.11.3 + - @firebase/component@0.6.16 + - @firebase/installations@0.6.16 + +## 0.10.14 + +### Patch Changes + +- Updated dependencies [[`8593fa0`](https://github.com/firebase/firebase-js-sdk/commit/8593fa05bd884c2f1f6f3b4ae062efa48af93d24)]: + - @firebase/util@1.11.2 + - @firebase/component@0.6.15 + - @firebase/installations@0.6.15 + +## 0.10.13 + +### Patch Changes + +- Updated dependencies [[`ea1f913`](https://github.com/firebase/firebase-js-sdk/commit/ea1f9139e6baec0269fbb91233fd3f7f4b0d5875), [`0e12766`](https://github.com/firebase/firebase-js-sdk/commit/0e127664946ba324c6566a02b393dafd23fc1ddb)]: + - @firebase/util@1.11.1 + - @firebase/installations@0.6.14 + - @firebase/component@0.6.14 + +## 0.10.12 + +### Patch Changes + +- Updated dependencies [[`777f465`](https://github.com/firebase/firebase-js-sdk/commit/777f465ff37495ff933a29583769ce8a6a2b59b5)]: + - @firebase/util@1.11.0 + - @firebase/installations@0.6.13 + - @firebase/component@0.6.13 + +## 0.10.11 + +### Patch Changes + +- Updated dependencies [[`25a6204c1`](https://github.com/firebase/firebase-js-sdk/commit/25a6204c1531b6c772e5368d12b2411ae1d21bbc)]: + - @firebase/util@1.10.3 + - @firebase/component@0.6.12 + - @firebase/installations@0.6.12 + +## 0.10.10 + +### Patch Changes + +- [`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1) [#8604](https://github.com/firebase/firebase-js-sdk/pull/8604) - Upgrade to TypeScript 5.5.4 + +- Updated dependencies [[`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1)]: + - @firebase/component@0.6.11 + - @firebase/installations@0.6.11 + - @firebase/logger@0.4.4 + - @firebase/util@1.10.2 + +## 0.10.9 + +### Patch Changes + +- [`479226bf3`](https://github.com/firebase/firebase-js-sdk/commit/479226bf3ebd99017bb12fa21440c75715658702) [#8475](https://github.com/firebase/firebase-js-sdk/pull/8475) - Remove ES5 bundles. The minimum required ES version is now ES2017. + +- Updated dependencies [[`479226bf3`](https://github.com/firebase/firebase-js-sdk/commit/479226bf3ebd99017bb12fa21440c75715658702)]: + - @firebase/installations@0.6.10 + - @firebase/component@0.6.10 + - @firebase/logger@0.4.3 + - @firebase/util@1.10.1 + +## 0.10.8 + +### Patch Changes + +- Updated dependencies [[`16d62d4fa`](https://github.com/firebase/firebase-js-sdk/commit/16d62d4fa16faddb8cb676c0af3f29b8a5824741)]: + - @firebase/util@1.10.0 + - @firebase/component@0.6.9 + - @firebase/installations@0.6.9 + +## 0.10.7 + +### Patch Changes + +- [`a9f844066`](https://github.com/firebase/firebase-js-sdk/commit/a9f844066045d8567ae143bae77d184ac227690d) [#8395](https://github.com/firebase/firebase-js-sdk/pull/8395) - Revert introduction of safevalues to prevent issues from arising in Browser CommonJS environments due to ES5 incompatibility. For more information, see [GitHub PR #8395](https://github.com/firebase/firebase-js-sdk/pull/8395) + +## 0.10.6 + +### Patch Changes + +- [`f58d48cd4`](https://github.com/firebase/firebase-js-sdk/commit/f58d48cd42c9f09eab4d8b2a606af360528917f8) [#8301](https://github.com/firebase/firebase-js-sdk/pull/8301) - Begin using [safevalues](https://github.com/google/safevalues) to sanitize HTML vulnerable to XSS. + +## 0.10.5 + +### Patch Changes + +- Updated dependencies [[`192561b15`](https://github.com/firebase/firebase-js-sdk/commit/192561b1552a08840d8e341f30f3dbe275465558)]: + - @firebase/util@1.9.7 + - @firebase/component@0.6.8 + - @firebase/installations@0.6.8 + +## 0.10.4 + +### Patch Changes + +- [`f66769cca`](https://github.com/firebase/firebase-js-sdk/commit/f66769cca243019354f88ac9dc8de07caf9de56e) [#8243](https://github.com/firebase/firebase-js-sdk/pull/8243) (fixes [#8210](https://github.com/firebase/firebase-js-sdk/issues/8210)) - Analytics - fixed an issue where setConsent was clobbering the consentSettings before passing them to the gtag implementation. + +## 0.10.3 + +### Patch Changes + +- [`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7) [#8237](https://github.com/firebase/firebase-js-sdk/pull/8237) - Bump all packages so staging works. + +- Updated dependencies [[`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7)]: + - @firebase/component@0.6.7 + - @firebase/installations@0.6.7 + - @firebase/logger@0.4.2 + - @firebase/util@1.9.6 + +## 0.10.2 + +### Patch Changes + +- [`0c5150106`](https://github.com/firebase/firebase-js-sdk/commit/0c515010607bf2223b468acb94c672b1279ed1a0) [#8079](https://github.com/firebase/firebase-js-sdk/pull/8079) - Update `repository.url` field in all `package.json` files to NPM's preferred format. + +- Updated dependencies [[`0c5150106`](https://github.com/firebase/firebase-js-sdk/commit/0c515010607bf2223b468acb94c672b1279ed1a0)]: + - @firebase/installations@0.6.6 + - @firebase/component@0.6.6 + - @firebase/logger@0.4.1 + - @firebase/util@1.9.5 + +## 0.10.1 + +### Patch Changes + +- [`bf59c0aed`](https://github.com/firebase/firebase-js-sdk/commit/bf59c0aedefabae9bff4d777e1591fe496259293) - Analytics - added two new consent options to the ConsentSettings interface: ad_personalization and ad_user_data. + +- Updated dependencies [[`434f8418c`](https://github.com/firebase/firebase-js-sdk/commit/434f8418c3db3ae98489a8461c437c248c039070)]: + - @firebase/util@1.9.4 + - @firebase/installations@0.6.5 + - @firebase/component@0.6.5 + +## 0.10.0 + +### Minor Changes + +- [`0a27d2fbf`](https://github.com/firebase/firebase-js-sdk/commit/0a27d2fbf268f07099d4fa5ecab7fbf35a579780) [#7158](https://github.com/firebase/firebase-js-sdk/pull/7158) - Add method `getGoogleAnalyticsClientId()` to retrieve an unique identifier for a web client. This allows users to log purchase and other events from their backends using Google Analytics 4 Measurement Protocol and to have those events be connected to actions taken on the client within their Firebase web app. `getGoogleAnalyticsClientId()` will simplify this event recording process. + +## 0.9.5 + +### Patch Changes + +- [`3435ba945`](https://github.com/firebase/firebase-js-sdk/commit/3435ba945a9febf5a0aece05517a5656f58b246f) [#7155](https://github.com/firebase/firebase-js-sdk/pull/7155) (fixes [#7048](https://github.com/firebase/firebase-js-sdk/issues/7048)) - Use the Trusted Types API when composing the gtag URL. + +## 0.9.4 + +### Patch Changes + +- Updated dependencies [[`c59f537b1`](https://github.com/firebase/firebase-js-sdk/commit/c59f537b1262b5d7997291b8c1e9324d378effb6)]: + - @firebase/util@1.9.3 + - @firebase/component@0.6.4 + - @firebase/installations@0.6.4 + +## 0.9.3 + +### Patch Changes + +- Updated dependencies [[`d071bd1ac`](https://github.com/firebase/firebase-js-sdk/commit/d071bd1acaa0583b4dd3454387fc58eafddb5c30)]: + - @firebase/util@1.9.2 + - @firebase/component@0.6.3 + - @firebase/installations@0.6.3 + +## 0.9.2 + +### Patch Changes + +- Updated dependencies [[`0bab0b7a7`](https://github.com/firebase/firebase-js-sdk/commit/0bab0b7a786d1563bf665904c7097d1fe06efce5)]: + - @firebase/util@1.9.1 + - @firebase/component@0.6.2 + - @firebase/installations@0.6.2 + +## 0.9.1 + +### Patch Changes + +- Updated dependencies [[`d4114a4f7`](https://github.com/firebase/firebase-js-sdk/commit/d4114a4f7da3f469c0c900416ac8beee58885ec3), [`06dc1364d`](https://github.com/firebase/firebase-js-sdk/commit/06dc1364d7560f4c563e1ccc89af9cad4cd91df8)]: + - @firebase/util@1.9.0 + - @firebase/component@0.6.1 + - @firebase/installations@0.6.1 + +## 0.9.0 + +### Minor Changes + +- [`1625f7a95`](https://github.com/firebase/firebase-js-sdk/commit/1625f7a95cc3ffb666845db0a8044329be74b5be) [#6799](https://github.com/firebase/firebase-js-sdk/pull/6799) - Update TypeScript version to 4.7.4. + +### Patch Changes + +- Updated dependencies [[`c20633ed3`](https://github.com/firebase/firebase-js-sdk/commit/c20633ed35056cbadc9d65d9ceddf4e28d1ea666), [`1625f7a95`](https://github.com/firebase/firebase-js-sdk/commit/1625f7a95cc3ffb666845db0a8044329be74b5be)]: + - @firebase/util@1.8.0 + - @firebase/component@0.6.0 + - @firebase/installations@0.6.0 + - @firebase/logger@0.4.0 + +## 0.8.4 + +### Patch Changes + +- [`4af28c1a4`](https://github.com/firebase/firebase-js-sdk/commit/4af28c1a42bd25ce2353f694ca1724c6101cbce5) [#6682](https://github.com/firebase/firebase-js-sdk/pull/6682) - Upgrade TypeScript to 4.7.4. + +- Updated dependencies [[`4af28c1a4`](https://github.com/firebase/firebase-js-sdk/commit/4af28c1a42bd25ce2353f694ca1724c6101cbce5)]: + - @firebase/component@0.5.21 + - @firebase/installations@0.5.16 + - @firebase/logger@0.3.4 + - @firebase/util@1.7.3 + +## 0.8.3 + +### Patch Changes + +- [`03d1fabcb`](https://github.com/firebase/firebase-js-sdk/commit/03d1fabcb652b3af61631d1e1100ed13efa6fc87) [#6671](https://github.com/firebase/firebase-js-sdk/pull/6671) - Correct `id` type in `setUserId` + +- Updated dependencies [[`807f06aa2`](https://github.com/firebase/firebase-js-sdk/commit/807f06aa26438a91aaea08fd38efb6c706bb8a5d)]: + - @firebase/util@1.7.2 + - @firebase/component@0.5.20 + - @firebase/installations@0.5.15 + +## 0.8.2 + +### Patch Changes + +- [`1fbc4c4b7`](https://github.com/firebase/firebase-js-sdk/commit/1fbc4c4b7f893ac1f973ccc29205771adec536ca) [#6655](https://github.com/firebase/firebase-js-sdk/pull/6655) - Update to allow for multiple instance of gtag with different datalayer names + +- Updated dependencies [[`171b78b76`](https://github.com/firebase/firebase-js-sdk/commit/171b78b762826a640d267dd4dd172ad9459c4561), [`29d034072`](https://github.com/firebase/firebase-js-sdk/commit/29d034072c20af394ce384e42aa10a37d5dfcb18)]: + - @firebase/util@1.7.1 + - @firebase/component@0.5.19 + - @firebase/installations@0.5.14 + +## 0.8.1 + +### Patch Changes + +- Updated dependencies [[`fdd4ab464`](https://github.com/firebase/firebase-js-sdk/commit/fdd4ab464b59a107bdcc195df3f01e32efd89ed4)]: + - @firebase/util@1.7.0 + - @firebase/installations@0.5.13 + - @firebase/component@0.5.18 + +## 0.8.0 + +### Minor Changes + +- [`1d3a34d7d`](https://github.com/firebase/firebase-js-sdk/commit/1d3a34d7da5bf3c267d014efb587e03c46ff3064) [#6376](https://github.com/firebase/firebase-js-sdk/pull/6376) - Add function `setConsent()` to set the applicable end user "consent" state. + +* [`69e2ee064`](https://github.com/firebase/firebase-js-sdk/commit/69e2ee064e0729d8da823f1e60f6fb7f3bbe5700) [#6367](https://github.com/firebase/firebase-js-sdk/pull/6367) - Add function `setDefaultEventParameters()` to set data that will be logged on every Analytics SDK event + +### Patch Changes + +- Updated dependencies [[`b12af44a5`](https://github.com/firebase/firebase-js-sdk/commit/b12af44a5c7500e1192d6cc1a4afc4d77efadbaf)]: + - @firebase/util@1.6.3 + - @firebase/component@0.5.17 + - @firebase/installations@0.5.12 + +## 0.7.11 + +### Patch Changes + +- [`835f1d46a`](https://github.com/firebase/firebase-js-sdk/commit/835f1d46a6780535bc660ef7dc23293350d5fe43) [#6357](https://github.com/firebase/firebase-js-sdk/pull/6357) - Fix typo in GtagConfigParams + +- Updated dependencies [[`efe2000fc`](https://github.com/firebase/firebase-js-sdk/commit/efe2000fc499e2c85c4e5e0fef6741ff3bad2eb0)]: + - @firebase/util@1.6.2 + - @firebase/component@0.5.16 + - @firebase/installations@0.5.11 + +## 0.7.10 + +### Patch Changes + +- [`2cd1cc76f`](https://github.com/firebase/firebase-js-sdk/commit/2cd1cc76f2a308135cd60f424fe09084a34b5cb5) [#6307](https://github.com/firebase/firebase-js-sdk/pull/6307) (fixes [#6300](https://github.com/firebase/firebase-js-sdk/issues/6300)) - fix: add type declarations to exports field + +- Updated dependencies [[`2cd1cc76f`](https://github.com/firebase/firebase-js-sdk/commit/2cd1cc76f2a308135cd60f424fe09084a34b5cb5)]: + - @firebase/component@0.5.15 + - @firebase/installations@0.5.10 + - @firebase/logger@0.3.3 + - @firebase/util@1.6.1 + +## 0.7.9 + +### Patch Changes + +- Updated dependencies [[`9c5c9c36d`](https://github.com/firebase/firebase-js-sdk/commit/9c5c9c36da80b98b73cfd60ef2e2965087e9f801)]: + - @firebase/util@1.6.0 + - @firebase/installations@0.5.9 + - @firebase/component@0.5.14 + +## 0.7.8 + +### Patch Changes + +- Updated dependencies [[`e9e5f6b3c`](https://github.com/firebase/firebase-js-sdk/commit/e9e5f6b3ca9d61323b22f87986d9959f5297ec59)]: + - @firebase/util@1.5.2 + - @firebase/component@0.5.13 + - @firebase/installations@0.5.8 + +## 0.7.7 + +### Patch Changes + +- Updated dependencies [[`3198d58dc`](https://github.com/firebase/firebase-js-sdk/commit/3198d58dcedbf7583914dbcc76984f6f7df8d2ef)]: + - @firebase/util@1.5.1 + - @firebase/component@0.5.12 + - @firebase/installations@0.5.7 + +## 0.7.6 + +### Patch Changes + +- Updated dependencies [[`2d672cead`](https://github.com/firebase/firebase-js-sdk/commit/2d672cead167187cb714cd89b638c0884ba58f03), [`ddeff8384`](https://github.com/firebase/firebase-js-sdk/commit/ddeff8384ab8a927f02244e2591db525fd58c7dd)]: + - @firebase/installations@0.5.6 + - @firebase/util@1.5.0 + - @firebase/component@0.5.11 + +## 0.7.5 + +### Patch Changes + +- Updated dependencies [[`3b481f572`](https://github.com/firebase/firebase-js-sdk/commit/3b481f572456e1eab3435bfc25717770d95a8c49)]: + - @firebase/util@1.4.3 + - @firebase/component@0.5.10 + - @firebase/installations@0.5.5 + +## 0.7.4 + +### Patch Changes + +- [`3281315fa`](https://github.com/firebase/firebase-js-sdk/commit/3281315fae9c6f535f9d5052ee17d60861ea569a) [#5708](https://github.com/firebase/firebase-js-sdk/pull/5708) (fixes [#1487](https://github.com/firebase/firebase-js-sdk/issues/1487)) - Update build scripts to work with the exports field + +- Updated dependencies [[`3281315fa`](https://github.com/firebase/firebase-js-sdk/commit/3281315fae9c6f535f9d5052ee17d60861ea569a)]: + - @firebase/component@0.5.9 + - @firebase/installations@0.5.4 + - @firebase/logger@0.3.2 + - @firebase/util@1.4.2 + +## 0.7.3 + +### Patch Changes + +- [`2322b6023`](https://github.com/firebase/firebase-js-sdk/commit/2322b6023c628cd9f4f4172767c17d215dd91684) [#5693](https://github.com/firebase/firebase-js-sdk/pull/5693) - Add exports field to all packages + +- Updated dependencies [[`2322b6023`](https://github.com/firebase/firebase-js-sdk/commit/2322b6023c628cd9f4f4172767c17d215dd91684)]: + - @firebase/component@0.5.8 + - @firebase/installations@0.5.3 + - @firebase/logger@0.3.1 + - @firebase/util@1.4.1 + +## 0.7.2 + +### Patch Changes + +- [`93795c780`](https://github.com/firebase/firebase-js-sdk/commit/93795c7801d6b28ccbbe5855fd2f3fc377b1db5f) [#5596](https://github.com/firebase/firebase-js-sdk/pull/5596) - report build variants for packages + +- Updated dependencies [[`93795c780`](https://github.com/firebase/firebase-js-sdk/commit/93795c7801d6b28ccbbe5855fd2f3fc377b1db5f)]: + - @firebase/installations@0.5.2 + +## 0.7.1 + +### Patch Changes + +- [`b835b4cba`](https://github.com/firebase/firebase-js-sdk/commit/b835b4cbabc4b7b180ae38b908c49205ce31a422) [#5506](https://github.com/firebase/firebase-js-sdk/pull/5506) - checking isSupported led to runtime errors in certain environments + +- Updated dependencies [[`a99943fe3`](https://github.com/firebase/firebase-js-sdk/commit/a99943fe3bd5279761aa29d138ec91272b06df39), [`b835b4cba`](https://github.com/firebase/firebase-js-sdk/commit/b835b4cbabc4b7b180ae38b908c49205ce31a422)]: + - @firebase/logger@0.3.0 + - @firebase/util@1.4.0 + - @firebase/component@0.5.7 + - @firebase/installations@0.5.1 + +## 0.7.0 + +### Minor Changes + +- [`cdada6c68`](https://github.com/firebase/firebase-js-sdk/commit/cdada6c68f9740d13dd6674bcb658e28e68253b6) [#5345](https://github.com/firebase/firebase-js-sdk/pull/5345) (fixes [#5015](https://github.com/firebase/firebase-js-sdk/issues/5015)) - Release modularized SDKs + +### Patch Changes + +- Updated dependencies [[`cdada6c68`](https://github.com/firebase/firebase-js-sdk/commit/cdada6c68f9740d13dd6674bcb658e28e68253b6)]: + - @firebase/installations@0.5.0 + +## 0.6.18 + +### Patch Changes + +- Updated dependencies [[`bb6b5abff`](https://github.com/firebase/firebase-js-sdk/commit/bb6b5abff6f89ce9ec1bd66ff4e795a059a98eec), [`3c6a11c8d`](https://github.com/firebase/firebase-js-sdk/commit/3c6a11c8d0b35afddb50e9c3e0c4d2e30f642131)]: + - @firebase/component@0.5.6 + - @firebase/util@1.3.0 + - @firebase/installations@0.4.32 + +## 0.6.17 + +### Patch Changes + +- Updated dependencies [[`bd50d8310`](https://github.com/firebase/firebase-js-sdk/commit/bd50d83107be3d87064f72800c608abc94ae3456)]: + - @firebase/analytics-types@0.6.0 + +## 0.6.16 + +### Patch Changes + +- Updated dependencies [[`a3cbe719b`](https://github.com/firebase/firebase-js-sdk/commit/a3cbe719b1bd733a5c4c15ee0d0e6388d512054c)]: + - @firebase/util@1.2.0 + - @firebase/component@0.5.5 + - @firebase/installations@0.4.31 + +## 0.6.15 + +### Patch Changes + +- Updated dependencies [[`02586c975`](https://github.com/firebase/firebase-js-sdk/commit/02586c9754318b01a0051561d2c7c4906059b5af)]: + - @firebase/analytics-types@0.5.0 + +## 0.6.14 + +### Patch Changes + +- Updated dependencies [[`56a6a9d4a`](https://github.com/firebase/firebase-js-sdk/commit/56a6a9d4af2766154584a0f66d3c4d8024d74ba5)]: + - @firebase/component@0.5.4 + - @firebase/installations@0.4.30 + +## 0.6.13 + +### Patch Changes + +- Updated dependencies [[`725ab4684`](https://github.com/firebase/firebase-js-sdk/commit/725ab4684ef0999a12f71e704c204a00fb030e5d)]: + - @firebase/component@0.5.3 + - @firebase/installations@0.4.29 + +## 0.6.12 + +### Patch Changes + +- Updated dependencies [[`4c4b6aed9`](https://github.com/firebase/firebase-js-sdk/commit/4c4b6aed9757c9a7e75fb698a15e53274f93880b)]: + - @firebase/component@0.5.2 + - @firebase/installations@0.4.28 + +## 0.6.11 + +### Patch Changes + +- Updated dependencies [[`5fbc5fb01`](https://github.com/firebase/firebase-js-sdk/commit/5fbc5fb0140d7da980fd7ebbfbae810f8c64ae19)]: + - @firebase/component@0.5.1 + - @firebase/installations@0.4.27 + +## 0.6.10 + +### Patch Changes + +- Updated dependencies [[`c34ac7a92`](https://github.com/firebase/firebase-js-sdk/commit/c34ac7a92a616915f38d192654db7770d81747ae), [`ac4ad08a2`](https://github.com/firebase/firebase-js-sdk/commit/ac4ad08a284397ec966e991dd388bb1fba857467)]: + - @firebase/component@0.5.0 + - @firebase/util@1.1.0 + - @firebase/installations@0.4.26 + +## 0.6.9 + +### Patch Changes + +- Updated dependencies [[`7354a0ed4`](https://github.com/firebase/firebase-js-sdk/commit/7354a0ed438f4e3df6577e4927e8c8f8f1fbbfda)]: + - @firebase/util@1.0.0 + - @firebase/component@0.4.1 + - @firebase/installations@0.4.25 + +## 0.6.8 + +### Patch Changes + +- Updated dependencies [[`f24d8961b`](https://github.com/firebase/firebase-js-sdk/commit/f24d8961b3b87821413297688803fc85113086b3)]: + - @firebase/component@0.4.0 + - @firebase/installations@0.4.24 + +## 0.6.7 + +### Patch Changes + +- Updated dependencies [[`de5f90501`](https://github.com/firebase/firebase-js-sdk/commit/de5f9050137acc9ed1490082e5aa429b5de3cb2a)]: + - @firebase/util@0.4.1 + - @firebase/component@0.3.1 + - @firebase/installations@0.4.23 + +## 0.6.6 + +### Patch Changes + +- Updated dependencies [[`5c1a83ed7`](https://github.com/firebase/firebase-js-sdk/commit/5c1a83ed70bae979322bd8751c0885d683ce4bf3)]: + - @firebase/component@0.3.0 + - @firebase/installations@0.4.22 + +## 0.6.5 + +### Patch Changes + +- Updated dependencies [[`ec95df3d0`](https://github.com/firebase/firebase-js-sdk/commit/ec95df3d07e5f091f2a7f7327e46417f64d04b4e)]: + - @firebase/util@0.4.0 + - @firebase/component@0.2.1 + - @firebase/installations@0.4.21 + +## 0.6.4 + +### Patch Changes + +- Updated dependencies [[`6afe42613`](https://github.com/firebase/firebase-js-sdk/commit/6afe42613ed3d7a842d378dc1a09a795811db2ac)]: + - @firebase/component@0.2.0 + - @firebase/installations@0.4.20 + +## 0.6.3 + +### Patch Changes + +- [`74bf52009`](https://github.com/firebase/firebase-js-sdk/commit/74bf52009b291a62deabfd865084d4e0fcacc483) [#4458](https://github.com/firebase/firebase-js-sdk/pull/4458) - Fixed a behavior causing `gtag` to be downloaded twice on Firebase Analytics initialization. This did not seem to affect the functionality of Firebase Analytics but adds noise to the logs when users are trying to debug. + ## 0.6.2 ### Patch Changes diff --git a/packages-exp/functions-exp/api-extractor.json b/packages/analytics/api-extractor.json similarity index 100% rename from packages-exp/functions-exp/api-extractor.json rename to packages/analytics/api-extractor.json diff --git a/packages/analytics/index.test.ts b/packages/analytics/index.test.ts deleted file mode 100644 index 1dc31949c80..00000000000 --- a/packages/analytics/index.test.ts +++ /dev/null @@ -1,360 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { FirebaseAnalytics } from '@firebase/analytics-types'; -import { SinonStub, stub, useFakeTimers } from 'sinon'; -import './testing/setup'; -import { - settings as analyticsSettings, - factory as analyticsFactory, - resetGlobalVars, - getGlobalVars -} from './index'; -import { - getFakeApp, - getFakeInstallations -} from './testing/get-fake-firebase-services'; -import { FirebaseApp } from '@firebase/app-types'; -import { GtagCommand, EventName } from './src/constants'; -import { findGtagScriptOnPage } from './src/helpers'; -import { removeGtagScript } from './testing/gtag-script-util'; -import { Deferred } from '@firebase/util'; -import { AnalyticsError } from './src/errors'; -import { FirebaseInstallations } from '@firebase/installations-types'; - -let analyticsInstance: FirebaseAnalytics = {} as FirebaseAnalytics; -const fakeMeasurementId = 'abcd-efgh'; -const fakeAppParams = { appId: 'abcdefgh12345:23405', apiKey: 'AAbbCCdd12345' }; -let fetchStub: SinonStub = stub(); -const customGtagName = 'customGtag'; -const customDataLayerName = 'customDataLayer'; -let clock: sinon.SinonFakeTimers; - -// Fake indexedDB.open() request -let fakeRequest = { - onsuccess: () => {}, - result: { - close: () => {} - } -}; -let idbOpenStub = stub(); - -function stubFetch(status: number, body: object): void { - fetchStub = stub(window, 'fetch'); - const mockResponse = new Response(JSON.stringify(body), { - status - }); - fetchStub.returns(Promise.resolve(mockResponse)); -} - -// Stub indexedDB.open() because sinon's clock does not know -// how to wait for the real indexedDB callbacks to resolve. -function stubIdbOpen(): void { - (fakeRequest = { - onsuccess: () => {}, - result: { - close: () => {} - } - }), - (idbOpenStub = stub(indexedDB, 'open').returns(fakeRequest as any)); -} - -describe('FirebaseAnalytics instance tests', () => { - describe('Initialization', () => { - beforeEach(() => resetGlobalVars()); - - it('Throws if no appId in config', () => { - const app = getFakeApp({ apiKey: fakeAppParams.apiKey }); - const installations = getFakeInstallations(); - expect(() => analyticsFactory(app, installations)).to.throw( - AnalyticsError.NO_APP_ID - ); - }); - it('Throws if no apiKey or measurementId in config', () => { - const app = getFakeApp({ appId: fakeAppParams.appId }); - const installations = getFakeInstallations(); - expect(() => analyticsFactory(app, installations)).to.throw( - AnalyticsError.NO_API_KEY - ); - }); - it('Warns if config has no apiKey but does have a measurementId', () => { - const warnStub = stub(console, 'warn'); - const app = getFakeApp({ - appId: fakeAppParams.appId, - measurementId: fakeMeasurementId - }); - const installations = getFakeInstallations(); - analyticsFactory(app, installations); - expect(warnStub.args[0][1]).to.include( - `Falling back to the measurement ID ${fakeMeasurementId}` - ); - warnStub.restore(); - }); - it('Throws if creating an instance with already-used appId', () => { - const app = getFakeApp(fakeAppParams); - const installations = getFakeInstallations(); - resetGlobalVars(false, { [fakeAppParams.appId]: Promise.resolve() }); - expect(() => analyticsFactory(app, installations)).to.throw( - AnalyticsError.ALREADY_EXISTS - ); - }); - }); - describe('Standard app, page already has user gtag script', () => { - let app: FirebaseApp = {} as FirebaseApp; - let fidDeferred: Deferred; - const gtagStub: SinonStub = stub(); - before(() => { - clock = useFakeTimers(); - resetGlobalVars(); - app = getFakeApp(fakeAppParams); - fidDeferred = new Deferred(); - const installations = getFakeInstallations('fid-1234', () => - fidDeferred.resolve() - ); - window['gtag'] = gtagStub; - window['dataLayer'] = []; - stubFetch(200, { measurementId: fakeMeasurementId }); - stubIdbOpen(); - analyticsInstance = analyticsFactory(app, installations); - }); - after(() => { - delete window['gtag']; - delete window['dataLayer']; - removeGtagScript(); - fetchStub.restore(); - clock.restore(); - idbOpenStub.restore(); - }); - it('Contains reference to parent app', () => { - expect(analyticsInstance.app).to.equal(app); - }); - it('Calls gtag correctly on logEvent (instance)', async () => { - analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, { - currency: 'USD' - }); - // Successfully resolves fake IDB open request. - fakeRequest.onsuccess(); - // Clear promise chain started by logEvent. - await clock.runAllAsync(); - expect(gtagStub).to.have.been.calledWith('js'); - expect(gtagStub).to.have.been.calledWith( - GtagCommand.CONFIG, - fakeMeasurementId, - { - 'firebase_id': 'fid-1234', - origin: 'firebase', - update: true - } - ); - expect(gtagStub).to.have.been.calledWith( - GtagCommand.EVENT, - EventName.ADD_PAYMENT_INFO, - { - 'send_to': 'abcd-efgh', - currency: 'USD' - } - ); - }); - it('setCurrentScreen() method exists on instance', () => { - expect(analyticsInstance.setCurrentScreen).to.be.instanceOf(Function); - }); - it('setUserId() method exists on instance', () => { - expect(analyticsInstance.setUserId).to.be.instanceOf(Function); - }); - it('setUserProperties() method exists on instance', () => { - expect(analyticsInstance.setUserProperties).to.be.instanceOf(Function); - }); - it('setAnalyticsCollectionEnabled() method exists on instance', () => { - expect(analyticsInstance.setAnalyticsCollectionEnabled).to.be.instanceOf( - Function - ); - }); - }); - - describe('Standard app, mismatched environment', () => { - let app: FirebaseApp = {} as FirebaseApp; - let installations: FirebaseInstallations = {} as FirebaseInstallations; - const gtagStub: SinonStub = stub(); - let fidDeferred: Deferred; - let warnStub: SinonStub; - let cookieStub: SinonStub; - beforeEach(() => { - clock = useFakeTimers(); - resetGlobalVars(); - app = getFakeApp(fakeAppParams); - fidDeferred = new Deferred(); - installations = getFakeInstallations('fid-1234', () => - fidDeferred.resolve() - ); - window['gtag'] = gtagStub; - window['dataLayer'] = []; - stubFetch(200, { measurementId: fakeMeasurementId }); - warnStub = stub(console, 'warn'); - stubIdbOpen(); - }); - afterEach(() => { - delete window['gtag']; - delete window['dataLayer']; - fetchStub.restore(); - clock.restore(); - warnStub.restore(); - idbOpenStub.restore(); - gtagStub.resetHistory(); - }); - it('Warns on initialization if cookies not available', async () => { - cookieStub = stub(navigator, 'cookieEnabled').value(false); - analyticsInstance = analyticsFactory(app, installations); - expect(warnStub.args[0][1]).to.include( - AnalyticsError.INVALID_ANALYTICS_CONTEXT - ); - expect(warnStub.args[0][1]).to.include('Cookies'); - cookieStub.restore(); - }); - it('Warns on initialization if in browser extension', async () => { - window.chrome = { runtime: { id: 'blah' } }; - analyticsInstance = analyticsFactory(app, installations); - expect(warnStub.args[0][1]).to.include( - AnalyticsError.INVALID_ANALYTICS_CONTEXT - ); - expect(warnStub.args[0][1]).to.include('browser extension'); - window.chrome = undefined; - }); - it('Warns on logEvent if indexedDB API not available', async () => { - const idbStub = stub(window, 'indexedDB').value(undefined); - analyticsInstance = analyticsFactory(app, installations); - analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, { - currency: 'USD' - }); - // Clear promise chain started by logEvent. - await clock.runAllAsync(); - // gtag config call omits FID - expect(gtagStub).to.be.calledWith('config', 'abcd-efgh', { - update: true, - origin: 'firebase' - }); - expect(warnStub.args[0][1]).to.include( - AnalyticsError.INDEXEDDB_UNAVAILABLE - ); - expect(warnStub.args[0][1]).to.include('IndexedDB is not available'); - idbStub.restore(); - }); - it('Warns on logEvent if indexedDB.open() not allowed', async () => { - idbOpenStub.restore(); - idbOpenStub = stub(indexedDB, 'open').throws('idb open error test'); - analyticsInstance = analyticsFactory(app, installations); - analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, { - currency: 'USD' - }); - // Clear promise chain started by logEvent. - await clock.runAllAsync(); - // gtag config call omits FID - expect(gtagStub).to.be.calledWith('config', 'abcd-efgh', { - update: true, - origin: 'firebase' - }); - expect(warnStub.args[0][1]).to.include( - AnalyticsError.INDEXEDDB_UNAVAILABLE - ); - expect(warnStub.args[0][1]).to.include('idb open error test'); - }); - }); - - describe('Page has user gtag script with custom gtag and dataLayer names', () => { - let app: FirebaseApp = {} as FirebaseApp; - let fidDeferred: Deferred; - const gtagStub: SinonStub = stub(); - before(() => { - clock = useFakeTimers(); - resetGlobalVars(); - app = getFakeApp(fakeAppParams); - fidDeferred = new Deferred(); - const installations = getFakeInstallations('fid-1234', () => - fidDeferred.resolve() - ); - window[customGtagName] = gtagStub; - window[customDataLayerName] = []; - analyticsSettings({ - dataLayerName: customDataLayerName, - gtagName: customGtagName - }); - stubIdbOpen(); - stubFetch(200, { measurementId: fakeMeasurementId }); - analyticsInstance = analyticsFactory(app, installations); - }); - after(() => { - delete window[customGtagName]; - delete window[customDataLayerName]; - removeGtagScript(); - fetchStub.restore(); - clock.restore(); - idbOpenStub.restore(); - }); - it('Calls gtag correctly on logEvent (instance)', async () => { - analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, { - currency: 'USD' - }); - // Successfully resolves fake IDB open request. - fakeRequest.onsuccess(); - // Clear promise chain started by logEvent. - await clock.runAllAsync(); - expect(gtagStub).to.have.been.calledWith('js'); - expect(gtagStub).to.have.been.calledWith( - GtagCommand.CONFIG, - fakeMeasurementId, - { - 'firebase_id': 'fid-1234', - origin: 'firebase', - update: true - } - ); - expect(gtagStub).to.have.been.calledWith( - GtagCommand.EVENT, - EventName.ADD_PAYMENT_INFO, - { - 'send_to': 'abcd-efgh', - currency: 'USD' - } - ); - }); - }); - - describe('Page has no existing gtag script or dataLayer', () => { - it('Adds the script tag to the page', async () => { - resetGlobalVars(); - const app = getFakeApp(fakeAppParams); - const installations = getFakeInstallations(); - stubFetch(200, {}); - stubIdbOpen(); - analyticsInstance = analyticsFactory(app, installations); - - const { initializationPromisesMap } = getGlobalVars(); - // Successfully resolves fake IDB open request. - fakeRequest.onsuccess(); - await initializationPromisesMap[fakeAppParams.appId]; - expect(findGtagScriptOnPage()).to.not.be.null; - expect(typeof window['gtag']).to.equal('function'); - expect(Array.isArray(window['dataLayer'])).to.be.true; - - delete window['gtag']; - delete window['dataLayer']; - removeGtagScript(); - fetchStub.restore(); - idbOpenStub.restore(); - }); - }); -}); diff --git a/packages/analytics/index.ts b/packages/analytics/index.ts deleted file mode 100644 index 7fff6ac181e..00000000000 --- a/packages/analytics/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import firebase from '@firebase/app'; -import '@firebase/installations'; -import { FirebaseAnalytics } from '@firebase/analytics-types'; -import { FirebaseAnalyticsInternal } from '@firebase/analytics-interop-types'; -import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { - factory, - settings, - resetGlobalVars, - getGlobalVars -} from './src/factory'; -import { EventName } from './src/constants'; -import { - Component, - ComponentType, - ComponentContainer -} from '@firebase/component'; -import { ERROR_FACTORY, AnalyticsError } from './src/errors'; -import { - isIndexedDBAvailable, - validateIndexedDBOpenable, - areCookiesEnabled, - isBrowserExtension -} from '@firebase/util'; -import { name, version } from './package.json'; - -declare global { - interface Window { - [key: string]: unknown; - } -} - -/** - * Type constant for Firebase Analytics. - */ -const ANALYTICS_TYPE = 'analytics'; - -export function registerAnalytics(instance: _FirebaseNamespace): void { - instance.INTERNAL.registerComponent( - new Component( - ANALYTICS_TYPE, - container => { - // getImmediate for FirebaseApp will always succeed - const app = container.getProvider('app').getImmediate(); - const installations = container - .getProvider('installations') - .getImmediate(); - - return factory(app, installations); - }, - ComponentType.PUBLIC - ).setServiceProps({ - settings, - EventName, - isSupported - }) - ); - - instance.INTERNAL.registerComponent( - new Component('analytics-internal', internalFactory, ComponentType.PRIVATE) - ); - - instance.registerVersion(name, version); - - function internalFactory( - container: ComponentContainer - ): FirebaseAnalyticsInternal { - try { - const analytics = container.getProvider(ANALYTICS_TYPE).getImmediate(); - return { - logEvent: analytics.logEvent - }; - } catch (e) { - throw ERROR_FACTORY.create(AnalyticsError.INTEROP_COMPONENT_REG_FAILED, { - reason: e - }); - } - } -} - -export { factory, settings, resetGlobalVars, getGlobalVars }; - -registerAnalytics(firebase as _FirebaseNamespace); - -/** - * Define extension behavior of `registerAnalytics` - */ -declare module '@firebase/app-types' { - interface FirebaseNamespace { - analytics(app?: FirebaseApp): FirebaseAnalytics; - } - interface FirebaseApp { - analytics(): FirebaseAnalytics; - } -} - -/** - * this is a public static method provided to users that wraps four different checks: - * - * 1. check if it's not a browser extension environment. - * 1. check if cookie is enabled in current browser. - * 3. check if IndexedDB is supported by the browser environment. - * 4. check if the current browser context is valid for using IndexedDB. - * - */ -async function isSupported(): Promise { - if (isBrowserExtension()) { - return false; - } - if (!areCookiesEnabled()) { - return false; - } - if (!isIndexedDBAvailable()) { - return false; - } - - try { - const isDBOpenable: boolean = await validateIndexedDBOpenable(); - return isDBOpenable; - } catch (error) { - return false; - } -} diff --git a/packages/analytics/package.json b/packages/analytics/package.json index b6cba0a4535..66fb8b4230b 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -1,55 +1,70 @@ { "name": "@firebase/analytics", - "version": "0.6.2", + "version": "0.10.19", "description": "A analytics package for new firebase packages", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", - "esm2017": "dist/index.esm2017.js", - "files": ["dist"], + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", + "exports": { + ".": { + "types": "./dist/analytics-public.d.ts", + "require": "./dist/index.cjs.js", + "default": "./dist/esm/index.esm.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], "scripts": { "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c", + "build": "rollup -c && yarn api-report", + "build:release": "yarn build && yarn typings:public", "build:deps": "lerna run --scope @firebase/analytics --include-dependencies build", "dev": "rollup -c -w", - "test": "run-p lint test:all", - "test:all": "run-p test:browser test:integration", + "test": "run-p --npm-path npm lint test:all", + "test:all": "run-p --npm-path npm test:browser test:integration", "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", - "test:browser": "karma start --single-run --nocache", - "test:integration": "karma start ./karma.integration.conf.js --single-run --nocache" + "test:browser": "karma start --nocache", + "test:integration": "karma start ./karma.integration.conf.js --nocache", + "trusted-type-check": "tsec -p tsconfig.json --noEmit", + "api-report": "api-extractor run --local --verbose", + "doc": "api-documenter markdown --input temp --output docs", + "build:doc": "yarn build && yarn doc", + "typings:public": "node ../../scripts/build/use_typings.js ./dist/analytics-public.d.ts" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app": "0.x" }, "dependencies": { - "@firebase/analytics-types": "0.4.0", - "@firebase/installations": "0.4.19", - "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.4", - "@firebase/component": "0.1.21", - "tslib": "^1.11.1" + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/component": "0.7.0", + "tslib": "^2.1.0" }, "license": "Apache-2.0", "devDependencies": { - "@firebase/app": "0.6.13", - "rollup": "2.35.1", - "@rollup/plugin-commonjs": "15.1.0", - "@rollup/plugin-json": "4.1.0", - "@rollup/plugin-node-resolve": "9.0.0", - "rollup-plugin-typescript2": "0.29.0", - "typescript": "4.0.5" + "@firebase/app": "0.14.5", + "rollup": "2.79.2", + "rollup-plugin-dts": "5.3.1", + "@rollup/plugin-commonjs": "21.1.0", + "@rollup/plugin-json": "6.1.0", + "@rollup/plugin-node-resolve": "16.0.0", + "rollup-plugin-typescript2": "0.36.0", + "typescript": "5.5.4" }, "repository": { "directory": "packages/analytics", "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" + "url": "git+https://github.com/firebase/firebase-js-sdk.git" }, "bugs": { "url": "https://github.com/firebase/firebase-js-sdk/issues" }, - "typings": "dist/index.d.ts", + "typings": "dist/src/index.d.ts", "nyc": { "extension": [ ".ts" diff --git a/packages/analytics/rollup.config.js b/packages/analytics/rollup.config.js index 003f5e73e9f..f119da4bd5f 100644 --- a/packages/analytics/rollup.config.js +++ b/packages/analytics/rollup.config.js @@ -17,67 +17,78 @@ import json from '@rollup/plugin-json'; import typescriptPlugin from 'rollup-plugin-typescript2'; +import replace from 'rollup-plugin-replace'; import typescript from 'typescript'; +import dts from 'rollup-plugin-dts'; +import { generateBuildTargetReplaceConfig } from '../../scripts/build/rollup_replace_build_target'; +import { emitModulePackageFile } from '../../scripts/build/rollup_emit_module_package_file'; import pkg from './package.json'; +import tsconfig from './tsconfig.json'; -const deps = Object.keys( - Object.assign({}, pkg.peerDependencies, pkg.dependencies) -); - -/** - * ES5 Builds - */ -const es5BuildPlugins = [ - typescriptPlugin({ - typescript - }), - json() -]; - -const es5Builds = [ - /** - * Browser Builds - */ - { - input: 'index.ts', - output: [ - { file: pkg.main, format: 'cjs', sourcemap: true }, - { file: pkg.module, format: 'es', sourcemap: true } - ], - plugins: es5BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) - } +const deps = [ + ...Object.keys(Object.assign({}, pkg.peerDependencies, pkg.dependencies)) ]; -/** - * ES2017 Builds - */ -const es2017BuildPlugins = [ +const buildPlugins = [ typescriptPlugin({ typescript, tsconfigOverride: { - compilerOptions: { - target: 'es2017' - } + exclude: [...tsconfig.exclude, '**/*.test.ts'] } }), json({ preferConst: true }) ]; -const es2017Builds = [ - /** - * Browser Builds - */ +/** + * ESM builds + */ +const esmBuilds = [ { - input: 'index.ts', + input: 'src/index.ts', output: { - file: pkg.esm2017, + file: pkg.browser, format: 'es', sourcemap: true }, - plugins: es2017BuildPlugins, - external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + plugins: [ + ...buildPlugins, + replace(generateBuildTargetReplaceConfig('esm', 2020)), + emitModulePackageFile() + ] } ]; -export default [...es5Builds, ...es2017Builds]; +/** + * CJS builds + */ +const cjsBuilds = [ + { + input: 'src/index.ts', + output: { + file: pkg.main, + format: 'cjs', + sourcemap: true + }, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + plugins: [ + ...buildPlugins, + replace(generateBuildTargetReplaceConfig('cjs', 2020)) + ] + } +]; + +const google3TypingsBuild = { + input: 'dist/src/index.d.ts', + output: { + file: 'dist/src/global_index.d.ts', + format: 'es' + }, + plugins: [ + dts({ + respectExternal: true + }) + ] +}; + +export default [...esmBuilds, ...cjsBuilds, google3TypingsBuild]; diff --git a/packages/analytics/src/api.test.ts b/packages/analytics/src/api.test.ts new file mode 100644 index 00000000000..eacaf3ee811 --- /dev/null +++ b/packages/analytics/src/api.test.ts @@ -0,0 +1,157 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { SinonStub, stub } from 'sinon'; +import '../testing/setup'; +import { getFullApp } from '../testing/get-fake-firebase-services'; +import { + getAnalytics, + initializeAnalytics, + setConsent, + setDefaultEventParameters +} from './api'; +import { FirebaseApp, deleteApp } from '@firebase/app'; +import { AnalyticsError } from './errors'; +import * as init from './initialize-analytics'; +const fakeAppParams = { appId: 'abcdefgh12345:23405', apiKey: 'AAbbCCdd12345' }; +import * as factory from './factory'; +import { + defaultConsentSettingsForInit, + defaultEventParametersForInit +} from './functions'; +import { ConsentSettings } from './public-types'; + +describe('FirebaseAnalytics API tests', () => { + let initStub: SinonStub = stub(); + let app: FirebaseApp; + const wrappedGtag: SinonStub = stub(); + + beforeEach(() => { + initStub = stub(init, '_initializeAnalytics').resolves( + 'FAKE_MEASUREMENT_ID' + ); + }); + + afterEach(async () => { + await initStub(); + initStub.restore(); + if (app) { + return deleteApp(app); + } + }); + + after(() => { + delete window['gtag']; + delete window['dataLayer']; + }); + + it('initializeAnalytics() with same (no) options returns same instance', () => { + app = getFullApp(fakeAppParams); + const analyticsInstance = initializeAnalytics(app); + const newInstance = initializeAnalytics(app); + expect(analyticsInstance).to.equal(newInstance); + }); + it('initializeAnalytics() with same options returns same instance', () => { + app = getFullApp(fakeAppParams); + const analyticsInstance = initializeAnalytics(app, { + config: { 'send_page_view': false } + }); + const newInstance = initializeAnalytics(app, { + config: { 'send_page_view': false } + }); + expect(analyticsInstance).to.equal(newInstance); + }); + it('initializeAnalytics() with different options throws', () => { + app = getFullApp(fakeAppParams); + initializeAnalytics(app, { + config: { 'send_page_view': false } + }); + expect(() => + initializeAnalytics(app, { + config: { 'send_page_view': true } + }) + ).to.throw(AnalyticsError.ALREADY_INITIALIZED); + }); + it('initializeAnalytics() with different options (one undefined) throws', () => { + app = getFullApp(fakeAppParams); + initializeAnalytics(app); + expect(() => + initializeAnalytics(app, { + config: { 'send_page_view': true } + }) + ).to.throw(AnalyticsError.ALREADY_INITIALIZED); + }); + it('getAnalytics() returns same instance created by previous getAnalytics()', () => { + app = getFullApp(fakeAppParams); + const analyticsInstance = getAnalytics(app); + expect(getAnalytics(app)).to.equal(analyticsInstance); + }); + it('getAnalytics() returns same instance created by initializeAnalytics()', () => { + app = getFullApp(fakeAppParams); + const analyticsInstance = initializeAnalytics(app); + expect(getAnalytics(app)).to.equal(analyticsInstance); + }); + it('setDefaultEventParameters() updates defaultEventParametersForInit if gtag does not exist ', () => { + const eventParametersForInit = { + 'github_user': 'dwyfrequency', + 'company': 'google' + }; + app = getFullApp(fakeAppParams); + setDefaultEventParameters(eventParametersForInit); + expect(defaultEventParametersForInit).to.deep.equal(eventParametersForInit); + }); + it('setDefaultEventParameters() calls gtag set if wrappedGtagFunction exists', () => { + const eventParametersForInit = { + 'github_user': 'dwyfrequency', + 'company': 'google' + }; + stub(factory, 'wrappedGtagFunction').get(() => wrappedGtag); + app = getFullApp(fakeAppParams); + setDefaultEventParameters(eventParametersForInit); + expect(wrappedGtag).to.have.been.calledWithExactly( + 'set', + eventParametersForInit + ); + }); + it('setConsent() updates defaultConsentSettingsForInit if gtag does not exist ', () => { + const consentParametersForInit: ConsentSettings = { + 'analytics_storage': 'granted', + 'functionality_storage': 'denied' + }; + stub(factory, 'wrappedGtagFunction').get(() => undefined); + app = getFullApp(fakeAppParams); + setConsent(consentParametersForInit); + expect(defaultConsentSettingsForInit).to.deep.equal( + consentParametersForInit + ); + }); + it('setConsent() calls gtag consent "update" if wrappedGtagFunction exists', () => { + const consentParametersForInit: ConsentSettings = { + 'analytics_storage': 'granted', + 'functionality_storage': 'denied' + }; + stub(factory, 'wrappedGtagFunction').get(() => wrappedGtag); + app = getFullApp(fakeAppParams); + setConsent(consentParametersForInit); + expect(wrappedGtag).to.have.been.calledWithExactly( + 'consent', + 'update', + consentParametersForInit + ); + }); +}); diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts new file mode 100644 index 00000000000..baf136b3e35 --- /dev/null +++ b/packages/analytics/src/api.ts @@ -0,0 +1,775 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable camelcase */ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { _getProvider, FirebaseApp, getApp } from '@firebase/app'; +import { + Analytics, + AnalyticsCallOptions, + AnalyticsSettings, + ConsentSettings, + CustomParams, + EventNameString, + EventParams +} from './public-types'; +import { Provider } from '@firebase/component'; +import { + isIndexedDBAvailable, + validateIndexedDBOpenable, + areCookiesEnabled, + isBrowserExtension, + getModularInstance, + deepEqual +} from '@firebase/util'; +import { ANALYTICS_TYPE, GtagCommand } from './constants'; +import { + AnalyticsService, + initializationPromisesMap, + wrappedGtagFunction +} from './factory'; +import { logger } from './logger'; +import { + logEvent as internalLogEvent, + setCurrentScreen as internalSetCurrentScreen, + setUserId as internalSetUserId, + setUserProperties as internalSetUserProperties, + setAnalyticsCollectionEnabled as internalSetAnalyticsCollectionEnabled, + _setConsentDefaultForInit, + _setDefaultEventParametersForInit, + internalGetGoogleAnalyticsClientId +} from './functions'; +import { ERROR_FACTORY, AnalyticsError } from './errors'; + +export { settings } from './factory'; + +declare module '@firebase/component' { + interface NameServiceMapping { + [ANALYTICS_TYPE]: AnalyticsService; + } +} + +/** + * Returns an {@link Analytics} instance for the given app. + * + * @public + * + * @param app - The {@link @firebase/app#FirebaseApp} to use. + */ +export function getAnalytics(app: FirebaseApp = getApp()): Analytics { + app = getModularInstance(app); + // Dependencies + const analyticsProvider: Provider<'analytics'> = _getProvider( + app, + ANALYTICS_TYPE + ); + + if (analyticsProvider.isInitialized()) { + return analyticsProvider.getImmediate(); + } + + return initializeAnalytics(app); +} + +/** + * Returns an {@link Analytics} instance for the given app. + * + * @public + * + * @param app - The {@link @firebase/app#FirebaseApp} to use. + */ +export function initializeAnalytics( + app: FirebaseApp, + options: AnalyticsSettings = {} +): Analytics { + // Dependencies + const analyticsProvider: Provider<'analytics'> = _getProvider( + app, + ANALYTICS_TYPE + ); + if (analyticsProvider.isInitialized()) { + const existingInstance = analyticsProvider.getImmediate(); + if (deepEqual(options, analyticsProvider.getOptions())) { + return existingInstance; + } else { + throw ERROR_FACTORY.create(AnalyticsError.ALREADY_INITIALIZED); + } + } + const analyticsInstance = analyticsProvider.initialize({ options }); + return analyticsInstance; +} + +/** + * This is a public static method provided to users that wraps four different checks: + * + * 1. Check if it's not a browser extension environment. + * 2. Check if cookies are enabled in current browser. + * 3. Check if IndexedDB is supported by the browser environment. + * 4. Check if the current browser context is valid for using `IndexedDB.open()`. + * + * @public + * + */ +export async function isSupported(): Promise { + if (isBrowserExtension()) { + return false; + } + if (!areCookiesEnabled()) { + return false; + } + if (!isIndexedDBAvailable()) { + return false; + } + + try { + const isDBOpenable: boolean = await validateIndexedDBOpenable(); + return isDBOpenable; + } catch (error) { + return false; + } +} + +/** + * Use gtag `config` command to set `screen_name`. + * + * @public + * + * @deprecated Use {@link logEvent} with `eventName` as 'screen_view' and add relevant `eventParams`. + * See {@link https://firebase.google.com/docs/analytics/screenviews | Track Screenviews}. + * + * @param analyticsInstance - The {@link Analytics} instance. + * @param screenName - Screen name to set. + */ +export function setCurrentScreen( + analyticsInstance: Analytics, + screenName: string, + options?: AnalyticsCallOptions +): void { + analyticsInstance = getModularInstance(analyticsInstance); + internalSetCurrentScreen( + wrappedGtagFunction, + initializationPromisesMap[analyticsInstance.app.options.appId!], + screenName, + options + ).catch(e => logger.error(e)); +} + +/** + * Retrieves a unique Google Analytics identifier for the web client. + * See {@link https://developers.google.com/analytics/devguides/collection/ga4/reference/config#client_id | client_id}. + * + * @public + * + * @param app - The {@link @firebase/app#FirebaseApp} to use. + */ +export async function getGoogleAnalyticsClientId( + analyticsInstance: Analytics +): Promise { + analyticsInstance = getModularInstance(analyticsInstance); + return internalGetGoogleAnalyticsClientId( + wrappedGtagFunction, + initializationPromisesMap[analyticsInstance.app.options.appId!] + ); +} + +/** + * Use gtag `config` command to set `user_id`. + * + * @public + * + * @param analyticsInstance - The {@link Analytics} instance. + * @param id - User ID to set. + */ +export function setUserId( + analyticsInstance: Analytics, + id: string | null, + options?: AnalyticsCallOptions +): void { + analyticsInstance = getModularInstance(analyticsInstance); + internalSetUserId( + wrappedGtagFunction, + initializationPromisesMap[analyticsInstance.app.options.appId!], + id, + options + ).catch(e => logger.error(e)); +} + +/** + * Use gtag `config` command to set all params specified. + * + * @public + */ +export function setUserProperties( + analyticsInstance: Analytics, + properties: CustomParams, + options?: AnalyticsCallOptions +): void { + analyticsInstance = getModularInstance(analyticsInstance); + internalSetUserProperties( + wrappedGtagFunction, + initializationPromisesMap[analyticsInstance.app.options.appId!], + properties, + options + ).catch(e => logger.error(e)); +} + +/** + * Sets whether Google Analytics collection is enabled for this app on this device. + * Sets global `window['ga-disable-analyticsId'] = true;` + * + * @public + * + * @param analyticsInstance - The {@link Analytics} instance. + * @param enabled - If true, enables collection, if false, disables it. + */ +export function setAnalyticsCollectionEnabled( + analyticsInstance: Analytics, + enabled: boolean +): void { + analyticsInstance = getModularInstance(analyticsInstance); + internalSetAnalyticsCollectionEnabled( + initializationPromisesMap[analyticsInstance.app.options.appId!], + enabled + ).catch(e => logger.error(e)); +} + +/** + * Adds data that will be set on every event logged from the SDK, including automatic ones. + * With gtag's "set" command, the values passed persist on the current page and are passed with + * all subsequent events. + * @public + * @param customParams - Any custom params the user may pass to gtag.js. + */ +export function setDefaultEventParameters(customParams: CustomParams): void { + // Check if reference to existing gtag function on window object exists + if (wrappedGtagFunction) { + wrappedGtagFunction(GtagCommand.SET, customParams); + } else { + _setDefaultEventParametersForInit(customParams); + } +} + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'add_payment_info', + eventParams?: { + coupon?: EventParams['coupon']; + currency?: EventParams['currency']; + items?: EventParams['items']; + payment_type?: EventParams['payment_type']; + value?: EventParams['value']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'add_shipping_info', + eventParams?: { + coupon?: EventParams['coupon']; + currency?: EventParams['currency']; + items?: EventParams['items']; + shipping_tier?: EventParams['shipping_tier']; + value?: EventParams['value']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'add_to_cart' | 'add_to_wishlist' | 'remove_from_cart', + eventParams?: { + currency?: EventParams['currency']; + value?: EventParams['value']; + items?: EventParams['items']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'begin_checkout', + eventParams?: { + currency?: EventParams['currency']; + coupon?: EventParams['coupon']; + value?: EventParams['value']; + items?: EventParams['items']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'checkout_progress', + eventParams?: { + currency?: EventParams['currency']; + coupon?: EventParams['coupon']; + value?: EventParams['value']; + items?: EventParams['items']; + checkout_step?: EventParams['checkout_step']; + checkout_option?: EventParams['checkout_option']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * See + * {@link https://developers.google.com/analytics/devguides/collection/ga4/exceptions + * | Measure exceptions}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'exception', + eventParams?: { + description?: EventParams['description']; + fatal?: EventParams['fatal']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'generate_lead', + eventParams?: { + value?: EventParams['value']; + currency?: EventParams['currency']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'login', + eventParams?: { + method?: EventParams['method']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * See + * {@link https://developers.google.com/analytics/devguides/collection/ga4/views + * | Page views}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'page_view', + eventParams?: { + page_title?: string; + page_location?: string; + page_path?: string; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'purchase' | 'refund', + eventParams?: { + value?: EventParams['value']; + currency?: EventParams['currency']; + transaction_id: EventParams['transaction_id']; + tax?: EventParams['tax']; + shipping?: EventParams['shipping']; + items?: EventParams['items']; + coupon?: EventParams['coupon']; + affiliation?: EventParams['affiliation']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * See {@link https://firebase.google.com/docs/analytics/screenviews + * | Track Screenviews}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'screen_view', + eventParams?: { + firebase_screen: EventParams['firebase_screen']; + firebase_screen_class: EventParams['firebase_screen_class']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'search' | 'view_search_results', + eventParams?: { + search_term?: EventParams['search_term']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'select_content', + eventParams?: { + content_type?: EventParams['content_type']; + item_id?: EventParams['item_id']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'select_item', + eventParams?: { + items?: EventParams['items']; + item_list_name?: EventParams['item_list_name']; + item_list_id?: EventParams['item_list_id']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'select_promotion' | 'view_promotion', + eventParams?: { + items?: EventParams['items']; + promotion_id?: EventParams['promotion_id']; + promotion_name?: EventParams['promotion_name']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'set_checkout_option', + eventParams?: { + checkout_step?: EventParams['checkout_step']; + checkout_option?: EventParams['checkout_option']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'share', + eventParams?: { + method?: EventParams['method']; + content_type?: EventParams['content_type']; + item_id?: EventParams['item_id']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'sign_up', + eventParams?: { + method?: EventParams['method']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'timing_complete', + eventParams?: { + name: string; + value: number; + event_category?: string; + event_label?: string; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'view_cart' | 'view_item', + eventParams?: { + currency?: EventParams['currency']; + items?: EventParams['items']; + value?: EventParams['value']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: 'view_item_list', + eventParams?: { + items?: EventParams['items']; + item_list_name?: EventParams['item_list_name']; + item_list_id?: EventParams['item_list_id']; + [key: string]: any; + }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * @public + * List of recommended event parameters can be found in + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: CustomEventName, + eventParams?: { [key: string]: any }, + options?: AnalyticsCallOptions +): void; + +/** + * Sends a Google Analytics event with given `eventParams`. This method + * automatically associates this logged event with this Firebase web + * app instance on this device. + * List of official event parameters can be found in the gtag.js + * reference documentation: + * {@link https://developers.google.com/gtagjs/reference/ga4-events + * | the GA4 reference documentation}. + * + * @public + */ +export function logEvent( + analyticsInstance: Analytics, + eventName: string, + eventParams?: EventParams, + options?: AnalyticsCallOptions +): void { + analyticsInstance = getModularInstance(analyticsInstance); + internalLogEvent( + wrappedGtagFunction, + initializationPromisesMap[analyticsInstance.app.options.appId!], + eventName, + eventParams, + options + ).catch(e => logger.error(e)); +} + +/** + * Any custom event name string not in the standard list of recommended + * event names. + * @public + */ +export type CustomEventName = T extends EventNameString ? never : T; + +/** + * Sets the applicable end user consent state for this web app across all gtag references once + * Firebase Analytics is initialized. + * + * Use the {@link ConsentSettings} to specify individual consent type values. By default consent + * types are set to "granted". + * @public + * @param consentSettings - Maps the applicable end user consent state for gtag.js. + */ +export function setConsent(consentSettings: ConsentSettings): void { + // Check if reference to existing gtag function on window object exists + if (wrappedGtagFunction) { + wrappedGtagFunction(GtagCommand.CONSENT, 'update', consentSettings); + } else { + _setConsentDefaultForInit(consentSettings); + } +} diff --git a/packages/analytics/src/constants.ts b/packages/analytics/src/constants.ts index 2b24c0f59ec..e95e76e0ace 100644 --- a/packages/analytics/src/constants.ts +++ b/packages/analytics/src/constants.ts @@ -15,6 +15,11 @@ * limitations under the License. */ +/** + * Type constant for Firebase Analytics. + */ +export const ANALYTICS_TYPE = 'analytics'; + // Key to attach FID to in gtag params. export const GA_FID_KEY = 'firebase_id'; export const ORIGIN_KEY = 'origin'; @@ -26,44 +31,10 @@ export const DYNAMIC_CONFIG_URL = export const GTAG_URL = 'https://www.googletagmanager.com/gtag/js'; -export enum GtagCommand { +export const enum GtagCommand { EVENT = 'event', SET = 'set', - CONFIG = 'config' -} - -/* - * Officially recommended event names for gtag.js - * Any other string is also allowed. - */ -export enum EventName { - ADD_SHIPPING_INFO = 'add_shipping_info', - ADD_PAYMENT_INFO = 'add_payment_info', - ADD_TO_CART = 'add_to_cart', - ADD_TO_WISHLIST = 'add_to_wishlist', - BEGIN_CHECKOUT = 'begin_checkout', - /** @deprecated */ - CHECKOUT_PROGRESS = 'checkout_progress', - EXCEPTION = 'exception', - GENERATE_LEAD = 'generate_lead', - LOGIN = 'login', - PAGE_VIEW = 'page_view', - PURCHASE = 'purchase', - REFUND = 'refund', - REMOVE_FROM_CART = 'remove_from_cart', - SCREEN_VIEW = 'screen_view', - SEARCH = 'search', - SELECT_CONTENT = 'select_content', - SELECT_ITEM = 'select_item', - SELECT_PROMOTION = 'select_promotion', - /** @deprecated */ - SET_CHECKOUT_OPTION = 'set_checkout_option', - SHARE = 'share', - SIGN_UP = 'sign_up', - TIMING_COMPLETE = 'timing_complete', - VIEW_CART = 'view_cart', - VIEW_ITEM = 'view_item', - VIEW_ITEM_LIST = 'view_item_list', - VIEW_PROMOTION = 'view_promotion', - VIEW_SEARCH_RESULTS = 'view_search_results' + CONFIG = 'config', + CONSENT = 'consent', + GET = 'get' } diff --git a/packages/analytics/src/errors.ts b/packages/analytics/src/errors.ts index 6381eac4871..56c0ffe066c 100644 --- a/packages/analytics/src/errors.ts +++ b/packages/analytics/src/errors.ts @@ -20,13 +20,16 @@ import { ErrorFactory, ErrorMap } from '@firebase/util'; export const enum AnalyticsError { ALREADY_EXISTS = 'already-exists', ALREADY_INITIALIZED = 'already-initialized', + ALREADY_INITIALIZED_SETTINGS = 'already-initialized-settings', INTEROP_COMPONENT_REG_FAILED = 'interop-component-reg-failed', INVALID_ANALYTICS_CONTEXT = 'invalid-analytics-context', INDEXEDDB_UNAVAILABLE = 'indexeddb-unavailable', FETCH_THROTTLE = 'fetch-throttle', CONFIG_FETCH_FAILED = 'config-fetch-failed', NO_API_KEY = 'no-api-key', - NO_APP_ID = 'no-app-id' + NO_APP_ID = 'no-app-id', + NO_CLIENT_ID = 'no-client-id', + INVALID_GTAG_RESOURCE = 'invalid-gtag-resource' } const ERRORS: ErrorMap = { @@ -35,6 +38,11 @@ const ERRORS: ErrorMap = { ' already exists. ' + 'Only one Firebase Analytics instance can be created for each appId.', [AnalyticsError.ALREADY_INITIALIZED]: + 'initializeAnalytics() cannot be called again with different options than those ' + + 'it was initially called with. It can be called again with the same options to ' + + 'return the existing instance, or getAnalytics() can be used ' + + 'to get a reference to the already-initialized instance.', + [AnalyticsError.ALREADY_INITIALIZED_SETTINGS]: 'Firebase Analytics has already been initialized.' + 'settings() must be called before initializing any Analytics instance' + 'or it will have no effect.', @@ -58,7 +66,10 @@ const ERRORS: ErrorMap = { 'contain a valid API key.', [AnalyticsError.NO_APP_ID]: 'The "appId" field is empty in the local Firebase config. Firebase Analytics requires this field to' + - 'contain a valid app ID.' + 'contain a valid app ID.', + [AnalyticsError.NO_CLIENT_ID]: 'The "client_id" field is empty.', + [AnalyticsError.INVALID_GTAG_RESOURCE]: + 'Trusted Types detected an invalid gtag resource: {$gtagURL}.' }; interface ErrorParams { @@ -71,6 +82,7 @@ interface ErrorParams { }; [AnalyticsError.INVALID_ANALYTICS_CONTEXT]: { errorInfo: string }; [AnalyticsError.INDEXEDDB_UNAVAILABLE]: { errorInfo: string }; + [AnalyticsError.INVALID_GTAG_RESOURCE]: { gtagURL: string }; } export const ERROR_FACTORY = new ErrorFactory( diff --git a/packages/analytics/src/factory.ts b/packages/analytics/src/factory.ts index 3bd742304c0..b0ef0e04aa4 100644 --- a/packages/analytics/src/factory.ts +++ b/packages/analytics/src/factory.ts @@ -15,44 +15,33 @@ * limitations under the License. */ -import { - FirebaseAnalytics, - Gtag, - SettingsOptions, - DynamicConfig, - MinimalDynamicConfig -} from '@firebase/analytics-types'; -import { - logEvent, - setCurrentScreen, - setUserId, - setUserProperties, - setAnalyticsCollectionEnabled -} from './functions'; -import { - insertScriptTag, - getOrCreateDataLayer, - wrapOrCreateGtag, - findGtagScriptOnPage -} from './helpers'; +import { SettingsOptions, Analytics, AnalyticsSettings } from './public-types'; +import { Gtag, DynamicConfig, MinimalDynamicConfig } from './types'; +import { getOrCreateDataLayer, wrapOrCreateGtag } from './helpers'; import { AnalyticsError, ERROR_FACTORY } from './errors'; -import { FirebaseApp } from '@firebase/app-types'; -import { FirebaseInstallations } from '@firebase/installations-types'; +import { _FirebaseInstallationsInternal } from '@firebase/installations'; import { areCookiesEnabled, isBrowserExtension } from '@firebase/util'; -import { initializeIds } from './initialize-ids'; +import { _initializeAnalytics } from './initialize-analytics'; import { logger } from './logger'; -import { FirebaseService } from '@firebase/app-types/private'; +import { FirebaseApp, _FirebaseService } from '@firebase/app'; -interface FirebaseAnalyticsInternal - extends FirebaseAnalytics, - FirebaseService {} +/** + * Analytics Service class. + */ +export class AnalyticsService implements Analytics, _FirebaseService { + constructor(public app: FirebaseApp) {} + _delete(): Promise { + delete initializationPromisesMap[this.app.options.appId!]; + return Promise.resolve(); + } +} /** * Maps appId to full initialization promise. Wrapped gtag calls must wait on * all or some of these, depending on the call's `send_to` param and the status * of the dynamic config fetches (see below). */ -let initializationPromisesMap: { +export let initializationPromisesMap: { [appId: string]: Promise; // Promise contains measurement ID string. } = {}; @@ -61,9 +50,9 @@ let initializationPromisesMap: { * wait on all these to be complete in order to determine if it can selectively * wait for only certain initialization (FID) promises or if it must wait for all. */ -let dynamicConfigPromisesList: Array> = []; +let dynamicConfigPromisesList: Array< + Promise +> = []; /** * Maps fetched measurementIds to appId. Populated when the app's dynamic config @@ -93,7 +82,7 @@ let gtagCoreFunction: Gtag; * Wrapper around gtag function that ensures FID is sent with all * relevant event and config calls. */ -let wrappedGtagFunction: Gtag; +export let wrappedGtagFunction: Gtag; /** * Flag to ensure page initialization steps (creation or wrapping of @@ -103,6 +92,7 @@ let globalInitDone: boolean = false; /** * For testing + * @internal */ export function resetGlobalVars( newGlobalInitDone = false, @@ -118,6 +108,7 @@ export function resetGlobalVars( /** * For testing + * @internal */ export function getGlobalVars(): { initializationPromisesMap: { [appId: string]: Promise }; @@ -132,9 +123,16 @@ export function getGlobalVars(): { } /** - * This must be run before calling firebase.analytics() or it won't + * Configures Firebase Analytics to use custom `gtag` or `dataLayer` names. + * Intended to be used if `gtag.js` script has been installed on + * this page independently of Firebase Analytics, and is using non-default + * names for either the `gtag` function or for `dataLayer`. + * Must be called before calling `getAnalytics()` or it won't * have any effect. - * @param options Custom gtag and dataLayer names. + * + * @public + * + * @param options - Custom gtag and dataLayer names. */ export function settings(options: SettingsOptions): void { if (globalInitDone) { @@ -172,10 +170,15 @@ function warnOnBrowserContextMismatch(): void { } } +/** + * Analytics instance factory. + * @internal + */ export function factory( app: FirebaseApp, - installations: FirebaseInstallations -): FirebaseAnalytics { + installations: _FirebaseInstallationsInternal, + options?: AnalyticsSettings +): AnalyticsService { warnOnBrowserContextMismatch(); const appId = app.options.appId; if (!appId) { @@ -202,10 +205,6 @@ export function factory( // Steps here should only be done once per page: creation or wrapping // of dataLayer and global gtag function. - // Detect if user has already put the gtag + + + + + + + + + + + + + +
+ + + + + + + +
+
+ + +
+
+ + +
+
+ + + [anonymous] / + uid: + +
+
+ + / + + + + + + + +
+ + + +
+
+
+
+ + +
+ +
+
+
+ + + +
+
+ +
Development mode APIs
+
+
+ + +
+ +
+ + +
Web Worker Testing
+
+ +
+ +
Service Worker Testing
+
+ +
+ + +
Auth State Persistence
+
+ + +
+ + +
Language code
+
+ + + +
+ + +
Sign Up
+
+ + + +
+ + + +
Sign In
+
+ + + +
+
+ + +
+ +
+ + + + + +
+ +
+ + + + + +
+ + +
Sign In with Email Link
+
+ + + +
+
+ + +
+ + +
Password Reset
+
+ + +
+
+ + + + +
+ +
Fetch Sign In Methods
+
+ + +
+ + +
Update Current User
+
+ +
+
+ +
+
+
+ +
Update Profile
+
+ + +
+
+ + +
+
+ + + +
+ + + +
Linking/Unlinking
+ +
+ + + +
+
+ + + + + +
+ +
+ + + + + + + +
+ +
+ + + + + +
+ +
+ + +
+ + +
Enroll Second Factor
+ +
+
+
+ + + + + + +
+
+
+ + +
Other Actions
+ +
+ + +
+ + + + + + + +
Delete account
+ +
+ +
+
Web
+
+ +
+
Android
+
+
+ +
+ + +
+ +
+
+
iOS
+
+
+ +
+
+
+
+ + +
+ +
+
+
+

+              
+            
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/auth-compat/demo/public/manifest.json b/packages/auth-compat/demo/public/manifest.json new file mode 100644 index 00000000000..ed13b7176b0 --- /dev/null +++ b/packages/auth-compat/demo/public/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "Firebase Auth Test App", + "short_name": "FirebaseAuthTest", + "start_url": "/", + "display": "standalone", + "background_color": "#fff", + "lang": "en-US", + "description": "Test app to test all functionality for Firebase Auth.", + "prefer_related_applications": false, + "theme_color": "#fff", + "scope": "/", + "orientation": "portrait-primary" +} diff --git a/packages-exp/auth-compat-exp/demo/public/sample-config.js b/packages/auth-compat/demo/public/sample-config.js similarity index 100% rename from packages-exp/auth-compat-exp/demo/public/sample-config.js rename to packages/auth-compat/demo/public/sample-config.js diff --git a/packages/auth-compat/demo/public/script.js b/packages/auth-compat/demo/public/script.js new file mode 100644 index 00000000000..35defe3c5c3 --- /dev/null +++ b/packages/auth-compat/demo/public/script.js @@ -0,0 +1,1862 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Common javascript for the application. + */ + +var app = null; +var auth = null; +var tempApp = null; +var tempAuth = null; +var currentTab = null; +var lastUser = null; +var applicationVerifier = null; +var multiFactorErrorResolver = null; +var selectedMultiFactorHint = null; +var recaptchaSize = 'normal'; + +// Fix for IE8 when developer's console is not opened. +if (!window.console) { + window.console = { + log: function () {}, + error: function () {} + }; +} + +// The corresponding Font Awesome icons for each provider. +var providersIcons = { + 'google.com': 'fa-google', + 'facebook.com': 'fa-facebook-official', + 'twitter.com': 'fa-twitter-square', + 'github.com': 'fa-github', + 'yahoo.com': 'fa-yahoo', + 'phone': 'fa-phone' +}; + +/** + * Logs the message in the console and on the log window in the app + * using the level given. + * @param {?Object} message Object or message to log. + * @param {string} level The level of log (log, error, debug). + * @private + */ +function logAtLevel_(message, level) { + if (message != null) { + var messageDiv = $('
'); + messageDiv.addClass(level); + if (typeof message === 'object') { + messageDiv.text(JSON.stringify(message, null, ' ')); + } else { + messageDiv.text(message); + } + $('.logs').append(messageDiv); + } + console[level](message); +} + +/** + * Logs info level. + * @param {string} message Object or message to log. + */ +function log(message) { + logAtLevel_(message, 'log'); +} + +/** + * Clear the logs. + */ +function clearLogs() { + $('.logs').text(''); +} + +/** + * Displays for a few seconds a box with a specific message and then fades + * it out. + * @param {string} message Small message to display. + * @param {string} cssClass The class(s) to give the alert box. + * @private + */ +function alertMessage_(message, cssClass) { + var alertBox = $('
') + .addClass(cssClass) + .css('display', 'none') + .text(message); + // When modals are visible, display the alert in the modal layer above the + // grey background. + var visibleModal = $('.modal.in'); + if (visibleModal.size() > 0) { + // Check first if the model has an overlaying-alert. If not, append the + // overlaying-alert container. + if (visibleModal.find('.overlaying-alert').size() == 0) { + var $overlayingAlert = $( + '
' + ); + visibleModal.append($overlayingAlert); + } + visibleModal.find('.overlaying-alert').prepend(alertBox); + } else { + $('#alert-messages').prepend(alertBox); + } + alertBox.fadeIn({ + complete: function () { + setTimeout(function () { + alertBox.slideUp(400, function () { + // On completion, remove the alert element from the DOM. + alertBox.remove(); + }); + }, 3000); + } + }); +} + +/** + * Alerts a small success message in a overlaying alert box. + * @param {string} message Small message to display. + */ +function alertSuccess(message) { + alertMessage_(message, 'alert alert-success'); +} + +/** + * Alerts a small error message in a overlaying alert box. + * @param {string} message Small message to display. + */ +function alertError(message) { + alertMessage_(message, 'alert alert-danger'); +} + +/** + * Returns the active user (i.e. currentUser or lastUser). + * @return {!firebase.User} + */ +function activeUser() { + var type = $('input[name=toggle-user-selection]:checked').val(); + if (type == 'lastUser') { + return lastUser; + } else { + return auth.currentUser; + } +} + +/** + * Refreshes the current user data in the UI, displaying a user info box if + * a user is signed in, or removing it. + */ +function refreshUserData() { + if (activeUser()) { + var user = activeUser(); + $('.profile').show(); + $('body').addClass('user-info-displayed'); + $('div.profile-email,span.profile-email').text(user.email || 'No Email'); + $('div.profile-phone,span.profile-phone').text( + user.phoneNumber || 'No Phone' + ); + $('div.profile-uid,span.profile-uid').text(user.uid); + $('div.profile-name,span.profile-name').text(user.displayName || 'No Name'); + $('input.profile-name').val(user.displayName); + $('input.photo-url').val(user.photoURL); + if (user.photoURL != null) { + var photoURL = user.photoURL; + // Append size to the photo URL for Google hosted images to avoid requesting + // the image with its original resolution (using more bandwidth than needed) + // when it is going to be presented in smaller size. + if ( + photoURL.indexOf('googleusercontent.com') != -1 || + photoURL.indexOf('ggpht.com') != -1 + ) { + photoURL = photoURL + '?sz=' + $('img.profile-image').height(); + } + $('img.profile-image').attr('src', photoURL).show(); + } else { + $('img.profile-image').hide(); + } + $('.profile-email-verified').toggle(user.emailVerified); + $('.profile-email-not-verified').toggle(!user.emailVerified); + $('.profile-anonymous').toggle(user.isAnonymous); + // Display/Hide providers icons. + $('.profile-providers').empty(); + if (user['providerData'] && user['providerData'].length) { + var providersCount = user['providerData'].length; + for (var i = 0; i < providersCount; i++) { + addProviderIcon(user['providerData'][i]['providerId']); + } + } + // Show enrolled second factors if available for the active user. + showMultiFactorStatus(user); + // Change color. + if (user == auth.currentUser) { + $('#user-info').removeClass('last-user'); + $('#user-info').addClass('current-user'); + } else { + $('#user-info').removeClass('current-user'); + $('#user-info').addClass('last-user'); + } + } else { + $('.profile').slideUp(); + $('body').removeClass('user-info-displayed'); + $('input.profile-data').val(''); + } +} + +/** + * Sets last signed in user and updates UI. + * @param {?firebase.User} user The last signed in user. + */ +function setLastUser(user) { + lastUser = user; + if (user) { + // Displays the toggle. + $('#toggle-user').show(); + $('#toggle-user-placeholder').hide(); + } else { + $('#toggle-user').hide(); + $('#toggle-user-placeholder').show(); + } +} + +/** + * Add a provider icon to the profile info. + * @param {string} providerId The providerId of the provider. + */ +function addProviderIcon(providerId) { + var pElt = $('') + .addClass('fa ' + providersIcons[providerId]) + .attr('title', providerId) + .data({ + 'toggle': 'tooltip', + 'placement': 'bottom' + }); + $('.profile-providers').append(pElt); + pElt.tooltip(); +} + +/** + * Updates the active user's multi-factor enrollment status. + * @param {!firebase.User} activeUser The corresponding user. + */ +function showMultiFactorStatus(activeUser) { + var enrolledFactors = + (activeUser.multiFactor && activeUser.multiFactor.enrolledFactors) || []; + var $listGroup = $('#user-info .dropdown-menu.enrolled-second-factors'); + // Hide the drop down menu initially. + $listGroup.empty().parent().hide(); + if (enrolledFactors.length) { + // If enrolled factors are available, show the drop down menu. + $listGroup.parent().show(); + // Populate the enrolled factors. + showMultiFactors( + $listGroup, + enrolledFactors, + // On row click, do nothing. This is needed to prevent the drop down + // menu from closing. + function (e) { + e.preventDefault(); + e.stopPropagation(); + }, + // On delete click unenroll the selected factor. + function (e) { + e.preventDefault(); + // Get the corresponding second factor index. + var index = parseInt($(this).attr('data-index'), 10); + // Get the second factor info. + var info = enrolledFactors[index]; + // Get the display name. If not available, use uid. + var label = info && (info.displayName || info.uid); + if (label) { + $('#enrolled-factors-drop-down').removeClass('open'); + activeUser.multiFactor.unenroll(info).then(function () { + refreshUserData(); + alertSuccess('Multi-factor successfully unenrolled.'); + }, onAuthError); + } + } + ); + } +} + +/** + * Updates the UI when the user is successfully authenticated. + * @param {!firebase.User} user User authenticated. + */ +function onAuthSuccess(user) { + console.log(user); + alertSuccess('User authenticated, id: ' + user.uid); + refreshUserData(); +} + +/** + * Displays an error message when the authentication failed. + * @param {!firebase.auth.Error} error Error message to display. + */ +function onAuthError(error) { + logAtLevel_(error, 'error'); + if (error.code == 'auth/multi-factor-auth-required') { + // Handle second factor sign-in. + handleMultiFactorSignIn(error.resolver); + } else { + alertError('Error: ' + error.code); + } +} + +/** + * Changes the UI when the user has been signed out. + */ +function signOut() { + log('User successfully signed out.'); + alertSuccess('User successfully signed out.'); + refreshUserData(); +} + +/** + * Saves the new language code provided in the language code input field. + */ +function onSetLanguageCode() { + var languageCode = $('#language-code').val() || null; + try { + auth.languageCode = languageCode; + alertSuccess('Language code changed to "' + languageCode + '".'); + } catch (error) { + alertError('Error: ' + error.code); + } +} + +/** + * Switches Auth instance language to device language. + */ +function onUseDeviceLanguage() { + auth.useDeviceLanguage(); + $('#language-code').val(auth.languageCode); + alertSuccess('Using device language "' + auth.languageCode + '".'); +} + +/** + * Changes the Auth state persistence to the specified one. + */ +function onSetPersistence() { + var type = $('#persistence-type').val(); + try { + auth.setPersistence(type).then( + function () { + log('Persistence state change to "' + type + '".'); + alertSuccess('Persistence state change to "' + type + '".'); + }, + function (error) { + alertError('Error: ' + error.code); + } + ); + } catch (error) { + alertError('Error: ' + error.code); + } +} + +/** + * Signs up a new user with an email and a password. + */ +function onSignUp() { + var email = $('#signup-email').val(); + var password = $('#signup-password').val(); + auth + .createUserWithEmailAndPassword(email, password) + .then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Signs in a user with an email and a password. + */ +function onSignInWithEmailAndPassword() { + var email = $('#signin-email').val(); + var password = $('#signin-password').val(); + auth + .signInWithEmailAndPassword(email, password) + .then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Signs in a user with an email link. + */ +function onSignInWithEmailLink() { + var email = $('#sign-in-with-email-link-email').val(); + var link = $('#sign-in-with-email-link-link').val() || undefined; + if (auth.isSignInWithEmailLink(link)) { + auth.signInWithEmailLink(email, link).then(onAuthSuccess, onAuthError); + } else { + alertError('Sign in link is invalid'); + } +} + +/** + * Links a user with an email link. + */ +function onLinkWithEmailLink() { + var email = $('#link-with-email-link-email').val(); + var link = $('#link-with-email-link-link').val() || undefined; + var credential = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + link + ); + activeUser() + .linkWithCredential(credential) + .then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Re-authenticate a user with email link credential. + */ +function onReauthenticateWithEmailLink() { + var email = $('#link-with-email-link-email').val(); + var link = $('#link-with-email-link-link').val() || undefined; + var credential = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + link + ); + activeUser() + .reauthenticateWithCredential(credential) + .then(function (result) { + logAdditionalUserInfo(result); + refreshUserData(); + alertSuccess('User reauthenticated!'); + }, onAuthError); +} + +/** + * Signs in with a custom token. + * @param {DOMEvent} event HTML DOM event returned by the listener. + */ +function onSignInWithCustomToken(event) { + // The token can be directly specified on the html element. + var token = $('#user-custom-token').val(); + + auth + .signInWithCustomToken(token) + .then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Signs in anonymously. + */ +function onSignInAnonymously() { + auth.signInAnonymously().then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Signs in with a generic IdP credential. + */ +function onSignInWithGenericIdPCredential() { + var providerId = $('#signin-generic-idp-provider-id').val(); + var idToken = $('#signin-generic-idp-id-token').val() || undefined; + var rawNonce = $('#signin-generic-idp-raw-nonce').val() || undefined; + var accessToken = $('#signin-generic-idp-access-token').val() || undefined; + var provider = new firebase.auth.OAuthProvider(providerId); + auth + .signInWithCredential( + provider.credential({ + idToken: idToken, + accessToken: accessToken, + rawNonce: rawNonce + }) + ) + .then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Initializes the ApplicationVerifier. + * @param {string} submitButtonId The ID of the DOM element of the button to + * which we attach the invisible reCAPTCHA. This is required even in visible + * mode. + */ +function makeApplicationVerifier(submitButtonId) { + var container = + recaptchaSize === 'invisible' ? submitButtonId : 'recaptcha-container'; + applicationVerifier = new firebase.auth.RecaptchaVerifier(container, { + 'size': recaptchaSize + }); +} + +/** + * Clears the ApplicationVerifier. + */ +function clearApplicationVerifier() { + if (applicationVerifier) { + applicationVerifier.clear(); + applicationVerifier = null; + } +} + +/** + * Sends a phone number verification code for sign-in. + */ +function onSignInVerifyPhoneNumber() { + var phoneNumber = $('#signin-phone-number').val(); + var provider = new firebase.auth.PhoneAuthProvider(auth); + // Clear existing reCAPTCHA as an existing reCAPTCHA could be targeted for a + // link/re-auth operation. + clearApplicationVerifier(); + // Initialize a reCAPTCHA application verifier. + makeApplicationVerifier('signin-verify-phone-number'); + provider.verifyPhoneNumber(phoneNumber, applicationVerifier).then( + function (verificationId) { + clearApplicationVerifier(); + $('#signin-phone-verification-id').val(verificationId); + alertSuccess('Phone verification sent!'); + }, + function (error) { + clearApplicationVerifier(); + onAuthError(error); + } + ); +} + +/** + * Confirms a phone number verification for sign-in. + */ +function onSignInConfirmPhoneVerification() { + var verificationId = $('#signin-phone-verification-id').val(); + var verificationCode = $('#signin-phone-verification-code').val(); + var credential = firebase.auth.PhoneAuthProvider.credential( + verificationId, + verificationCode + ); + signInOrLinkCredential(credential); +} + +/** + * Sends a phone number verification code for linking or reauth. + */ +function onLinkReauthVerifyPhoneNumber() { + var phoneNumber = $('#link-reauth-phone-number').val(); + var provider = new firebase.auth.PhoneAuthProvider(auth); + // Clear existing reCAPTCHA as an existing reCAPTCHA could be targeted for a + // sign-in operation. + clearApplicationVerifier(); + // Initialize a reCAPTCHA application verifier. + makeApplicationVerifier('link-reauth-verify-phone-number'); + provider.verifyPhoneNumber(phoneNumber, applicationVerifier).then( + function (verificationId) { + clearApplicationVerifier(); + $('#link-reauth-phone-verification-id').val(verificationId); + alertSuccess('Phone verification sent!'); + }, + function (error) { + clearApplicationVerifier(); + onAuthError(error); + } + ); +} + +/** + * Updates the user's phone number. + */ +function onUpdateConfirmPhoneVerification() { + if (!activeUser()) { + alertError('You need to sign in before linking an account.'); + return; + } + var verificationId = $('#link-reauth-phone-verification-id').val(); + var verificationCode = $('#link-reauth-phone-verification-code').val(); + var credential = firebase.auth.PhoneAuthProvider.credential( + verificationId, + verificationCode + ); + activeUser() + .updatePhoneNumber(credential) + .then(function () { + refreshUserData(); + alertSuccess('Phone number updated!'); + }, onAuthError); +} + +/** + * Confirms a phone number verification for linking. + */ +function onLinkConfirmPhoneVerification() { + var verificationId = $('#link-reauth-phone-verification-id').val(); + var verificationCode = $('#link-reauth-phone-verification-code').val(); + var credential = firebase.auth.PhoneAuthProvider.credential( + verificationId, + verificationCode + ); + signInOrLinkCredential(credential); +} + +/** + * Confirms a phone number verification for reauthentication. + */ +function onReauthConfirmPhoneVerification() { + var verificationId = $('#link-reauth-phone-verification-id').val(); + var verificationCode = $('#link-reauth-phone-verification-code').val(); + var credential = firebase.auth.PhoneAuthProvider.credential( + verificationId, + verificationCode + ); + activeUser() + .reauthenticateWithCredential(credential) + .then(function (result) { + logAdditionalUserInfo(result); + refreshUserData(); + alertSuccess('User reauthenticated!'); + }, onAuthError); +} + +/** + * Sends a phone number verification code for enrolling second factor. + */ +function onStartEnrollWithPhoneMultiFactor() { + var phoneNumber = $('#enroll-mfa-phone-number').val(); + if (!phoneNumber || !activeUser()) { + return; + } + var provider = new firebase.auth.PhoneAuthProvider(auth); + // Clear existing reCAPTCHA as an existing reCAPTCHA could be targeted for a + // sign-in operation. + clearApplicationVerifier(); + // Initialize a reCAPTCHA application verifier. + makeApplicationVerifier('enroll-mfa-verify-phone-number'); + activeUser() + .multiFactor.getSession() + .then(function (multiFactorSession) { + var phoneInfoOptions = { + 'phoneNumber': phoneNumber, + 'session': multiFactorSession + }; + return provider.verifyPhoneNumber(phoneInfoOptions, applicationVerifier); + }) + .then( + function (verificationId) { + clearApplicationVerifier(); + $('#enroll-mfa-phone-verification-id').val(verificationId); + alertSuccess('Phone verification sent!'); + }, + function (error) { + clearApplicationVerifier(); + onAuthError(error); + } + ); +} + +/** + * Confirms a phone number verification for MFA enrollment. + */ +function onFinalizeEnrollWithPhoneMultiFactor() { + var verificationId = $('#enroll-mfa-phone-verification-id').val(); + var verificationCode = $('#enroll-mfa-phone-verification-code').val(); + if (!verificationId || !verificationCode || !activeUser()) { + return; + } + var credential = firebase.auth.PhoneAuthProvider.credential( + verificationId, + verificationCode + ); + var multiFactorAssertion = + firebase.auth.PhoneMultiFactorGenerator.assertion(credential); + var displayName = $('#enroll-mfa-phone-display-name').val() || undefined; + + activeUser() + .multiFactor.enroll(multiFactorAssertion, displayName) + .then(function () { + refreshUserData(); + alertSuccess('Phone number enrolled!'); + }, onAuthError); +} + +/** + * Signs in or links a provider's credential, based on current tab opened. + * @param {!firebase.auth.AuthCredential} credential The provider's credential. + */ +function signInOrLinkCredential(credential) { + if (currentTab == '#user-section') { + if (!activeUser()) { + alertError('You need to sign in before linking an account.'); + return; + } + activeUser() + .linkWithCredential(credential) + .then(function (result) { + logAdditionalUserInfo(result); + refreshUserData(); + alertSuccess('Provider linked!'); + }, onAuthError); + } else { + auth + .signInWithCredential(credential) + .then(onAuthUserCredentialSuccess, onAuthError); + } +} + +/** @return {!Object} The Action Code Settings object. */ +function getActionCodeSettings() { + var actionCodeSettings = {}; + var url = $('#continueUrl').val(); + var apn = $('#apn').val(); + var amv = $('#amv').val(); + var ibi = $('#ibi').val(); + var installApp = $('input[name=install-app]:checked').val() == 'Yes'; + var handleCodeInApp = $('input[name=handle-in-app]:checked').val() == 'Yes'; + if (url || apn || ibi) { + actionCodeSettings['url'] = url; + if (apn) { + actionCodeSettings['android'] = { + 'packageName': apn, + 'installApp': !!installApp, + 'minimumVersion': amv || undefined + }; + } + if (ibi) { + actionCodeSettings['iOS'] = { + 'bundleId': ibi + }; + } + actionCodeSettings['handleCodeInApp'] = handleCodeInApp; + } + return actionCodeSettings; +} + +/** Reset action code settings form. */ +function onActionCodeSettingsReset() { + $('#continueUrl').val(''); + $('#apn').val(''); + $('#amv').val(''); + $('#ibi').val(''); +} + +/** + * Changes the user's email. + */ +function onChangeEmail() { + var email = $('#changed-email').val(); + activeUser() + .updateEmail(email) + .then(function () { + refreshUserData(); + alertSuccess('Email changed!'); + }, onAuthError); +} + +/** + * Changes the user's password. + */ +function onChangePassword() { + var password = $('#changed-password').val(); + activeUser() + .updatePassword(password) + .then(function () { + refreshUserData(); + alertSuccess('Password changed!'); + }, onAuthError); +} + +/** + * Changes the user's password. + */ +function onUpdateProfile() { + var displayName = $('#display-name').val(); + var photoURL = $('#photo-url').val(); + activeUser() + .updateProfile({ + 'displayName': displayName, + 'photoURL': photoURL + }) + .then(function () { + refreshUserData(); + alertSuccess('Profile updated!'); + }, onAuthError); +} + +/** + * Sends sign in with email link to the user. + */ +function onSendSignInLinkToEmail() { + var email = $('#sign-in-with-email-link-email').val(); + auth.sendSignInLinkToEmail(email, getActionCodeSettings()).then(function () { + alertSuccess('Email sent!'); + }, onAuthError); +} + +/** + * Sends sign in with email link to the user and pass in current url. + */ +function onSendSignInLinkToEmailCurrentUrl() { + var email = $('#sign-in-with-email-link-email').val(); + var actionCodeSettings = { + 'url': window.location.href, + 'handleCodeInApp': true + }; + + auth.sendSignInLinkToEmail(email, actionCodeSettings).then(function () { + if ('localStorage' in window && window['localStorage'] !== null) { + window.localStorage.setItem( + 'emailForSignIn', + // Save the email and the timestamp. + JSON.stringify({ + email: email, + timestamp: new Date().getTime() + }) + ); + } + alertSuccess('Email sent!'); + }, onAuthError); +} + +/** + * Sends email link to link the user. + */ +function onSendLinkEmailLink() { + var email = $('#link-with-email-link-email').val(); + auth.sendSignInLinkToEmail(email, getActionCodeSettings()).then(function () { + alertSuccess('Email sent!'); + }, onAuthError); +} + +/** + * Sends password reset email to the user. + */ +function onSendPasswordResetEmail() { + var email = $('#password-reset-email').val(); + auth.sendPasswordResetEmail(email, getActionCodeSettings()).then(function () { + alertSuccess('Email sent!'); + }, onAuthError); +} + +/** + * Verifies the password reset code entered by the user. + */ +function onVerifyPasswordResetCode() { + var code = $('#password-reset-code').val(); + auth.verifyPasswordResetCode(code).then(function () { + alertSuccess('Password reset code is valid!'); + }, onAuthError); +} + +/** + * Confirms the password reset with the code and password supplied by the user. + */ +function onConfirmPasswordReset() { + var code = $('#password-reset-code').val(); + var password = $('#password-reset-password').val(); + auth.confirmPasswordReset(code, password).then(function () { + alertSuccess('Password has been changed!'); + }, onAuthError); +} + +/** + * Gets the list of possible sign in methods for the given email address. + */ +function onFetchSignInMethodsForEmail() { + var email = $('#fetch-sign-in-methods-email').val(); + auth.fetchSignInMethodsForEmail(email).then(function (signInMethods) { + log('Sign in methods for ' + email + ' :'); + log(signInMethods); + if (signInMethods.length == 0) { + alertSuccess('Sign In Methods for ' + email + ': N/A'); + } else { + alertSuccess( + 'Sign In Methods for ' + email + ': ' + signInMethods.join(', ') + ); + } + }, onAuthError); +} + +/** + * Fetches and logs the user's providers data. + */ +function onGetProviderData() { + log('Providers data:'); + log(activeUser()['providerData']); +} + +/** + * Links a signed in user with an email and password account. + */ +function onLinkWithEmailAndPassword() { + var email = $('#link-email').val(); + var password = $('#link-password').val(); + activeUser() + .linkWithCredential( + firebase.auth.EmailAuthProvider.credential(email, password) + ) + .then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Links with a generic IdP credential. + */ +function onLinkWithGenericIdPCredential() { + var providerId = $('#link-generic-idp-provider-id').val(); + var idToken = $('#link-generic-idp-id-token').val() || undefined; + var rawNonce = $('#link-generic-idp-raw-nonce').val() || undefined; + var accessToken = $('#link-generic-idp-access-token').val() || undefined; + var provider = new firebase.auth.OAuthProvider(providerId); + activeUser() + .linkWithCredential( + provider.credential({ + idToken: idToken, + accessToken: accessToken, + rawNonce: rawNonce + }) + ) + .then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Unlinks the specified provider. + */ +function onUnlinkProvider() { + var providerId = $('#unlinked-provider-id').val(); + activeUser() + .unlink(providerId) + .then(function (user) { + alertSuccess('Provider unlinked from user.'); + refreshUserData(); + }, onAuthError); +} + +/** + * Sends email verification to the user. + */ +function onSendEmailVerification() { + activeUser() + .sendEmailVerification(getActionCodeSettings()) + .then(function () { + alertSuccess('Email verification sent!'); + }, onAuthError); +} + +/** + * Confirms the email verification code given. + */ +function onApplyActionCode() { + var code = $('#email-verification-code').val(); + auth.applyActionCode(code).then(function () { + alertSuccess('Email successfully verified!'); + refreshUserData(); + }, onAuthError); +} + +/** + * Gets or refreshes the ID token. + * @param {boolean} forceRefresh Whether to force the refresh of the token + * or not. + */ +function getIdToken(forceRefresh) { + if (activeUser() == null) { + alertError('No user logged in.'); + return; + } + if (activeUser().getIdToken) { + activeUser() + .getIdToken(forceRefresh) + .then(alertSuccess, function () { + log('No token'); + }); + } else { + activeUser() + .getToken(forceRefresh) + .then(alertSuccess, function () { + log('No token'); + }); + } +} + +/** + * Gets or refreshes the ID token result. + * @param {boolean} forceRefresh Whether to force the refresh of the token + * or not + */ +function getIdTokenResult(forceRefresh) { + if (activeUser() == null) { + alertError('No user logged in.'); + return; + } + activeUser() + .getIdTokenResult(forceRefresh) + .then(function (idTokenResult) { + alertSuccess(JSON.stringify(idTokenResult)); + }, onAuthError); +} + +/** + * Triggers the retrieval of the ID token result. + */ +function onGetIdTokenResult() { + getIdTokenResult(false); +} + +/** + * Triggers the refresh of the ID token result. + */ +function onRefreshTokenResult() { + getIdTokenResult(true); +} + +/** + * Triggers the retrieval of the ID token. + */ +function onGetIdToken() { + getIdToken(false); +} + +/** + * Triggers the refresh of the ID token. + */ +function onRefreshToken() { + getIdToken(true); +} + +/** + * Signs out the user. + */ +function onSignOut() { + setLastUser(auth.currentUser); + auth.signOut().then(signOut, onAuthError); +} + +/** + * Handles multi-factor sign-in completion. + * @param {!firebase.auth.MultiFactorResolver} resolver The multi-factor error + * resolver. + */ +function handleMultiFactorSignIn(resolver) { + // Save multi-factor error resolver. + multiFactorErrorResolver = resolver; + // Populate 2nd factor options from resolver. + var $listGroup = $('#multiFactorModal div.enrolled-second-factors'); + // Populate the list of 2nd factors in the list group specified. + showMultiFactors( + $listGroup, + multiFactorErrorResolver.hints, + // On row click, select the corresponding second factor to complete + // sign-in with. + function (e) { + e.preventDefault(); + // Remove all other active entries. + $listGroup.find('a').removeClass('active'); + // Mark current entry as active. + $(this).addClass('active'); + // Select current factor. + onSelectMultiFactorHint(parseInt($(this).attr('data-index'), 10)); + }, + // Do not show delete option + null + ); + // Hide phone form (other second factor types could be supported). + $('#multi-factor-phone').addClass('hidden'); + // Show second factor recovery dialog. + $('#multiFactorModal').modal(); +} + +/** + * Displays the list of multi-factors in the provided list group. + * @param {!jQuery} $listGroup The list group where the enrolled + * factors will be displayed. + * @param {!Array} multiFactorInfo The list of + * multi-factors to display. + * @param {?function(!jQuery.Event)} onClick The click handler when a second + * factor is clicked. + * @param {?function(!jQuery.Event)} onDelete The click handler when a second + * factor is delete. If not provided, no delete button is shown. + */ +function showMultiFactors($listGroup, multiFactorInfo, onClick, onDelete) { + // Append entry to list. + $listGroup.empty(); + $.each(multiFactorInfo, function (i) { + // Append entry to list. + var info = multiFactorInfo[i]; + var displayName = info.displayName || 'N/A'; + var $a = $('') + .addClass('list-group-item') + .addClass('list-group-item-action') + // Set index on entry. + .attr('data-index', i) + .appendTo($listGroup); + $a.append($('

').text(info.uid)); + $a.append($('').text(info.factorId)); + $a.append($('

').text(displayName)); + if (info.phoneNumber) { + $a.append($('').text(info.phoneNumber)); + } + // Check if a delete button is to be displayed. + if (onDelete) { + var $deleteBtn = $( + '' + + '' + + '' + ); + // Append delete button to row. + $a.append($deleteBtn); + // Add delete button click handler. + $a.find('button.delete-factor').click(onDelete); + } + // On entry click. + if (onClick) { + $a.click(onClick); + } + }); +} + +/** + * Handles the user selection of second factor to complete sign-in with. + * @param {number} index The selected multi-factor hint index. + */ +function onSelectMultiFactorHint(index) { + // Hide all forms for handling each type of second factors. + // Currently only phone is supported. + $('#multi-factor-phone').addClass('hidden'); + if ( + !multiFactorErrorResolver || + typeof multiFactorErrorResolver.hints[index] === 'undefined' + ) { + return; + } + + if (multiFactorErrorResolver.hints[index].factorId == 'phone') { + // Save selected second factor. + selectedMultiFactorHint = multiFactorErrorResolver.hints[index]; + // Show options for phone 2nd factor. + // Get reCAPTCHA ready. + clearApplicationVerifier(); + makeApplicationVerifier('send-2fa-phone-code'); + // Show sign-in with phone second factor menu. + $('#multi-factor-phone').removeClass('hidden'); + // Clear all input. + $('#multi-factor-sign-in-verification-id').val(''); + $('#multi-factor-sign-in-verification-code').val(''); + } else { + // 2nd factor not found or not supported by app. + alertError('Selected 2nd factor is not supported!'); + } +} + +/** + * Start sign-in with the 2nd factor phone number. + * @param {!jQuery.Event} event The jQuery event object. + */ +function onStartSignInWithPhoneMultiFactor(event) { + event.preventDefault(); + // Make sure a second factor is selected. + if (!selectedMultiFactorHint || !multiFactorErrorResolver) { + return; + } + // Initialize a reCAPTCHA application verifier. + var provider = new firebase.auth.PhoneAuthProvider(auth); + var signInRequest = { + multiFactorHint: selectedMultiFactorHint, + session: multiFactorErrorResolver.session + }; + provider.verifyPhoneNumber(signInRequest, applicationVerifier).then( + function (verificationId) { + clearApplicationVerifier(); + $('#multi-factor-sign-in-verification-id').val(verificationId); + alertSuccess('Phone verification sent!'); + }, + function (error) { + clearApplicationVerifier(); + onAuthError(error); + } + ); +} + +/** + * Completes sign-in with the 2nd factor phone assertion. + * @param {!jQuery.Event} event The jQuery event object. + */ +function onFinalizeSignInWithPhoneMultiFactor(event) { + event.preventDefault(); + var verificationId = $('#multi-factor-sign-in-verification-id').val(); + var code = $('#multi-factor-sign-in-verification-code').val(); + if (!code || !verificationId || !multiFactorErrorResolver) { + return; + } + var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, code); + var assertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred); + multiFactorErrorResolver + .resolveSignIn(assertion) + .then(function (userCredential) { + onAuthUserCredentialSuccess(userCredential); + $('#multiFactorModal').modal('hide'); + }, onAuthError); +} + +/** + * Adds a new row to insert an OAuth custom parameter key/value pair. + * @param {!jQuery.Event} event The jQuery event object. + */ +function onPopupRedirectAddCustomParam(event) { + // Form container. + var html = '

'; + // OAuth parameter key input. + html += + ''; + // OAuth parameter value input. + html += + ''; + // Button to remove current key/value pair. + html += ''; + html += ''; + // Create jQuery node. + var $node = $(html); + // Add button click event listener to remove item. + $node.find('button').on('click', function (e) { + // Remove button click event listener. + $(this).off('click'); + // Get row container and remove it. + $(this).closest('form.customParamItem').remove(); + e.preventDefault(); + }); + // Append constructed row to parameter list container. + $('#popup-redirect-custom-parameters').append($node); +} + +/** + * Performs the corresponding popup/redirect action for a generic provider. + */ +function onPopupRedirectGenericProviderClick() { + var providerId = $('#popup-redirect-generic-providerid').val(); + var provider = new firebase.auth.OAuthProvider(providerId); + signInWithPopupRedirect(provider); +} + +/** + * Performs the corresponding popup/redirect action for a SAML provider. + */ +function onPopupRedirectSamlProviderClick() { + var providerId = $('#popup-redirect-saml-providerid').val(); + var provider = new firebase.auth.SAMLAuthProvider(providerId); + signInWithPopupRedirect(provider); +} + +/** + * Performs the corresponding popup/redirect action based on user's selection. + * @param {!jQuery.Event} event The jQuery event object. + */ +function onPopupRedirectProviderClick(event) { + var providerId = $(event.currentTarget).data('provider'); + var provider = null; + switch (providerId) { + case 'google.com': + provider = new firebase.auth.GoogleAuthProvider(); + break; + case 'facebook.com': + provider = new firebase.auth.FacebookAuthProvider(); + break; + case 'github.com': + provider = new firebase.auth.GithubAuthProvider(); + break; + case 'twitter.com': + provider = new firebase.auth.TwitterAuthProvider(); + break; + default: + return; + } + signInWithPopupRedirect(provider); +} + +/** + * Performs a popup/redirect action based on a given provider and the user's + * selections. + * @param {!firebase.auth.AuthProvider} provider The provider with which to + * sign in. + */ +function signInWithPopupRedirect(provider) { + var action = $('input[name=popup-redirect-action]:checked').val(); + var type = $('input[name=popup-redirect-type]:checked').val(); + var method = null; + var inst = null; + if (action == 'link' || action == 'reauthenticate') { + if (!activeUser()) { + alertError('No user logged in.'); + return; + } + inst = activeUser(); + method = action + 'With'; + } else { + inst = auth; + method = 'signInWith'; + } + if (type == 'popup') { + method += 'Popup'; + } else { + method += 'Redirect'; + } + // Get custom OAuth parameters. + var customParameters = {}; + // For each entry. + $('form.customParamItem').each(function (index) { + // Get parameter key. + var key = $(this).find('input.customParamKey').val(); + // Get parameter value. + var value = $(this).find('input.customParamValue').val(); + // Save to list if valid. + if (key && value) { + customParameters[key] = value; + } + }); + console.log('customParameters: ', customParameters); + // For older jscore versions that do not support this. + if (provider.setCustomParameters) { + // Set custom parameters on current provider. + provider.setCustomParameters(customParameters); + } + + // Add scopes for providers who do have scopes available (i.e. not Twitter). + if (provider.addScope) { + // String.prototype.trim not available in IE8. + var scopes = $.trim($('#scopes').val()).split(/\s*,\s*/); + for (var i = 0; i < scopes.length; i++) { + provider.addScope(scopes[i]); + } + } + console.log('Provider:'); + console.log(provider); + if (type == 'popup') { + inst[method](provider).then(function (response) { + console.log('Popup response:'); + console.log(response); + alertSuccess(action + ' with ' + provider['providerId'] + ' successful!'); + logAdditionalUserInfo(response); + onAuthSuccess(activeUser()); + }, onAuthError); + } else { + try { + inst[method](provider).catch(onAuthError); + } catch (error) { + console.log('Error while calling ' + method); + console.error(error); + } + } +} + +/** + * Displays user credential result. + * @param {!firebase.auth.UserCredential} result The UserCredential result + * object. + */ +function onAuthUserCredentialSuccess(result) { + onAuthSuccess(result.user); + logAdditionalUserInfo(result); +} + +/** + * Displays redirect result. + */ +function onGetRedirectResult() { + auth.getRedirectResult().then(function (response) { + log('Redirect results:'); + if (response.credential) { + log('Credential:'); + log(response.credential); + } else { + log('No credential'); + } + if (response.user) { + log("User's id:"); + log(response.user.uid); + } else { + log('No user'); + } + logAdditionalUserInfo(response); + console.log(response); + }, onAuthError); +} + +/** + * Logs additional user info returned by a sign-in event, if available. + * @param {!Object} response + */ +function logAdditionalUserInfo(response) { + if (response.additionalUserInfo) { + if (response.additionalUserInfo.username) { + log( + response.additionalUserInfo['providerId'] + + ' username: ' + + response.additionalUserInfo.username + ); + } + if (response.additionalUserInfo.profile) { + log(response.additionalUserInfo['providerId'] + ' profile information:'); + log(JSON.stringify(response.additionalUserInfo.profile, null, 2)); + } + if (typeof response.additionalUserInfo.isNewUser !== 'undefined') { + log( + response.additionalUserInfo['providerId'] + + ' isNewUser: ' + + response.additionalUserInfo.isNewUser + ); + } + if (response.credential) { + log('credential: ' + JSON.stringify(response.credential.toJSON())); + } + } +} + +/** + * Deletes the user account. + */ +function onDelete() { + activeUser() + ['delete']() + .then(function () { + log('User successfully deleted.'); + alertSuccess('User successfully deleted.'); + refreshUserData(); + }, onAuthError); +} + +/** + * Gets a specific query parameter from the current URL. + * @param {string} name Name of the parameter. + * @return {string} The query parameter requested. + */ +function getParameterByName(name) { + var url = window.location.href; + name = name.replace(/[\[\]]/g, '\\$&'); + var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); + var results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +} + +/** + * Detects if an action code is passed in the URL, and populates accordingly + * the input field for the confirm email verification process. + */ +function populateActionCodes() { + var emailForSignIn = null; + var signInTime = 0; + if ('localStorage' in window && window['localStorage'] !== null) { + try { + // Try to parse as JSON first using new storage format. + var emailForSignInData = JSON.parse( + window.localStorage.getItem('emailForSignIn') + ); + emailForSignIn = emailForSignInData['email'] || null; + signInTime = emailForSignInData['timestamp'] || 0; + } catch (e) { + // JSON parsing failed. This means the email is stored in the old string + // format. + emailForSignIn = window.localStorage.getItem('emailForSignIn'); + } + if (emailForSignIn) { + // Clear old codes. Old format codes should be cleared immediately. + if (new Date().getTime() - signInTime >= 1 * 24 * 3600 * 1000) { + // Remove email from storage. + window.localStorage.removeItem('emailForSignIn'); + } + } + } + var actionCode = getParameterByName('oobCode'); + if (actionCode != null) { + var mode = getParameterByName('mode'); + if (mode == 'verifyEmail') { + $('#email-verification-code').val(actionCode); + } else if (mode == 'resetPassword') { + $('#password-reset-code').val(actionCode); + } else if (mode == 'signIn') { + if (emailForSignIn) { + $('#sign-in-with-email-link-email').val(emailForSignIn); + $('#sign-in-with-email-link-link').val(window.location.href); + onSignInWithEmailLink(); + // Remove email from storage as the code is only usable once. + window.localStorage.removeItem('emailForSignIn'); + } + } else { + $('#email-verification-code').val(actionCode); + $('#password-reset-code').val(actionCode); + } + } +} + +/** + * Provides basic Database checks for authenticated and unauthenticated access. + * The Database node being tested has the following rule: + * "users": { + * "$user_id": { + * ".read": "$user_id === auth.uid", + * ".write": "$user_id === auth.uid" + * } + * } + * This applies when Real-time database service is available. + */ +function checkDatabaseAuthAccess() { + var randomString = Math.floor(Math.random() * 10000000).toString(); + var dbRef; + var dbPath; + var errMessage; + // Run this check only when Database module is available. + if ( + typeof firebase !== 'undefined' && + typeof firebase.database !== 'undefined' + ) { + if (lastUser && !firebase.auth().currentUser) { + dbPath = 'users/' + lastUser.uid; + // After sign out, confirm read/write access to users/$user_id blocked. + dbRef = firebase.database().ref(dbPath); + dbRef + .set({ + 'test': randomString + }) + .then(function () { + alertError( + 'Error: Unauthenticated write to Database node ' + + dbPath + + ' unexpectedly succeeded!' + ); + }) + .catch(function (error) { + errMessage = error.message.toLowerCase(); + // Permission denied error should be thrown. + if (errMessage.indexOf('permission_denied') == -1) { + alertError('Error: ' + error.code); + return; + } + dbRef + .once('value') + .then(function () { + alertError( + 'Error: Unauthenticated read to Database node ' + + dbPath + + ' unexpectedly succeeded!' + ); + }) + .catch(function (error) { + errMessage = error.message.toLowerCase(); + // Permission denied error should be thrown. + if (errMessage.indexOf('permission_denied') == -1) { + alertError('Error: ' + error.code); + return; + } + log( + 'Unauthenticated read/write to Database node ' + + dbPath + + ' failed as expected!' + ); + }); + }); + } else if (firebase.auth().currentUser) { + dbPath = 'users/' + firebase.auth().currentUser.uid; + // Confirm read/write access to users/$user_id allowed. + dbRef = firebase.database().ref(dbPath); + dbRef + .set({ + 'test': randomString + }) + .then(function () { + return dbRef.once('value'); + }) + .then(function (snapshot) { + if (snapshot.val().test === randomString) { + // read/write successful. + log( + 'Authenticated read/write to Database node ' + + dbPath + + ' succeeded!' + ); + } else { + throw new Error( + 'Authenticated read/write to Database node ' + dbPath + ' failed!' + ); + } + // Clean up: clear that node's content. + return dbRef.remove(); + }) + .catch(function (error) { + alertError('Error: ' + error.code); + }); + } + } +} + +/** Runs all web worker tests if web workers are supported. */ +function onRunWebWorkTests() { + if (!webWorker) { + alertError('Error: Web workers are not supported in the current browser!'); + return; + } + var onError = function (error) { + alertError('Error code: ' + error.code + ' message: ' + error.message); + }; + auth + .signInWithPopup(new firebase.auth.GoogleAuthProvider()) + .then(function (result) { + runWebWorkerTests(result.credential.idToken); + }, onError); +} + +/** Runs service worker tests if supported. */ +function onRunServiceWorkTests() { + $.ajax('/checkIfAuthenticated').then( + function (data, textStatus, jqXHR) { + alertSuccess('User authenticated: ' + data.uid); + }, + function (jqXHR, textStatus, errorThrown) { + alertError(jqXHR.status + ': ' + JSON.stringify(jqXHR.responseJSON)); + } + ); +} + +/** Copy current user of auth to tempAuth. */ +function onCopyActiveUser() { + tempAuth.updateCurrentUser(activeUser()).then( + function () { + alertSuccess('Copied active user to temp Auth'); + }, + function (error) { + alertError('Error: ' + error.code); + } + ); +} + +/** Copy last user to auth. */ +function onCopyLastUser() { + // If last user is null, NULL_USER error will be thrown. + auth.updateCurrentUser(lastUser).then( + function () { + alertSuccess('Copied last user to Auth'); + }, + function (error) { + alertError('Error: ' + error.code); + } + ); +} + +/** Applies selected auth settings change. */ +function onApplyAuthSettingsChange() { + try { + auth.settings.appVerificationDisabledForTesting = + $('input[name=enable-app-verification]:checked').val() == 'No'; + alertSuccess('Auth settings changed'); + } catch (error) { + alertError('Error: ' + error.code); + } +} + +/** + * Initiates the application by setting event listeners on the various buttons. + */ +function initApp() { + log('Initializing app...'); + app = firebase.initializeApp(config); + auth = app.auth(); + + tempApp = firebase.initializeApp( + { + 'apiKey': config['apiKey'], + 'authDomain': config['authDomain'] + }, + auth['app']['name'] + '-temp' + ); + tempAuth = tempApp.auth(); + + // Listen to reCAPTCHA config togglers. + initRecaptchaToggle(function (size) { + clearApplicationVerifier(); + recaptchaSize = size; + }); + + // The action code for email verification or password reset + // can be passed in the url address as a parameter, and for convenience + // this preloads the input field. + populateActionCodes(); + + // Allows to login the user if previously logged in. + if (auth.onIdTokenChanged) { + auth.onIdTokenChanged(function (user) { + refreshUserData(); + if (user) { + user.getIdTokenResult(false).then( + function (idTokenResult) { + log(JSON.stringify(idTokenResult)); + }, + function () { + log('No token.'); + } + ); + } else { + log('No user logged in.'); + } + }); + } + + if (auth.onAuthStateChanged) { + auth.onAuthStateChanged(function (user) { + if (user) { + log('user state change detected: ' + user.uid); + } else { + log('user state change detected: no user'); + } + // Check Database Auth access. + checkDatabaseAuthAccess(); + }); + } + + if (tempAuth.onAuthStateChanged) { + tempAuth.onAuthStateChanged(function (user) { + if (user) { + log('user state change on temp Auth detect: ' + JSON.stringify(user)); + alertSuccess('user state change on temp Auth detect: ' + user.uid); + } + }); + } + + // We check for redirect result to refresh user's data. + auth.getRedirectResult().then(function (response) { + refreshUserData(); + logAdditionalUserInfo(response); + }, onAuthError); + + // Bootstrap tooltips. + $('[data-toggle="tooltip"]').tooltip(); + + // Auto submit the choose library type form. + $('#library-form').on('change', 'input.library-option', function () { + $('#library-form').submit(); + }); + + // To clear the logs in the page. + $('.clear-logs').click(clearLogs); + + // Disables JS forms. + $('form.no-submit').on('submit', function () { + return false; + }); + + // Keeps track of the current tab opened. + $('#tab-menu a').click(function (event) { + currentTab = $(event.currentTarget).attr('href'); + }); + + // Toggles user. + $('input[name=toggle-user-selection]').change(refreshUserData); + + // Actions listeners. + $('#sign-up-with-email-and-password').click(onSignUp); + $('#sign-in-with-email-and-password').click(onSignInWithEmailAndPassword); + $('.sign-in-with-custom-token').click(onSignInWithCustomToken); + $('#sign-in-anonymously').click(onSignInAnonymously); + $('#sign-in-with-generic-idp-credential').click( + onSignInWithGenericIdPCredential + ); + $('#signin-verify-phone-number').click(onSignInVerifyPhoneNumber); + $('#signin-confirm-phone-verification').click( + onSignInConfirmPhoneVerification + ); + // On enter click in verification code, complete phone sign-in. This prevents + // reCAPTCHA from being re-rendered (default behavior on enter). + $('#signin-phone-verification-code').keypress(function (e) { + if (e.which == 13) { + onSignInConfirmPhoneVerification(); + e.preventDefault(); + } + }); + $('#sign-in-with-email-link').click(onSignInWithEmailLink); + $('#link-with-email-link').click(onLinkWithEmailLink); + $('#reauth-with-email-link').click(onReauthenticateWithEmailLink); + + $('#change-email').click(onChangeEmail); + $('#change-password').click(onChangePassword); + $('#update-profile').click(onUpdateProfile); + + $('#send-sign-in-link-to-email').click(onSendSignInLinkToEmail); + $('#send-sign-in-link-to-email-current-url').click( + onSendSignInLinkToEmailCurrentUrl + ); + $('#send-link-email-link').click(onSendLinkEmailLink); + + $('#send-password-reset-email').click(onSendPasswordResetEmail); + $('#verify-password-reset-code').click(onVerifyPasswordResetCode); + $('#confirm-password-reset').click(onConfirmPasswordReset); + + $('#get-provider-data').click(onGetProviderData); + $('#link-with-email-and-password').click(onLinkWithEmailAndPassword); + $('#link-with-generic-idp-credential').click(onLinkWithGenericIdPCredential); + $('#unlink-provider').click(onUnlinkProvider); + $('#link-reauth-verify-phone-number').click(onLinkReauthVerifyPhoneNumber); + $('#update-confirm-phone-verification').click( + onUpdateConfirmPhoneVerification + ); + $('#link-confirm-phone-verification').click(onLinkConfirmPhoneVerification); + $('#reauth-confirm-phone-verification').click( + onReauthConfirmPhoneVerification + ); + // On enter click in verification code, complete phone sign-in. This prevents + // reCAPTCHA from being re-rendered (default behavior on enter). + $('#link-reauth-phone-verification-code').keypress(function (e) { + if (e.which == 13) { + // User first option as default. + onUpdateConfirmPhoneVerification(); + e.preventDefault(); + } + }); + + $('#send-email-verification').click(onSendEmailVerification); + $('#confirm-email-verification').click(onApplyActionCode); + $('#get-token-result').click(onGetIdTokenResult); + $('#refresh-token-result').click(onRefreshTokenResult); + $('#get-token').click(onGetIdToken); + $('#refresh-token').click(onRefreshToken); + $('#get-token-worker').click(onGetCurrentUserDataFromWebWorker); + $('#sign-out').click(onSignOut); + + $('.popup-redirect-provider').click(onPopupRedirectProviderClick); + $('#popup-redirect-generic').click(onPopupRedirectGenericProviderClick); + $('#popup-redirect-get-redirect-result').click(onGetRedirectResult); + $('#popup-redirect-add-custom-parameter').click( + onPopupRedirectAddCustomParam + ); + $('#popup-redirect-saml').click(onPopupRedirectSamlProviderClick); + + $('#action-code-settings-reset').click(onActionCodeSettingsReset); + + $('#delete').click(onDelete); + + $('#set-persistence').click(onSetPersistence); + + $('#set-language-code').click(onSetLanguageCode); + $('#use-device-language').click(onUseDeviceLanguage); + + $('#fetch-sign-in-methods-for-email').click(onFetchSignInMethodsForEmail); + + $('#run-web-worker-tests').click(onRunWebWorkTests); + $('#run-service-worker-tests').click(onRunServiceWorkTests); + $('#copy-active-user').click(onCopyActiveUser); + $('#copy-last-user').click(onCopyLastUser); + + $('#apply-auth-settings-change').click(onApplyAuthSettingsChange); + + // Multi-factor operations. + // Starts multi-factor sign-in with selected phone number. + $('#send-2fa-phone-code').click(onStartSignInWithPhoneMultiFactor); + // Completes multi-factor sign-in with supplied SMS code. + $('#sign-in-with-phone-multi-factor').click( + onFinalizeSignInWithPhoneMultiFactor + ); + // Starts multi-factor enrollment with phone number. + $('#enroll-mfa-verify-phone-number').click(onStartEnrollWithPhoneMultiFactor); + // Completes multi-factor enrollment with supplied SMS code. + $('#enroll-mfa-confirm-phone-verification').click( + onFinalizeEnrollWithPhoneMultiFactor + ); +} + +$(initApp); diff --git a/packages/auth-compat/demo/public/style.css b/packages/auth-compat/demo/public/style.css new file mode 100644 index 00000000000..732d93eae6a --- /dev/null +++ b/packages/auth-compat/demo/public/style.css @@ -0,0 +1,207 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +body { + padding-top: 70px; +} +body.user-info-displayed { + padding-top: 120px; +} + +@media (min-width: 768px) { + body { + padding-bottom: 100px; + } +} +@media (max-width: 767px) { + body { + padding-bottom: 15px; + } +} + +#tab-menu { + margin-bottom: 15px; +} + +#user-info { + display: none; + padding: 10px 15px; + position: fixed; + top: 50px; + width: 100%; + z-index: 1000; +} + +#toggle-user-placeholder { + margin-bottom: 15px; +} + +#user-info.current-user, +#toggle-user-placeholder .current-user, +#toggle-user label.active.current-user { + background-color: #d6e9c6; + border: 1px solid #dff0d8; + color: #3c763d; +} + +#user-info.last-user, +#toggle-user label.active.last-user { + background-color: #d9edf7; + border: 1px solid #bce8f1; + color: #31708f; +} + +#recaptcha-container { + border: 5px solid red; + bottom: 0; + position: fixed; + right: 0; + z-index: 1500; +} + +#recaptcha-container:empty { + border: none; +} + +.logs { + color: #555; + font-family: 'Courier New', Courier; + font-size: 0.9em; + word-wrap: break-word; +} + +.logs > .error { + color: #d9534f; +} + +/* Margin top for small screens when the logs are below the buttons */ +@media (max-width: 767px) { + .logs { + margin-top: 20px; + } +} + +.overlaying-alert { + bottom: 15px; + pointer-events: none; + position: fixed; + width: 100%; + word-wrap: break-word; + z-index: 1010; +} + +.actions { + margin-bottom: 15px; +} + +.group { + border-top: 1px solid #555; + color: #555; + font-size: 0.9em; + font-weight: bold; + letter-spacing: 0.2em; + margin-bottom: 15px; + padding: 2px 0 0 10px; +} + +button + .group, +div + .group, +.btn-block + .group, +.form + .group { + margin-top: 30px; +} + +.form { + text-align: center; +} + +.form-bordered { + border: 1px solid #CCC; + border-radius: 9px; + padding: 5px; +} + +button + .form, +input + .form, +.form + .form { + margin: 15px 0; +} + +.form + button, +.form-control + .btn-block, +.form-control + .form-control { + margin-top: 5px; +} + +/* Bootstrap .hidden adds the !important which invalidates jQuery .show() */ +.hidden, +.hide, +.profile, +.overlaying-alert > .alert, +#toggle-user { + display: none; +} + +.profile { + line-height: 30px; +} + +@media (max-width: 767px) { + .profile { + /* Use a smaller line height for small screens so user information doesn't + take up half the screen. */ + line-height: normal; + } +} + +.profile-uid { + font-family: 'Courier New', Courier; +} + +.profile-image { + float: left; + height: 30px; + margin-right: 10px; +} + +.profile-email-not-verified { + color: #d9534f; +} + +.profile-providers { + color: #333; +} + +.profile-providers > i { + margin: 0 5px; +} + +.radio-block { + margin-bottom: 15px; + width: 100%; +} + +.radio-block > label { + width: 50%; +} + +/** Overrides default drop down menu styles for enrolled factors display. */ +.enrolled-second-factors { + left: initial; + min-width: 400px; + right: 0px; + width: 100%; +} diff --git a/packages/auth-compat/demo/rollup.config.js b/packages/auth-compat/demo/rollup.config.js new file mode 100644 index 00000000000..08b52dfe26b --- /dev/null +++ b/packages/auth-compat/demo/rollup.config.js @@ -0,0 +1,87 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import json from '@rollup/plugin-json'; +import resolveModule from '@rollup/plugin-node-resolve'; +import sourcemaps from 'rollup-plugin-sourcemaps'; +import typescriptPlugin from 'rollup-plugin-typescript2'; +import typescript from 'typescript'; + +const plugins = [ + sourcemaps(), + resolveModule(), + typescriptPlugin({ + typescript, + include: ['../**/*.ts'] + }), + json() +]; + +/** + * Individual Component Builds + */ +const umdBuilds = [ + /** + * App UMD Builds + */ + { + input: '../../firebase/compat/app/index.ts', + output: { + file: 'public/dist/firebase-app.js', + sourcemap: true, + format: 'umd', + name: 'firebase' + }, + plugins: [...plugins] + }, + { + input: `../index.ts`, + output: { + file: `public/dist/firebase-auth.js`, + format: 'umd', + sourcemap: true, + extend: true, + name: 'firebase', + globals: { + '@firebase/app-compat': 'firebase', + '@firebase/app': 'firebase.INTERNAL.modularAPIs' + }, + /** + * use iife to avoid below error in the old Safari browser + * SyntaxError: Functions cannot be declared in a nested block in strict mode + * https://github.com/firebase/firebase-js-sdk/issues/1228 + * + */ + intro: ` + try { + (function() {`, + outro: ` + }).apply(this, arguments); + } catch(err) { + console.error(err); + throw new Error( + 'Cannot instantiate firebase-auth.js - ' + + 'be sure to load firebase-app.js first.' + ); + }` + }, + plugins: [...plugins], + external: ['@firebase/app-compat', '@firebase/app'] + } +]; + +export default [...umdBuilds]; diff --git a/packages-exp/auth-compat-exp/demo/tsconfig.json b/packages/auth-compat/demo/tsconfig.json similarity index 100% rename from packages-exp/auth-compat-exp/demo/tsconfig.json rename to packages/auth-compat/demo/tsconfig.json diff --git a/packages/auth-compat/demo/yarn.lock b/packages/auth-compat/demo/yarn.lock new file mode 100644 index 00000000000..02d205be7cb --- /dev/null +++ b/packages/auth-compat/demo/yarn.lock @@ -0,0 +1,4846 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" + integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== + dependencies: + "@babel/highlight" "^7.14.5" + +"@babel/helper-validator-identifier@^7.14.5": + version "7.15.7" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" + integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== + +"@babel/highlight@^7.14.5": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" + integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@firebase/logger@^0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@firebase/logger/-/logger-0.3.0.tgz#a3992e40f62c10276dbfcb8b4ab376b7e25d7fd9" + integrity sha512-7oQ+TctqekfgZImWkKuda50JZfkmAKMgh5qY4aR4pwRyqZXuJXN1H/BKkHvN1y0S4XWtF0f/wiCLKHhyi1ppPA== + dependencies: + tslib "^2.1.0" + +"@firebase/util@^1.0.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@firebase/util/-/util-1.3.0.tgz#e71113bdd5073e9736ceca665b54d9f6df232b20" + integrity sha512-SESvmYwuKOVCZ1ZxLbberbx+9cnbxpCa4CG2FUSQYqN6Ab8KyltegMDIsqMw5KyIBZ4n1phfHoOa22xo5NzAlQ== + dependencies: + tslib "^2.1.0" + +"@gar/promisify@^1.0.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" + integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== + +"@hutson/parse-repository-url@^3.0.0": + version "3.0.2" + resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" + integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== + +"@lerna/add@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f" + integrity sha512-cpmAH1iS3k8JBxNvnMqrGTTjbY/ZAiKa1ChJzFevMYY3eeqbvhsBKnBcxjRXtdrJ6bd3dCQM+ZtK+0i682Fhng== + dependencies: + "@lerna/bootstrap" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + npm-package-arg "^8.1.0" + p-map "^4.0.0" + pacote "^11.2.6" + semver "^7.3.4" + +"@lerna/bootstrap@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-4.0.0.tgz#5f5c5e2c6cfc8fcec50cb2fbe569a8c607101891" + integrity sha512-RkS7UbeM2vu+kJnHzxNRCLvoOP9yGNgkzRdy4UV2hNalD7EP41bLvRVOwRYQ7fhc2QcbhnKNdOBihYRL0LcKtw== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/has-npm-version" "4.0.0" + "@lerna/npm-install" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/rimraf-dir" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/symlink-binary" "4.0.0" + "@lerna/symlink-dependencies" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + get-port "^5.1.1" + multimatch "^5.0.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" + read-package-tree "^5.3.1" + semver "^7.3.4" + +"@lerna/changed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/changed/-/changed-4.0.0.tgz#b9fc76cea39b9292a6cd263f03eb57af85c9270b" + integrity sha512-cD+KuPRp6qiPOD+BO6S6SN5cARspIaWSOqGBpGnYzLb4uWT8Vk4JzKyYtc8ym1DIwyoFXHosXt8+GDAgR8QrgQ== + dependencies: + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/listable" "4.0.0" + "@lerna/output" "4.0.0" + +"@lerna/check-working-tree@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-4.0.0.tgz#257e36a602c00142e76082a19358e3e1ae8dbd58" + integrity sha512-/++bxM43jYJCshBiKP5cRlCTwSJdRSxVmcDAXM+1oUewlZJVSVlnks5eO0uLxokVFvLhHlC5kHMc7gbVFPHv6Q== + dependencies: + "@lerna/collect-uncommitted" "4.0.0" + "@lerna/describe-ref" "4.0.0" + "@lerna/validation-error" "4.0.0" + +"@lerna/child-process@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-4.0.0.tgz#341b96a57dffbd9705646d316e231df6fa4df6e1" + integrity sha512-XtCnmCT9eyVsUUHx6y/CTBYdV9g2Cr/VxyseTWBgfIur92/YKClfEtJTbOh94jRT62hlKLqSvux/UhxXVh613Q== + dependencies: + chalk "^4.1.0" + execa "^5.0.0" + strong-log-transformer "^2.1.0" + +"@lerna/clean@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/clean/-/clean-4.0.0.tgz#8f778b6f2617aa2a936a6b5e085ae62498e57dc5" + integrity sha512-uugG2iN9k45ITx2jtd8nEOoAtca8hNlDCUM0N3lFgU/b1mEQYAPRkqr1qs4FLRl/Y50ZJ41wUz1eazS+d/0osA== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/rimraf-dir" "4.0.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" + +"@lerna/cli@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/cli/-/cli-4.0.0.tgz#8eabd334558836c1664df23f19acb95e98b5bbf3" + integrity sha512-Neaw3GzFrwZiRZv2g7g6NwFjs3er1vhraIniEs0jjVLPMNC4eata0na3GfE5yibkM/9d3gZdmihhZdZ3EBdvYA== + dependencies: + "@lerna/global-options" "4.0.0" + dedent "^0.7.0" + npmlog "^4.1.2" + yargs "^16.2.0" + +"@lerna/collect-uncommitted@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-4.0.0.tgz#855cd64612969371cfc2453b90593053ff1ba779" + integrity sha512-ufSTfHZzbx69YNj7KXQ3o66V4RC76ffOjwLX0q/ab//61bObJ41n03SiQEhSlmpP+gmFbTJ3/7pTe04AHX9m/g== + dependencies: + "@lerna/child-process" "4.0.0" + chalk "^4.1.0" + npmlog "^4.1.2" + +"@lerna/collect-updates@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-4.0.0.tgz#8e208b1bafd98a372ff1177f7a5e288f6bea8041" + integrity sha512-bnNGpaj4zuxsEkyaCZLka9s7nMs58uZoxrRIPJ+nrmrZYp1V5rrd+7/NYTuunOhY2ug1sTBvTAxj3NZQ+JKnOw== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/describe-ref" "4.0.0" + minimatch "^3.0.4" + npmlog "^4.1.2" + slash "^3.0.0" + +"@lerna/command@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/command/-/command-4.0.0.tgz#991c7971df8f5bf6ae6e42c808869a55361c1b98" + integrity sha512-LM9g3rt5FsPNFqIHUeRwWXLNHJ5NKzOwmVKZ8anSp4e1SPrv2HNc1V02/9QyDDZK/w+5POXH5lxZUI1CHaOK/A== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/project" "4.0.0" + "@lerna/validation-error" "4.0.0" + "@lerna/write-log-file" "4.0.0" + clone-deep "^4.0.1" + dedent "^0.7.0" + execa "^5.0.0" + is-ci "^2.0.0" + npmlog "^4.1.2" + +"@lerna/conventional-commits@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-4.0.0.tgz#660fb2c7b718cb942ead70110df61f18c6f99750" + integrity sha512-CSUQRjJHFrH8eBn7+wegZLV3OrNc0Y1FehYfYGhjLE2SIfpCL4bmfu/ViYuHh9YjwHaA+4SX6d3hR+xkeseKmw== + dependencies: + "@lerna/validation-error" "4.0.0" + conventional-changelog-angular "^5.0.12" + conventional-changelog-core "^4.2.2" + conventional-recommended-bump "^6.1.0" + fs-extra "^9.1.0" + get-stream "^6.0.0" + lodash.template "^4.5.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + pify "^5.0.0" + semver "^7.3.4" + +"@lerna/create-symlink@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-4.0.0.tgz#8c5317ce5ae89f67825443bd7651bf4121786228" + integrity sha512-I0phtKJJdafUiDwm7BBlEUOtogmu8+taxq6PtIrxZbllV9hWg59qkpuIsiFp+no7nfRVuaasNYHwNUhDAVQBig== + dependencies: + cmd-shim "^4.1.0" + fs-extra "^9.1.0" + npmlog "^4.1.2" + +"@lerna/create@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/create/-/create-4.0.0.tgz#b6947e9b5dfb6530321952998948c3e63d64d730" + integrity sha512-mVOB1niKByEUfxlbKTM1UNECWAjwUdiioIbRQZEeEabtjCL69r9rscIsjlGyhGWCfsdAG5wfq4t47nlDXdLLag== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + fs-extra "^9.1.0" + globby "^11.0.2" + init-package-json "^2.0.2" + npm-package-arg "^8.1.0" + p-reduce "^2.1.0" + pacote "^11.2.6" + pify "^5.0.0" + semver "^7.3.4" + slash "^3.0.0" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^3.0.0" + whatwg-url "^8.4.0" + yargs-parser "20.2.4" + +"@lerna/describe-ref@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-4.0.0.tgz#53c53b4ea65fdceffa072a62bfebe6772c45d9ec" + integrity sha512-eTU5+xC4C5Gcgz+Ey4Qiw9nV2B4JJbMulsYJMW8QjGcGh8zudib7Sduj6urgZXUYNyhYpRs+teci9M2J8u+UvQ== + dependencies: + "@lerna/child-process" "4.0.0" + npmlog "^4.1.2" + +"@lerna/diff@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/diff/-/diff-4.0.0.tgz#6d3071817aaa4205a07bf77cfc6e932796d48b92" + integrity sha512-jYPKprQVg41+MUMxx6cwtqsNm0Yxx9GDEwdiPLwcUTFx+/qKCEwifKNJ1oGIPBxyEHX2PFCOjkK39lHoj2qiag== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/validation-error" "4.0.0" + npmlog "^4.1.2" + +"@lerna/exec@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/exec/-/exec-4.0.0.tgz#eb6cb95cb92d42590e9e2d628fcaf4719d4a8be6" + integrity sha512-VGXtL/b/JfY84NB98VWZpIExfhLOzy0ozm/0XaS4a2SmkAJc5CeUfrhvHxxkxiTBLkU+iVQUyYEoAT0ulQ8PCw== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/profiler" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + p-map "^4.0.0" + +"@lerna/filter-options@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-4.0.0.tgz#ac94cc515d7fa3b47e2f7d74deddeabb1de5e9e6" + integrity sha512-vV2ANOeZhOqM0rzXnYcFFCJ/kBWy/3OA58irXih9AMTAlQLymWAK0akWybl++sUJ4HB9Hx12TOqaXbYS2NM5uw== + dependencies: + "@lerna/collect-updates" "4.0.0" + "@lerna/filter-packages" "4.0.0" + dedent "^0.7.0" + npmlog "^4.1.2" + +"@lerna/filter-packages@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-4.0.0.tgz#b1f70d70e1de9cdd36a4e50caa0ac501f8d012f2" + integrity sha512-+4AJIkK7iIiOaqCiVTYJxh/I9qikk4XjNQLhE3kixaqgMuHl1NQ99qXRR0OZqAWB9mh8Z1HA9bM5K1HZLBTOqA== + dependencies: + "@lerna/validation-error" "4.0.0" + multimatch "^5.0.0" + npmlog "^4.1.2" + +"@lerna/get-npm-exec-opts@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-4.0.0.tgz#dc955be94a4ae75c374ef9bce91320887d34608f" + integrity sha512-yvmkerU31CTWS2c7DvmAWmZVeclPBqI7gPVr5VATUKNWJ/zmVcU4PqbYoLu92I9Qc4gY1TuUplMNdNuZTSL7IQ== + dependencies: + npmlog "^4.1.2" + +"@lerna/get-packed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-4.0.0.tgz#0989d61624ac1f97e393bdad2137c49cd7a37823" + integrity sha512-rfWONRsEIGyPJTxFzC8ECb3ZbsDXJbfqWYyeeQQDrJRPnEJErlltRLPLgC2QWbxFgFPsoDLeQmFHJnf0iDfd8w== + dependencies: + fs-extra "^9.1.0" + ssri "^8.0.1" + tar "^6.1.0" + +"@lerna/github-client@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-4.0.0.tgz#2ced67721363ef70f8e12ffafce4410918f4a8a4" + integrity sha512-2jhsldZtTKXYUBnOm23Lb0Fx8G4qfSXF9y7UpyUgWUj+YZYd+cFxSuorwQIgk5P4XXrtVhsUesIsli+BYSThiw== + dependencies: + "@lerna/child-process" "4.0.0" + "@octokit/plugin-enterprise-rest" "^6.0.1" + "@octokit/rest" "^18.1.0" + git-url-parse "^11.4.4" + npmlog "^4.1.2" + +"@lerna/gitlab-client@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-4.0.0.tgz#00dad73379c7b38951d4b4ded043504c14e2b67d" + integrity sha512-OMUpGSkeDWFf7BxGHlkbb35T7YHqVFCwBPSIR6wRsszY8PAzCYahtH3IaJzEJyUg6vmZsNl0FSr3pdA2skhxqA== + dependencies: + node-fetch "^2.6.1" + npmlog "^4.1.2" + whatwg-url "^8.4.0" + +"@lerna/global-options@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/global-options/-/global-options-4.0.0.tgz#c7d8b0de6a01d8a845e2621ea89e7f60f18c6a5f" + integrity sha512-TRMR8afAHxuYBHK7F++Ogop2a82xQjoGna1dvPOY6ltj/pEx59pdgcJfYcynYqMkFIk8bhLJJN9/ndIfX29FTQ== + +"@lerna/has-npm-version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-4.0.0.tgz#d3fc3292c545eb28bd493b36e6237cf0279f631c" + integrity sha512-LQ3U6XFH8ZmLCsvsgq1zNDqka0Xzjq5ibVN+igAI5ccRWNaUsE/OcmsyMr50xAtNQMYMzmpw5GVLAivT2/YzCg== + dependencies: + "@lerna/child-process" "4.0.0" + semver "^7.3.4" + +"@lerna/import@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/import/-/import-4.0.0.tgz#bde656c4a451fa87ae41733ff8a8da60547c5465" + integrity sha512-FaIhd+4aiBousKNqC7TX1Uhe97eNKf5/SC7c5WZANVWtC7aBWdmswwDt3usrzCNpj6/Wwr9EtEbYROzxKH8ffg== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + fs-extra "^9.1.0" + p-map-series "^2.1.0" + +"@lerna/info@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/info/-/info-4.0.0.tgz#b9fb0e479d60efe1623603958a831a88b1d7f1fc" + integrity sha512-8Uboa12kaCSZEn4XRfPz5KU9XXoexSPS4oeYGj76s2UQb1O1GdnEyfjyNWoUl1KlJ2i/8nxUskpXIftoFYH0/Q== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/output" "4.0.0" + envinfo "^7.7.4" + +"@lerna/init@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/init/-/init-4.0.0.tgz#dadff67e6dfb981e8ccbe0e6a310e837962f6c7a" + integrity sha512-wY6kygop0BCXupzWj5eLvTUqdR7vIAm0OgyV9WHpMYQGfs1V22jhztt8mtjCloD/O0nEe4tJhdG62XU5aYmPNQ== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + write-json-file "^4.3.0" + +"@lerna/link@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/link/-/link-4.0.0.tgz#c3a38aabd44279d714e90f2451e31b63f0fb65ba" + integrity sha512-KlvPi7XTAcVOByfaLlOeYOfkkDcd+bejpHMCd1KcArcFTwijOwXOVi24DYomIeHvy6HsX/IUquJ4PPUJIeB4+w== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/symlink-dependencies" "4.0.0" + p-map "^4.0.0" + slash "^3.0.0" + +"@lerna/list@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/list/-/list-4.0.0.tgz#24b4e6995bd73f81c556793fe502b847efd9d1d7" + integrity sha512-L2B5m3P+U4Bif5PultR4TI+KtW+SArwq1i75QZ78mRYxPc0U/piau1DbLOmwrdqr99wzM49t0Dlvl6twd7GHFg== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/listable" "4.0.0" + "@lerna/output" "4.0.0" + +"@lerna/listable@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/listable/-/listable-4.0.0.tgz#d00d6cb4809b403f2b0374fc521a78e318b01214" + integrity sha512-/rPOSDKsOHs5/PBLINZOkRIX1joOXUXEtyUs5DHLM8q6/RP668x/1lFhw6Dx7/U+L0+tbkpGtZ1Yt0LewCLgeQ== + dependencies: + "@lerna/query-graph" "4.0.0" + chalk "^4.1.0" + columnify "^1.5.4" + +"@lerna/log-packed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-4.0.0.tgz#95168fe2e26ac6a71e42f4be857519b77e57a09f" + integrity sha512-+dpCiWbdzgMAtpajLToy9PO713IHoE6GV/aizXycAyA07QlqnkpaBNZ8DW84gHdM1j79TWockGJo9PybVhrrZQ== + dependencies: + byte-size "^7.0.0" + columnify "^1.5.4" + has-unicode "^2.0.1" + npmlog "^4.1.2" + +"@lerna/npm-conf@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-4.0.0.tgz#b259fd1e1cee2bf5402b236e770140ff9ade7fd2" + integrity sha512-uS7H02yQNq3oejgjxAxqq/jhwGEE0W0ntr8vM3EfpCW1F/wZruwQw+7bleJQ9vUBjmdXST//tk8mXzr5+JXCfw== + dependencies: + config-chain "^1.1.12" + pify "^5.0.0" + +"@lerna/npm-dist-tag@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-4.0.0.tgz#d1e99b4eccd3414142f0548ad331bf2d53f3257a" + integrity sha512-F20sg28FMYTgXqEQihgoqSfwmq+Id3zT23CnOwD+XQMPSy9IzyLf1fFVH319vXIw6NF6Pgs4JZN2Qty6/CQXGw== + dependencies: + "@lerna/otplease" "4.0.0" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" + npmlog "^4.1.2" + +"@lerna/npm-install@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-4.0.0.tgz#31180be3ab3b7d1818a1a0c206aec156b7094c78" + integrity sha512-aKNxq2j3bCH3eXl3Fmu4D54s/YLL9WSwV8W7X2O25r98wzrO38AUN6AB9EtmAx+LV/SP15et7Yueg9vSaanRWg== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/get-npm-exec-opts" "4.0.0" + fs-extra "^9.1.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + signal-exit "^3.0.3" + write-pkg "^4.0.0" + +"@lerna/npm-publish@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-4.0.0.tgz#84eb62e876fe949ae1fd62c60804423dbc2c4472" + integrity sha512-vQb7yAPRo5G5r77DRjHITc9piR9gvEKWrmfCH7wkfBnGWEqu7n8/4bFQ7lhnkujvc8RXOsYpvbMQkNfkYibD/w== + dependencies: + "@lerna/otplease" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + fs-extra "^9.1.0" + libnpmpublish "^4.0.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + pify "^5.0.0" + read-package-json "^3.0.0" + +"@lerna/npm-run-script@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-4.0.0.tgz#dfebf4f4601442e7c0b5214f9fb0d96c9350743b" + integrity sha512-Jmyh9/IwXJjOXqKfIgtxi0bxi1pUeKe5bD3S81tkcy+kyng/GNj9WSqD5ZggoNP2NP//s4CLDAtUYLdP7CU9rA== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/get-npm-exec-opts" "4.0.0" + npmlog "^4.1.2" + +"@lerna/otplease@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/otplease/-/otplease-4.0.0.tgz#84972eb43448f8a1077435ba1c5e59233b725850" + integrity sha512-Sgzbqdk1GH4psNiT6hk+BhjOfIr/5KhGBk86CEfHNJTk9BK4aZYyJD4lpDbDdMjIV4g03G7pYoqHzH765T4fxw== + dependencies: + "@lerna/prompt" "4.0.0" + +"@lerna/output@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/output/-/output-4.0.0.tgz#b1d72215c0e35483e4f3e9994debc82c621851f2" + integrity sha512-Un1sHtO1AD7buDQrpnaYTi2EG6sLF+KOPEAMxeUYG5qG3khTs2Zgzq5WE3dt2N/bKh7naESt20JjIW6tBELP0w== + dependencies: + npmlog "^4.1.2" + +"@lerna/pack-directory@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-4.0.0.tgz#8b617db95d20792f043aaaa13a9ccc0e04cb4c74" + integrity sha512-NJrmZNmBHS+5aM+T8N6FVbaKFScVqKlQFJNY2k7nsJ/uklNKsLLl6VhTQBPwMTbf6Tf7l6bcKzpy7aePuq9UiQ== + dependencies: + "@lerna/get-packed" "4.0.0" + "@lerna/package" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + npm-packlist "^2.1.4" + npmlog "^4.1.2" + tar "^6.1.0" + temp-write "^4.0.0" + +"@lerna/package-graph@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-4.0.0.tgz#16a00253a8ac810f72041481cb46bcee8d8123dd" + integrity sha512-QED2ZCTkfXMKFoTGoccwUzjHtZMSf3UKX14A4/kYyBms9xfFsesCZ6SLI5YeySEgcul8iuIWfQFZqRw+Qrjraw== + dependencies: + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/validation-error" "4.0.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + semver "^7.3.4" + +"@lerna/package@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/package/-/package-4.0.0.tgz#1b4c259c4bcff45c876ee1d591a043aacbc0d6b7" + integrity sha512-l0M/izok6FlyyitxiQKr+gZLVFnvxRQdNhzmQ6nRnN9dvBJWn+IxxpM+cLqGACatTnyo9LDzNTOj2Db3+s0s8Q== + dependencies: + load-json-file "^6.2.0" + npm-package-arg "^8.1.0" + write-pkg "^4.0.0" + +"@lerna/prerelease-id-from-version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-4.0.0.tgz#c7e0676fcee1950d85630e108eddecdd5b48c916" + integrity sha512-GQqguzETdsYRxOSmdFZ6zDBXDErIETWOqomLERRY54f4p+tk4aJjoVdd9xKwehC9TBfIFvlRbL1V9uQGHh1opg== + dependencies: + semver "^7.3.4" + +"@lerna/profiler@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/profiler/-/profiler-4.0.0.tgz#8a53ab874522eae15d178402bff90a14071908e9" + integrity sha512-/BaEbqnVh1LgW/+qz8wCuI+obzi5/vRE8nlhjPzdEzdmWmZXuCKyWSEzAyHOJWw1ntwMiww5dZHhFQABuoFz9Q== + dependencies: + fs-extra "^9.1.0" + npmlog "^4.1.2" + upath "^2.0.1" + +"@lerna/project@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/project/-/project-4.0.0.tgz#ff84893935833533a74deff30c0e64ddb7f0ba6b" + integrity sha512-o0MlVbDkD5qRPkFKlBZsXZjoNTWPyuL58564nSfZJ6JYNmgAptnWPB2dQlAc7HWRZkmnC2fCkEdoU+jioPavbg== + dependencies: + "@lerna/package" "4.0.0" + "@lerna/validation-error" "4.0.0" + cosmiconfig "^7.0.0" + dedent "^0.7.0" + dot-prop "^6.0.1" + glob-parent "^5.1.1" + globby "^11.0.2" + load-json-file "^6.2.0" + npmlog "^4.1.2" + p-map "^4.0.0" + resolve-from "^5.0.0" + write-json-file "^4.3.0" + +"@lerna/prompt@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/prompt/-/prompt-4.0.0.tgz#5ec69a803f3f0db0ad9f221dad64664d3daca41b" + integrity sha512-4Ig46oCH1TH5M7YyTt53fT6TuaKMgqUUaqdgxvp6HP6jtdak6+amcsqB8YGz2eQnw/sdxunx84DfI9XpoLj4bQ== + dependencies: + inquirer "^7.3.3" + npmlog "^4.1.2" + +"@lerna/publish@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/publish/-/publish-4.0.0.tgz#f67011305adeba120066a3b6d984a5bb5fceef65" + integrity sha512-K8jpqjHrChH22qtkytA5GRKIVFEtqBF6JWj1I8dWZtHs4Jywn8yB1jQ3BAMLhqmDJjWJtRck0KXhQQKzDK2UPg== + dependencies: + "@lerna/check-working-tree" "4.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/describe-ref" "4.0.0" + "@lerna/log-packed" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/npm-dist-tag" "4.0.0" + "@lerna/npm-publish" "4.0.0" + "@lerna/otplease" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/pack-directory" "4.0.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + "@lerna/version" "4.0.0" + fs-extra "^9.1.0" + libnpmaccess "^4.0.1" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" + npmlog "^4.1.2" + p-map "^4.0.0" + p-pipe "^3.1.0" + pacote "^11.2.6" + semver "^7.3.4" + +"@lerna/pulse-till-done@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-4.0.0.tgz#04bace7d483a8205c187b806bcd8be23d7bb80a3" + integrity sha512-Frb4F7QGckaybRhbF7aosLsJ5e9WuH7h0KUkjlzSByVycxY91UZgaEIVjS2oN9wQLrheLMHl6SiFY0/Pvo0Cxg== + dependencies: + npmlog "^4.1.2" + +"@lerna/query-graph@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-4.0.0.tgz#09dd1c819ac5ee3f38db23931143701f8a6eef63" + integrity sha512-YlP6yI3tM4WbBmL9GCmNDoeQyzcyg1e4W96y/PKMZa5GbyUvkS2+Jc2kwPD+5KcXou3wQZxSPzR3Te5OenaDdg== + dependencies: + "@lerna/package-graph" "4.0.0" + +"@lerna/resolve-symlink@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-4.0.0.tgz#6d006628a210c9b821964657a9e20a8c9a115e14" + integrity sha512-RtX8VEUzqT+uLSCohx8zgmjc6zjyRlh6i/helxtZTMmc4+6O4FS9q5LJas2uGO2wKvBlhcD6siibGt7dIC3xZA== + dependencies: + fs-extra "^9.1.0" + npmlog "^4.1.2" + read-cmd-shim "^2.0.0" + +"@lerna/rimraf-dir@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-4.0.0.tgz#2edf3b62d4eb0ef4e44e430f5844667d551ec25a" + integrity sha512-QNH9ABWk9mcMJh2/muD9iYWBk1oQd40y6oH+f3wwmVGKYU5YJD//+zMiBI13jxZRtwBx0vmBZzkBkK1dR11cBg== + dependencies: + "@lerna/child-process" "4.0.0" + npmlog "^4.1.2" + path-exists "^4.0.0" + rimraf "^3.0.2" + +"@lerna/run-lifecycle@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-4.0.0.tgz#e648a46f9210a9bcd7c391df6844498cb5079334" + integrity sha512-IwxxsajjCQQEJAeAaxF8QdEixfI7eLKNm4GHhXHrgBu185JcwScFZrj9Bs+PFKxwb+gNLR4iI5rpUdY8Y0UdGQ== + dependencies: + "@lerna/npm-conf" "4.0.0" + npm-lifecycle "^3.1.5" + npmlog "^4.1.2" + +"@lerna/run-topologically@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-4.0.0.tgz#af846eeee1a09b0c2be0d1bfb5ef0f7b04bb1827" + integrity sha512-EVZw9hGwo+5yp+VL94+NXRYisqgAlj0jWKWtAIynDCpghRxCE5GMO3xrQLmQgqkpUl9ZxQFpICgYv5DW4DksQA== + dependencies: + "@lerna/query-graph" "4.0.0" + p-queue "^6.6.2" + +"@lerna/run@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run/-/run-4.0.0.tgz#4bc7fda055a729487897c23579694f6183c91262" + integrity sha512-9giulCOzlMPzcZS/6Eov6pxE9gNTyaXk0Man+iCIdGJNMrCnW7Dme0Z229WWP/UoxDKg71F2tMsVVGDiRd8fFQ== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/npm-run-script" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/profiler" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/timer" "4.0.0" + "@lerna/validation-error" "4.0.0" + p-map "^4.0.0" + +"@lerna/symlink-binary@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-4.0.0.tgz#21009f62d53a425f136cb4c1a32c6b2a0cc02d47" + integrity sha512-zualodWC4q1QQc1pkz969hcFeWXOsVYZC5AWVtAPTDfLl+TwM7eG/O6oP+Rr3fFowspxo6b1TQ6sYfDV6HXNWA== + dependencies: + "@lerna/create-symlink" "4.0.0" + "@lerna/package" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + +"@lerna/symlink-dependencies@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-4.0.0.tgz#8910eca084ae062642d0490d8972cf2d98e9ebbd" + integrity sha512-BABo0MjeUHNAe2FNGty1eantWp8u83BHSeIMPDxNq0MuW2K3CiQRaeWT3EGPAzXpGt0+hVzBrA6+OT0GPn7Yuw== + dependencies: + "@lerna/create-symlink" "4.0.0" + "@lerna/resolve-symlink" "4.0.0" + "@lerna/symlink-binary" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + +"@lerna/timer@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/timer/-/timer-4.0.0.tgz#a52e51bfcd39bfd768988049ace7b15c1fd7a6da" + integrity sha512-WFsnlaE7SdOvjuyd05oKt8Leg3ENHICnvX3uYKKdByA+S3g+TCz38JsNs7OUZVt+ba63nC2nbXDlUnuT2Xbsfg== + +"@lerna/validation-error@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-4.0.0.tgz#af9d62fe8304eaa2eb9a6ba1394f9aa807026d35" + integrity sha512-1rBOM5/koiVWlRi3V6dB863E1YzJS8v41UtsHgMr6gB2ncJ2LsQtMKlJpi3voqcgh41H8UsPXR58RrrpPpufyw== + dependencies: + npmlog "^4.1.2" + +"@lerna/version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/version/-/version-4.0.0.tgz#532659ec6154d8a8789c5ab53878663e244e3228" + integrity sha512-otUgiqs5W9zGWJZSCCMRV/2Zm2A9q9JwSDS7s/tlKq4mWCYriWo7+wsHEA/nPTMDyYyBO5oyZDj+3X50KDUzeA== + dependencies: + "@lerna/check-working-tree" "4.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/conventional-commits" "4.0.0" + "@lerna/github-client" "4.0.0" + "@lerna/gitlab-client" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + chalk "^4.1.0" + dedent "^0.7.0" + load-json-file "^6.2.0" + minimatch "^3.0.4" + npmlog "^4.1.2" + p-map "^4.0.0" + p-pipe "^3.1.0" + p-reduce "^2.1.0" + p-waterfall "^2.1.1" + semver "^7.3.4" + slash "^3.0.0" + temp-write "^4.0.0" + write-json-file "^4.3.0" + +"@lerna/write-log-file@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-4.0.0.tgz#18221a38a6a307d6b0a5844dd592ad53fa27091e" + integrity sha512-XRG5BloiArpXRakcnPHmEHJp+4AtnhRtpDIHSghmXD5EichI1uD73J7FgPp30mm2pDRq3FdqB0NbwSEsJ9xFQg== + dependencies: + npmlog "^4.1.2" + write-file-atomic "^3.0.3" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@npmcli/ci-detect@^1.0.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a" + integrity sha512-oN3y7FAROHhrAt7Rr7PnTSwrHrZVRTS2ZbyxeQwSSYD0ifwM3YNgQqbaRmjcWoPyq77MjchusjJDspbzMmip1Q== + +"@npmcli/fs@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.0.0.tgz#589612cfad3a6ea0feafcb901d29c63fd52db09f" + integrity sha512-8ltnOpRR/oJbOp8vaGUnipOi3bqkcW+sLHFlyXIr08OGHmVJLB1Hn7QtGXbYcpVtH1gAYZTlmDXtE4YV0+AMMQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/git@^2.1.0": + version "2.1.0" + resolved "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz#2fbd77e147530247d37f325930d457b3ebe894f6" + integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== + dependencies: + "@npmcli/promise-spawn" "^1.3.2" + lru-cache "^6.0.0" + mkdirp "^1.0.4" + npm-pick-manifest "^6.1.1" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.5" + which "^2.0.2" + +"@npmcli/installed-package-contents@^1.0.6": + version "1.0.7" + resolved "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz#ab7408c6147911b970a8abe261ce512232a3f4fa" + integrity sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw== + dependencies: + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + +"@npmcli/node-gyp@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.2.tgz#3cdc1f30e9736dbc417373ed803b42b1a0a29ede" + integrity sha512-yrJUe6reVMpktcvagumoqD9r08fH1iRo01gn1u0zoCApa9lnZGEigVKUd2hzsCId4gdtkZZIVscLhNxMECKgRg== + +"@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": + version "1.3.2" + resolved "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz#42d4e56a8e9274fba180dabc0aea6e38f29274f5" + integrity sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg== + dependencies: + infer-owner "^1.0.4" + +"@npmcli/run-script@^1.8.2": + version "1.8.6" + resolved "https://registry.npmjs.org/@npmcli/run-script/-/run-script-1.8.6.tgz#18314802a6660b0d4baa4c3afe7f1ad39d8c28b7" + integrity sha512-e42bVZnC6VluBZBAFEr3YrdqSspG3bgilyg4nSLBJ7TRGNCzxHa92XAHxQBLYg0BmgwO4b2mf3h/l5EkEWRn3g== + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + node-gyp "^7.1.0" + read-package-json-fast "^2.0.1" + +"@octokit/auth-token@^2.4.4": + version "2.5.0" + resolved "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" + integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g== + dependencies: + "@octokit/types" "^6.0.3" + +"@octokit/core@^3.5.1": + version "3.5.1" + resolved "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" + integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== + dependencies: + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.6.0" + "@octokit/request-error" "^2.0.5" + "@octokit/types" "^6.0.3" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.12" + resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" + integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA== + dependencies: + "@octokit/types" "^6.0.3" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.5.8": + version "4.8.0" + resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" + integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg== + dependencies: + "@octokit/request" "^5.6.0" + "@octokit/types" "^6.0.3" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^10.5.0": + version "10.6.0" + resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-10.6.0.tgz#13278af3cbe7bb141dc4ae02c24eaff209efadfb" + integrity sha512-/iQtZq+zuQJrwawFyjixh333xPu4/KJKk0bFM/Omm4kFlTGw0dWXfq6xCOe5DqONW0faW29Cc9r6p2mvl72aTQ== + +"@octokit/plugin-enterprise-rest@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" + integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== + +"@octokit/plugin-paginate-rest@^2.16.0": + version "2.16.5" + resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.16.5.tgz#4d8098410f4c4697d33979f06f38d2ed2574adf1" + integrity sha512-2PfRGymdBypqRes4Xelu0BAZZRCV/Qg0xgo8UB10UKoghCM+zg640+T5WkRsRD0edwfLBPP3VsJgDyDTG4EIYg== + dependencies: + "@octokit/types" "^6.31.0" + +"@octokit/plugin-request-log@^1.0.4": + version "1.0.4" + resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" + integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== + +"@octokit/plugin-rest-endpoint-methods@5.11.1": + version "5.11.1" + resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.11.1.tgz#308476ae5de133ab4d30a6fa6c8f2766ff2524a0" + integrity sha512-EE69SuO08wtnIy9q/HftGDr7/Im1txzDfeYr+I4T/JkMSNEiedUUE5RuCWkEQAwwbeEU4kVTwSEQZb9Af77/PA== + dependencies: + "@octokit/types" "^6.30.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": + version "2.1.0" + resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" + integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== + dependencies: + "@octokit/types" "^6.0.3" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.6.0": + version "5.6.1" + resolved "https://registry.npmjs.org/@octokit/request/-/request-5.6.1.tgz#f97aff075c37ab1d427c49082fefeef0dba2d8ce" + integrity sha512-Ls2cfs1OfXaOKzkcxnqw5MR6drMA/zWX/LIS/p8Yjdz7QKTPQLMsB3R+OvoxE6XnXeXEE2X7xe4G4l4X0gRiKQ== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.1.0" + "@octokit/types" "^6.16.1" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + universal-user-agent "^6.0.0" + +"@octokit/rest@^18.1.0": + version "18.11.0" + resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.11.0.tgz#d7c5f1cef0a8bedaf8f7d8bc8feb80ee840c7b40" + integrity sha512-e30+ERbA4nXkzkaCDgfxS9H1A43Z1GvV5nqLfkxS81rYKbFE6+sEsrXsTRzV1aWLsRIQ+B75Vgnyzjw/ioTyVA== + dependencies: + "@octokit/core" "^3.5.1" + "@octokit/plugin-paginate-rest" "^2.16.0" + "@octokit/plugin-request-log" "^1.0.4" + "@octokit/plugin-rest-endpoint-methods" "5.11.1" + +"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.30.0", "@octokit/types@^6.31.0": + version "6.31.0" + resolved "https://registry.npmjs.org/@octokit/types/-/types-6.31.0.tgz#b444852100090d1c5d0015614860c6131dc217e8" + integrity sha512-xobpvYmMYoFSxZB6jL1TPTMMZkxZIBlY145ZKibBJDKCczP1FrLLougtuVOZywGVZdcYs8oq2Bxb3aMjqIFeiw== + dependencies: + "@octokit/openapi-types" "^10.5.0" + +"@rollup/plugin-commonjs@21.1.0": + version "21.1.0" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-21.1.0.tgz#45576d7b47609af2db87f55a6d4b46e44fc3a553" + integrity sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA== + dependencies: + "@rollup/pluginutils" "^3.1.0" + commondir "^1.0.1" + estree-walker "^2.0.1" + glob "^7.1.6" + is-reference "^1.2.1" + magic-string "^0.25.7" + resolve "^1.17.0" + +"@rollup/plugin-json@4.1.0": + version "4.1.0" + resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" + integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== + dependencies: + "@rollup/pluginutils" "^3.0.8" + +"@rollup/plugin-node-resolve@13.3.0": + version "13.3.0" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c" + integrity sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + deepmerge "^4.2.2" + is-builtin-module "^3.1.0" + is-module "^1.0.0" + resolve "^1.19.0" + +"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + +"@rollup/pluginutils@^4.1.2": + version "4.2.1" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@types/estree@*": + version "0.0.50" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" + integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/minimatch@^3.0.3": + version "3.0.5" + resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + +"@types/minimist@^1.2.0": + version "1.2.2" + resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" + integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== + +"@types/node@*": + version "16.9.6" + resolved "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz#040a64d7faf9e5d9e940357125f0963012e66f04" + integrity sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ== + +"@types/normalize-package-data@^2.4.0": + version "2.4.1" + resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" + +"@yarn-tool/resolve-package@^1.0.40": + version "1.0.46" + resolved "https://registry.npmjs.org/@yarn-tool/resolve-package/-/resolve-package-1.0.46.tgz#db7354380e5ca7682294af59e5ab0f7fce640ac1" + integrity sha512-RJcBGTVywUqYGRtGkPSgJC/ozf0wK/xjUy66tXkbpL35U0o1oef4S0v23euxA/CiukqBWr2fRGtGY6FidESdTg== + dependencies: + pkg-dir "< 6 >= 5" + tslib "^2.3.1" + upath2 "^3.1.12" + +JSONStream@^1.0.4: + version "1.3.5" + resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +abbrev@1: + version "1.1.1" + resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +add-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" + integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= + +agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agentkeepalive@^4.1.3: + version "4.1.4" + resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b" + integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +aproba@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@~1.1.2: + version "1.1.7" + resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + +array-ify@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" + integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +before-after-hook@^2.2.0: + version "2.2.2" + resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" + integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +builtin-modules@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" + integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== + +builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= + +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= + +byte-size@^7.0.0: + version "7.0.1" + resolved "https://registry.npmjs.org/byte-size/-/byte-size-7.0.1.tgz#b1daf3386de7ab9d706b941a748dbfc71130dee3" + integrity sha512-crQdqyCwhokxwV1UyDzLZanhkugAgft7vt0qbbdt60C6Zf3CAiGmtUCylbtYwrU6loOUw3euGrNtW1J651ot1A== + +cacache@^15.0.5, cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +chownr@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + +cmd-shim@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd" + integrity sha512-lb9L7EM4I/ZRVuljLPEtUJOP+xiQVknZ4ZMpMgEp4JzNldPb27HU03hi6K1/6CoIuit/Zm/LQXySErFeXxDprw== + dependencies: + mkdirp-infer-owner "^2.0.0" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +columnify@^1.5.4: + version "1.5.4" + resolved "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" + integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs= + dependencies: + strip-ansi "^3.0.0" + wcwidth "^1.0.0" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +compare-func@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" + integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== + dependencies: + array-ify "^1.0.0" + dot-prop "^5.1.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + +config-chain@^1.1.12: + version "1.1.13" + resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +conventional-changelog-angular@^5.0.12: + version "5.0.13" + resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c" + integrity sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA== + dependencies: + compare-func "^2.0.0" + q "^1.5.1" + +conventional-changelog-core@^4.2.2: + version "4.2.4" + resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz#e50d047e8ebacf63fac3dc67bf918177001e1e9f" + integrity sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg== + dependencies: + add-stream "^1.0.0" + conventional-changelog-writer "^5.0.0" + conventional-commits-parser "^3.2.0" + dateformat "^3.0.0" + get-pkg-repo "^4.0.0" + git-raw-commits "^2.0.8" + git-remote-origin-url "^2.0.0" + git-semver-tags "^4.1.1" + lodash "^4.17.15" + normalize-package-data "^3.0.0" + q "^1.5.1" + read-pkg "^3.0.0" + read-pkg-up "^3.0.0" + through2 "^4.0.0" + +conventional-changelog-preset-loader@^2.3.4: + version "2.3.4" + resolved "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" + integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== + +conventional-changelog-writer@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz#c4042f3f1542f2f41d7d2e0d6cad23aba8df8eec" + integrity sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g== + dependencies: + conventional-commits-filter "^2.0.7" + dateformat "^3.0.0" + handlebars "^4.7.6" + json-stringify-safe "^5.0.1" + lodash "^4.17.15" + meow "^8.0.0" + semver "^6.0.0" + split "^1.0.0" + through2 "^4.0.0" + +conventional-commits-filter@^2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz#f8d9b4f182fce00c9af7139da49365b136c8a0b3" + integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== + dependencies: + lodash.ismatch "^4.4.0" + modify-values "^1.0.0" + +conventional-commits-parser@^3.2.0: + version "3.2.2" + resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.2.tgz#190fb9900c6e02be0c0bca9b03d57e24982639fd" + integrity sha512-Jr9KAKgqAkwXMRHjxDwO/zOCDKod1XdAESHAGuJX38iZ7ZzVti/tvVoysO0suMsdAObp9NQ2rHSsSbnAqZ5f5g== + dependencies: + JSONStream "^1.0.4" + is-text-path "^1.0.1" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" + +conventional-recommended-bump@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz#cfa623285d1de554012f2ffde70d9c8a22231f55" + integrity sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw== + dependencies: + concat-stream "^2.0.0" + conventional-changelog-preset-loader "^2.3.4" + conventional-commits-filter "^2.0.7" + conventional-commits-parser "^3.2.0" + git-raw-commits "^2.0.8" + git-semver-tags "^4.1.1" + meow "^8.0.0" + q "^1.5.1" + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^7.0.0: + version "7.0.1" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +dargs@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" + integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +dateformat@^3.0.0: + version "3.0.3" + resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" + integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== + +debug@4, debug@^4.1.0, debug@^4.3.1: + version "4.3.2" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + +debuglog@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= + +decamelize-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0: + version "1.2.0" + resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= + dependencies: + clone "^1.0.2" + +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + +detect-indent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" + integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= + +detect-indent@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +dezalgo@^1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dot-prop@^5.1.0: + version "5.3.0" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + +duplexer@^0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +envinfo@^7.7.4: + version "7.8.1" + resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.18.0-next.2: + version "1.18.6" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.6.tgz#2c44e3ea7a6255039164d26559777a6d978cb456" + integrity sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.1" + is-regex "^1.1.4" + is-string "^1.0.7" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +estree-walker@^0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" + integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +eventemitter3@^4.0.4: + version "4.0.7" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.1.1: + version "3.2.7" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" + integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= + +find-cache-dir@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^1.2.7: + version "1.2.7" + resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== + dependencies: + minipass "^2.6.0" + +fs-minipass@^2.0.0, fs-minipass@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + 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" + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-pkg-repo@^4.0.0: + version "4.2.1" + resolved "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385" + integrity sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA== + dependencies: + "@hutson/parse-repository-url" "^3.0.0" + hosted-git-info "^4.0.0" + through2 "^2.0.0" + yargs "^16.2.0" + +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +git-raw-commits@^2.0.8: + version "2.0.10" + resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1" + integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ== + dependencies: + dargs "^7.0.0" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" + +git-remote-origin-url@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" + integrity sha1-UoJlna4hBxRaERJhEq0yFuxfpl8= + dependencies: + gitconfiglocal "^1.0.0" + pify "^2.3.0" + +git-semver-tags@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz#63191bcd809b0ec3e151ba4751c16c444e5b5780" + integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== + dependencies: + meow "^8.0.0" + semver "^6.0.0" + +git-up@^4.0.0: + version "4.0.5" + resolved "https://registry.npmjs.org/git-up/-/git-up-4.0.5.tgz#e7bb70981a37ea2fb8fe049669800a1f9a01d759" + integrity sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA== + dependencies: + is-ssh "^1.3.0" + parse-url "^6.0.0" + +git-url-parse@^11.4.4: + version "11.6.0" + resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.6.0.tgz#c634b8de7faa66498a2b88932df31702c67df605" + integrity sha512-WWUxvJs5HsyHL6L08wOusa/IXYtMuCAhrMmnTjQPpBU0TTHyDhnOATNH3xNQz7YOQUsqIIPTGr4xiVti1Hsk5g== + dependencies: + git-up "^4.0.0" + +gitconfiglocal@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" + integrity sha1-QdBF84UaXqiPA/JMocYXgRRGS5s= + dependencies: + ini "^1.3.2" + +glob-parent@^5.1.1, glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.2.0" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + 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" + +globby@^11.0.2: + version "11.0.4" + resolved "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" + integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3: + version "4.2.8" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== + +handlebars@^4.7.6: + version "4.7.7" + resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has-unicode@^2.0.0, has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" + integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg== + dependencies: + lru-cache "^6.0.0" + +http-cache-semantics@^4.1.0: + version "4.1.1" + resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= + dependencies: + ms "^2.0.0" + +iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ignore-walk@^3.0.3: + version "3.0.4" + resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" + integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== + dependencies: + minimatch "^3.0.4" + +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6" + integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@^1.3.2, ini@^1.3.4: + version "1.3.8" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +init-package-json@^2.0.2: + version "2.0.5" + resolved "https://registry.npmjs.org/init-package-json/-/init-package-json-2.0.5.tgz#78b85f3c36014db42d8f32117252504f68022646" + integrity sha512-u1uGAtEFu3VA6HNl/yUWw57jmKEMx8SKOxHhxjGnOFUiIlFnohKDFg4ZrPpv9wWqk44nDxGJAtqjdQFm+9XXQA== + dependencies: + npm-package-arg "^8.1.5" + promzard "^0.3.0" + read "~1.0.1" + read-package-json "^4.1.1" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^3.0.0" + +inquirer@^7.3.3: + version "7.3.3" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +ip@^1.1.5: + version "1.1.9" + resolved "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396" + integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-builtin-module@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.1.0.tgz#6fdb24313b1c03b75f8b9711c0feb8c30b903b00" + integrity sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg== + dependencies: + builtin-modules "^3.0.0" + +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-core-module@^2.2.0, is-core-module@^2.5.0: + version "2.6.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19" + integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ== + dependencies: + has "^1.0.3" + +is-core-module@^2.8.1: + version "2.9.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= + +is-negative-zero@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" + integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== + +is-number-object@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" + integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + +is-plain-obj@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +is-reference@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-ssh@^1.3.0: + version "1.3.3" + resolved "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.3.tgz#7f133285ccd7f2c2c7fc897b771b53d95a2b2c7e" + integrity sha512-NKzJmQzJfEEma3w5cJNcUMxoXfDjz0Zj0eyCalHn2E6VOwlzjZo0yuO2fcBSf8zhFuVCL/82/r5gRcoi6aEPVQ== + dependencies: + protocols "^1.1.0" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-text-path@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" + integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= + dependencies: + text-extensions "^1.0.0" + +is-typedarray@^1.0.0, is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jest-worker@^24.0.0: + version "24.9.0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" + integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== + dependencies: + merge-stream "^2.0.0" + supports-color "^6.1.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.2.0, jsonparse@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kind-of@^6.0.2, kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +lerna@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e" + integrity sha512-DD/i1znurfOmNJb0OBw66NmNqiM8kF6uIrzrJ0wGE3VNdzeOhz9ziWLYiRaZDGGwgbcjOo6eIfcx9O5Qynz+kg== + dependencies: + "@lerna/add" "4.0.0" + "@lerna/bootstrap" "4.0.0" + "@lerna/changed" "4.0.0" + "@lerna/clean" "4.0.0" + "@lerna/cli" "4.0.0" + "@lerna/create" "4.0.0" + "@lerna/diff" "4.0.0" + "@lerna/exec" "4.0.0" + "@lerna/import" "4.0.0" + "@lerna/info" "4.0.0" + "@lerna/init" "4.0.0" + "@lerna/link" "4.0.0" + "@lerna/list" "4.0.0" + "@lerna/publish" "4.0.0" + "@lerna/run" "4.0.0" + "@lerna/version" "4.0.0" + import-local "^3.0.2" + npmlog "^4.1.2" + +libnpmaccess@^4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-4.0.3.tgz#dfb0e5b0a53c315a2610d300e46b4ddeb66e7eec" + integrity sha512-sPeTSNImksm8O2b6/pf3ikv4N567ERYEpeKRPSmqlNt1dTZbvgpJIzg5vAhXHpw2ISBsELFRelk0jEahj1c6nQ== + dependencies: + aproba "^2.0.0" + minipass "^3.1.1" + npm-package-arg "^8.1.2" + npm-registry-fetch "^11.0.0" + +libnpmpublish@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-4.0.2.tgz#be77e8bf5956131bcb45e3caa6b96a842dec0794" + integrity sha512-+AD7A2zbVeGRCFI2aO//oUmapCwy7GHqPXFJh3qpToSRNU+tXKJ2YFUgjt04LPPAf2dlEH95s6EhIHM1J7bmOw== + dependencies: + normalize-package-data "^3.0.2" + npm-package-arg "^8.1.2" + npm-registry-fetch "^11.0.0" + semver "^7.1.3" + ssri "^8.0.1" + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +load-json-file@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" + integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== + dependencies: + graceful-fs "^4.1.15" + parse-json "^5.0.0" + strip-bom "^4.0.0" + type-fest "^0.6.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + +lodash.ismatch@^4.4.0: + version "4.4.0" + resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" + integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= + +lodash.template@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + +lodash@^4.17.15, lodash@^4.17.19, lodash@^4.7.0: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +magic-string@^0.25.2, magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.0, make-dir@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-fetch-happen@^8.0.9: + version "8.0.14" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222" + integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.0.5" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + promise-retry "^2.0.1" + socks-proxy-agent "^5.0.0" + ssri "^8.0.0" + +make-fetch-happen@^9.0.1: + version "9.1.0" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + +map-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-obj@^4.0.0: + version "4.3.0" + resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" + integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== + +meow@^8.0.0: + version "8.1.2" + resolved "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" + integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + +mime-db@1.49.0: + version "1.49.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" + integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.32" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" + integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== + dependencies: + mime-db "1.49.0" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist-options@4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-json-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz#7edbb92588fbfc2ff1db2fc10397acb7b6b44aa7" + integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + dependencies: + jsonparse "^1.3.1" + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^2.6.0, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.1.5" + resolved "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732" + integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw== + dependencies: + yallist "^4.0.0" + +minizlib@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp-infer-owner@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316" + integrity sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw== + dependencies: + chownr "^2.0.0" + infer-owner "^1.0.4" + mkdirp "^1.0.3" + +mkdirp@^0.5.1, mkdirp@^0.5.5: + version "0.5.5" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +modify-values@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" + integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.0.0: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multimatch@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" + integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + +mute-stream@0.0.8, mute-stream@~0.0.4: + version "0.0.8" + resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +negotiator@^0.6.2: + version "0.6.2" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +neo-async@^2.6.0: + version "2.6.2" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-fetch@^2.6.1: + version "2.6.5" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd" + integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ== + dependencies: + whatwg-url "^5.0.0" + +node-gyp@^5.0.2: + version "5.1.1" + resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.1.tgz#eb915f7b631c937d282e33aed44cb7a025f62a3e" + integrity sha512-WH0WKGi+a4i4DUt2mHnvocex/xPLp9pYt5R6M2JdFB7pJ7Z34hveZ4nDTGTiLXCkitA9T8HFZjhinBCiVHYcWw== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.2" + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.1.2" + request "^2.88.0" + rimraf "^2.6.3" + semver "^5.7.1" + tar "^4.4.12" + which "^1.3.1" + +node-gyp@^7.1.0: + version "7.1.2" + resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" + integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.3" + nopt "^5.0.0" + npmlog "^4.1.2" + request "^2.88.2" + rimraf "^3.0.2" + semver "^7.3.2" + tar "^6.0.2" + which "^2.0.2" + +nopt@^4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" + integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== + dependencies: + abbrev "1" + osenv "^0.1.4" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-package-data@^3.0.0, normalize-package-data@^3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" + integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== + dependencies: + hosted-git-info "^4.0.1" + is-core-module "^2.5.0" + semver "^7.3.4" + validate-npm-package-license "^3.0.1" + +normalize-url@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +npm-bundled@^1.1.1: + version "1.1.2" + resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" + integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== + dependencies: + npm-normalize-package-bin "^1.0.1" + +npm-install-checks@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz#a37facc763a2fde0497ef2c6d0ac7c3fbe00d7b4" + integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== + dependencies: + semver "^7.1.1" + +npm-lifecycle@^3.1.5: + version "3.1.5" + resolved "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz#9882d3642b8c82c815782a12e6a1bfeed0026309" + integrity sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g== + dependencies: + byline "^5.0.0" + graceful-fs "^4.1.15" + node-gyp "^5.0.2" + resolve-from "^4.0.0" + slide "^1.1.6" + uid-number "0.0.6" + umask "^1.1.0" + which "^1.3.1" + +npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" + integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== + +npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-package-arg@^8.1.2, npm-package-arg@^8.1.5: + version "8.1.5" + resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz#3369b2d5fe8fdc674baa7f1786514ddc15466e44" + integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q== + dependencies: + hosted-git-info "^4.0.1" + semver "^7.3.4" + validate-npm-package-name "^3.0.0" + +npm-packlist@^2.1.4: + version "2.2.2" + resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.2.2.tgz#076b97293fa620f632833186a7a8f65aaa6148c8" + integrity sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg== + dependencies: + glob "^7.1.6" + ignore-walk "^3.0.3" + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz#7b5484ca2c908565f43b7f27644f36bb816f5148" + integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA== + dependencies: + npm-install-checks "^4.0.0" + npm-normalize-package-bin "^1.0.1" + npm-package-arg "^8.1.2" + semver "^7.3.4" + +npm-registry-fetch@^11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz#68c1bb810c46542760d62a6a965f85a702d43a76" + integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== + dependencies: + make-fetch-happen "^9.0.1" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" + +npm-registry-fetch@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz#86f3feb4ce00313bc0b8f1f8f69daae6face1661" + integrity sha512-PuFYYtnQ8IyVl6ib9d3PepeehcUeHN9IO5N/iCRhyg9tStQcqGQBRVHmfmMWPDERU3KwZoHFvbJ4FPXPspvzbA== + dependencies: + "@npmcli/ci-detect" "^1.0.0" + lru-cache "^6.0.0" + make-fetch-happen "^8.0.9" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +npmlog@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + 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@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-inspect@^1.11.0, object-inspect@^1.9.0: + version "1.11.0" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" + integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.getownpropertydescriptors@^2.0.3: + version "2.1.2" + resolved "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7" + integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.2" + +once@^1.3.0, once@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map-series@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" + integrity sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-pipe@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-3.1.0.tgz#48b57c922aa2e1af6a6404cb7c6bf0eb9cc8e60e" + integrity sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw== + +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + +p-reduce@^2.0.0, p-reduce@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a" + integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== + +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +p-waterfall@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/p-waterfall/-/p-waterfall-2.1.1.tgz#63153a774f472ccdc4eb281cdb2967fcf158b2ee" + integrity sha512-RRTnDb2TBG/epPRI2yYXsimO0v3BXC8Yd3ogr1545IaqKK17VGhbWVeGGN+XfCm/08OK8635nH31c8bATkHuSw== + dependencies: + p-reduce "^2.0.0" + +pacote@^11.2.6: + version "11.3.5" + resolved "https://registry.npmjs.org/pacote/-/pacote-11.3.5.tgz#73cf1fc3772b533f575e39efa96c50be8c3dc9d2" + integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg== + dependencies: + "@npmcli/git" "^2.1.0" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^1.8.2" + cacache "^15.0.5" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.3" + mkdirp "^1.0.3" + npm-package-arg "^8.0.1" + npm-packlist "^2.1.4" + npm-pick-manifest "^6.0.0" + npm-registry-fetch "^11.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.1.0" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse-path@^4.0.0: + version "4.0.3" + resolved "https://registry.npmjs.org/parse-path/-/parse-path-4.0.3.tgz#82d81ec3e071dcc4ab49aa9f2c9c0b8966bb22bf" + integrity sha512-9Cepbp2asKnWTJ9x2kpw6Fe8y9JDbqwahGCTvklzd/cEq5C5JC59x2Xb0Kx+x0QZ8bvNquGO8/BWP0cwBHzSAA== + dependencies: + is-ssh "^1.3.0" + protocols "^1.4.0" + qs "^6.9.4" + query-string "^6.13.8" + +parse-url@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/parse-url/-/parse-url-6.0.0.tgz#f5dd262a7de9ec00914939220410b66cff09107d" + integrity sha512-cYyojeX7yIIwuJzledIHeLUBVJ6COVLeT4eF+2P6aKVzwvgKQPndCBv3+yQ7pcWjqToYwaligxzSYNNmGoMAvw== + dependencies: + is-ssh "^1.3.0" + normalize-url "^6.1.0" + parse-path "^4.0.0" + protocols "^1.4.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-network-drive@^1.0.13: + version "1.0.13" + resolved "https://registry.npmjs.org/path-is-network-drive/-/path-is-network-drive-1.0.13.tgz#c9aa0183eb72c328aa83f43def93ddcb9d7ec4d4" + integrity sha512-Hg74mRN6mmXV+gTm3INjFK40ncAmC/Lo4qoQaSZ+GT3hZzlKdWQSqAjqyPeW0SvObP2W073WyYEBWY9d3wOm3A== + dependencies: + tslib "^2.3.1" + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6, path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-strip-sep@^1.0.10: + version "1.0.10" + resolved "https://registry.npmjs.org/path-strip-sep/-/path-strip-sep-1.0.10.tgz#2be4e789406b298af8709ff79af716134b733b98" + integrity sha512-JpCy+8LAJQQTO1bQsb/84s1g+/Stm3h39aOpPRBQ/paMUGVPPZChLTOTKHoaCkc/6sKuF7yVsnq5Pe1S6xQGcA== + dependencies: + tslib "^2.3.1" + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picomatch@^2.2.2, picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pify@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" + integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== + +"pkg-dir@< 6 >= 5": + version "5.0.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" + integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== + dependencies: + find-up "^5.0.0" + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + +promzard@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" + integrity sha1-JqXW7ox97kyxIggwWs+5O6OCqe4= + dependencies: + read "1" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= + +protocols@^1.1.0, protocols@^1.4.0: + version "1.4.8" + resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" + integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qs@^6.9.4: + version "6.10.1" + resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +query-string@^6.13.8: + version "6.14.1" + resolved "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== + dependencies: + decode-uri-component "^0.2.0" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + +read-cmd-shim@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz#4a50a71d6f0965364938e9038476f7eede3928d9" + integrity sha512-HJpV9bQpkl6KwjxlJcBoqu9Ba0PQg8TqSNIOrulGt54a0uup0HtevreFHzYzkm0lpnleRdNBzXznKrgxglEHQw== + +read-package-json-fast@^2.0.1: + version "2.0.3" + resolved "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz#323ca529630da82cb34b36cc0b996693c98c2b83" + integrity sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ== + dependencies: + json-parse-even-better-errors "^2.3.0" + npm-normalize-package-bin "^1.0.1" + +read-package-json@^2.0.0: + version "2.1.2" + resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz#6992b2b66c7177259feb8eaac73c3acd28b9222a" + integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^2.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-json@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-3.0.1.tgz#c7108f0b9390257b08c21e3004d2404c806744b9" + integrity sha512-aLcPqxovhJTVJcsnROuuzQvv6oziQx4zd3JvG0vGCL5MjTONUc4uJ90zCBC6R7W7oUKBNoR/F8pkyfVwlbxqng== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-json@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-4.1.1.tgz#153be72fce801578c1c86b8ef2b21188df1b9eea" + integrity sha512-P82sbZJ3ldDrWCOSKxJT0r/CXMWR0OR3KRh55SgKo3p91GSIEEC32v3lSHAvO/UcH3/IoL7uqhOFBduAnwdldw== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-tree@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" + integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== + dependencies: + read-package-json "^2.0.0" + readdir-scoped-modules "^1.0.0" + util-promisify "^2.1.0" + +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" + integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= + dependencies: + find-up "^2.0.0" + read-pkg "^3.0.0" + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +read@1, read@~1.0.1: + version "1.0.7" + resolved "https://registry.npmjs.org/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" + integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= + dependencies: + mute-stream "~0.0.4" + +readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2: + version "3.6.0" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^2.0.6, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + 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" + +readdir-scoped-modules@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" + integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + graceful-fs "^4.1.2" + once "^1.3.0" + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +request@^2.88.0, request@^2.88.2: + version "2.88.2" + resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.10.0, resolve@^1.17.0, resolve@^1.19.0: + version "1.20.0" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +resolve@^1.20.0: + version "1.22.0" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup-plugin-replace@2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz#f41ae5372e11e7a217cde349c8b5d5fd115e70e3" + integrity sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA== + dependencies: + magic-string "^0.25.2" + rollup-pluginutils "^2.6.0" + +rollup-plugin-sourcemaps@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz#bf93913ffe056e414419607f1d02780d7ece84ed" + integrity sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw== + dependencies: + "@rollup/pluginutils" "^3.0.9" + source-map-resolve "^0.6.0" + +rollup-plugin-typescript2@0.31.2: + version "0.31.2" + resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.31.2.tgz#463aa713a7e2bf85b92860094b9f7fb274c5a4d8" + integrity sha512-hRwEYR1C8xDGVVMFJQdEVnNAeWRvpaY97g5mp3IeLnzhNXzSVq78Ye/BJ9PAaUfN4DXa/uDnqerifMOaMFY54Q== + dependencies: + "@rollup/pluginutils" "^4.1.2" + "@yarn-tool/resolve-package" "^1.0.40" + find-cache-dir "^3.3.2" + fs-extra "^10.0.0" + resolve "^1.20.0" + tslib "^2.3.1" + +rollup-plugin-uglify@6.0.4: + version "6.0.4" + resolved "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-6.0.4.tgz#65a0959d91586627f1e46a7db966fd504ec6c4e6" + integrity sha512-ddgqkH02klveu34TF0JqygPwZnsbhHVI6t8+hGTcYHngPkQb5MIHI0XiztXIN/d6V9j+efwHAqEL7LspSxQXGw== + dependencies: + "@babel/code-frame" "^7.0.0" + jest-worker "^24.0.0" + serialize-javascript "^2.1.2" + uglify-js "^3.4.9" + +rollup-pluginutils@^2.6.0: + version "2.8.2" + resolved "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" + integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== + dependencies: + estree-walker "^0.6.1" + +rollup@2.79.1: + version "2.79.1" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@^6.6.0: + version "6.6.7" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +"semver@2 || 3 || 4 || 5", semver@^5.6.0, semver@^5.7.1: + version "5.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: + version "7.6.0" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + dependencies: + lru-cache "^6.0.0" + +serialize-javascript@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" + integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.4" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.4.tgz#366a4684d175b9cab2081e3681fda3747b6c51d7" + integrity sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slide@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= + +smart-buffer@^4.1.0: + version "4.2.0" + resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz#032fb583048a29ebffec2e6a73fca0761f48177e" + integrity sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ== + dependencies: + agent-base "^6.0.2" + debug "4" + socks "^2.3.3" + +socks-proxy-agent@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.0.tgz#869cf2d7bd10fea96c7ad3111e81726855e285c3" + integrity sha512-57e7lwCN4Tzt3mXz25VxOErJKXlPfXmkMLnk310v/jwW20jWRVcgsOit+xNkN3eIEdB47GwnfAEBLacZ/wVIKg== + dependencies: + agent-base "^6.0.2" + debug "^4.3.1" + socks "^2.6.1" + +socks@^2.3.3, socks@^2.6.1: + version "2.6.1" + resolved "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" + integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== + dependencies: + ip "^1.1.5" + smart-buffer "^4.1.0" + +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= + dependencies: + is-plain-obj "^1.0.0" + +sort-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18" + integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== + dependencies: + is-plain-obj "^2.0.0" + +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.10" + resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b" + integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA== + +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + +split2@^3.0.0: + version "3.2.2" + resolved "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + +split@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + 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" + +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strong-log-transformer@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" + integrity sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA== + dependencies: + duplexer "^0.1.1" + minimist "^1.2.0" + through "^2.3.4" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tar@^4.4.12: + version "4.4.19" + resolved "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" + integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== + dependencies: + chownr "^1.1.4" + fs-minipass "^1.2.7" + minipass "^2.9.0" + minizlib "^1.3.3" + mkdirp "^0.5.5" + safe-buffer "^5.2.1" + yallist "^3.1.1" + +tar@^6.0.2, tar@^6.1.0: + version "6.1.11" + resolved "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" + integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= + +temp-write@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/temp-write/-/temp-write-4.0.0.tgz#cd2e0825fc826ae72d201dc26eef3bf7e6fc9320" + integrity sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw== + dependencies: + graceful-fs "^4.1.15" + is-stream "^2.0.0" + make-dir "^3.0.0" + temp-dir "^1.0.0" + uuid "^3.3.2" + +text-extensions@^1.0.0: + version "1.9.0" + resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" + integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through2@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== + dependencies: + readable-stream "3" + +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: + version "2.3.8" + resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + +trim-newlines@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" + integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== + +tslib@2.4.0, tslib@^2.3.1: + version "2.4.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.1.0: + version "2.3.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-fest@^0.18.0: + version "0.18.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" + integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8" + integrity sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typescript@4.7.4: + version "4.7.4" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + +uglify-js@^3.1.4, uglify-js@^3.4.9: + version "3.14.2" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.2.tgz#d7dd6a46ca57214f54a2d0a43cad0f35db82ac99" + integrity sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A== + +uid-number@0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= + +umask@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" + integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= + +unbox-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +upath2@^3.1.12: + version "3.1.12" + resolved "https://registry.npmjs.org/upath2/-/upath2-3.1.12.tgz#441b3dfbadde21731017bd1b7beb169498efd0a9" + integrity sha512-yC3eZeCyCXFWjy7Nu4pgjLhXNYjuzuUmJiRgSSw6TJp8Emc+E4951HGPJf+bldFC5SL7oBLeNbtm1fGzXn2gxw== + dependencies: + path-is-network-drive "^1.0.13" + path-strip-sep "^1.0.10" + tslib "^2.3.1" + +upath@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" + integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util-promisify@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz#3c2236476c4d32c5ff3c47002add7c13b9a82a53" + integrity sha1-PCI2R2xNMsX/PEcAKt18E7moKlM= + dependencies: + object.getownpropertydescriptors "^2.0.3" + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= + dependencies: + builtins "^1.0.3" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +wcwidth@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= + dependencies: + defaults "^1.0.3" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +whatwg-url@^8.4.0: + version "8.7.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== + dependencies: + lodash "^4.7.0" + tr46 "^2.1.0" + webidl-conversions "^6.1.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^2.4.2: + version "2.4.3" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +write-json-file@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz#65bbdc9ecd8a1458e15952770ccbadfcff5fe62a" + integrity sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ== + dependencies: + detect-indent "^5.0.0" + graceful-fs "^4.1.15" + make-dir "^2.1.0" + pify "^4.0.1" + sort-keys "^2.0.0" + write-file-atomic "^2.4.2" + +write-json-file@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d" + integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== + dependencies: + detect-indent "^6.0.0" + graceful-fs "^4.1.15" + is-plain-obj "^2.0.0" + make-dir "^3.0.0" + sort-keys "^4.0.0" + write-file-atomic "^3.0.0" + +write-pkg@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/write-pkg/-/write-pkg-4.0.0.tgz#675cc04ef6c11faacbbc7771b24c0abbf2a20039" + integrity sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA== + dependencies: + sort-keys "^2.0.0" + type-fest "^0.4.1" + write-json-file "^3.2.0" + +xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.0, yallist@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2, yargs-parser@^20.2.3: + version "20.2.9" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/packages/auth-compat/index.node.ts b/packages/auth-compat/index.node.ts new file mode 100644 index 00000000000..1513148bea9 --- /dev/null +++ b/packages/auth-compat/index.node.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This is the file that people using Node.js will actually import. You should + * only include this file if you have something specific about your + * implementation that mandates having a separate entrypoint. Otherwise you can + * just use index.ts + */ +export * from './index'; +import { FetchProvider } from '@firebase/auth/internal'; +import './index'; + +FetchProvider.initialize(fetch, Headers, Response); diff --git a/packages/auth-compat/index.ts b/packages/auth-compat/index.ts new file mode 100644 index 00000000000..2cf2cabffe7 --- /dev/null +++ b/packages/auth-compat/index.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable camelcase */ + +import firebase, { _FirebaseNamespace } from '@firebase/app-compat'; +import * as impl from '@firebase/auth/internal'; +import { + Component, + ComponentType, + InstantiationMode +} from '@firebase/component'; +import { FirebaseError } from '@firebase/util'; + +import * as types from '@firebase/auth-types'; +import { name, version } from './package.json'; +import { Auth } from './src/auth'; +import { PhoneAuthProvider as CompatAuthProvider } from './src/phone_auth_provider'; +import { RecaptchaVerifier as CompatRecaptchaVerifier } from './src/recaptcha_verifier'; + +const AUTH_TYPE = 'auth-compat'; + +declare module '@firebase/component' { + interface NameServiceMapping { + 'auth-compat': types.FirebaseAuth; + } +} + +declare module '@firebase/app-compat' { + interface FirebaseNamespace { + auth: { + (app?: FirebaseApp): types.FirebaseAuth; + Auth: typeof types.FirebaseAuth; + EmailAuthProvider: typeof types.EmailAuthProvider; + EmailAuthProvider_Instance: typeof types.EmailAuthProvider_Instance; + FacebookAuthProvider: typeof types.FacebookAuthProvider; + FacebookAuthProvider_Instance: typeof types.FacebookAuthProvider_Instance; + GithubAuthProvider: typeof types.GithubAuthProvider; + GithubAuthProvider_Instance: typeof types.GithubAuthProvider_Instance; + GoogleAuthProvider: typeof types.GoogleAuthProvider; + GoogleAuthProvider_Instance: typeof types.GoogleAuthProvider_Instance; + OAuthProvider: typeof types.OAuthProvider; + SAMLAuthProvider: typeof types.SAMLAuthProvider; + PhoneAuthProvider: typeof types.PhoneAuthProvider; + PhoneAuthProvider_Instance: typeof types.PhoneAuthProvider_Instance; + PhoneMultiFactorGenerator: typeof types.PhoneMultiFactorGenerator; + RecaptchaVerifier: typeof types.RecaptchaVerifier; + RecaptchaVerifier_Instance: typeof types.RecaptchaVerifier_Instance; + TwitterAuthProvider: typeof types.TwitterAuthProvider; + TwitterAuthProvider_Instance: typeof types.TwitterAuthProvider_Instance; + }; + } + interface FirebaseApp { + auth?(): types.FirebaseAuth; + } +} + +// Create auth components to register with firebase. +// Provides Auth public APIs. +function registerAuthCompat(instance: _FirebaseNamespace): void { + instance.INTERNAL.registerComponent( + new Component( + AUTH_TYPE, + container => { + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app-compat').getImmediate(); + const authProvider = container.getProvider('auth'); + return new Auth(app, authProvider); + }, + ComponentType.PUBLIC + ) + .setServiceProps({ + ActionCodeInfo: { + Operation: { + EMAIL_SIGNIN: impl.ActionCodeOperation.EMAIL_SIGNIN, + PASSWORD_RESET: impl.ActionCodeOperation.PASSWORD_RESET, + RECOVER_EMAIL: impl.ActionCodeOperation.RECOVER_EMAIL, + REVERT_SECOND_FACTOR_ADDITION: + impl.ActionCodeOperation.REVERT_SECOND_FACTOR_ADDITION, + VERIFY_AND_CHANGE_EMAIL: + impl.ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, + VERIFY_EMAIL: impl.ActionCodeOperation.VERIFY_EMAIL + } + }, + EmailAuthProvider: impl.EmailAuthProvider, + FacebookAuthProvider: impl.FacebookAuthProvider, + GithubAuthProvider: impl.GithubAuthProvider, + GoogleAuthProvider: impl.GoogleAuthProvider, + OAuthProvider: impl.OAuthProvider, + SAMLAuthProvider: impl.SAMLAuthProvider, + PhoneAuthProvider: CompatAuthProvider, + PhoneMultiFactorGenerator: impl.PhoneMultiFactorGenerator, + RecaptchaVerifier: CompatRecaptchaVerifier, + TwitterAuthProvider: impl.TwitterAuthProvider, + Auth, + AuthCredential: impl.AuthCredential, + Error: FirebaseError + }) + .setInstantiationMode(InstantiationMode.LAZY) + .setMultipleInstances(false) + ); + + instance.registerVersion(name, version); +} + +registerAuthCompat(firebase as _FirebaseNamespace); diff --git a/packages/auth-compat/karma.conf.js b/packages/auth-compat/karma.conf.js new file mode 100644 index 00000000000..6283ffbc3f1 --- /dev/null +++ b/packages/auth-compat/karma.conf.js @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const karmaBase = require('../../config/karma.base'); +const webpackBase = require('../../config/webpack.test'); +const { argv } = require('yargs'); + +const files = ['src/**/*.test.ts']; + +module.exports = function (config) { + const karmaConfig = Object.assign({}, karmaBase, { + browsers: getTestBrowsers(argv), + // files to load into karma + files: getTestFiles(), + preprocessors: { '**/*.ts': ['webpack', 'sourcemap'] }, + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'], + client: Object.assign({}, karmaBase.client, getClientConfig()) + }); + + config.set(karmaConfig); +}; + +function getTestFiles() { + if (argv.integration) { + return ['test/**/*.test.ts']; + } else { + return ['src/**/*.test.ts']; + } +} + +function getTestBrowsers(argv) { + let browsers = ['ChromeHeadless']; + if (process.env?.BROWSERS && argv.unit) { + browsers = process.env?.BROWSERS?.split(','); + } + return browsers; +} + +function getClientConfig() { + if (!argv.integration) { + return {}; + } + + if (!process.env.GCLOUD_PROJECT || !process.env.FIREBASE_AUTH_EMULATOR_HOST) { + console.error( + 'Local testing against emulator requested, but ' + + 'GCLOUD_PROJECT and FIREBASE_AUTH_EMULATOR_HOST env variables ' + + 'are missing' + ); + process.exit(1); + } + + return { + authAppConfig: { + apiKey: 'local-api-key', + projectId: process.env.GCLOUD_PROJECT, + authDomain: 'local-auth-domain' + }, + authEmulatorHost: process.env.FIREBASE_AUTH_EMULATOR_HOST + }; +} + +module.exports.files = getTestFiles(); diff --git a/packages/auth-compat/package.json b/packages/auth-compat/package.json new file mode 100644 index 00000000000..47f326fd714 --- /dev/null +++ b/packages/auth-compat/package.json @@ -0,0 +1,86 @@ +{ + "name": "@firebase/auth-compat", + "version": "0.6.1", + "description": "FirebaseAuth compatibility package that uses API style compatible with Firebase@8 and prior versions", + "author": "Firebase (https://firebase.google.com/)", + "main": "dist/index.node.cjs.js", + "browser": "dist/index.esm.js", + "module": "dist/index.esm.js", + "exports": { + ".": { + "types": "./dist/auth-compat/index.d.ts", + "node": { + "types": "./dist/auth-compat/index.node.d.ts", + "import": "./dist/esm/index.node.esm.js", + "require": "./dist/index.node.cjs.js" + }, + "browser": { + "require": "./dist/index.cjs.js", + "import": "./dist/index.esm.js" + }, + "default": "./dist/index.esm.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "scripts": { + "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "build": "rollup -c", + "build:deps": "lerna run --scope @firebase/auth-compat --include-dependencies build", + "build:release": "yarn build && yarn add-compat-overloads", + "dev": "rollup -c -w", + "test": "run-p --npm-path npm lint test:all", + "test:all": "run-p --npm-path npm test:browser test:node test:integration", + "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all", + "test:browser": "karma start", + "test:browser:unit": "karma start --unit", + "test:browser:integration": "karma start --integration", + "test:node": "ts-node -O '{\"module\": \"commonjs\", \"target\": \"es6\"}' scripts/run_node_tests.ts", + "test:node:integration": "ts-node -O '{\"module\": \"commonjs\", \"target\": \"es6\"}' scripts/run_node_tests.ts --integration", + "test:webdriver": "rollup -c test/integration/webdriver/static/rollup.config.js && ts-node -O '{\"module\": \"commonjs\", \"target\": \"es6\"}' scripts/run_node_tests.ts --webdriver", + "test:integration": "firebase emulators:exec --project demo-emulatedproject --only auth \"run-s --npm-path npm test:browser:integration test:node:integration test:webdriver\"", + "trusted-type-check": "tsec -p tsconfig.json --noEmit", + "add-compat-overloads": "ts-node-script ../../scripts/build/create-overloads.ts -i ../auth/dist/auth-public.d.ts -o dist/auth-compat/index.d.ts -a -r Auth:types.FirebaseAuth -r User:types.User -r FirebaseApp:FirebaseAppCompat --moduleToEnhance @firebase/auth" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + }, + "dependencies": { + "@firebase/auth": "1.11.1", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "license": "Apache-2.0", + "devDependencies": { + "@firebase/app-compat": "0.5.5", + "@rollup/plugin-json": "6.1.0", + "rollup": "2.79.2", + "rollup-plugin-replace": "2.2.0", + "rollup-plugin-typescript2": "0.36.0", + "selenium-webdriver": "4.30.0", + "typescript": "5.5.4" + }, + "repository": { + "directory": "packages/auth-compat", + "type": "git", + "url": "git+https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "typings": "dist/auth-compat/index.d.ts", + "nyc": { + "extension": [ + ".ts" + ], + "reportDir": "./coverage/node" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/packages/auth-compat/rollup.config.js b/packages/auth-compat/rollup.config.js new file mode 100644 index 00000000000..b2872f977fd --- /dev/null +++ b/packages/auth-compat/rollup.config.js @@ -0,0 +1,124 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import json from '@rollup/plugin-json'; +import resolve from '@rollup/plugin-node-resolve'; +import { uglify } from 'rollup-plugin-uglify'; +import typescriptPlugin from 'rollup-plugin-typescript2'; +import typescript from 'typescript'; +import { emitModulePackageFile } from '../../scripts/build/rollup_emit_module_package_file'; +import pkg from './package.json'; + +const deps = Object.keys( + Object.assign({}, pkg.peerDependencies, pkg.dependencies) +); + +const buildPlugins = [json(), resolve(), typescriptPlugin({ typescript })]; + +const browserBuilds = [ + { + input: 'index.ts', + output: { + file: pkg.browser, + format: 'es', + sourcemap: true + }, + plugins: buildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + treeshake: { + moduleSideEffects: false + } + }, + { + input: 'index.ts', + output: { + file: 'dist/index.cjs.js', + format: 'cjs', + sourcemap: true + }, + plugins: buildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + treeshake: { + moduleSideEffects: false + } + } +]; + +const nodeBuilds = [ + { + input: 'index.node.ts', + output: { + file: pkg.main, + format: 'cjs', + sourcemap: true + }, + plugins: buildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + treeshake: { + moduleSideEffects: true + } + }, + { + input: 'index.node.ts', + output: [ + { file: pkg.exports['.'].node.import, format: 'es', sourcemap: true } + ], + plugins: [...buildPlugins, emitModulePackageFile()], + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)), + treeshake: { + moduleSideEffects: true + } + } +]; + +const umdBuild = { + input: `./index.ts`, + output: { + compact: true, + file: `dist/firebase-auth.js`, + format: 'umd', + sourcemap: true, + extend: true, + name: 'firebase', + globals: { + '@firebase/app-compat': 'firebase', + '@firebase/app': 'firebase.INTERNAL.modularAPIs' + }, + /** + * use iife to avoid below error in the old Safari browser + * SyntaxError: Functions cannot be declared in a nested block in strict mode + * https://github.com/firebase/firebase-js-sdk/issues/1228 + * + */ + intro: ` + try { + (function() {`, + outro: ` + }).apply(this, arguments); + } catch(err) { + console.error(err); + throw new Error( + 'Cannot instantiate firebase-auth.js - ' + + 'be sure to load firebase-app.js first.' + ); + }` + }, + plugins: [...buildPlugins, uglify()], + external: ['@firebase/app-compat', '@firebase/app'] +}; + +export default [...browserBuilds, ...nodeBuilds, umdBuild]; diff --git a/packages/auth-compat/scripts/run_node_tests.ts b/packages/auth-compat/scripts/run_node_tests.ts new file mode 100644 index 00000000000..286e6c6767d --- /dev/null +++ b/packages/auth-compat/scripts/run_node_tests.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolve } from 'path'; + +import { spawn } from 'child-process-promise'; +import * as yargs from 'yargs'; + +const argv = yargs + .options({ + integration: { + type: 'boolean' + }, + webdriver: { + type: 'boolean' + } + }) + .parseSync(); + +const nyc = resolve(__dirname, '../../../node_modules/.bin/nyc'); +const mocha = resolve(__dirname, '../../../node_modules/.bin/mocha'); + +process.env.TS_NODE_COMPILER_OPTIONS = '{"module":"commonjs", "target": "es6"}'; +process.env.COMPAT_LAYER = 'true'; + +let testConfig = ['src/**/*.test.ts']; + +if (argv.integration) { + testConfig = ['test/integration/flows/**.test.ts']; +} else if (argv.webdriver) { + testConfig = ['../auth/test/integration/webdriver/**/*.test.ts', '--delay']; +} + +let args = [ + '--reporter', + 'lcovonly', + mocha, + ...testConfig, + '--file', + '../auth/src/platform_browser/iframe/gapi.iframes.ts', + '--config', + '../../config/mocharc.node.js' +]; + +// Make sure that the environment variables are present for local test +if (argv.integration || argv.webdriver) { + if (!process.env.GCLOUD_PROJECT || !process.env.FIREBASE_AUTH_EMULATOR_HOST) { + console.error( + 'Local testing against emulator requested, but ' + + 'GCLOUD_PROJECT and FIREBASE_AUTH_EMULATOR_HOST env variables ' + + 'are missing' + ); + process.exit(1); + } +} + +args = args.concat(argv._ as string[]); + +const spawned = spawn(nyc, args, { + stdio: 'inherit', + cwd: process.cwd() +}); + +const childProcess = spawned.childProcess; +spawned.catch(() => { + childProcess.kill(); + process.exit(1); +}); + +process.once('exit', () => childProcess.kill()); +process.once('SIGINT', () => childProcess.kill('SIGINT')); +process.once('SIGTERM', () => childProcess.kill('SIGTERM')); diff --git a/packages/auth-compat/src/auth.test.ts b/packages/auth-compat/src/auth.test.ts new file mode 100644 index 00000000000..c2e73ea5df9 --- /dev/null +++ b/packages/auth-compat/src/auth.test.ts @@ -0,0 +1,180 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseApp } from '@firebase/app-compat'; +import * as exp from '@firebase/auth/internal'; +import { Provider } from '@firebase/component'; +import { expect, use } from 'chai'; +import * as sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { Auth } from './auth'; +import { CompatPopupRedirectResolver } from './popup_redirect'; +import * as platform from './platform'; +import { + FAKE_APP_CHECK_CONTROLLER_PROVIDER, + FAKE_HEARTBEAT_CONTROLLER_PROVIDER +} from '../test/helpers/helpers'; + +use(sinonChai); + +function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// For the most part, the auth methods just call straight through. Some parts +// of the auth compat layer are more complicated: these tests cover those +describe('auth compat', () => { + context('redirect persistence key storage', () => { + let underlyingAuth: exp.AuthImpl; + let app: FirebaseApp; + let providerStub: sinon.SinonStubbedInstance>; + + beforeEach(() => { + app = { options: { apiKey: 'api-key' } } as FirebaseApp; + underlyingAuth = new exp.AuthImpl( + app, + FAKE_HEARTBEAT_CONTROLLER_PROVIDER, + FAKE_APP_CHECK_CONTROLLER_PROVIDER, + { + apiKey: 'api-key' + } as exp.ConfigInternal + ); + sinon.stub(underlyingAuth, '_initializeWithPersistence'); + + providerStub = sinon.createStubInstance(Provider); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('saves the persistence into session storage if available', async () => { + if (typeof self !== 'undefined') { + underlyingAuth._initializationPromise = Promise.resolve(); + sinon.stub(underlyingAuth, '_getPersistenceType').returns('TEST'); + sinon + .stub(underlyingAuth, '_initializationPromise') + .value(Promise.resolve()); + sinon.stub( + exp._getInstance( + CompatPopupRedirectResolver + ), + '_openRedirect' + ); + providerStub.isInitialized.returns(true); + providerStub.getImmediate.returns(underlyingAuth); + const authCompat = new Auth( + app, + providerStub as unknown as Provider<'auth'> + ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + await authCompat.signInWithRedirect(new exp.GoogleAuthProvider()); + expect( + sessionStorage.getItem('firebase:persistence:api-key:undefined') + ).to.eq('TEST'); + } + }); + + it('does not save persistence if property throws DOMException', async () => { + if (typeof self !== 'undefined') { + sinon.stub(platform, '_getSelfWindow').returns({ + get sessionStorage(): Storage { + throw new DOMException('Nope!'); + } + } as unknown as Window); + const setItemSpy = sinon.spy(sessionStorage, 'setItem'); + sinon.stub(underlyingAuth, '_getPersistenceType').returns('TEST'); + sinon + .stub(underlyingAuth, '_initializationPromise') + .value(Promise.resolve()); + sinon.stub( + exp._getInstance( + CompatPopupRedirectResolver + ), + '_openRedirect' + ); + providerStub.isInitialized.returns(true); + providerStub.getImmediate.returns(underlyingAuth); + const authCompat = new Auth( + app, + providerStub as unknown as Provider<'auth'> + ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + await authCompat.signInWithRedirect(new exp.GoogleAuthProvider()); + await delay(50); + expect(setItemSpy).not.to.have.been.calledWith( + 'firebase:persistence:api-key:undefined', + 'TEST' + ); + } + }); + + it('pulls the persistence and sets as the main persistence if set', () => { + if (typeof self !== 'undefined') { + sessionStorage.setItem( + 'firebase:persistence:api-key:undefined', + 'none' + ); + providerStub.isInitialized.returns(false); + providerStub.initialize.returns(underlyingAuth); + new Auth(app, providerStub as unknown as Provider<'auth'>); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(providerStub.initialize).to.have.been.calledWith({ + options: { + popupRedirectResolver: CompatPopupRedirectResolver, + persistence: [ + exp.inMemoryPersistence, + exp.indexedDBLocalPersistence, + exp.browserLocalPersistence, + exp.browserSessionPersistence + ] + } + }); + } + }); + + it('does not die if sessionStorage errors', async () => { + if (typeof self !== 'undefined') { + sinon.stub(platform, '_getSelfWindow').returns({ + get sessionStorage(): Storage { + throw new DOMException('Nope!'); + } + } as unknown as Window); + sessionStorage.setItem( + 'firebase:persistence:api-key:undefined', + 'none' + ); + providerStub.isInitialized.returns(false); + providerStub.initialize.returns(underlyingAuth); + new Auth(app, providerStub as unknown as Provider<'auth'>); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + await delay(50); + expect(providerStub.initialize).to.have.been.calledWith({ + options: { + popupRedirectResolver: CompatPopupRedirectResolver, + persistence: [ + exp.indexedDBLocalPersistence, + exp.browserLocalPersistence, + exp.browserSessionPersistence, + exp.inMemoryPersistence + ] + } + }); + } + }); + }); +}); diff --git a/packages/auth-compat/src/auth.ts b/packages/auth-compat/src/auth.ts new file mode 100644 index 00000000000..ee3ad5eb751 --- /dev/null +++ b/packages/auth-compat/src/auth.ts @@ -0,0 +1,402 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseApp, _FirebaseService } from '@firebase/app-compat'; +import * as exp from '@firebase/auth/internal'; +import * as compat from '@firebase/auth-types'; +import { Provider } from '@firebase/component'; +import { ErrorFn, Observer, Unsubscribe } from '@firebase/util'; + +import { + _validatePersistenceArgument, + Persistence, + _getPersistencesFromRedirect, + _savePersistenceForRedirect +} from './persistence'; +import { _isPopupRedirectSupported } from './platform'; +import { CompatPopupRedirectResolver } from './popup_redirect'; +import { User } from './user'; +import { + convertConfirmationResult, + convertCredential +} from './user_credential'; +import { ReverseWrapper, Wrapper } from './wrap'; + +const _assert: typeof exp._assert = exp._assert; + +export class Auth + implements compat.FirebaseAuth, Wrapper, _FirebaseService +{ + static Persistence = Persistence; + readonly _delegate: exp.AuthImpl; + + constructor(readonly app: FirebaseApp, provider: Provider<'auth'>) { + if (provider.isInitialized()) { + this._delegate = provider.getImmediate() as exp.AuthImpl; + this.linkUnderlyingAuth(); + return; + } + + const { apiKey } = app.options; + // TODO: platform needs to be determined using heuristics + _assert(apiKey, exp.AuthErrorCode.INVALID_API_KEY, { + appName: app.name + }); + + // TODO: platform needs to be determined using heuristics + _assert(apiKey, exp.AuthErrorCode.INVALID_API_KEY, { + appName: app.name + }); + + // Only use a popup/redirect resolver in browser environments + const resolver = + typeof window !== 'undefined' ? CompatPopupRedirectResolver : undefined; + this._delegate = provider.initialize({ + options: { + persistence: buildPersistenceHierarchy(apiKey, app.name), + popupRedirectResolver: resolver + } + }) as exp.AuthImpl; + + this._delegate._updateErrorMap(exp.debugErrorMap); + this.linkUnderlyingAuth(); + } + + get emulatorConfig(): compat.EmulatorConfig | null { + return this._delegate.emulatorConfig; + } + + get currentUser(): compat.User | null { + if (!this._delegate.currentUser) { + return null; + } + + return User.getOrCreate(this._delegate.currentUser); + } + get languageCode(): string | null { + return this._delegate.languageCode; + } + set languageCode(languageCode: string | null) { + this._delegate.languageCode = languageCode; + } + get settings(): compat.AuthSettings { + return this._delegate.settings; + } + get tenantId(): string | null { + return this._delegate.tenantId; + } + set tenantId(tid: string | null) { + this._delegate.tenantId = tid; + } + useDeviceLanguage(): void { + this._delegate.useDeviceLanguage(); + } + signOut(): Promise { + return this._delegate.signOut(); + } + useEmulator(url: string, options?: { disableWarnings: boolean }): void { + exp.connectAuthEmulator(this._delegate, url, options); + } + applyActionCode(code: string): Promise { + return exp.applyActionCode(this._delegate, code); + } + + checkActionCode(code: string): Promise { + return exp.checkActionCode(this._delegate, code); + } + + confirmPasswordReset(code: string, newPassword: string): Promise { + return exp.confirmPasswordReset(this._delegate, code, newPassword); + } + + async createUserWithEmailAndPassword( + email: string, + password: string + ): Promise { + return convertCredential( + this._delegate, + exp.createUserWithEmailAndPassword(this._delegate, email, password) + ); + } + fetchProvidersForEmail(email: string): Promise { + return this.fetchSignInMethodsForEmail(email); + } + fetchSignInMethodsForEmail(email: string): Promise { + return exp.fetchSignInMethodsForEmail(this._delegate, email); + } + isSignInWithEmailLink(emailLink: string): boolean { + return exp.isSignInWithEmailLink(this._delegate, emailLink); + } + async getRedirectResult(): Promise { + _assert( + _isPopupRedirectSupported(), + this._delegate, + exp.AuthErrorCode.OPERATION_NOT_SUPPORTED + ); + const credential = await exp.getRedirectResult( + this._delegate, + CompatPopupRedirectResolver + ); + if (!credential) { + return { + credential: null, + user: null + }; + } + return convertCredential(this._delegate, Promise.resolve(credential)); + } + + // This function should only be called by frameworks (e.g. FirebaseUI-web) to log their usage. + // It is not intended for direct use by developer apps. NO jsdoc here to intentionally leave it + // out of autogenerated documentation pages to reduce accidental misuse. + addFrameworkForLogging(framework: string): void { + exp.addFrameworkForLogging(this._delegate, framework); + } + + onAuthStateChanged( + nextOrObserver: Observer | ((a: compat.User | null) => unknown), + errorFn?: (error: compat.Error) => unknown, + completed?: Unsubscribe + ): Unsubscribe { + const { next, error, complete } = wrapObservers( + nextOrObserver, + errorFn, + completed + ); + return this._delegate.onAuthStateChanged(next!, error, complete); + } + onIdTokenChanged( + nextOrObserver: Observer | ((a: compat.User | null) => unknown), + errorFn?: (error: compat.Error) => unknown, + completed?: Unsubscribe + ): Unsubscribe { + const { next, error, complete } = wrapObservers( + nextOrObserver, + errorFn, + completed + ); + return this._delegate.onIdTokenChanged(next!, error, complete); + } + sendSignInLinkToEmail( + email: string, + actionCodeSettings: compat.ActionCodeSettings + ): Promise { + return exp.sendSignInLinkToEmail(this._delegate, email, actionCodeSettings); + } + sendPasswordResetEmail( + email: string, + actionCodeSettings?: compat.ActionCodeSettings | null + ): Promise { + return exp.sendPasswordResetEmail( + this._delegate, + email, + actionCodeSettings || undefined + ); + } + async setPersistence(persistence: string): Promise { + _validatePersistenceArgument(this._delegate, persistence); + let converted; + switch (persistence) { + case Persistence.SESSION: + converted = exp.browserSessionPersistence; + break; + case Persistence.LOCAL: + // Not using isIndexedDBAvailable() since it only checks if indexedDB is defined. + const isIndexedDBFullySupported = await exp + ._getInstance(exp.indexedDBLocalPersistence) + ._isAvailable(); + converted = isIndexedDBFullySupported + ? exp.indexedDBLocalPersistence + : exp.browserLocalPersistence; + break; + case Persistence.NONE: + converted = exp.inMemoryPersistence; + break; + default: + return exp._fail(exp.AuthErrorCode.ARGUMENT_ERROR, { + appName: this._delegate.name + }); + } + + return this._delegate.setPersistence(converted); + } + + signInAndRetrieveDataWithCredential( + credential: compat.AuthCredential + ): Promise { + return this.signInWithCredential(credential); + } + signInAnonymously(): Promise { + return convertCredential( + this._delegate, + exp.signInAnonymously(this._delegate) + ); + } + signInWithCredential( + credential: compat.AuthCredential + ): Promise { + return convertCredential( + this._delegate, + exp.signInWithCredential(this._delegate, credential as exp.AuthCredential) + ); + } + signInWithCustomToken(token: string): Promise { + return convertCredential( + this._delegate, + exp.signInWithCustomToken(this._delegate, token) + ); + } + signInWithEmailAndPassword( + email: string, + password: string + ): Promise { + return convertCredential( + this._delegate, + exp.signInWithEmailAndPassword(this._delegate, email, password) + ); + } + signInWithEmailLink( + email: string, + emailLink?: string + ): Promise { + return convertCredential( + this._delegate, + exp.signInWithEmailLink(this._delegate, email, emailLink) + ); + } + signInWithPhoneNumber( + phoneNumber: string, + applicationVerifier: compat.ApplicationVerifier + ): Promise { + return convertConfirmationResult( + this._delegate, + exp.signInWithPhoneNumber( + this._delegate, + phoneNumber, + applicationVerifier + ) + ); + } + async signInWithPopup( + provider: compat.AuthProvider + ): Promise { + _assert( + _isPopupRedirectSupported(), + this._delegate, + exp.AuthErrorCode.OPERATION_NOT_SUPPORTED + ); + return convertCredential( + this._delegate, + exp.signInWithPopup( + this._delegate, + provider as exp.AuthProvider, + CompatPopupRedirectResolver + ) + ); + } + async signInWithRedirect(provider: compat.AuthProvider): Promise { + _assert( + _isPopupRedirectSupported(), + this._delegate, + exp.AuthErrorCode.OPERATION_NOT_SUPPORTED + ); + + await _savePersistenceForRedirect(this._delegate); + return exp.signInWithRedirect( + this._delegate, + provider as exp.AuthProvider, + CompatPopupRedirectResolver + ); + } + updateCurrentUser(user: compat.User | null): Promise { + // remove ts-ignore once overloads are defined for exp functions to accept compat objects + // @ts-ignore + return this._delegate.updateCurrentUser(user); + } + verifyPasswordResetCode(code: string): Promise { + return exp.verifyPasswordResetCode(this._delegate, code); + } + unwrap(): exp.Auth { + return this._delegate; + } + _delete(): Promise { + return this._delegate._delete(); + } + private linkUnderlyingAuth(): void { + (this._delegate as unknown as ReverseWrapper).wrapped = () => this; + } +} + +function wrapObservers( + nextOrObserver: Observer | ((a: compat.User | null) => unknown), + error?: (error: compat.Error) => unknown, + complete?: Unsubscribe +): Partial> { + let next = nextOrObserver; + if (typeof nextOrObserver !== 'function') { + ({ next, error, complete } = nextOrObserver); + } + + // We know 'next' is now a function + const oldNext = next as (a: compat.User | null) => unknown; + + const newNext = (user: exp.User | null): unknown => + oldNext(user && User.getOrCreate(user as exp.User)); + return { + next: newNext, + error: error as ErrorFn, + complete + }; +} + +function buildPersistenceHierarchy( + apiKey: string, + appName: string +): exp.Persistence[] { + // Note this is slightly different behavior: in this case, the stored + // persistence is checked *first* rather than last. This is because we want + // to prefer stored persistence type in the hierarchy. This is an empty + // array if window is not available or there is no pending redirect + const persistences = _getPersistencesFromRedirect(apiKey, appName); + + // If "self" is available, add indexedDB + if ( + typeof self !== 'undefined' && + !persistences.includes(exp.indexedDBLocalPersistence) + ) { + persistences.push(exp.indexedDBLocalPersistence); + } + + // If "window" is available, add HTML Storage persistences + if (typeof window !== 'undefined') { + for (const persistence of [ + exp.browserLocalPersistence, + exp.browserSessionPersistence + ]) { + if (!persistences.includes(persistence)) { + persistences.push(persistence); + } + } + } + + // Add in-memory as a final fallback + if (!persistences.includes(exp.inMemoryPersistence)) { + persistences.push(exp.inMemoryPersistence); + } + + return persistences; +} diff --git a/packages/auth-compat/src/persistence.ts b/packages/auth-compat/src/persistence.ts new file mode 100644 index 00000000000..3c839823a7c --- /dev/null +++ b/packages/auth-compat/src/persistence.ts @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as exp from '@firebase/auth/internal'; +import { isIndexedDBAvailable, isNode, isReactNative } from '@firebase/util'; +import { _getSelfWindow, _isWebStorageSupported, _isWorker } from './platform'; + +export const Persistence = { + LOCAL: 'local', + NONE: 'none', + SESSION: 'session' +}; + +const _assert: typeof exp._assert = exp._assert; + +const PERSISTENCE_KEY = 'persistence'; + +/** + * Validates that an argument is a valid persistence value. If an invalid type + * is specified, an error is thrown synchronously. + */ +export function _validatePersistenceArgument( + auth: exp.Auth, + persistence: string +): void { + _assert( + Object.values(Persistence).includes(persistence), + auth, + exp.AuthErrorCode.INVALID_PERSISTENCE + ); + // Validate if the specified type is supported in the current environment. + if (isReactNative()) { + // This is only supported in a browser. + _assert( + persistence !== Persistence.SESSION, + auth, + exp.AuthErrorCode.UNSUPPORTED_PERSISTENCE + ); + return; + } + if (isNode()) { + // Only none is supported in Node.js. + _assert( + persistence === Persistence.NONE, + auth, + exp.AuthErrorCode.UNSUPPORTED_PERSISTENCE + ); + return; + } + if (_isWorker()) { + // In a worker environment, either LOCAL or NONE are supported. + // If indexedDB not supported and LOCAL provided, throw an error + _assert( + persistence === Persistence.NONE || + (persistence === Persistence.LOCAL && isIndexedDBAvailable()), + auth, + exp.AuthErrorCode.UNSUPPORTED_PERSISTENCE + ); + return; + } + // This is restricted by what the browser supports. + _assert( + persistence === Persistence.NONE || _isWebStorageSupported(), + auth, + exp.AuthErrorCode.UNSUPPORTED_PERSISTENCE + ); +} + +export async function _savePersistenceForRedirect( + auth: exp.AuthInternal +): Promise { + await auth._initializationPromise; + const session = getSessionStorageIfAvailable(); + const key = exp._persistenceKeyName( + PERSISTENCE_KEY, + auth.config.apiKey, + auth.name + ); + if (session) { + session.setItem(key, auth._getPersistenceType()); + } +} + +export function _getPersistencesFromRedirect( + apiKey: string, + appName: string +): exp.Persistence[] { + const session = getSessionStorageIfAvailable(); + if (!session) { + return []; + } + + const key = exp._persistenceKeyName(PERSISTENCE_KEY, apiKey, appName); + const persistence = session.getItem(key); + + switch (persistence) { + case Persistence.NONE: + return [exp.inMemoryPersistence]; + case Persistence.LOCAL: + return [exp.indexedDBLocalPersistence, exp.browserSessionPersistence]; + case Persistence.SESSION: + return [exp.browserSessionPersistence]; + default: + return []; + } +} + +/** Returns session storage, or null if the property access errors */ +function getSessionStorageIfAvailable(): Storage | null { + try { + return _getSelfWindow()?.sessionStorage || null; + } catch (e) { + return null; + } +} diff --git a/packages/auth-compat/src/phone_auth_provider.ts b/packages/auth-compat/src/phone_auth_provider.ts new file mode 100644 index 00000000000..41e1a859ffa --- /dev/null +++ b/packages/auth-compat/src/phone_auth_provider.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as exp from '@firebase/auth/internal'; +import * as compat from '@firebase/auth-types'; +import firebase from '@firebase/app-compat'; +import { Compat } from '@firebase/util'; +import { unwrap } from './wrap'; + +export class PhoneAuthProvider + implements compat.PhoneAuthProvider, Compat +{ + providerId = 'phone'; + readonly _delegate: exp.PhoneAuthProvider; + + static PHONE_SIGN_IN_METHOD = exp.PhoneAuthProvider.PHONE_SIGN_IN_METHOD; + static PROVIDER_ID = exp.PhoneAuthProvider.PROVIDER_ID; + + static credential( + verificationId: string, + verificationCode: string + ): compat.AuthCredential { + return exp.PhoneAuthProvider.credential(verificationId, verificationCode); + } + + constructor() { + // TODO: remove ts-ignore when moving types from auth-types to auth-compat + // @ts-ignore + this._delegate = new exp.PhoneAuthProvider(unwrap(firebase.auth!())); + } + + verifyPhoneNumber( + phoneInfoOptions: + | string + | compat.PhoneSingleFactorInfoOptions + | compat.PhoneMultiFactorEnrollInfoOptions + | compat.PhoneMultiFactorSignInInfoOptions, + applicationVerifier: compat.ApplicationVerifier + ): Promise { + return this._delegate.verifyPhoneNumber( + // The implementation matches but the types are subtly incompatible + // eslint-disable-next-line @typescript-eslint/no-explicit-any + phoneInfoOptions as any, + applicationVerifier + ); + } + + unwrap(): exp.PhoneAuthProvider { + return this._delegate; + } +} diff --git a/packages-exp/auth-compat-exp/src/platform.ts b/packages/auth-compat/src/platform.ts similarity index 80% rename from packages-exp/auth-compat-exp/src/platform.ts rename to packages/auth-compat/src/platform.ts index 680997d3266..f1ac4e797b1 100644 --- a/packages-exp/auth-compat-exp/src/platform.ts +++ b/packages/auth-compat/src/platform.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import * as impl from '@firebase/auth-exp/internal'; +import * as impl from '@firebase/auth/internal'; import { getUA, isBrowserExtension, @@ -31,6 +31,8 @@ declare global { } } +const CORDOVA_ONDEVICEREADY_TIMEOUT_MS = 1000; + function _getCurrentScheme(): string | null { return self?.location?.protocol || null; } @@ -47,9 +49,11 @@ function _isHttpOrHttps(): boolean { * @return {boolean} Whether the app is rendered in a mobile iOS or Android * Cordova environment. */ -function _isAndroidOrIosCordovaScheme(ua: string = getUA()): boolean { +export function _isAndroidOrIosCordovaScheme(ua: string = getUA()): boolean { return !!( - (_getCurrentScheme() === 'file:' || _getCurrentScheme() === 'ionic:') && + (_getCurrentScheme() === 'file:' || + _getCurrentScheme() === 'ionic:' || + _getCurrentScheme() === 'capacitor:') && ua.toLowerCase().match(/iphone|ipad|ipod|android/) ); } @@ -147,15 +151,29 @@ export function _isPopupRedirectSupported(): boolean { ); } -export function _getClientPlatform(): impl.ClientPlatform { - if (isNode()) { - return impl.ClientPlatform.NODE; - } - if (isReactNative()) { - return impl.ClientPlatform.REACT_NATIVE; - } - if (_isWorker()) { - return impl.ClientPlatform.WORKER; +/** Quick check that indicates the platform *may* be Cordova */ +export function _isLikelyCordova(): boolean { + return _isAndroidOrIosCordovaScheme() && typeof document !== 'undefined'; +} + +export async function _isCordova(): Promise { + if (!_isLikelyCordova()) { + return false; } - return impl.ClientPlatform.BROWSER; + + return new Promise(resolve => { + const timeoutId = setTimeout(() => { + // We've waited long enough; the telltale Cordova event didn't happen + resolve(false); + }, CORDOVA_ONDEVICEREADY_TIMEOUT_MS); + + document.addEventListener('deviceready', () => { + clearTimeout(timeoutId); + resolve(true); + }); + }); +} + +export function _getSelfWindow(): Window | null { + return typeof window !== 'undefined' ? window : null; } diff --git a/packages/auth-compat/src/popup_redirect.test.ts b/packages/auth-compat/src/popup_redirect.test.ts new file mode 100644 index 00000000000..9426d928076 --- /dev/null +++ b/packages/auth-compat/src/popup_redirect.test.ts @@ -0,0 +1,206 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import sinonChai from 'sinon-chai'; +import * as sinon from 'sinon'; +import * as exp from '@firebase/auth/internal'; +import * as platform from './platform'; +import { CompatPopupRedirectResolver } from './popup_redirect'; +import { FirebaseApp } from '@firebase/app-compat'; +import { + FAKE_APP_CHECK_CONTROLLER_PROVIDER, + FAKE_HEARTBEAT_CONTROLLER_PROVIDER +} from '../test/helpers/helpers'; + +use(sinonChai); + +describe('popup_redirect/CompatPopupRedirectResolver', () => { + // Do not run these tests in node; in node, this resolver + // is never instantiated. + if (typeof window === 'undefined') { + console.log( + 'Skipping popup/redirect resolver tests in non-browser environment' + ); + return; + } + + let compatResolver: CompatPopupRedirectResolver; + let auth: exp.AuthImpl; + + beforeEach(() => { + compatResolver = new CompatPopupRedirectResolver(); + const app = { options: { apiKey: 'api-key' } } as FirebaseApp; + auth = new exp.AuthImpl( + app, + FAKE_HEARTBEAT_CONTROLLER_PROVIDER, + FAKE_APP_CHECK_CONTROLLER_PROVIDER, + { + apiKey: 'api-key' + } as exp.ConfigInternal + ); + }); + + afterEach(() => { + sinon.restore(); + }); + + context('initialization and resolver selection', () => { + const browserResolver = exp._getInstance( + exp.browserPopupRedirectResolver + ); + const cordovaResolver = exp._getInstance( + exp.cordovaPopupRedirectResolver + ); + + beforeEach(() => { + sinon.stub(browserResolver, '_initialize'); + sinon.stub(cordovaResolver, '_initialize'); + }); + + it('selects the Cordova resolver if in Cordova', async () => { + sinon.stub(platform, '_isCordova').returns(Promise.resolve(true)); + await compatResolver._initialize(auth); + expect(cordovaResolver._initialize).to.have.been.calledWith(auth); + expect(browserResolver._initialize).not.to.have.been.called; + }); + + it('selects the Browser resolver if in Browser', async () => { + sinon.stub(platform, '_isCordova').returns(Promise.resolve(false)); + await compatResolver._initialize(auth); + expect(cordovaResolver._initialize).not.to.have.been.called; + expect(browserResolver._initialize).to.have.been.calledWith(auth); + }); + }); + + context('callthrough methods', () => { + let underlyingResolver: sinon.SinonStubbedInstance; + let provider: exp.AuthProvider; + + beforeEach(() => { + underlyingResolver = sinon.createStubInstance(FakeResolver); + ( + compatResolver as unknown as { + underlyingResolver: exp.PopupRedirectResolverInternal; + } + ).underlyingResolver = underlyingResolver; + provider = new exp.GoogleAuthProvider(); + }); + + it('_openPopup', async () => { + await compatResolver._openPopup( + auth, + provider, + exp.AuthEventType.LINK_VIA_POPUP, + 'eventId' + ); + expect(underlyingResolver._openPopup).to.have.been.calledWith( + auth, + provider, + exp.AuthEventType.LINK_VIA_POPUP, + 'eventId' + ); + }); + + it('_openRedirect', async () => { + await compatResolver._openRedirect( + auth, + provider, + exp.AuthEventType.LINK_VIA_REDIRECT, + 'eventId' + ); + expect(underlyingResolver._openRedirect).to.have.been.calledWith( + auth, + provider, + exp.AuthEventType.LINK_VIA_REDIRECT, + 'eventId' + ); + }); + + it('_isIframeWebStorageSupported', () => { + const cb = (): void => {}; + compatResolver._isIframeWebStorageSupported(auth, cb); + expect( + underlyingResolver._isIframeWebStorageSupported + ).to.have.been.calledWith(auth, cb); + }); + + it('_originValidation', async () => { + await compatResolver._originValidation(auth); + expect(underlyingResolver._originValidation).to.have.been.calledWith( + auth + ); + }); + }); + + context('_shouldInitProactively', () => { + it('returns true if platform may be cordova', () => { + sinon.stub(platform, '_isLikelyCordova').returns(true); + expect(compatResolver._shouldInitProactively).to.be.true; + }); + + it('returns true if cordova is false but browser value is true', () => { + sinon + .stub( + exp._getInstance( + exp.browserPopupRedirectResolver + ), + '_shouldInitProactively' + ) + .value(true); + sinon.stub(platform, '_isLikelyCordova').returns(false); + expect(compatResolver._shouldInitProactively).to.be.true; + }); + + it('returns false if not cordova and not browser early init', () => { + sinon + .stub( + exp._getInstance( + exp.browserPopupRedirectResolver + ), + '_shouldInitProactively' + ) + .value(false); + sinon.stub(platform, '_isLikelyCordova').returns(false); + expect(compatResolver._shouldInitProactively).to.be.false; + }); + }); +}); + +class FakeResolver implements exp.PopupRedirectResolverInternal { + _completeRedirectFn = async (): Promise => null; + _overrideRedirectResult = (): void => {}; + _redirectPersistence = exp.inMemoryPersistence; + _shouldInitProactively = true; + + _initialize(): Promise { + throw new Error('Method not implemented.'); + } + _openPopup(): Promise { + throw new Error('Method not implemented.'); + } + _openRedirect(): Promise { + throw new Error('Method not implemented.'); + } + _isIframeWebStorageSupported(): void { + throw new Error('Method not implemented.'); + } + + _originValidation(): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/auth-compat/src/popup_redirect.ts b/packages/auth-compat/src/popup_redirect.ts new file mode 100644 index 00000000000..9fc1a92dcc9 --- /dev/null +++ b/packages/auth-compat/src/popup_redirect.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as exp from '@firebase/auth/internal'; +import { _isCordova, _isLikelyCordova } from './platform'; + +const _assert: typeof exp._assert = exp._assert; + +/** Platform-agnostic popup-redirect resolver */ +export class CompatPopupRedirectResolver + implements exp.PopupRedirectResolverInternal +{ + // Create both resolvers for dynamic resolution later + private readonly browserResolver: exp.PopupRedirectResolverInternal = + exp._getInstance(exp.browserPopupRedirectResolver); + private readonly cordovaResolver: exp.PopupRedirectResolverInternal = + exp._getInstance(exp.cordovaPopupRedirectResolver); + // The actual resolver in use: either browserResolver or cordovaResolver. + private underlyingResolver: exp.PopupRedirectResolverInternal | null = null; + _redirectPersistence = exp.browserSessionPersistence; + + _completeRedirectFn: ( + auth: exp.Auth, + resolver: exp.PopupRedirectResolver, + bypassAuthState: boolean + ) => Promise = exp._getRedirectResult; + _overrideRedirectResult = exp._overrideRedirectResult; + + async _initialize(auth: exp.AuthImpl): Promise { + await this.selectUnderlyingResolver(); + return this.assertedUnderlyingResolver._initialize(auth); + } + + async _openPopup( + auth: exp.AuthImpl, + provider: exp.AuthProvider, + authType: exp.AuthEventType, + eventId?: string + ): Promise { + await this.selectUnderlyingResolver(); + return this.assertedUnderlyingResolver._openPopup( + auth, + provider, + authType, + eventId + ); + } + + async _openRedirect( + auth: exp.AuthImpl, + provider: exp.AuthProvider, + authType: exp.AuthEventType, + eventId?: string + ): Promise { + await this.selectUnderlyingResolver(); + return this.assertedUnderlyingResolver._openRedirect( + auth, + provider, + authType, + eventId + ); + } + + _isIframeWebStorageSupported( + auth: exp.AuthImpl, + cb: (support: boolean) => unknown + ): void { + this.assertedUnderlyingResolver._isIframeWebStorageSupported(auth, cb); + } + + _originValidation(auth: exp.Auth): Promise { + return this.assertedUnderlyingResolver._originValidation(auth); + } + + get _shouldInitProactively(): boolean { + return _isLikelyCordova() || this.browserResolver._shouldInitProactively; + } + + private get assertedUnderlyingResolver(): exp.PopupRedirectResolverInternal { + _assert(this.underlyingResolver, exp.AuthErrorCode.INTERNAL_ERROR); + return this.underlyingResolver; + } + + private async selectUnderlyingResolver(): Promise { + if (this.underlyingResolver) { + return; + } + + // We haven't yet determined whether or not we're in Cordova; go ahead + // and determine that state now. + const isCordova = await _isCordova(); + this.underlyingResolver = isCordova + ? this.cordovaResolver + : this.browserResolver; + } +} diff --git a/packages/auth-compat/src/recaptcha_verifier.ts b/packages/auth-compat/src/recaptcha_verifier.ts new file mode 100644 index 00000000000..71997ade43a --- /dev/null +++ b/packages/auth-compat/src/recaptcha_verifier.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import firebase, { FirebaseApp } from '@firebase/app-compat'; +import * as exp from '@firebase/auth/internal'; +import * as compat from '@firebase/auth-types'; +import { Compat } from '@firebase/util'; + +const _assert: typeof exp._assert = exp._assert; + +export class RecaptchaVerifier + implements compat.RecaptchaVerifier, Compat +{ + readonly _delegate: exp.RecaptchaVerifier; + type: string; + constructor( + container: HTMLElement | string, + parameters?: object | null, + app: FirebaseApp = firebase.app() + ) { + // API key is required for web client RPC calls. + _assert(app.options?.apiKey, exp.AuthErrorCode.INVALID_API_KEY, { + appName: app.name + }); + this._delegate = new exp.RecaptchaVerifier( + // TODO: remove ts-ignore when moving types from auth-types to auth-compat + // @ts-ignore + app.auth!(), + container, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters as any + ); + this.type = this._delegate.type; + } + clear(): void { + this._delegate.clear(); + } + render(): Promise { + return this._delegate.render(); + } + verify(): Promise { + return this._delegate.verify(); + } +} diff --git a/packages/auth-compat/src/user.ts b/packages/auth-compat/src/user.ts new file mode 100644 index 00000000000..69ba4f003b6 --- /dev/null +++ b/packages/auth-compat/src/user.ts @@ -0,0 +1,231 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as exp from '@firebase/auth/internal'; +import * as compat from '@firebase/auth-types'; +import { Compat } from '@firebase/util'; +import { _savePersistenceForRedirect } from './persistence'; +import { CompatPopupRedirectResolver } from './popup_redirect'; +import { + convertConfirmationResult, + convertCredential +} from './user_credential'; + +export class User implements compat.User, Compat { + // Maintain a map so that there's always a 1:1 mapping between new User and + // legacy compat users + private static readonly USER_MAP = new WeakMap(); + + readonly multiFactor: compat.MultiFactorUser; + + private constructor(readonly _delegate: exp.User) { + this.multiFactor = exp.multiFactor(_delegate); + } + + static getOrCreate(user: exp.User): User { + if (!User.USER_MAP.has(user)) { + User.USER_MAP.set(user, new User(user)); + } + + return User.USER_MAP.get(user)!; + } + + delete(): Promise { + return this._delegate.delete(); + } + reload(): Promise { + return this._delegate.reload(); + } + toJSON(): object { + return this._delegate.toJSON(); + } + getIdTokenResult(forceRefresh?: boolean): Promise { + return this._delegate.getIdTokenResult(forceRefresh); + } + getIdToken(forceRefresh?: boolean): Promise { + return this._delegate.getIdToken(forceRefresh); + } + linkAndRetrieveDataWithCredential( + credential: compat.AuthCredential + ): Promise { + return this.linkWithCredential(credential); + } + async linkWithCredential( + credential: compat.AuthCredential + ): Promise { + return convertCredential( + this.auth, + exp.linkWithCredential(this._delegate, credential as exp.AuthCredential) + ); + } + async linkWithPhoneNumber( + phoneNumber: string, + applicationVerifier: compat.ApplicationVerifier + ): Promise { + return convertConfirmationResult( + this.auth, + exp.linkWithPhoneNumber(this._delegate, phoneNumber, applicationVerifier) + ); + } + async linkWithPopup( + provider: compat.AuthProvider + ): Promise { + return convertCredential( + this.auth, + exp.linkWithPopup( + this._delegate, + provider as exp.AuthProvider, + CompatPopupRedirectResolver + ) + ); + } + async linkWithRedirect(provider: compat.AuthProvider): Promise { + await _savePersistenceForRedirect(exp._castAuth(this.auth)); + return exp.linkWithRedirect( + this._delegate, + provider as exp.AuthProvider, + CompatPopupRedirectResolver + ); + } + reauthenticateAndRetrieveDataWithCredential( + credential: compat.AuthCredential + ): Promise { + return this.reauthenticateWithCredential(credential); + } + async reauthenticateWithCredential( + credential: compat.AuthCredential + ): Promise { + return convertCredential( + this.auth as unknown as exp.Auth, + exp.reauthenticateWithCredential( + this._delegate, + credential as exp.AuthCredential + ) + ); + } + reauthenticateWithPhoneNumber( + phoneNumber: string, + applicationVerifier: compat.ApplicationVerifier + ): Promise { + return convertConfirmationResult( + this.auth, + exp.reauthenticateWithPhoneNumber( + this._delegate, + phoneNumber, + applicationVerifier + ) + ); + } + reauthenticateWithPopup( + provider: compat.AuthProvider + ): Promise { + return convertCredential( + this.auth, + exp.reauthenticateWithPopup( + this._delegate, + provider as exp.AuthProvider, + CompatPopupRedirectResolver + ) + ); + } + async reauthenticateWithRedirect( + provider: compat.AuthProvider + ): Promise { + await _savePersistenceForRedirect(exp._castAuth(this.auth)); + return exp.reauthenticateWithRedirect( + this._delegate, + provider as exp.AuthProvider, + CompatPopupRedirectResolver + ); + } + sendEmailVerification( + actionCodeSettings?: compat.ActionCodeSettings | null + ): Promise { + return exp.sendEmailVerification(this._delegate, actionCodeSettings); + } + async unlink(providerId: string): Promise { + await exp.unlink(this._delegate, providerId); + return this; + } + updateEmail(newEmail: string): Promise { + return exp.updateEmail(this._delegate, newEmail); + } + updatePassword(newPassword: string): Promise { + return exp.updatePassword(this._delegate, newPassword); + } + updatePhoneNumber(phoneCredential: compat.AuthCredential): Promise { + return exp.updatePhoneNumber( + this._delegate, + phoneCredential as exp.PhoneAuthCredential + ); + } + updateProfile(profile: { + displayName?: string | null; + photoURL?: string | null; + }): Promise { + return exp.updateProfile(this._delegate, profile); + } + verifyBeforeUpdateEmail( + newEmail: string, + actionCodeSettings?: compat.ActionCodeSettings | null + ): Promise { + return exp.verifyBeforeUpdateEmail( + this._delegate, + newEmail, + actionCodeSettings + ); + } + get emailVerified(): boolean { + return this._delegate.emailVerified; + } + get isAnonymous(): boolean { + return this._delegate.isAnonymous; + } + get metadata(): compat.UserMetadata { + return this._delegate.metadata; + } + get phoneNumber(): string | null { + return this._delegate.phoneNumber; + } + get providerData(): Array { + return this._delegate.providerData; + } + get refreshToken(): string { + return this._delegate.refreshToken; + } + get tenantId(): string | null { + return this._delegate.tenantId; + } + get displayName(): string | null { + return this._delegate.displayName; + } + get email(): string | null { + return this._delegate.email; + } + get photoURL(): string | null { + return this._delegate.photoURL; + } + get providerId(): string { + return this._delegate.providerId; + } + get uid(): string { + return this._delegate.uid; + } + private get auth(): exp.Auth { + return (this._delegate as exp.UserImpl).auth as unknown as exp.Auth; + } +} diff --git a/packages/auth-compat/src/user_credential.ts b/packages/auth-compat/src/user_credential.ts new file mode 100644 index 00000000000..435fc61d08c --- /dev/null +++ b/packages/auth-compat/src/user_credential.ts @@ -0,0 +1,206 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as exp from '@firebase/auth/internal'; +import * as compat from '@firebase/auth-types'; +import { FirebaseError } from '@firebase/util'; +import { Auth } from './auth'; +import { User } from './user'; +import { unwrap, wrapped } from './wrap'; + +function credentialFromResponse( + userCredential: exp.UserCredentialInternal +): exp.AuthCredential | null { + return credentialFromObject(userCredential); +} + +function attachExtraErrorFields(auth: exp.Auth, e: FirebaseError): void { + // The response contains all fields from the server which may or may not + // actually match the underlying type + const response = (e.customData as exp.TaggedWithTokenResponse | undefined) + ?._tokenResponse as unknown as Record; + if ((e as FirebaseError)?.code === 'auth/multi-factor-auth-required') { + const mfaErr = e as compat.MultiFactorError; + mfaErr.resolver = new MultiFactorResolver( + auth, + exp.getMultiFactorResolver(auth, e as exp.MultiFactorError) + ); + } else if (response) { + const credential = credentialFromObject(e); + const credErr = e as compat.AuthError; + if (credential) { + credErr.credential = credential; + credErr.tenantId = response.tenantId || undefined; + credErr.email = response.email || undefined; + credErr.phoneNumber = response.phoneNumber || undefined; + } + } +} + +function credentialFromObject( + object: FirebaseError | exp.UserCredential +): exp.AuthCredential | null { + const { _tokenResponse } = ( + object instanceof FirebaseError ? object.customData : object + ) as exp.TaggedWithTokenResponse; + if (!_tokenResponse) { + return null; + } + + // Handle phone Auth credential responses, as they have a different format + // from other backend responses (i.e. no providerId). This is also only the + // case for user credentials (does not work for errors). + if (!(object instanceof FirebaseError)) { + if ('temporaryProof' in _tokenResponse && 'phoneNumber' in _tokenResponse) { + return exp.PhoneAuthProvider.credentialFromResult(object); + } + } + + const providerId = _tokenResponse.providerId; + + // Email and password is not supported as there is no situation where the + // server would return the password to the client. + if (!providerId || providerId === exp.ProviderId.PASSWORD) { + return null; + } + + let provider: Pick< + typeof exp.OAuthProvider, + 'credentialFromResult' | 'credentialFromError' + >; + switch (providerId) { + case exp.ProviderId.GOOGLE: + provider = exp.GoogleAuthProvider; + break; + case exp.ProviderId.FACEBOOK: + provider = exp.FacebookAuthProvider; + break; + case exp.ProviderId.GITHUB: + provider = exp.GithubAuthProvider; + break; + case exp.ProviderId.TWITTER: + provider = exp.TwitterAuthProvider; + break; + default: + const { + oauthIdToken, + oauthAccessToken, + oauthTokenSecret, + pendingToken, + nonce + } = _tokenResponse as exp.SignInWithIdpResponse; + if ( + !oauthAccessToken && + !oauthTokenSecret && + !oauthIdToken && + !pendingToken + ) { + return null; + } + // TODO(avolkovi): uncomment this and get it working with SAML & OIDC + if (pendingToken) { + if (providerId.startsWith('saml.')) { + return exp.SAMLAuthCredential._create(providerId, pendingToken); + } else { + // OIDC and non-default providers excluding Twitter. + return exp.OAuthCredential._fromParams({ + providerId, + signInMethod: providerId, + pendingToken, + idToken: oauthIdToken, + accessToken: oauthAccessToken + }); + } + } + return new exp.OAuthProvider(providerId).credential({ + idToken: oauthIdToken, + accessToken: oauthAccessToken, + rawNonce: nonce + }); + } + + return object instanceof FirebaseError + ? provider.credentialFromError(object) + : provider.credentialFromResult(object); +} + +export function convertCredential( + auth: exp.Auth, + credentialPromise: Promise +): Promise { + return credentialPromise + .catch(e => { + if (e instanceof FirebaseError) { + attachExtraErrorFields(auth, e); + } + throw e; + }) + .then(credential => { + const operationType = credential.operationType; + const user = credential.user; + + return { + operationType, + credential: credentialFromResponse( + credential as exp.UserCredentialInternal + ), + additionalUserInfo: exp.getAdditionalUserInfo( + credential as exp.UserCredential + ), + user: User.getOrCreate(user) + }; + }); +} + +export async function convertConfirmationResult( + auth: exp.Auth, + confirmationResultPromise: Promise +): Promise { + const confirmationResultExp = await confirmationResultPromise; + return { + verificationId: confirmationResultExp.verificationId, + confirm: (verificationCode: string) => + convertCredential(auth, confirmationResultExp.confirm(verificationCode)) + }; +} + +class MultiFactorResolver implements compat.MultiFactorResolver { + readonly auth: Auth; + constructor( + auth: exp.Auth, + private readonly resolver: exp.MultiFactorResolver + ) { + this.auth = wrapped(auth); + } + + get session(): compat.MultiFactorSession { + return this.resolver.session; + } + + get hints(): compat.MultiFactorInfo[] { + return this.resolver.hints; + } + + resolveSignIn( + assertion: compat.MultiFactorAssertion + ): Promise { + return convertCredential( + unwrap(this.auth), + this.resolver.resolveSignIn(assertion as exp.MultiFactorAssertion) + ); + } +} diff --git a/packages/auth-compat/src/wrap.ts b/packages/auth-compat/src/wrap.ts new file mode 100644 index 00000000000..5bc3862a0d1 --- /dev/null +++ b/packages/auth-compat/src/wrap.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Forward direction wrapper from Compat --unwrap-> Exp */ +export interface Wrapper { + unwrap(): T; +} + +/** Reverse direction wrapper from Exp --wrapped--> Compat */ +export interface ReverseWrapper { + wrapped(): T; +} + +export function unwrap(object: unknown): T { + return (object as Wrapper).unwrap(); +} + +export function wrapped(object: unknown): T { + return (object as ReverseWrapper).wrapped(); +} diff --git a/packages/auth-compat/test/helpers/helpers.ts b/packages/auth-compat/test/helpers/helpers.ts new file mode 100644 index 00000000000..3cdf16255bf --- /dev/null +++ b/packages/auth-compat/test/helpers/helpers.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as sinon from 'sinon'; +import firebase from '@firebase/app-compat'; +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import '@firebase/auth-compat'; +import { Provider } from '@firebase/component'; +import '../..'; + +import * as exp from '@firebase/auth/internal'; +import { + getAppConfig, + getEmulatorUrl +} from '../../../auth/test/helpers/integration/settings'; +import { resetEmulator } from '../../../auth/test/helpers/integration/emulator_rest_helpers'; + +// Heartbeat is fully tested in core auth impl +export const FAKE_HEARTBEAT_CONTROLLER_PROVIDER = { + getImmediate(): undefined { + return undefined; + } +} as unknown as Provider<'heartbeat'>; + +// App Check is fully tested in core auth impl +export const FAKE_APP_CHECK_CONTROLLER_PROVIDER = { + getImmediate(): undefined { + return undefined; + } +} as unknown as Provider<'app-check-internal'>; + +export function initializeTestInstance(): void { + firebase.initializeApp(getAppConfig()); + const stub = stubConsoleToSilenceEmulatorWarnings(); + firebase.auth().useEmulator(getEmulatorUrl()!); + stub.restore(); +} + +export async function cleanUpTestInstance(): Promise { + for (const app of firebase.apps) { + await app.delete(); + } + await resetEmulator(); +} + +export function randomEmail(): string { + return `${exp._generateEventId('test.email.')}@integration.test`; +} + +function stubConsoleToSilenceEmulatorWarnings(): sinon.SinonStub { + const originalConsoleInfo = console.info.bind(console); + return sinon.stub(console, 'info').callsFake((...args: unknown[]) => { + if ( + !JSON.stringify(args[0]).includes( + 'WARNING: You are using the Auth Emulator' + ) + ) { + originalConsoleInfo(...args); + } + }); +} diff --git a/packages/auth-compat/test/integration/flows/anonymous.test.ts b/packages/auth-compat/test/integration/flows/anonymous.test.ts new file mode 100644 index 00000000000..45f0fb7f733 --- /dev/null +++ b/packages/auth-compat/test/integration/flows/anonymous.test.ts @@ -0,0 +1,121 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import firebase from '@firebase/app-compat'; +import { FirebaseError } from '@firebase/util'; +import { + cleanUpTestInstance, + initializeTestInstance, + randomEmail +} from '../../helpers/helpers'; + +use(chaiAsPromised); + +describe('Integration test: anonymous auth', () => { + beforeEach(() => { + initializeTestInstance(); + }); + + afterEach(async () => { + await cleanUpTestInstance(); + }); + + it('signs in anonymously', async () => { + const userCred = await firebase.auth().signInAnonymously(); + expect(firebase.auth().currentUser).to.eq(userCred.user); + expect(userCred.operationType).to.eq('signIn'); + + const user = userCred.user!; + expect(user.isAnonymous).to.be.true; + expect(user.uid).to.be.a('string'); + }); + + it('second sign in on the same device yields same user', async () => { + const { user: userA } = await firebase.auth().signInAnonymously(); + const { user: userB } = await firebase.auth().signInAnonymously(); + + expect(userA!.uid).to.eq(userB!.uid); + }); + + context('email/password interaction', () => { + let email: string; + + beforeEach(() => { + email = randomEmail(); + }); + + it('anonymous / email-password accounts remain independent', async () => { + let anonCred = await firebase.auth().signInAnonymously(); + const emailCred = await firebase + .auth() + .createUserWithEmailAndPassword(email, 'password'); + expect(emailCred.user!.uid).not.to.eql(anonCred.user!.uid); + + await firebase.auth().signOut(); + anonCred = await firebase.auth().signInAnonymously(); + const emailSignIn = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + expect(emailCred.user!.uid).to.eql(emailSignIn.user!.uid); + expect(emailSignIn.user!.uid).not.to.eql(anonCred.user!.uid); + }); + + it('account can be upgraded by setting email and password', async () => { + const { user: anonUser } = await firebase.auth().signInAnonymously(); + await anonUser!.updateEmail(email); + await anonUser!.updatePassword('password'); + + await firebase.auth().signOut(); + + const { user: emailPassUser } = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + expect(emailPassUser!.uid).to.eq(anonUser!.uid); + }); + + it('account can be linked using email and password', async () => { + const { user: anonUser } = await firebase.auth().signInAnonymously(); + const cred = firebase.auth.EmailAuthProvider.credential( + email, + 'password' + ); + await anonUser!.linkWithCredential(cred); + await firebase.auth().signOut(); + + const { user: emailPassUser } = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + expect(emailPassUser!.uid).to.eq(anonUser!.uid); + }); + + it('account cannot be linked with existing email/password', async () => { + await firebase.auth().createUserWithEmailAndPassword(email, 'password'); + const { user: anonUser } = await firebase.auth().signInAnonymously(); + const cred = firebase.auth.EmailAuthProvider.credential( + email, + 'password' + ); + await expect(anonUser!.linkWithCredential(cred)).to.be.rejectedWith( + FirebaseError, + 'auth/email-already-in-use' + ); + }); + }); +}); diff --git a/packages/auth-compat/test/integration/flows/custom.test.ts b/packages/auth-compat/test/integration/flows/custom.test.ts new file mode 100644 index 00000000000..7fe45521c97 --- /dev/null +++ b/packages/auth-compat/test/integration/flows/custom.test.ts @@ -0,0 +1,214 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseError } from '@firebase/util'; +import { expect, use } from 'chai'; +import firebase from '@firebase/app-compat'; +// eslint-disable-next-line import/no-extraneous-dependencies +import '@firebase/auth-compat'; + +import chaiAsPromised from 'chai-as-promised'; +import { + cleanUpTestInstance, + initializeTestInstance, + randomEmail +} from '../../helpers/helpers'; + +use(chaiAsPromised); + +describe('Integration test: custom auth', () => { + let customToken: string; + let uid: string; + + beforeEach(() => { + initializeTestInstance(); + uid = randomEmail(); + customToken = JSON.stringify({ + uid, + claims: { + customClaim: 'some-claim' + } + }); + }); + + afterEach(async () => { + await cleanUpTestInstance(); + }); + + it('signs in with custom token', async () => { + const cred = await firebase.auth().signInWithCustomToken(customToken); + expect(firebase.auth().currentUser).to.eq(cred.user); + expect(cred.operationType).to.eq('signIn'); + + const { user } = cred; + expect(user!.isAnonymous).to.be.false; + expect(user!.uid).to.eq(uid); + expect((await user!.getIdTokenResult(false)).claims.customClaim).to.eq( + 'some-claim' + ); + expect(user!.providerId).to.eq('firebase'); + expect(cred.additionalUserInfo!.providerId).to.be.null; + expect(cred.additionalUserInfo!.isNewUser).to.be.true; + }); + + it('uid will overwrite existing user, joining accounts', async () => { + const { user: anonUser } = await firebase.auth().signInAnonymously(); + const customCred = await firebase.auth().signInWithCustomToken( + JSON.stringify({ + uid: anonUser!.uid + }) + ); + + expect(firebase.auth().currentUser).to.eq(customCred.user); + expect(customCred.user!.uid).to.eq(anonUser!.uid); + expect(customCred.user!.isAnonymous).to.be.false; + }); + + it('allows the user to delete the account', async () => { + let { user } = await firebase.auth().signInWithCustomToken(customToken); + await user!.updateProfile({ displayName: 'Display Name' }); + expect(user!.displayName).to.eq('Display Name'); + + await user!.delete(); + await expect(user!.reload()).to.be.rejectedWith( + FirebaseError, + 'auth/user-token-expired' + ); + expect(firebase.auth().currentUser).to.be.null; + + ({ user } = await firebase.auth().signInWithCustomToken(customToken)); + // New user in the system: the display name should be missing + expect(user!.displayName).to.be.null; + }); + + it('sign in can be called twice successively', async () => { + const { user: userA } = await firebase + .auth() + .signInWithCustomToken(customToken); + const { user: userB } = await firebase + .auth() + .signInWithCustomToken(customToken); + expect(userA!.uid).to.eq(userB!.uid); + }); + + it('allows user to update profile', async () => { + let { user } = await firebase.auth().signInWithCustomToken(customToken); + await user!.updateProfile({ + displayName: 'Display Name', + photoURL: 'photo-url' + }); + expect(user!.displayName).to.eq('Display Name'); + expect(user!.photoURL).to.eq('photo-url'); + + await firebase.auth().signOut(); + + user = (await firebase.auth().signInWithCustomToken(customToken)).user!; + expect(user.displayName).to.eq('Display Name'); + expect(user.photoURL).to.eq('photo-url'); + }); + + it('token can be refreshed', async () => { + const { user } = await firebase.auth().signInWithCustomToken(customToken); + const origToken = await user!.getIdToken(); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(await user!.getIdToken(true)).not.to.eq(origToken); + }); + + it('signing in will not override anonymous user', async () => { + const { user: anonUser } = await firebase.auth().signInAnonymously(); + const { user: customUser } = await firebase + .auth() + .signInWithCustomToken(customToken); + expect(firebase.auth().currentUser).to.eql(customUser); + expect(customUser!.uid).not.to.eql(anonUser!.uid); + }); + + context('email/password interaction', () => { + let email: string; + let customToken: string; + + beforeEach(() => { + email = randomEmail(); + customToken = JSON.stringify({ + uid: email + }); + }); + + it('custom / email-password accounts remain independent', async () => { + let customCred = await firebase.auth().signInWithCustomToken(customToken); + const emailCred = await firebase + .auth() + .createUserWithEmailAndPassword(email, 'password'); + expect(emailCred.user!.uid).not.to.eql(customCred.user!.uid); + + await firebase.auth().signOut(); + customCred = await firebase.auth().signInWithCustomToken(customToken); + const emailSignIn = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + expect(emailCred.user!.uid).to.eql(emailSignIn.user!.uid); + expect(emailSignIn.user!.uid).not.to.eql(customCred.user!.uid); + }); + + it('account can have email / password attached', async () => { + const { user: customUser } = await firebase + .auth() + .signInWithCustomToken(customToken); + await customUser!.updateEmail(email); + await customUser!.updatePassword('password'); + + await firebase.auth().signOut(); + + const { user: emailPassUser } = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + expect(emailPassUser!.uid).to.eq(customUser!.uid); + }); + + it('account can be linked using email and password', async () => { + const { user: customUser } = await firebase + .auth() + .signInWithCustomToken(customToken); + const cred = firebase.auth.EmailAuthProvider.credential( + email, + 'password' + ); + await customUser!.linkWithCredential(cred); + await firebase.auth().signOut(); + + const { user: emailPassUser } = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + expect(emailPassUser!.uid).to.eq(customUser!.uid); + }); + + it('account cannot be linked with existing email/password', async () => { + await firebase.auth().createUserWithEmailAndPassword(email, 'password'); + const { user: customUser } = await firebase + .auth() + .signInWithCustomToken(customToken); + const cred = firebase.auth.EmailAuthProvider.credential( + email, + 'password' + ); + await expect(customUser!.linkWithCredential(cred)).to.be.rejectedWith( + FirebaseError, + 'auth/email-already-in-use' + ); + }); + }); +}); diff --git a/packages/auth-compat/test/integration/flows/email.test.ts b/packages/auth-compat/test/integration/flows/email.test.ts new file mode 100644 index 00000000000..24829276c23 --- /dev/null +++ b/packages/auth-compat/test/integration/flows/email.test.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { FirebaseError } from '@firebase/util'; +import firebase from '@firebase/app-compat'; +import { + cleanUpTestInstance, + initializeTestInstance, + randomEmail +} from '../../helpers/helpers'; +import { UserCredential } from '@firebase/auth-types'; + +use(chaiAsPromised); + +describe('Integration test: email/password auth', () => { + let email: string; + beforeEach(() => { + initializeTestInstance(); + email = randomEmail(); + }); + + afterEach(() => cleanUpTestInstance()); + + it('allows user to sign up', async () => { + const userCred = await firebase + .auth() + .createUserWithEmailAndPassword(email, 'password'); + expect(firebase.auth().currentUser).to.eq(userCred.user); + expect(userCred.operationType).to.eq('signIn'); + + const user = userCred.user!; + expect(user.isAnonymous).to.be.false; + expect(user.uid).to.be.a('string'); + expect(user.email).to.eq(email); + expect(user.emailVerified).to.be.false; + expect(user.providerData.length).to.eq(1); + expect(user.providerData[0]!.providerId).to.eq('password'); + expect(user.providerData[0]!.email).to.eq(email); + + const additionalUserInfo = userCred.additionalUserInfo!; + expect(additionalUserInfo.isNewUser).to.be.true; + expect(additionalUserInfo.providerId).to.eq('password'); + }); + + it('errors when createUser called twice', async () => { + await firebase.auth().createUserWithEmailAndPassword(email, 'password'); + await expect( + firebase.auth().createUserWithEmailAndPassword(email, 'password') + ).to.be.rejectedWith(FirebaseError, 'auth/email-already-in-use'); + }); + + context('with existing user', () => { + let signUpCred: UserCredential; + + beforeEach(async () => { + signUpCred = await firebase + .auth() + .createUserWithEmailAndPassword(email, 'password'); + await firebase.auth().signOut(); + }); + + it('allows the user to sign in with signInWithEmailAndPassword', async () => { + const signInCred = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + expect(firebase.auth().currentUser).to.eq(signInCred.user); + + expect(signInCred.operationType).to.eq('signIn'); + expect(signInCred.user!.uid).to.eq(signUpCred.user!.uid); + const additionalUserInfo = signInCred.additionalUserInfo!; + expect(additionalUserInfo.isNewUser).to.be.false; + expect(additionalUserInfo.providerId).to.eq('password'); + }); + + it('allows the user to sign in with signInWithCredential', async () => { + const credential = firebase.auth.EmailAuthProvider.credential( + email, + 'password' + ); + const signInCred = await firebase.auth().signInWithCredential(credential); + expect(firebase.auth().currentUser).to.eq(signInCred.user); + + expect(signInCred.operationType).to.eq('signIn'); + expect(signInCred.user!.uid).to.eq(signUpCred.user!.uid); + const additionalUserInfo = signInCred.additionalUserInfo!; + expect(additionalUserInfo.isNewUser).to.be.false; + expect(additionalUserInfo.providerId).to.eq('password'); + }); + + it('allows the user to update profile', async () => { + let { user } = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + await user!.updateProfile({ + displayName: 'Display Name', + photoURL: 'photo-url' + }); + expect(user!.displayName).to.eq('Display Name'); + expect(user!.photoURL).to.eq('photo-url'); + + await firebase.auth().signOut(); + + user = ( + await firebase.auth().signInWithEmailAndPassword(email, 'password') + ).user; + expect(user!.displayName).to.eq('Display Name'); + expect(user!.photoURL).to.eq('photo-url'); + }); + + it('allows the user to delete the account', async () => { + const { user } = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + await user!.delete(); + + await expect(user!.reload()).to.be.rejectedWith( + FirebaseError, + 'auth/user-token-expired' + ); + + expect(firebase.auth().currentUser).to.be.null; + await expect( + firebase.auth().signInWithEmailAndPassword(email, 'password') + ).to.be.rejectedWith(FirebaseError, 'auth/user-not-found'); + }); + + it('sign in can be called twice successively', async () => { + const { user: userA } = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + const { user: userB } = await firebase + .auth() + .signInWithEmailAndPassword(email, 'password'); + expect(userA!.uid).to.eq(userB!.uid); + }); + }); +}); diff --git a/packages/auth-compat/test/integration/flows/idp.test.ts b/packages/auth-compat/test/integration/flows/idp.test.ts new file mode 100644 index 00000000000..010e7240430 --- /dev/null +++ b/packages/auth-compat/test/integration/flows/idp.test.ts @@ -0,0 +1,276 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import firebase from '@firebase/app-compat'; +import { FirebaseError } from '@firebase/util'; +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { + cleanUpTestInstance, + initializeTestInstance, + randomEmail +} from '../../helpers/helpers'; + +use(chaiAsPromised); + +// These tests handle OAuth sign in, but they're totally headless (they don't +// use the popup/redirect flows). + +describe('Integration test: headless IdP', () => { + let oauthIdToken: string; + let email: string; + + beforeEach(() => { + initializeTestInstance(); + email = randomEmail(); + oauthIdToken = JSON.stringify({ + email, + 'email_verified': true, + sub: `oauthidp--${email}--oauthidp` + }); + }); + + afterEach(async () => { + await cleanUpTestInstance(); + }); + + it('signs in with an OAuth token', async () => { + const cred = await firebase + .auth() + .signInWithCredential( + firebase.auth.GoogleAuthProvider.credential(oauthIdToken) + ); + expect(firebase.auth().currentUser).to.eq(cred.user); + expect(cred.operationType).to.eq('signIn'); + + // Make sure the user is setup correctly + const user = cred.user!; + expect(user.isAnonymous).to.be.false; + expect(user.emailVerified).to.be.true; + expect(user.providerData.length).to.eq(1); + expect(user.providerData[0]!.providerId).to.eq('google.com'); + expect(user.providerData[0]!.email).to.eq(email); + + // Make sure the additional user info is good + const additionalUserInfo = cred.additionalUserInfo!; + expect(additionalUserInfo.isNewUser).to.be.true; + expect(additionalUserInfo.providerId).to.eq('google.com'); + }); + + it('allows the user to update profile', async () => { + const credential = + firebase.auth.GithubAuthProvider.credential(oauthIdToken); + const { user } = await firebase.auth().signInWithCredential(credential); + + await user!.updateProfile({ + displayName: 'David Copperfield', + photoURL: 'http://photo.test/david.png' + }); + + // Check everything first + expect(user!.displayName).to.eq('David Copperfield'); + expect(user!.photoURL).to.eq('http://photo.test/david.png'); + + await firebase.auth().signOut(); + + // Sign in again and double check; look at current user this time + await firebase.auth().signInWithCredential(credential); + expect(firebase.auth().currentUser!.displayName).to.eq('David Copperfield'); + expect(firebase.auth().currentUser!.photoURL).to.eq( + 'http://photo.test/david.png' + ); + }); + + it('allows the user to change the email', async () => { + const credential = + firebase.auth.FacebookAuthProvider.credential(oauthIdToken); + const { user } = await firebase.auth().signInWithCredential(credential); + + expect(user!.email).to.eq(email); + expect(user!.emailVerified).to.be.true; + + const newEmail = randomEmail(); + await user!.updateEmail(newEmail); + + // Check everything first + expect(user!.email).to.eq(newEmail); + expect(user!.emailVerified).to.be.false; + + await firebase.auth().signOut(); + + // Sign in again + await firebase.auth().signInWithCredential(credential); + expect(firebase.auth().currentUser!.email).to.eq(newEmail); + }); + + it('allows the user to set a password', async () => { + const credential = + firebase.auth.GoogleAuthProvider.credential(oauthIdToken); + const { user } = await firebase.auth().signInWithCredential(credential); + + expect(user!.providerData.length).to.eq(1); + expect(user!.providerData[0]!.providerId).to.eq('google.com'); + + // Set the password and check provider data + await user!.updatePassword('password'); + expect(user!.providerData.length).to.eq(2); + expect(user!.providerData.map(p => p!.providerId)).to.contain.members([ + 'google.com', + 'password' + ]); + + // Sign out and sign in again + await firebase.auth().signOut(); + await firebase.auth().signInWithEmailAndPassword(email, 'password'); + expect(firebase.auth().currentUser!.providerData.length).to.eq(2); + expect( + firebase.auth().currentUser!.providerData.map(p => p!.providerId) + ).to.contain.members(['google.com', 'password']); + + // Update email, then sign out/sign in again + const newEmail = randomEmail(); + await firebase.auth().currentUser!.updateEmail(newEmail); + await firebase.auth().signOut(); + await firebase.auth().signInWithEmailAndPassword(newEmail, 'password'); + expect(firebase.auth().currentUser!.providerData.length).to.eq(2); + expect( + firebase.auth().currentUser!.providerData.map(p => p!.providerId) + ).to.contain.members(['google.com', 'password']); + }); + + it('can link with multiple idps', async () => { + const googleEmail = randomEmail(); + const facebookEmail = randomEmail(); + + const googleCredential = firebase.auth.GoogleAuthProvider.credential( + JSON.stringify({ + sub: googleEmail, + email: googleEmail, + 'email_verified': true + }) + ); + + const facebookCredential = firebase.auth.FacebookAuthProvider.credential( + JSON.stringify({ + sub: facebookEmail, + email: facebookEmail + }) + ); + + // Link and then test everything + const { user } = await firebase + .auth() + .signInWithCredential(facebookCredential); + await user!.linkWithCredential(googleCredential); + expect(user!.email).to.eq(facebookEmail); + expect(user!.emailVerified).to.be.false; + expect(user!.providerData.length).to.eq(2); + expect( + user!.providerData.find(p => p!.providerId === 'google.com')!.email + ).to.eq(googleEmail); + expect( + user!.providerData.find(p => p!.providerId === 'facebook.com')!.email + ).to.eq(facebookEmail); + + // Unlink Google and check everything again + await user!.unlink('google.com'); + expect(user!.email).to.eq(facebookEmail); + expect(user!.emailVerified).to.be.false; + expect(user!.providerData.length).to.eq(1); + expect(user!.providerData[0]!.email).to.eq(facebookEmail); + expect(user!.providerData[0]!.providerId).to.eq('facebook.com'); + }); + + it('IdP account takes over unverified email', async () => { + const credential = + firebase.auth.GoogleAuthProvider.credential(oauthIdToken); + const { user: emailUser } = await firebase + .auth() + .createUserWithEmailAndPassword(email, 'password'); + + // Check early state + expect(emailUser!.emailVerified).to.be.false; + + // Sign in with the credential and expect auto-linking + const { user: googleUser } = await firebase + .auth() + .signInWithCredential(credential); + expect(googleUser!.uid).to.eq(emailUser!.uid); + expect(googleUser!.emailVerified).to.be.true; + expect(firebase.auth().currentUser).to.eq(googleUser); + expect(googleUser!.providerData.length).to.eq(1); + expect(firebase.auth().currentUser!.providerData[0]!.providerId).to.eq( + 'google.com' + ); + + // Signing in with password no longer works + await expect( + firebase.auth().signInWithEmailAndPassword(email, 'password') + ).to.be.rejectedWith(FirebaseError, 'auth/wrong-password'); + }); + + it('IdP accounts automatically link with verified emails', async () => { + const googleCredential = firebase.auth.GoogleAuthProvider.credential( + JSON.stringify({ + sub: email, + email, + 'email_verified': true + }) + ); + + const githubCredential = firebase.auth.GithubAuthProvider.credential( + JSON.stringify({ + sub: email, + email, + 'email_verified': true + }) + ); + + // First sign in with Google + const { user: initialUser } = await firebase + .auth() + .signInWithCredential(googleCredential); + expect(initialUser!.providerData.length).to.eq(1); + expect(initialUser!.providerData[0]!.providerId).to.eq('google.com'); + + await firebase.auth().signOut(); + + // Now with GitHub + const { user: githubUser } = await firebase + .auth() + .signInWithCredential(githubCredential); + expect(githubUser!.uid).to.eq(initialUser!.uid); + expect(githubUser!.providerData.length).to.eq(2); + expect(githubUser!.providerData.map(p => p!.providerId)).to.have.members([ + 'google.com', + 'github.com' + ]); + + await firebase.auth().signOut(); + + // Sign in once again with the initial credential + const { user: googleUser } = await firebase + .auth() + .signInWithCredential(googleCredential); + expect(googleUser!.uid).to.eq(initialUser!.uid); + expect(googleUser!.providerData.length).to.eq(2); + expect(googleUser!.providerData.map(p => p!.providerId)).to.have.members([ + 'google.com', + 'github.com' + ]); + }); +}); diff --git a/packages/auth-compat/test/integration/flows/oob.test.ts b/packages/auth-compat/test/integration/flows/oob.test.ts new file mode 100644 index 00000000000..da7d07cea38 --- /dev/null +++ b/packages/auth-compat/test/integration/flows/oob.test.ts @@ -0,0 +1,329 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseError } from '@firebase/util'; +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import firebase from '@firebase/app-compat'; +import { + getOobCodes, + OobCodeSession +} from '../../../../auth/test/helpers/integration/emulator_rest_helpers'; +import { + cleanUpTestInstance, + initializeTestInstance, + randomEmail +} from '../../helpers/helpers'; +import { ActionCodeSettings } from '@firebase/auth-types'; + +use(chaiAsPromised); + +declare const xit: typeof it; + +const BASE_SETTINGS: ActionCodeSettings = { + url: 'http://localhost/action_code_return', + handleCodeInApp: true +}; + +describe('Integration test: oob codes', () => { + let email: string; + + beforeEach(() => { + initializeTestInstance(); + email = randomEmail(); + }); + + afterEach(async () => { + await cleanUpTestInstance(); + }); + + async function code(toEmail: string): Promise { + const codes = await getOobCodes(); + return codes.reverse().find(({ email }) => email === toEmail)!; + } + + context('flows beginning with sendSignInLinkToEmail', () => { + let oobSession: OobCodeSession; + + beforeEach(async () => { + oobSession = await sendEmailLink(); + }); + + async function sendEmailLink(toEmail = email): Promise { + await firebase.auth().sendSignInLinkToEmail(toEmail, BASE_SETTINGS); + + // An email has been sent to the user. Normally you'd detect this state + // when the app redirects back. We will ask the emulator for the results + // and force the state instead. + return code(toEmail); + } + + it('allows user to sign in', async () => { + const { user, operationType } = await firebase + .auth() + .signInWithEmailLink(email, oobSession.oobLink); + + expect(operationType).to.eq('signIn'); + expect(user).to.eq(firebase.auth().currentUser); + expect(user!.uid).to.be.a('string'); + expect(user!.email).to.eq(email); + expect(user!.emailVerified).to.be.true; + expect(user!.isAnonymous).to.be.false; + }); + + it('sign in works with an email credential', async () => { + const cred = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user, operationType } = await firebase + .auth() + .signInWithCredential(cred); + + expect(operationType).to.eq('signIn'); + expect(user).to.eq(firebase.auth().currentUser); + expect(user!.uid).to.be.a('string'); + expect(user!.email).to.eq(email); + expect(user!.emailVerified).to.be.true; + expect(user!.isAnonymous).to.be.false; + }); + + it('reauthenticate works with email credential', async () => { + let cred = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user: oldUser } = await firebase + .auth() + .signInWithCredential(cred); + + const reauthSession = await sendEmailLink(); + cred = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + reauthSession.oobLink + ); + const { user: newUser, operationType } = + await oldUser!.reauthenticateWithCredential(cred); + + expect(newUser!.uid).to.eq(oldUser!.uid); + expect(operationType).to.eq('reauthenticate'); + expect(firebase.auth().currentUser).to.eq(newUser); + }); + + it('reauthenticate throws with different email', async () => { + let cred = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user: oldUser } = await firebase + .auth() + .signInWithCredential(cred); + + const newEmail = randomEmail(); + const reauthSession = await sendEmailLink(newEmail); + cred = firebase.auth.EmailAuthProvider.credentialWithLink( + newEmail, + reauthSession.oobLink + ); + await expect( + oldUser!.reauthenticateWithCredential(cred) + ).to.be.rejectedWith(FirebaseError, 'auth/user-mismatch'); + expect(firebase.auth().currentUser).to.eq(oldUser); + }); + + it('reauthenticate throws if user is deleted', async () => { + let cred = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user: oldUser } = await firebase + .auth() + .signInWithCredential(cred); + + await oldUser!.delete(); + const reauthSession = await sendEmailLink(email); + cred = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + reauthSession.oobLink + ); + await expect( + oldUser!.reauthenticateWithCredential(cred) + ).to.be.rejectedWith(FirebaseError, 'auth/user-mismatch'); + expect(firebase.auth().currentUser).to.be.null; + }); + + it('other accounts can be linked', async () => { + const cred = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user: original } = await firebase.auth().signInAnonymously(); + + expect(original!.isAnonymous).to.be.true; + const { user: linked, operationType } = + await original!.linkWithCredential(cred); + + expect(operationType).to.eq('link'); + expect(linked!.uid).to.eq(original!.uid); + expect(linked!.isAnonymous).to.be.false; + expect(firebase.auth().currentUser).to.eq(linked); + expect(linked!.email).to.eq(email); + expect(linked!.emailVerified).to.be.true; + }); + + it('can be linked to a custom token', async () => { + const { user: original } = await firebase.auth().signInWithCustomToken( + JSON.stringify({ + uid: 'custom-uid' + }) + ); + + const cred = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user: linked } = await original!.linkWithCredential(cred); + + expect(linked!.uid).to.eq(original!.uid); + expect(firebase.auth().currentUser).to.eq(linked); + expect(linked!.email).to.eq(email); + expect(linked!.emailVerified).to.be.true; + }); + + it('cannot link if original account is deleted', async () => { + const cred = firebase.auth.EmailAuthProvider.credentialWithLink( + email, + oobSession.oobLink + ); + const { user } = await firebase.auth().signInAnonymously(); + + expect(user!.isAnonymous).to.be.true; + await user!.delete(); + await expect(user!.linkWithCredential(cred)).to.be.rejectedWith( + FirebaseError, + 'auth/user-token-expired' + ); + }); + + it('code can only be used once', async () => { + const link = oobSession.oobLink; + await firebase.auth().signInWithEmailLink(email, link); + await expect( + firebase.auth().signInWithEmailLink(email, link) + ).to.be.rejectedWith(FirebaseError, 'auth/invalid-action-code'); + }); + + it('fetchSignInMethodsForEmail returns the correct values', async () => { + const { user } = await firebase + .auth() + .signInWithEmailLink(email, oobSession.oobLink); + expect(await firebase.auth().fetchSignInMethodsForEmail(email)).to.eql([ + 'emailLink' + ]); + + await user!.updatePassword('password'); + const updatedMethods = await firebase + .auth() + .fetchSignInMethodsForEmail(email); + expect(updatedMethods).to.have.length(2); + expect(updatedMethods).to.include('emailLink'); + expect(updatedMethods).to.include('password'); + }); + + it('throws an error if the wrong code is provided', async () => { + const otherSession = await sendEmailLink(randomEmail()); + await expect( + firebase.auth().signInWithEmailLink(email, otherSession.oobLink) + ).to.be.rejectedWith(FirebaseError, 'auth/invalid-email'); + }); + }); + + it('can be used to verify email', async () => { + // Create an unverified user + const { user } = await firebase + .auth() + .createUserWithEmailAndPassword(email, 'password'); + expect(user!.emailVerified).to.be.false; + expect(await firebase.auth().fetchSignInMethodsForEmail(email)).to.eql([ + 'password' + ]); + await user!.sendEmailVerification(); + + // Apply the email verification code + await firebase.auth().applyActionCode((await code(email)).oobCode); + await user!.reload(); + expect(user!.emailVerified).to.be.true; + }); + + it('can be used to initiate password reset', async () => { + const { user: original } = await firebase + .auth() + .createUserWithEmailAndPassword(email, 'password'); + await original!.sendEmailVerification(); // Can only reset verified user emails + await firebase.auth().applyActionCode((await code(email)).oobCode); + + // Send and confirm the password reset + await firebase.auth().sendPasswordResetEmail(email); + const oobCode = (await code(email)).oobCode; + expect(await firebase.auth().verifyPasswordResetCode(oobCode)).to.eq(email); + await firebase.auth().confirmPasswordReset(oobCode, 'new-password'); + + // Make sure the new password works and the old one doesn't + const { user } = await firebase + .auth() + .signInWithEmailAndPassword(email, 'new-password'); + expect(user!.uid).to.eq(original!.uid); + expect(user!.emailVerified).to.be.true; + expect(await firebase.auth().fetchSignInMethodsForEmail(email)).to.eql([ + 'password' + ]); + + await expect( + firebase.auth().signInWithEmailAndPassword(email, 'password') + ).to.be.rejectedWith(FirebaseError, 'auth/wrong-password'); + }); + + // Test is ignored for now as the emulator does not currently support the + // verify-and-change-email operation. + xit('verifyBeforeUpdateEmail waits until flow completes', async () => { + const updatedEmail = randomEmail(); + + // Create an initial user with the basic email + await firebase.auth().sendSignInLinkToEmail(email, BASE_SETTINGS); + const { user } = await firebase + .auth() + .signInWithEmailLink(email, (await code(email)).oobLink); + await user!.verifyBeforeUpdateEmail(updatedEmail, BASE_SETTINGS); + expect(user!.email).to.eq(email); + + // Finish the update email flow + await firebase.auth().applyActionCode((await code(updatedEmail)).oobCode); + await user!.reload(); + expect(user!.emailVerified).to.be.true; + expect(user!.email).to.eq(updatedEmail); + expect(firebase.auth().currentUser).to.eq(user); + + // Old email doesn't work but new one does + await expect( + firebase.auth().signInWithEmailAndPassword(email, 'password') + ).to.be.rejectedWith(FirebaseError, 'auth/alskdjf'); + const { user: newSignIn } = await firebase + .auth() + .signInWithEmailAndPassword(updatedEmail, 'password'); + expect(newSignIn!.uid).to.eq(user!.uid); + }); +}); diff --git a/packages/auth-compat/test/integration/flows/phone.test.ts b/packages/auth-compat/test/integration/flows/phone.test.ts new file mode 100644 index 00000000000..fc623c4177d --- /dev/null +++ b/packages/auth-compat/test/integration/flows/phone.test.ts @@ -0,0 +1,252 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { FirebaseError } from '@firebase/util'; +import firebase from '@firebase/app-compat'; +import { + cleanUpTestInstance, + initializeTestInstance +} from '../../helpers/helpers'; +import { getPhoneVerificationCodes } from '../../../../auth/test/helpers/integration/emulator_rest_helpers'; +import { + ConfirmationResult, + RecaptchaVerifier, + UserCredential +} from '@firebase/auth-types'; + +use(chaiAsPromised); + +const PHONE_A = { + phoneNumber: '+15555551000', + code: '123456' +}; + +const PHONE_B = { + phoneNumber: '+15555552000', + code: '654321' +}; + +describe('Integration test: phone auth', () => { + if (typeof document === 'undefined') { + console.warn('Skipping phone auth tests in Node environment'); + return; + } + + let verifier: RecaptchaVerifier; + let fakeRecaptchaContainer: HTMLElement; + + beforeEach(() => { + initializeTestInstance(); + fakeRecaptchaContainer = document.createElement('div'); + document.body.appendChild(fakeRecaptchaContainer); + verifier = new firebase.auth.RecaptchaVerifier( + fakeRecaptchaContainer, + undefined as any + ); + }); + + afterEach(async () => { + await cleanUpTestInstance(); + document.body.removeChild(fakeRecaptchaContainer); + }); + + function resetVerifier(): void { + verifier.clear(); + verifier = new firebase.auth.RecaptchaVerifier( + fakeRecaptchaContainer, + undefined as any + ); + } + + /** If in the emulator, search for the code in the API */ + async function code(crOrId: ConfirmationResult | string): Promise { + const codes = await getPhoneVerificationCodes(); + const vid = typeof crOrId === 'string' ? crOrId : crOrId.verificationId; + return codes[vid].code; + } + + it('allows user to sign up', async () => { + const cr = await firebase + .auth() + .signInWithPhoneNumber(PHONE_A.phoneNumber, verifier); + const userCred = await cr.confirm(await code(cr)); + + expect(firebase.auth().currentUser).to.eq(userCred.user); + expect(userCred.operationType).to.eq('signIn'); + + const user = userCred.user; + expect(user!.isAnonymous).to.be.false; + expect(user!.uid).to.be.a('string'); + expect(user!.phoneNumber).to.eq(PHONE_A.phoneNumber); + }); + + it('anonymous users can link (and unlink) phone number', async () => { + const { user } = await firebase.auth().signInAnonymously(); + const { uid: anonId } = user!; + + const cr = await user!.linkWithPhoneNumber(PHONE_A.phoneNumber, verifier); + const linkResult = await cr.confirm(await code(cr)); + expect(linkResult.operationType).to.eq('link'); + expect(linkResult.user!.uid).to.eq(user!.uid); + expect(linkResult.user!.phoneNumber).to.eq(PHONE_A.phoneNumber); + + await user!.unlink('phone'); + expect(firebase.auth().currentUser!.uid).to.eq(anonId); + // Is anonymous stays false even after unlinking + expect(firebase.auth().currentUser!.isAnonymous).to.be.false; + expect(firebase.auth().currentUser!.phoneNumber).to.be.null; + }); + + it('anonymous users can upgrade using phone number', async () => { + const { user } = await firebase.auth().signInAnonymously(); + const { uid: anonId } = user!; + + const provider = new firebase.auth.PhoneAuthProvider(); + const verificationId = await provider.verifyPhoneNumber( + PHONE_B.phoneNumber, + verifier + ); + + await user!.updatePhoneNumber( + firebase.auth.PhoneAuthProvider.credential( + verificationId, + await code(verificationId) + ) + ); + expect(user!.phoneNumber).to.eq(PHONE_B.phoneNumber); + + await firebase.auth().signOut(); + resetVerifier(); + + const cr = await firebase + .auth() + .signInWithPhoneNumber(PHONE_B.phoneNumber, verifier); + const { user: secondSignIn } = await cr.confirm(await code(cr)); + expect(secondSignIn!.uid).to.eq(anonId); + expect(secondSignIn!.isAnonymous).to.be.false; + expect(secondSignIn!.providerData[0]!.phoneNumber).to.eq( + PHONE_B.phoneNumber + ); + expect(secondSignIn!.providerData[0]!.providerId).to.eq('phone'); + }); + + context('with already-created user', () => { + let signUpCred: UserCredential; + + beforeEach(async () => { + const cr = await firebase + .auth() + .signInWithPhoneNumber(PHONE_A.phoneNumber, verifier); + signUpCred = await cr.confirm(await code(cr)); + resetVerifier(); + await firebase.auth().signOut(); + }); + + it('allows the user to sign in again', async () => { + const cr = await firebase + .auth() + .signInWithPhoneNumber(PHONE_A.phoneNumber, verifier); + const signInCred = await cr.confirm(await code(cr)); + + expect(signInCred.user!.uid).to.eq(signUpCred.user!.uid); + }); + + it('allows the user to update their phone number', async () => { + let cr = await firebase + .auth() + .signInWithPhoneNumber(PHONE_A.phoneNumber, verifier); + const { user } = await cr.confirm(await code(cr)); + + resetVerifier(); + + const provider = new firebase.auth.PhoneAuthProvider(); + const verificationId = await provider.verifyPhoneNumber( + PHONE_B.phoneNumber, + verifier + ); + + await user!.updatePhoneNumber( + firebase.auth.PhoneAuthProvider.credential( + verificationId, + await code(verificationId) + ) + ); + expect(user!.phoneNumber).to.eq(PHONE_B.phoneNumber); + + await firebase.auth().signOut(); + resetVerifier(); + + cr = await firebase + .auth() + .signInWithPhoneNumber(PHONE_B.phoneNumber, verifier); + const { user: secondSignIn } = await cr.confirm(await code(cr)); + expect(secondSignIn!.uid).to.eq(user!.uid); + }); + + it('allows the user to reauthenticate with phone number', async () => { + let cr = await firebase + .auth() + .signInWithPhoneNumber(PHONE_A.phoneNumber, verifier); + const { user } = await cr.confirm(await code(cr)); + const oldToken = await user!.getIdToken(); + + resetVerifier(); + + // Wait a bit to ensure the sign in time is different in the token + await new Promise((resolve): void => { + setTimeout(resolve, 1500); + }); + + cr = await user!.reauthenticateWithPhoneNumber( + PHONE_A.phoneNumber, + verifier + ); + await cr.confirm(await code(cr)); + + expect(await user!.getIdToken()).not.to.eq(oldToken); + }); + + it('prevents reauthentication with wrong phone number', async () => { + let cr = await firebase + .auth() + .signInWithPhoneNumber(PHONE_A.phoneNumber, verifier); + const { user } = await cr.confirm(await code(cr)); + + resetVerifier(); + + cr = await user!.reauthenticateWithPhoneNumber( + PHONE_B.phoneNumber, + verifier + ); + await expect(cr.confirm(await code(cr))).to.be.rejectedWith( + FirebaseError, + 'auth/user-mismatch' + ); + + // We need to manually delete PHONE_B number since a failed + // reauthenticateWithPhoneNumber does not trigger a state change + resetVerifier(); + cr = await firebase + .auth() + .signInWithPhoneNumber(PHONE_B.phoneNumber, verifier); + const { user: otherUser } = await cr.confirm(await code(cr)); + await otherUser!.delete(); + }); + }); +}); diff --git a/packages/auth-compat/test/integration/webdriver/static/anonymous.js b/packages/auth-compat/test/integration/webdriver/static/anonymous.js new file mode 100644 index 00000000000..97ff4138130 --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/anonymous.js @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export async function anonymousSignIn() { + return compat.auth().signInAnonymously(); +} diff --git a/packages/auth-compat/test/integration/webdriver/static/core.js b/packages/auth-compat/test/integration/webdriver/static/core.js new file mode 100644 index 00000000000..7b75cbb2fbc --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/core.js @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { clearPersistence } from './persistence'; + +export function reset() { + return clearPersistence(); +} + +export function authInit() { + return new Promise(resolve => { + compat.auth().onAuthStateChanged(user => { + resolve(); + }); + }); +} + +export function legacyAuthInit() { + return new Promise(resolve => { + legacyAuth.onAuthStateChanged(() => resolve()); + }); +} + +export async function userSnap() { + return compat.auth().currentUser; +} + +export async function legacyUserSnap() { + return legacyAuth.currentUser; +} + +export async function authSnap() { + return compat.auth(); +} + +export function signOut() { + return compat.auth().signOut(); +} diff --git a/packages/auth-compat/test/integration/webdriver/static/email.js b/packages/auth-compat/test/integration/webdriver/static/email.js new file mode 100644 index 00000000000..e266e450086 --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/email.js @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const TEST_PASSWORD = 'password'; + +export function createUser(email) { + return compat.auth().createUserWithEmailAndPassword(email, TEST_PASSWORD); +} diff --git a/packages/auth-compat/test/integration/webdriver/static/index.html b/packages/auth-compat/test/integration/webdriver/static/index.html new file mode 100644 index 00000000000..5f444cb33bb --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/index.html @@ -0,0 +1,24 @@ + + + + + +

Compatibility layer tests

+
diff --git a/packages/auth-compat/test/integration/webdriver/static/index.js b/packages/auth-compat/test/integration/webdriver/static/index.js new file mode 100644 index 00000000000..51285fd2e6f --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/index.js @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as redirect from './redirect'; +import firebase from '@firebase/app-compat'; +import '@firebase/auth-compat'; +import * as anonymous from './anonymous'; +import * as core from './core'; +import * as popup from './popup'; +import * as email from './email'; +import * as persistence from './persistence'; +import * as ui from './ui'; +import { loadScript } from './lazy_load'; + +window.core = core; +window.anonymous = anonymous; +window.redirect = redirect; +window.popup = popup; +window.email = email; +window.persistence = persistence; +window.ui = ui; + +window.compat = null; +window.legacyAuth = null; + +// The config and emulator URL are injected by the test. The test framework +// calls this function after that injection. +window.startAuth = async () => { + firebase.initializeApp(firebaseConfig); + firebase.auth().useEmulator(emulatorUrl); + window.compat = firebase; +}; + +window.startLegacySDK = async persistence => { + await loadScript('https://www.gstatic.com/firebasejs/8.3.0/firebase-app.js'); + await loadScript('https://www.gstatic.com/firebasejs/8.3.0/firebase-auth.js'); + + window.firebase.initializeApp(firebaseConfig); + // Make sure the firebase variable here is the legacy SDK + if (window.firebase.SDK_VERSION !== '8.3.0') { + reject( + new Error( + 'Not using correct legacy version; using ' + window.firebase.SDK_VERSION + ) + ); + } + const legacyAuth = window.firebase.auth(); + legacyAuth.useEmulator(emulatorUrl); + legacyAuth.setPersistence(persistence.toLowerCase()); + window.legacyAuth = legacyAuth; +}; diff --git a/packages/auth-compat/test/integration/webdriver/static/lazy_load.js b/packages/auth-compat/test/integration/webdriver/static/lazy_load.js new file mode 100644 index 00000000000..d6dc71ddee3 --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/lazy_load.js @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function loadScript(url) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = url; + script.onerror = reject; + script.onload = resolve; + document.head.appendChild(script); + }); +} + +export function loadCss(url) { + return new Promise((resolve, reject) => { + const link = document.createElement('link'); + link.type = 'text/css'; + link.rel = 'stylesheet'; + link.href = url; + link.onerror = reject; + link.onload = resolve; + document.head.appendChild(link); + }); +} diff --git a/packages/auth-compat/test/integration/webdriver/static/logged_in.html b/packages/auth-compat/test/integration/webdriver/static/logged_in.html new file mode 100644 index 00000000000..70eef244c32 --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/logged_in.html @@ -0,0 +1,19 @@ + + + +User logged in + diff --git a/packages/auth-compat/test/integration/webdriver/static/persistence.js b/packages/auth-compat/test/integration/webdriver/static/persistence.js new file mode 100644 index 00000000000..8b974a4974e --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/persistence.js @@ -0,0 +1,143 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const INDEXED_DB_NAME = 'firebaseLocalStorageDb'; + +// Save these variables for test utils use below, since some tests may delete them. +const indexedDB = window.indexedDB; +const localStorage = window.localStorage; +const sessionStorage = window.sessionStorage; + +export async function clearPersistence() { + sessionStorage.clear(); + localStorage.clear(); + // HACK: Deleting databases in Firefox sometimes take a few seconds. Let's just return early. + return withTimeout( + 1000, + dbPromise(indexedDB.deleteDatabase(INDEXED_DB_NAME)) + ).catch(e => { + console.error(e); + return; + }); +} + +export async function localStorageSnap() { + return dumpStorage(localStorage); +} +export async function localStorageSet(dict) { + setInStorage(localStorage, dict); +} +export async function sessionStorageSnap() { + return dumpStorage(sessionStorage); +} +export async function sessionStorageSet(dict) { + setInStorage(sessionStorage, dict); +} + +const DB_OBJECTSTORE_NAME = 'firebaseLocalStorage'; + +export async function indexedDBSnap() { + const db = await dbPromise(indexedDB.open(INDEXED_DB_NAME)); + let entries; + try { + const store = db + .transaction([DB_OBJECTSTORE_NAME], 'readonly') + .objectStore(DB_OBJECTSTORE_NAME); + entries = await dbPromise(store.getAll()); + } catch { + // May throw if DB_OBJECTSTORE_NAME is never created -- this is normal. + return {}; + } + const result = {}; + for (const { fbase_key: key, value } of entries) { + result[key] = value; + } + return result; +} + +export async function setPersistenceMemory() { + return compat.auth().setPersistence('none'); +} + +export async function setPersistenceSession() { + return compat.auth().setPersistence('session'); +} + +export async function setPersistenceLocalStorage() { + return compat.auth().setPersistence('local'); +} + +export async function setPersistenceIndexedDB() { + return compat.auth().setPersistence('local'); +} + +// Mock functions for testing edge cases +export async function makeIndexedDBReadonly() { + IDBObjectStore.prototype.add = IDBObjectStore.prototype.put = () => { + return { + error: 'add/put is disabled for test purposes', + readyState: 'done', + addEventListener(event, listener) { + if (event === 'error') { + void Promise.resolve({}).then(listener); + } + } + }; + }; +} + +function dumpStorage(storage) { + const result = {}; + for (let i = 0; i < storage.length; i++) { + const key = storage.key(i); + result[key] = JSON.parse(storage.getItem(key)); + } + return result; +} + +function setInStorage(storage, dict) { + for (const [key, value] of Object.entries(dict)) { + if (value === undefined) { + storage.removeItem(key); + } else { + storage.setItem(key, JSON.stringify(value)); + } + } +} + +function dbPromise(dbRequest) { + return new Promise((resolve, reject) => { + dbRequest.addEventListener('success', () => { + resolve(dbRequest.result); + }); + dbRequest.addEventListener('error', () => { + reject(dbRequest.error); + }); + dbRequest.addEventListener('blocked', () => { + reject(dbRequest.error || 'blocked'); + }); + }); +} + +function withTimeout(ms, promise) { + return Promise.race([ + new Promise((_, reject) => + setTimeout(() => reject(new Error('operation timed out')), ms) + ), + promise + ]); +} diff --git a/packages/auth-compat/test/integration/webdriver/static/popup.js b/packages/auth-compat/test/integration/webdriver/static/popup.js new file mode 100644 index 00000000000..54daaaee185 --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/popup.js @@ -0,0 +1,91 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// These functions are a little funky: WebDriver relies on callbacks to +// pass data back to the main Node process. Because of that setup, we can't +// return the popup tasks as pending promises as they won't resolve until +// the WebDriver is allowed to do other stuff. Instead, we'll store the +// promises in variables and provide a way to retrieve them later, unblocking +// the WebDriver process. +let popupPromise = null; +let popupCred = null; +let errorCred = null; + +export function idpPopup(optProvider) { + const provider = optProvider + ? new compat.auth.OAuthProvider(optProvider) + : new compat.auth.GoogleAuthProvider(); + popupPromise = compat.auth().signInWithPopup(provider); +} + +export function idpReauthPopup() { + popupPromise = compat + .auth() + .currentUser.reauthenticateWithPopup(new compat.auth.GoogleAuthProvider()); +} + +export function idpLinkPopup() { + popupPromise = compat + .auth() + .currentUser.linkWithPopup(new compat.auth.GoogleAuthProvider()); +} + +export function popupResult() { + return popupPromise; +} + +export async function generateCredentialFromResult() { + const result = await popupPromise; + popupCred = result.credential; + return popupCred; +} + +export async function signInWithPopupCredential() { + return compat.auth().signInWithCredential(popupCred); +} + +export async function linkWithErrorCredential() { + await compat.auth().currentUser.linkWithCredential(errorCred); +} + +// These below are not technically popup functions but they're helpers for +// the popup tests. + +export function createFakeGoogleUser(email) { + return compat + .auth() + .signInWithCredential( + compat.auth.GoogleAuthProvider.credential( + `{"sub": "__${email}__", "email": "${email}", "email_verified": true}` + ) + ); +} + +export async function tryToSignInUnverified(email) { + try { + await compat + .auth() + .signInWithCredential( + compat.auth.FacebookAuthProvider.credential( + `{"sub": "$$${email}$$", "email": "${email}", "email_verified": false}` + ) + ); + } catch (e) { + errorCred = e.credential; + throw e; + } +} diff --git a/packages/auth-compat/test/integration/webdriver/static/redirect.js b/packages/auth-compat/test/integration/webdriver/static/redirect.js new file mode 100644 index 00000000000..a3c23921748 --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/redirect.js @@ -0,0 +1,92 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +let redirectCred = null; +let errorCred = null; + +export function idpRedirect(optProvider) { + const provider = optProvider + ? new compat.auth.OAuthProvider(optProvider) + : new compat.auth.GoogleAuthProvider(); + compat.auth().signInWithRedirect(provider); +} + +export function idpReauthRedirect() { + compat + .auth() + .currentUser.reauthenticateWithRedirect( + new compat.auth.GoogleAuthProvider() + ); +} + +export function idpLinkRedirect() { + compat + .auth() + .currentUser.linkWithRedirect(new compat.auth.GoogleAuthProvider()); +} + +export async function redirectResult() { + const result = await compat.auth().getRedirectResult(); + if (result.user === null && result.credential === null) { + // In the new SDK (and consequently the tests), null is returned instead of + // a credential with a null user/cred + return null; + } + return result; +} + +export async function generateCredentialFromRedirectResultAndStore() { + const result = await compat.auth().getRedirectResult(); + redirectCred = result.credential; + return redirectCred; +} + +export async function signInWithRedirectCredential() { + return compat.auth().signInWithCredential(redirectCred); +} + +export async function linkWithErrorCredential() { + await compat.auth().currentUser.linkWithCredential(errorCred); +} + +// These below are not technically redirect functions but they're helpers for +// the redirect tests. + +export function createFakeGoogleUser(email) { + return compat + .auth() + .signInWithCredential( + compat.auth.GoogleAuthProvider.credential( + `{"sub": "__${email}__", "email": "${email}", "email_verified": true}` + ) + ); +} + +export async function tryToSignInUnverified(email) { + try { + await compat + .auth() + .signInWithCredential( + compat.auth.FacebookAuthProvider.credential( + `{"sub": "$$${email}$$", "email": "${email}", "email_verified": false}` + ) + ); + } catch (e) { + errorCred = e.credential; + throw e; + } +} diff --git a/packages/auth-compat/test/integration/webdriver/static/rollup.config.js b/packages/auth-compat/test/integration/webdriver/static/rollup.config.js new file mode 100644 index 00000000000..90adada6615 --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/rollup.config.js @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { nodeResolve } from '@rollup/plugin-node-resolve'; + +// This is run from the auth package.json +export default { + input: ['test/integration/webdriver/static/index.js'], + output: { + file: 'test/integration/webdriver/static/dist/bundle.js', + format: 'esm' + }, + plugins: [nodeResolve()] +}; diff --git a/packages/auth-compat/test/integration/webdriver/static/ui.js b/packages/auth-compat/test/integration/webdriver/static/ui.js new file mode 100644 index 00000000000..9d2a3416f11 --- /dev/null +++ b/packages/auth-compat/test/integration/webdriver/static/ui.js @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loadCss, loadScript } from './lazy_load'; + +export async function loadUiCode() { + await loadScript( + 'https://www.gstatic.com/firebasejs/ui/4.8.0/firebase-ui-auth.js' + ); + await loadCss( + 'https://www.gstatic.com/firebasejs/ui/4.8.0/firebase-ui-auth.css' + ); +} + +export async function startUi(signInFlow = 'redirect') { + // Hacky hack hack + window.firebase = compat; + + const uiConfig = { + signInSuccessUrl: '/logged_in.html', + signInFlow, + signInOptions: [ + compat.auth.GoogleAuthProvider.PROVIDER_ID, + compat.auth.FacebookAuthProvider.PROVIDER_ID, + compat.auth.TwitterAuthProvider.PROVIDER_ID, + compat.auth.GithubAuthProvider.PROVIDER_ID, + compat.auth.EmailAuthProvider.PROVIDER_ID, + compat.auth.PhoneAuthProvider.PROVIDER_ID, + firebaseui.auth.AnonymousAuthProvider.PROVIDER_ID + ] + }; + + // Initialize the FirebaseUI Widget using Firebase. + const ui = new firebaseui.auth.AuthUI(compat.auth()); + // The start method will wait until the DOM is loaded. + ui.start('#ui-root', uiConfig); +} diff --git a/packages/auth-compat/tsconfig.json b/packages/auth-compat/tsconfig.json new file mode 100644 index 00000000000..4e0ae05eebc --- /dev/null +++ b/packages/auth-compat/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "exclude": ["dist/**/*"] +} diff --git a/packages/auth-interop-types/CHANGELOG.md b/packages/auth-interop-types/CHANGELOG.md new file mode 100644 index 00000000000..93b59e90353 --- /dev/null +++ b/packages/auth-interop-types/CHANGELOG.md @@ -0,0 +1,43 @@ +# @firebase/auth-interop-types + +## 0.2.4 + +### Patch Changes + +- [`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1) [#8604](https://github.com/firebase/firebase-js-sdk/pull/8604) - Upgrade to TypeScript 5.5.4 + +## 0.2.3 + +### Patch Changes + +- [`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7) [#8237](https://github.com/firebase/firebase-js-sdk/pull/8237) - Bump all packages so staging works. + +## 0.2.2 + +### Patch Changes + +- [`0c5150106`](https://github.com/firebase/firebase-js-sdk/commit/0c515010607bf2223b468acb94c672b1279ed1a0) [#8079](https://github.com/firebase/firebase-js-sdk/pull/8079) - Update `repository.url` field in all `package.json` files to NPM's preferred format. + +## 0.2.1 + +### Patch Changes + +- [`e9bcd4c43`](https://github.com/firebase/firebase-js-sdk/commit/e9bcd4c43a0628ebce570f03f1e91dfa93fffca2) [#6940](https://github.com/firebase/firebase-js-sdk/pull/6940) - Remove unused peerDependencies. + +## 0.2.0 + +### Minor Changes + +- [`1625f7a95`](https://github.com/firebase/firebase-js-sdk/commit/1625f7a95cc3ffb666845db0a8044329be74b5be) [#6799](https://github.com/firebase/firebase-js-sdk/pull/6799) - Update TypeScript version to 4.7.4. + +## 0.1.7 + +### Patch Changes + +- [`4af28c1a4`](https://github.com/firebase/firebase-js-sdk/commit/4af28c1a42bd25ce2353f694ca1724c6101cbce5) [#6682](https://github.com/firebase/firebase-js-sdk/pull/6682) - Upgrade TypeScript to 4.7.4. + +## 0.1.6 + +### Patch Changes + +- [`3f370215a`](https://github.com/firebase/firebase-js-sdk/commit/3f370215aa571db6b41b92a7d8a9aaad2ea0ecd0) [#4808](https://github.com/firebase/firebase-js-sdk/pull/4808) (fixes [#4789](https://github.com/firebase/firebase-js-sdk/issues/4789)) - Update peerDependencies diff --git a/packages/auth-interop-types/package.json b/packages/auth-interop-types/package.json index f4b713d7986..ecea25d0c00 100644 --- a/packages/auth-interop-types/package.json +++ b/packages/auth-interop-types/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/auth-interop-types", - "version": "0.1.5", + "version": "0.2.4", "description": "@firebase/auth interop Types", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -11,19 +11,15 @@ "files": [ "index.d.ts" ], - "peerDependencies": { - "@firebase/app-types": "0.x", - "@firebase/util": "0.x" - }, "repository": { "directory": "packages/auth-types", "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" + "url": "git+https://github.com/firebase/firebase-js-sdk.git" }, "bugs": { "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.5" + "typescript": "5.5.4" } } diff --git a/packages/auth-interop-types/tsconfig.json b/packages/auth-interop-types/tsconfig.json index 9a785433d90..ad532c5f58b 100644 --- a/packages/auth-interop-types/tsconfig.json +++ b/packages/auth-interop-types/tsconfig.json @@ -3,7 +3,5 @@ "compilerOptions": { "noEmit": true }, - "exclude": [ - "dist/**/*" - ] + "exclude": ["dist/**/*"] } diff --git a/packages/auth-types/CHANGELOG.md b/packages/auth-types/CHANGELOG.md new file mode 100644 index 00000000000..16781f8b14a --- /dev/null +++ b/packages/auth-types/CHANGELOG.md @@ -0,0 +1,55 @@ +# @firebase/auth-types + +## 0.13.0 + +### Minor Changes + +- [`9d88e3a`](https://github.com/firebase/firebase-js-sdk/commit/9d88e3a85a7253694dd7cf58d7eb834e41af2b79) [#8738](https://github.com/firebase/firebase-js-sdk/pull/8738) - Added `ActionCodeSettings.linkDomain` to customize the Firebase Hosting link domain that is used in mobile out-of-band email action flows. Also, deprecated `ActionCodeSettings.dynamicLinkDomain`. + +## 0.12.3 + +### Patch Changes + +- [`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1) [#8604](https://github.com/firebase/firebase-js-sdk/pull/8604) - Upgrade to TypeScript 5.5.4 + +## 0.12.2 + +### Patch Changes + +- [`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7) [#8237](https://github.com/firebase/firebase-js-sdk/pull/8237) - Bump all packages so staging works. + +## 0.12.1 + +### Patch Changes + +- [`0c5150106`](https://github.com/firebase/firebase-js-sdk/commit/0c515010607bf2223b468acb94c672b1279ed1a0) [#8079](https://github.com/firebase/firebase-js-sdk/pull/8079) - Update `repository.url` field in all `package.json` files to NPM's preferred format. + +## 0.12.0 + +### Minor Changes + +- [`1625f7a95`](https://github.com/firebase/firebase-js-sdk/commit/1625f7a95cc3ffb666845db0a8044329be74b5be) [#6799](https://github.com/firebase/firebase-js-sdk/pull/6799) - Update TypeScript version to 4.7.4. + +## 0.11.1 + +### Patch Changes + +- [`4af28c1a4`](https://github.com/firebase/firebase-js-sdk/commit/4af28c1a42bd25ce2353f694ca1724c6101cbce5) [#6682](https://github.com/firebase/firebase-js-sdk/pull/6682) - Upgrade TypeScript to 4.7.4. + +## 0.11.0 + +### Minor Changes + +- [`cdada6c68`](https://github.com/firebase/firebase-js-sdk/commit/cdada6c68f9740d13dd6674bcb658e28e68253b6) [#5345](https://github.com/firebase/firebase-js-sdk/pull/5345) (fixes [#5015](https://github.com/firebase/firebase-js-sdk/issues/5015)) - Release modularized SDKs + +## 0.10.3 + +### Patch Changes + +- [`3f370215a`](https://github.com/firebase/firebase-js-sdk/commit/3f370215aa571db6b41b92a7d8a9aaad2ea0ecd0) [#4808](https://github.com/firebase/firebase-js-sdk/pull/4808) (fixes [#4789](https://github.com/firebase/firebase-js-sdk/issues/4789)) - Update peerDependencies + +## 0.10.2 + +### Patch Changes + +- [`4ab5a9ce5`](https://github.com/firebase/firebase-js-sdk/commit/4ab5a9ce5b6256a95d745f6dc40a5e5ddd2301f2) [#4481](https://github.com/firebase/firebase-js-sdk/pull/4481) - Add emulator methods to auth-types. diff --git a/packages/auth-types/index.d.ts b/packages/auth-types/index.d.ts index a92d0a72ee2..4b0192df925 100644 --- a/packages/auth-types/index.d.ts +++ b/packages/auth-types/index.d.ts @@ -130,6 +130,7 @@ export type ActionCodeSettings = { iOS?: { bundleId: string }; url: string; dynamicLinkDomain?: string; + linkDomain?: string; }; export type AdditionalUserInfo = { @@ -389,6 +390,15 @@ export class PhoneMultiFactorGenerator { ): PhoneMultiFactorAssertion; } +export interface EmulatorConfig { + readonly protocol: string; + readonly host: string; + readonly port: number | null; + readonly options: { + readonly disableWarnings: boolean; + }; +} + export class FirebaseAuth { private constructor(); @@ -407,6 +417,7 @@ export class FirebaseAuth { password: string ): Promise; currentUser: User | null; + readonly emulatorConfig: EmulatorConfig | null; fetchSignInMethodsForEmail(email: string): Promise>; isSignInWithEmailLink(emailLink: string): boolean; getRedirectResult(): Promise; @@ -455,6 +466,7 @@ export class FirebaseAuth { tenantId: string | null; updateCurrentUser(user: User | null): Promise; useDeviceLanguage(): void; + useEmulator(url: string, options?: { disableWarnings?: boolean }): void; verifyPasswordResetCode(code: string): Promise; } @@ -489,6 +501,6 @@ declare module '@firebase/app-types' { declare module '@firebase/component' { interface NameServiceMapping { - 'auth': FirebaseAuth; + 'auth-compat': FirebaseAuth; } } diff --git a/packages/auth-types/package.json b/packages/auth-types/package.json index 578f4eadc34..66dced1d568 100644 --- a/packages/auth-types/package.json +++ b/packages/auth-types/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/auth-types", - "version": "0.10.1", + "version": "0.13.0", "description": "@firebase/auth Types", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -13,17 +13,17 @@ ], "peerDependencies": { "@firebase/app-types": "0.x", - "@firebase/util": "0.x" + "@firebase/util": "1.x" }, "repository": { "directory": "packages/auth-types", "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk.git" + "url": "git+https://github.com/firebase/firebase-js-sdk.git" }, "bugs": { "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "4.0.5" + "typescript": "5.5.4" } } diff --git a/packages/auth-types/tsconfig.json b/packages/auth-types/tsconfig.json index 9a785433d90..ad532c5f58b 100644 --- a/packages/auth-types/tsconfig.json +++ b/packages/auth-types/tsconfig.json @@ -3,7 +3,5 @@ "compilerOptions": { "noEmit": true }, - "exclude": [ - "dist/**/*" - ] + "exclude": ["dist/**/*"] } diff --git a/packages/auth/.eslintrc.js b/packages/auth/.eslintrc.js new file mode 100644 index 00000000000..6cfe90711fc --- /dev/null +++ b/packages/auth/.eslintrc.js @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + extends: '../../config/.eslintrc.js', + ignorePatterns: ['demo/', 'scripts/'], + parserOptions: { + project: 'tsconfig.json', + // to make vscode-eslint work with monorepo + // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 + tsconfigRootDir: __dirname + } +}; diff --git a/packages/auth/.gitignore b/packages/auth/.gitignore deleted file mode 100644 index 4d24c36eafb..00000000000 --- a/packages/auth/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -# Node modules. -node_modules/ - -# Generated directories. -generated/ -out/ -dist/ - -# Generated files. -*.log -*.pyc -.DS_Store -*~ -*.swp - -# Local config. -**/.firebaserc -demo/public/config.js -demo/functions/node_modules/ diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index d2210d5c2e8..298371600db 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -1,5 +1,804 @@ # @firebase/auth +## 1.11.1 + +### Patch Changes + +- [`91c218d`](https://github.com/firebase/firebase-js-sdk/commit/91c218db2d14cb4f1b978b9073510b8bc8f91233) [#9313](https://github.com/firebase/firebase-js-sdk/pull/9313) - Expose `browserCookiePersistence` beta feature in public typings. + +- [`2615081`](https://github.com/firebase/firebase-js-sdk/commit/261508183c249dcec737448dde3aad7399f4668c) [#9297](https://github.com/firebase/firebase-js-sdk/pull/9297) (fixes [#9270](https://github.com/firebase/firebase-js-sdk/issues/9270)) - Export MISSING_PASSWORD via AuthErrorCodes in @firebase/auth. + +## 1.11.0 + +### Minor Changes + +- [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113) [#9128](https://github.com/firebase/firebase-js-sdk/pull/9128) - Update node "engines" version to a minimum of Node 20. + +### Patch Changes + +- [`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9) [#9167](https://github.com/firebase/firebase-js-sdk/pull/9167) - Set build targets to ES2020. + +- Updated dependencies [[`f18b25f`](https://github.com/firebase/firebase-js-sdk/commit/f18b25f73a05a696b6a9ed45702a84cc9dd5c6d9), [`25b60fd`](https://github.com/firebase/firebase-js-sdk/commit/25b60fdaabe910e1538684a3c490b0900fb5f113)]: + - @firebase/component@0.7.0 + - @firebase/logger@0.5.0 + - @firebase/util@1.13.0 + +## 1.10.8 + +### Patch Changes + +- Updated dependencies [[`42ac401`](https://github.com/firebase/firebase-js-sdk/commit/42ac4011787db6bb7a08f8c84f364ea86ea51e83)]: + - @firebase/util@1.12.1 + - @firebase/component@0.6.18 + +## 1.10.7 + +### Patch Changes + +- [`c0617a3`](https://github.com/firebase/firebase-js-sdk/commit/c0617a341a693c2578a21b35a4f7b27b726defef) [#9075](https://github.com/firebase/firebase-js-sdk/pull/9075) - Fixed issue where Firebase Auth cookie refresh attempts issues in Firebase Studio resulted in CORS errors. + +## 1.10.6 + +### Patch Changes + +- [`35ad526`](https://github.com/firebase/firebase-js-sdk/commit/35ad5266304e14425988fcf5ad06d028b37588ac) [#9053](https://github.com/firebase/firebase-js-sdk/pull/9053) - Revert "Fixed scroll behavior (#9043)" + +- [`b5df4ae`](https://github.com/firebase/firebase-js-sdk/commit/b5df4ae71c1b5b54d9237e7929d0f793189b82c9) [#9055](https://github.com/firebase/firebase-js-sdk/pull/9055) - Updated to only show banner when calling connect\*Emulator + +## 1.10.5 + +### Patch Changes + +- Updated dependencies [[`8a03143`](https://github.com/firebase/firebase-js-sdk/commit/8a03143b9217effdd86d68bdf195493c0979aa27)]: + - @firebase/util@1.12.0 + - @firebase/component@0.6.17 + +## 1.10.4 + +### Patch Changes + +- Updated dependencies [[`9bcd1ea`](https://github.com/firebase/firebase-js-sdk/commit/9bcd1ea9b8cc5b55692765d40df000da8ddef02b)]: + - @firebase/util@1.11.3 + - @firebase/component@0.6.16 + +## 1.10.3 + +### Patch Changes + +- [`8593fa0`](https://github.com/firebase/firebase-js-sdk/commit/8593fa05bd884c2f1f6f3b4ae062efa48af93d24) [#9031](https://github.com/firebase/firebase-js-sdk/pull/9031) - Add Emulator Overlay + +- Updated dependencies [[`8593fa0`](https://github.com/firebase/firebase-js-sdk/commit/8593fa05bd884c2f1f6f3b4ae062efa48af93d24)]: + - @firebase/util@1.11.2 + - @firebase/component@0.6.15 + +## 1.10.2 + +### Patch Changes + +- [`6a02778`](https://github.com/firebase/firebase-js-sdk/commit/6a02778e3d12af683e710b53dc6dfb64329e8229) [#8998](https://github.com/firebase/firebase-js-sdk/pull/8998) - Fix issue where auth port wasn't properly set when setting up cookies in Firebase Studio. + +- [`0e12766`](https://github.com/firebase/firebase-js-sdk/commit/0e127664946ba324c6566a02b393dafd23fc1ddb) [#8968](https://github.com/firebase/firebase-js-sdk/pull/8968) - Fix Auth Redirects on Firebase Studio + +- Updated dependencies [[`ea1f913`](https://github.com/firebase/firebase-js-sdk/commit/ea1f9139e6baec0269fbb91233fd3f7f4b0d5875), [`0e12766`](https://github.com/firebase/firebase-js-sdk/commit/0e127664946ba324c6566a02b393dafd23fc1ddb)]: + - @firebase/util@1.11.1 + - @firebase/component@0.6.14 + +## 1.10.1 + +### Patch Changes + +- [`1363ecc`](https://github.com/firebase/firebase-js-sdk/commit/1363ecc533de0ba5bfcae206a831acc33f9020a6) [#8912](https://github.com/firebase/firebase-js-sdk/pull/8912) - Fixed: `ActionCodeURL` not populating `languageCode` from the url. + +## 1.10.0 + +### Minor Changes + +- [`fb5d422`](https://github.com/firebase/firebase-js-sdk/commit/fb5d4227571e06df128048abf87cbb1da2ace1bc) [#8839](https://github.com/firebase/firebase-js-sdk/pull/8839) - Adding `Persistence.COOKIE` a new persistence method backed by cookies. The + `browserCookiePersistence` implementation is designed to be used in conjunction with middleware that + ensures both your front and backend authentication state remains synchronized. + +## 1.9.1 + +### Patch Changes + +- [`c791ecf`](https://github.com/firebase/firebase-js-sdk/commit/c791ecf3a03a0e4f56fcdc49b703578135bf8ce6) [#8750](https://github.com/firebase/firebase-js-sdk/pull/8750) - Fixed: invoking `connectAuthEmulator` multiple times with the same parameters will no longer cause + an error. Fixes [GitHub Issue #6824](https://github.com/firebase/firebase-js-sdk/issues/6824). +- Updated dependencies [[`777f465`](https://github.com/firebase/firebase-js-sdk/commit/777f465ff37495ff933a29583769ce8a6a2b59b5)]: + - @firebase/util@1.11.0 + - @firebase/component@0.6.13 + +## 1.9.0 + +### Minor Changes + +- [`9d88e3a`](https://github.com/firebase/firebase-js-sdk/commit/9d88e3a85a7253694dd7cf58d7eb834e41af2b79) [#8738](https://github.com/firebase/firebase-js-sdk/pull/8738) - Added `ActionCodeSettings.linkDomain` to customize the Firebase Hosting link domain that is used in mobile out-of-band email action flows. Also, deprecated `ActionCodeSettings.dynamicLinkDomain`. + +### Patch Changes + +- [`97d48c7`](https://github.com/firebase/firebase-js-sdk/commit/97d48c7650e2d4273b7f94c8964dfcb44113952a) [#8651](https://github.com/firebase/firebase-js-sdk/pull/8651) - `FirebaseServerApp` can now be initalized with an App Check token instead of invoking the App Check + `getToken` method. This should unblock the use of App Check enforced products in SSR environments + where the App Check SDK cannot be initialized. + +## 1.8.2 + +### Patch Changes + +- Updated dependencies [[`25a6204c1`](https://github.com/firebase/firebase-js-sdk/commit/25a6204c1531b6c772e5368d12b2411ae1d21bbc)]: + - @firebase/util@1.10.3 + - @firebase/component@0.6.12 + +## 1.8.1 + +### Patch Changes + +- [`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1) [#8604](https://github.com/firebase/firebase-js-sdk/pull/8604) - Upgrade to TypeScript 5.5.4 + +- Updated dependencies [[`b80711925`](https://github.com/firebase/firebase-js-sdk/commit/b807119252dacf46b0122344c2b6dfc503cecde1)]: + - @firebase/component@0.6.11 + - @firebase/logger@0.4.4 + - @firebase/util@1.10.2 + +## 1.8.0 + +### Minor Changes + +- [`b942e9e6e`](https://github.com/firebase/firebase-js-sdk/commit/b942e9e6e22d184d21f3e452cd35122592a3a372) [#8568](https://github.com/firebase/firebase-js-sdk/pull/8568) - [feature] Added reCAPTCHA Enterprise support for app verification during phone authentication. + +### Patch Changes + +- [`479226bf3`](https://github.com/firebase/firebase-js-sdk/commit/479226bf3ebd99017bb12fa21440c75715658702) [#8475](https://github.com/firebase/firebase-js-sdk/pull/8475) - Remove ES5 bundles. The minimum required ES version is now ES2017. + +- [`479226bf3`](https://github.com/firebase/firebase-js-sdk/commit/479226bf3ebd99017bb12fa21440c75715658702) [#8475](https://github.com/firebase/firebase-js-sdk/pull/8475) - Removed dependency on undici and node-fetch in our node bundles, replacing them with the native fetch implementation. + +- Updated dependencies [[`479226bf3`](https://github.com/firebase/firebase-js-sdk/commit/479226bf3ebd99017bb12fa21440c75715658702)]: + - @firebase/component@0.6.10 + - @firebase/logger@0.4.3 + - @firebase/util@1.10.1 + +## 1.7.9 + +### Patch Changes + +- [`16d62d4fa`](https://github.com/firebase/firebase-js-sdk/commit/16d62d4fa16faddb8cb676c0af3f29b8a5824741) [#8393](https://github.com/firebase/firebase-js-sdk/pull/8393) - Suppress the use of the `fetch` parameter `referrerPolicy` within Auth for `fetch` requests originating from Cloudflare Workers. Clouldflare Worker environments do not support this parameter and throw when it's used. + +- Updated dependencies [[`16d62d4fa`](https://github.com/firebase/firebase-js-sdk/commit/16d62d4fa16faddb8cb676c0af3f29b8a5824741)]: + - @firebase/util@1.10.0 + - @firebase/component@0.6.9 + +## 1.7.8 + +### Patch Changes + +- [`62348e116`](https://github.com/firebase/firebase-js-sdk/commit/62348e116c795d19c5ca58729c250805240ce345) [#8432](https://github.com/firebase/firebase-js-sdk/pull/8432) (fixes [#8431](https://github.com/firebase/firebase-js-sdk/issues/8431)) - Update undici dependency to 6.19.7 due to a memory leak in older versions. + +## 1.7.7 + +### Patch Changes + +- [`2ddbd4e49`](https://github.com/firebase/firebase-js-sdk/commit/2ddbd4e4900e148648a1bc4cb82932e096a7009e) [#8408](https://github.com/firebase/firebase-js-sdk/pull/8408) - Remove localStorage synchronization on storage events in Safari iframes. See [GitHub PR #8408](https://github.com/firebase/firebase-js-sdk/pull/8408). + +## 1.7.6 + +### Patch Changes + +- [`025f2a103`](https://github.com/firebase/firebase-js-sdk/commit/025f2a1037582da7d1afeb7a4d143cb7a154ec9d) [#8280](https://github.com/firebase/firebase-js-sdk/pull/8280) (fixes [#8279](https://github.com/firebase/firebase-js-sdk/issues/8279)) - Fixed typos in documentation and some internal variables and parameters. + +## 1.7.5 + +### Patch Changes + +- Updated dependencies [[`192561b15`](https://github.com/firebase/firebase-js-sdk/commit/192561b1552a08840d8e341f30f3dbe275465558)]: + - @firebase/util@1.9.7 + - @firebase/component@0.6.8 + +## 1.7.4 + +### Patch Changes + +- [`0af23e02e`](https://github.com/firebase/firebase-js-sdk/commit/0af23e02e0c90ae550dd3edf1c9244a8eba3aee1) [#8251](https://github.com/firebase/firebase-js-sdk/pull/8251) (fixes [#8222](https://github.com/firebase/firebase-js-sdk/issues/8222)) - Generate dts rollups for auth web extension and cordova + +## 1.7.3 + +### Patch Changes + +- [`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7) [#8237](https://github.com/firebase/firebase-js-sdk/pull/8237) - Bump all packages so staging works. + +- Updated dependencies [[`ab883d016`](https://github.com/firebase/firebase-js-sdk/commit/ab883d016015de0436346f586d8442b5703771b7)]: + - @firebase/component@0.6.7 + - @firebase/logger@0.4.2 + - @firebase/util@1.9.6 + +## 1.7.2 + +### Patch Changes + +- [`36b283f3f`](https://github.com/firebase/firebase-js-sdk/commit/36b283f3fc317d0fa7dde47e1048d2ee3690a9a0) [#8191](https://github.com/firebase/firebase-js-sdk/pull/8191) (fixes [#8115](https://github.com/firebase/firebase-js-sdk/issues/8115)) - Emit a module package file with esm2017 auth browser extension builds + +- [`55fef6d62`](https://github.com/firebase/firebase-js-sdk/commit/55fef6d627791f4704194c3913ebeb339a564906) [#7001](https://github.com/firebase/firebase-js-sdk/pull/7001) - Update jszip transient dependency from 3.7.1 to 3.10.1 in auth. + +## 1.7.1 + +### Patch Changes + +- [`fe09d8338`](https://github.com/firebase/firebase-js-sdk/commit/fe09d8338d7d5f7a82d8cd73cf825adbe5551975) [#8138](https://github.com/firebase/firebase-js-sdk/pull/8138) (fixes [#8132](https://github.com/firebase/firebase-js-sdk/issues/8132)) - Update undici version to 5.28.4 due to CVE-2024-30260. + +- [`ad8d5470d`](https://github.com/firebase/firebase-js-sdk/commit/ad8d5470dad9b9ec1bcd939609da4a1c439c8414) [#8134](https://github.com/firebase/firebase-js-sdk/pull/8134) - Updated dependencies. See GitHub PR #8098. + +## 1.7.0 + +### Minor Changes + +- [`ed84efe50`](https://github.com/firebase/firebase-js-sdk/commit/ed84efe50bfc365da8ebfacdd2b17b5cc2a9e596) [#8005](https://github.com/firebase/firebase-js-sdk/pull/8005) - Added the new `FirebaseServerApp` interface to bridge state + data between client and server runtime environments. This interface extends `FirebaseApp`. + +### Patch Changes + +- [`9ca1a4e4f`](https://github.com/firebase/firebase-js-sdk/commit/9ca1a4e4f9f13d56cde93cab6d83a8bc54f83539) [#8076](https://github.com/firebase/firebase-js-sdk/pull/8076) - Additional protection against misuse of the authTokenSyncURL experiment + +- [`c8a2568dd`](https://github.com/firebase/firebase-js-sdk/commit/c8a2568ddd2acd9162a99bce9ff4203fe8d6e0da) [#8097](https://github.com/firebase/firebase-js-sdk/pull/8097) - Updated transitive dependencies based on generated dependabot security reports. For more information see [PR #8088](https://github.com/firebase/firebase-js-sdk/pull/8088/files). + +- [`0c5150106`](https://github.com/firebase/firebase-js-sdk/commit/0c515010607bf2223b468acb94c672b1279ed1a0) [#8079](https://github.com/firebase/firebase-js-sdk/pull/8079) - Update `repository.url` field in all `package.json` files to NPM's preferred format. + +- Updated dependencies [[`0c5150106`](https://github.com/firebase/firebase-js-sdk/commit/0c515010607bf2223b468acb94c672b1279ed1a0)]: + - @firebase/component@0.6.6 + - @firebase/logger@0.4.1 + - @firebase/util@1.9.5 + +## 1.6.2 + +### Patch Changes + +- [`6d487d7de`](https://github.com/firebase/firebase-js-sdk/commit/6d487d7dee631498bed1aeccbb45d8f14ae911d1) [#8060](https://github.com/firebase/firebase-js-sdk/pull/8060) - Do not allow double slash at beginning of authTokenSyncURL. (follow-up fix to https://github.com/firebase/firebase-js-sdk/pull/8056) + +- [`245dd26e1`](https://github.com/firebase/firebase-js-sdk/commit/245dd26e19b6c16aca7e1b7e597ed5784c2984ba) [#8056](https://github.com/firebase/firebase-js-sdk/pull/8056) - Fix possible XSS vulnerability through **FIREBASE_DEFAULTS** settings. + +## 1.6.1 + +### Patch Changes + +- [`f3cec28df`](https://github.com/firebase/firebase-js-sdk/commit/f3cec28dfbdfc7f19c8218cf9d26956235d03fb0) [#8044](https://github.com/firebase/firebase-js-sdk/pull/8044) (fixes [#8038](https://github.com/firebase/firebase-js-sdk/issues/8038)) - Bump undici version to 5.28.3 due to security issue. + +## 1.6.0 + +### Minor Changes + +- [`2f7ad0ac4`](https://github.com/firebase/firebase-js-sdk/commit/2f7ad0ac43f5d085604324f6dc3921d9420bfccd) [#8001](https://github.com/firebase/firebase-js-sdk/pull/8001) - Added a Web-Extension package that strips the external JS loading for developers to use when building Chrome Extension app. + +### Patch Changes + +- Updated dependencies [[`434f8418c`](https://github.com/firebase/firebase-js-sdk/commit/434f8418c3db3ae98489a8461c437c248c039070)]: + - @firebase/util@1.9.4 + - @firebase/component@0.6.5 + +## 1.5.1 + +### Patch Changes + +- [`70e4cf6a6`](https://github.com/firebase/firebase-js-sdk/commit/70e4cf6a6544c4ccfa609c3f2c320980e7122101) [#7825](https://github.com/firebase/firebase-js-sdk/pull/7825) - Protection from enumerating an empty list in Auth's reading of IndexedDB results, as this causes errors in some macOS and iOS browser runtimes. + +## 1.5.0 + +### Minor Changes + +- [`bebecdaad`](https://github.com/firebase/firebase-js-sdk/commit/bebecdaad7fa552505055ab7705da478203078b6) [#7705](https://github.com/firebase/firebase-js-sdk/pull/7705) - Replaced node-fetch v2.6.7 dependency with the latest version of undici (v5.26.5) in Node.js SDK + builds for auth, firestore, functions and storage. + +### Patch Changes + +- [`b2163b33d`](https://github.com/firebase/firebase-js-sdk/commit/b2163b33d4076ba69849c82751fe225dc989c9de) [#7772](https://github.com/firebase/firebase-js-sdk/pull/7772) - Exposed INVALID_LOGIN_CREDENTIALS as auth/invalid-credential error and updated docs for various auth SDK methods. + +## 1.4.0 + +### Minor Changes + +- [`5f496e401`](https://github.com/firebase/firebase-js-sdk/commit/5f496e401782db29afd1bd433818a3fc1ef1da3c) [#7745](https://github.com/firebase/firebase-js-sdk/pull/7745) - [feature] Add sign-in with Apple token revocation support. + +### Patch Changes + +- [`f10acb360`](https://github.com/firebase/firebase-js-sdk/commit/f10acb36009dc9d5d4f0d0880f1357330e3f1d1b) [#7692](https://github.com/firebase/firebase-js-sdk/pull/7692) - Fixes https://github.com/firebase/firebase-js-sdk/issues/7675 + +## 1.3.2 + +### Patch Changes + +- [`33a2298af`](https://github.com/firebase/firebase-js-sdk/commit/33a2298af3dc669a23548ee1703de788435aa6b5) [#7720](https://github.com/firebase/firebase-js-sdk/pull/7720) - fixes github issue https://github.com/firebase/firebase-js-sdk/issues/7701. + +## 1.3.1 + +### Patch Changes + +- [`f002ef36a`](https://github.com/firebase/firebase-js-sdk/commit/f002ef36a6b427fd526696f9cd6077a217ccc6ef) [#7634](https://github.com/firebase/firebase-js-sdk/pull/7634) (fixes [#7633](https://github.com/firebase/firebase-js-sdk/issues/7633)) - Fix FetchProvider in non-browser environments, by trying to get the `fetch` implementation from not only `self` but also standard `globalThis`. + +- [`68927ced1`](https://github.com/firebase/firebase-js-sdk/commit/68927ced1159d9b79407c7823d7f48d30ccb591e) [#7685](https://github.com/firebase/firebase-js-sdk/pull/7685) - Create getProviderEnforcementState method to get reCAPTCHA Enterprise enforcement state of a provider. + This is an internal code change preparing for future features. + +- [`3533b32b1`](https://github.com/firebase/firebase-js-sdk/commit/3533b32b1be6a9800b1b58a6a2b08f50fae18eeb) [#7666](https://github.com/firebase/firebase-js-sdk/pull/7666) - Create handleRecaptchaFlow helper method + +## 1.3.0 + +### Minor Changes + +- [`309f7a914`](https://github.com/firebase/firebase-js-sdk/commit/309f7a914a9bef1becaa354ac01786e44712e256) [#7570](https://github.com/firebase/firebase-js-sdk/pull/7570) - Remove dependency on @react-native-async-storage/async-storage and add warnings to remind React Native users to manually import it. + +## 1.2.0 + +### Minor Changes + +- [`c9e2b0b8c`](https://github.com/firebase/firebase-js-sdk/commit/c9e2b0b8cd5fd0db3cac7bc3a00629ae34302189) [#7514](https://github.com/firebase/firebase-js-sdk/pull/7514) - Add a validatePassword method for validating passwords against the password policy configured for the project or a tenant. This method returns a status object that can be used to display the requirements of the password policy and whether each one was met. + +### Patch Changes + +- [`5dac8b37a`](https://github.com/firebase/firebase-js-sdk/commit/5dac8b37a974309398317c5231ca6a41af2a48a5) [#7498](https://github.com/firebase/firebase-js-sdk/pull/7498) - Fix auth event uncancellable bug #7383 + +- [`6c7d07923`](https://github.com/firebase/firebase-js-sdk/commit/6c7d079231f393196aa68ef8d6463dc32ffce798) [#7284](https://github.com/firebase/firebase-js-sdk/pull/7284) - Raise error if calling initializeRecaptchaConfig in node env + +## 1.1.0 + +### Minor Changes + +- [`8e15973fd`](https://github.com/firebase/firebase-js-sdk/commit/8e15973fde994cbee0d5ce95af575a7565ef9d8b) [#7384](https://github.com/firebase/firebase-js-sdk/pull/7384) - Implemented `authStateReady()`, which returns a promise that resolves immediately when the initial auth state is settled and currentUser is available. When the promise is resolved, the current user might be a valid user or null if there is no user signed in currently. + +### Patch Changes + +- [`e91f82a20`](https://github.com/firebase/firebase-js-sdk/commit/e91f82a20b2c8cea75a81f55bd71d878a3d908d6) [#7467](https://github.com/firebase/firebase-js-sdk/pull/7467) (fixes [#7448](https://github.com/firebase/firebase-js-sdk/issues/7448)) - Unpin `@react-native-async-storage/async-storage` dependency to give users more control over the exact version. + +## 1.0.0 + +### Major Changes + +- [`1af178f2b`](https://github.com/firebase/firebase-js-sdk/commit/1af178f2b2207af6435db3ae6b7f3bf16b8b6183) [#7351](https://github.com/firebase/firebase-js-sdk/pull/7351) - Changed the type of ParsedToken value from any to unknown + +- [`1ff891c0d`](https://github.com/firebase/firebase-js-sdk/commit/1ff891c0da15d391b62e186c14a57c59263dde65) [#7326](https://github.com/firebase/firebase-js-sdk/pull/7326) - Reorder RecaptchaVerifier parameters so auth is the first parameter + +- [`c2686ed60`](https://github.com/firebase/firebase-js-sdk/commit/c2686ed60fcc524851f85de7d634fcf2891f0651) [#7138](https://github.com/firebase/firebase-js-sdk/pull/7138) - Remove `firebase/auth/react-native` entry point. The React Native bundle should be automatically picked up by React Native build tools which recognize the `react-native` fields in `package.json` (at the top level and in `exports`). + +- [`f1c8d3806`](https://github.com/firebase/firebase-js-sdk/commit/f1c8d3806962a760aa0a78387e6b37140163eae6) [#7128](https://github.com/firebase/firebase-js-sdk/pull/7128) (fixes [#6493](https://github.com/firebase/firebase-js-sdk/issues/6493)) - Change `getAuth()` in the React Native bundle to default to importing `AsyncStorage` from `@react-native-async-storage/async-storage` instead of from the `react-native` core package (which has recently removed it). + +## 0.23.2 + +### Patch Changes + +- [`afdccd57a`](https://github.com/firebase/firebase-js-sdk/commit/afdccd57a93cedc3cff052dfb19c2863660ba592) [#7277](https://github.com/firebase/firebase-js-sdk/pull/7277) - Allow port numbers in authDomain + +## 0.23.1 + +### Patch Changes + +- [`1d6771eb3`](https://github.com/firebase/firebase-js-sdk/commit/1d6771eb358fd5cb9a6b53b7a0141b08f83f0b47) [#7140](https://github.com/firebase/firebase-js-sdk/pull/7140) - Increase the popup poller timeout to 8s to support blocking functions + Firefox + +## 0.23.0 + +### Minor Changes + +- [`b04f04081`](https://github.com/firebase/firebase-js-sdk/commit/b04f0408139f75c69b6f6eea396f3e961f658bd1) [#7191](https://github.com/firebase/firebase-js-sdk/pull/7191) - [feature] Added Firebase App Check support to Firebase Auth. + +- [`6b8e0c13d`](https://github.com/firebase/firebase-js-sdk/commit/6b8e0c13daaf476c7e6ea034006250d1f33dd828) [#7193](https://github.com/firebase/firebase-js-sdk/pull/7193) - [feature] Add reCAPTCHA enterprise support. + +## 0.22.0 + +### Minor Changes + +- [`965396d52`](https://github.com/firebase/firebase-js-sdk/commit/965396d522243fcc17b63558823ad761c87ae1ba) [#7177](https://github.com/firebase/firebase-js-sdk/pull/7177) - Fixed error message for missing password case. + +### Patch Changes + +- [`bd51cecba`](https://github.com/firebase/firebase-js-sdk/commit/bd51cecba5cfc1b1c1ca46bf94e65320da3da609) [#7179](https://github.com/firebase/firebase-js-sdk/pull/7179) (fixes [#7174](https://github.com/firebase/firebase-js-sdk/issues/7174)) - Fix typings for `TotpMultiFactorGenerator`. This fixes a reversion in 9.19.0. + +## 0.21.6 + +### Patch Changes + +- [`58bae8757`](https://github.com/firebase/firebase-js-sdk/commit/58bae875799ed2ace8232f5d9e7aaaaa7a84d064) [#7146](https://github.com/firebase/firebase-js-sdk/pull/7146) - Support TOTP as a multi-factor option in Firebase Auth/GCIP. + +- [`00737a1ab`](https://github.com/firebase/firebase-js-sdk/commit/00737a1abd469f3deb041d8ff482165cc16bc34e) [#7125](https://github.com/firebase/firebase-js-sdk/pull/7125) (fixes [#7118](https://github.com/firebase/firebase-js-sdk/issues/7118)) - Modify \_fail to use AuthErrorCode.NETWORK_REQUEST_FAILED + +## 0.21.5 + +### Patch Changes + +- [`e0b677e70`](https://github.com/firebase/firebase-js-sdk/commit/e0b677e70ed2fd9e488737c77ebe2fc65d3a0822) [#7066](https://github.com/firebase/firebase-js-sdk/pull/7066) - Explicitly set createdAt and lastLoginAt when cloning UserImpl + +## 0.21.4 + +### Patch Changes + +- [`c8a6e08b0`](https://github.com/firebase/firebase-js-sdk/commit/c8a6e08b01a52b3eca77ca9da8989dac2e77a972) [#7038](https://github.com/firebase/firebase-js-sdk/pull/7038) - Modify \_fail to use AuthErrorCode.INTERNAL_ERROR and pass in error message. + +- Updated dependencies [[`c59f537b1`](https://github.com/firebase/firebase-js-sdk/commit/c59f537b1262b5d7997291b8c1e9324d378effb6)]: + - @firebase/util@1.9.3 + - @firebase/component@0.6.4 + +## 0.21.3 + +### Patch Changes + +- [`d071bd1ac`](https://github.com/firebase/firebase-js-sdk/commit/d071bd1acaa0583b4dd3454387fc58eafddb5c30) [#7007](https://github.com/firebase/firebase-js-sdk/pull/7007) (fixes [#7005](https://github.com/firebase/firebase-js-sdk/issues/7005)) - Move exports.default fields to always be the last field. This fixes a bug caused in 9.17.0 that prevented some bundlers and frameworks from building. + +- Updated dependencies [[`d071bd1ac`](https://github.com/firebase/firebase-js-sdk/commit/d071bd1acaa0583b4dd3454387fc58eafddb5c30)]: + - @firebase/util@1.9.2 + - @firebase/component@0.6.3 + +## 0.21.2 + +### Patch Changes + +- [`6439f1173`](https://github.com/firebase/firebase-js-sdk/commit/6439f1173353f3857ab820675d572ea676340924) [#6973](https://github.com/firebase/firebase-js-sdk/pull/6973) - Expose TOKEN_EXPIRED error when mfa unenroll logs out the user. + +- [`0bab0b7a7`](https://github.com/firebase/firebase-js-sdk/commit/0bab0b7a786d1563bf665904c7097d1fe06efce5) [#6981](https://github.com/firebase/firebase-js-sdk/pull/6981) - Added browser CJS entry points (expected by Jest when using JSDOM mode). + +- Updated dependencies [[`0bab0b7a7`](https://github.com/firebase/firebase-js-sdk/commit/0bab0b7a786d1563bf665904c7097d1fe06efce5)]: + - @firebase/util@1.9.1 + - @firebase/component@0.6.2 + +## 0.21.1 + +### Patch Changes + +- [`50b8191f6`](https://github.com/firebase/firebase-js-sdk/commit/50b8191f6c51a936bd92a1a6a68af1cf201fc127) [#6914](https://github.com/firebase/firebase-js-sdk/pull/6914) (fixes [#6827](https://github.com/firebase/firebase-js-sdk/issues/6827)) - Fix to minimize a potential race condition between auth init and signInWithRedirect + +- Updated dependencies [[`d4114a4f7`](https://github.com/firebase/firebase-js-sdk/commit/d4114a4f7da3f469c0c900416ac8beee58885ec3), [`06dc1364d`](https://github.com/firebase/firebase-js-sdk/commit/06dc1364d7560f4c563e1ccc89af9cad4cd91df8)]: + - @firebase/util@1.9.0 + - @firebase/component@0.6.1 + +## 0.21.0 + +### Minor Changes + +- [`1625f7a95`](https://github.com/firebase/firebase-js-sdk/commit/1625f7a95cc3ffb666845db0a8044329be74b5be) [#6799](https://github.com/firebase/firebase-js-sdk/pull/6799) - Update TypeScript version to 4.7.4. + +### Patch Changes + +- [`e650f6498`](https://github.com/firebase/firebase-js-sdk/commit/e650f649854f3c39737fe4bade43f9eedc3e611f) [#6762](https://github.com/firebase/firebase-js-sdk/pull/6762) (fixes [#6736](https://github.com/firebase/firebase-js-sdk/issues/6736)) - move selenium-webdriver to devDependencies + +- Updated dependencies [[`c20633ed3`](https://github.com/firebase/firebase-js-sdk/commit/c20633ed35056cbadc9d65d9ceddf4e28d1ea666), [`1625f7a95`](https://github.com/firebase/firebase-js-sdk/commit/1625f7a95cc3ffb666845db0a8044329be74b5be)]: + - @firebase/util@1.8.0 + - @firebase/component@0.6.0 + - @firebase/logger@0.4.0 + +## 0.20.11 + +### Patch Changes + +- [`4af28c1a4`](https://github.com/firebase/firebase-js-sdk/commit/4af28c1a42bd25ce2353f694ca1724c6101cbce5) [#6682](https://github.com/firebase/firebase-js-sdk/pull/6682) - Upgrade TypeScript to 4.7.4. + +- Updated dependencies [[`4af28c1a4`](https://github.com/firebase/firebase-js-sdk/commit/4af28c1a42bd25ce2353f694ca1724c6101cbce5)]: + - @firebase/component@0.5.21 + - @firebase/logger@0.3.4 + - @firebase/util@1.7.3 + +## 0.20.10 + +### Patch Changes + +- Updated dependencies [[`807f06aa2`](https://github.com/firebase/firebase-js-sdk/commit/807f06aa26438a91aaea08fd38efb6c706bb8a5d)]: + - @firebase/util@1.7.2 + - @firebase/component@0.5.20 + +## 0.20.9 + +### Patch Changes + +- Updated dependencies [[`171b78b76`](https://github.com/firebase/firebase-js-sdk/commit/171b78b762826a640d267dd4dd172ad9459c4561), [`29d034072`](https://github.com/firebase/firebase-js-sdk/commit/29d034072c20af394ce384e42aa10a37d5dfcb18)]: + - @firebase/util@1.7.1 + - @firebase/component@0.5.19 + +## 0.20.8 + +### Patch Changes + +- [`fdd4ab464`](https://github.com/firebase/firebase-js-sdk/commit/fdd4ab464b59a107bdcc195df3f01e32efd89ed4) [#6526](https://github.com/firebase/firebase-js-sdk/pull/6526) - Add functionality to auto-initialize project config and emulator settings from global defaults provided by framework tooling. + +- Updated dependencies [[`fdd4ab464`](https://github.com/firebase/firebase-js-sdk/commit/fdd4ab464b59a107bdcc195df3f01e32efd89ed4)]: + - @firebase/util@1.7.0 + - @firebase/component@0.5.18 + +## 0.20.7 + +### Patch Changes + +- [`e06d9069c`](https://github.com/firebase/firebase-js-sdk/commit/e06d9069ca07429df248d9134aebdea1118e9427) [#6594](https://github.com/firebase/firebase-js-sdk/pull/6594) - Included a reference to AuthInternal in MultiFactorSessionImpl. + +* [`666c8ec1f`](https://github.com/firebase/firebase-js-sdk/commit/666c8ec1ff5cb5b8947a142e26c0a2ecb18f8bb4) [#6569](https://github.com/firebase/firebase-js-sdk/pull/6569) (fixes [#6553](https://github.com/firebase/firebase-js-sdk/issues/6553)) - Update custom claim type of `ParsedToken` to be `any` + +## 0.20.6 + +### Patch Changes + +- [`bea604ea3`](https://github.com/firebase/firebase-js-sdk/commit/bea604ea33c529e755cc3fcdc0a2ea75d04b9f19) [#6544](https://github.com/firebase/firebase-js-sdk/pull/6544) - Fix proactive refresh logic in Auth when RTDB/Firestore/Storage are in use + +## 0.20.5 + +### Patch Changes + +- [`1261d8323`](https://github.com/firebase/firebase-js-sdk/commit/1261d832345ff4505391a150cb9c32719da37eb0) [#6421](https://github.com/firebase/firebase-js-sdk/pull/6421) (fixes [#6133](https://github.com/firebase/firebase-js-sdk/issues/6133)) - Fix a bug causing ReCAPTCHA conflicts between Auth and App Check. + +* [`8c52a96ed`](https://github.com/firebase/firebase-js-sdk/commit/8c52a96edac5b65501ee4eeb234c4bb8e70a5dd5) [#6379](https://github.com/firebase/firebase-js-sdk/pull/6379) (fixes [#6331](https://github.com/firebase/firebase-js-sdk/issues/6331)) - Update user agent detection to better detect iPad; fixes bug for some iPad devices running Cordova apps + +* Updated dependencies [[`b12af44a5`](https://github.com/firebase/firebase-js-sdk/commit/b12af44a5c7500e1192d6cc1a4afc4d77efadbaf)]: + - @firebase/util@1.6.3 + - @firebase/component@0.5.17 + +## 0.20.4 + +### Patch Changes + +- Updated dependencies [[`efe2000fc`](https://github.com/firebase/firebase-js-sdk/commit/efe2000fc499e2c85c4e5e0fef6741ff3bad2eb0)]: + - @firebase/util@1.6.2 + - @firebase/component@0.5.16 + +## 0.20.3 + +### Patch Changes + +- [`2cd1cc76f`](https://github.com/firebase/firebase-js-sdk/commit/2cd1cc76f2a308135cd60f424fe09084a34b5cb5) [#6307](https://github.com/firebase/firebase-js-sdk/pull/6307) (fixes [#6300](https://github.com/firebase/firebase-js-sdk/issues/6300)) - fix: add type declarations to exports field + +* [`d4b52b612`](https://github.com/firebase/firebase-js-sdk/commit/d4b52b612cf73610c57a3c08a0415ab7b622a70a) [#6321](https://github.com/firebase/firebase-js-sdk/pull/6321) - Fix incorrect paths in package.json + +* Updated dependencies [[`2cd1cc76f`](https://github.com/firebase/firebase-js-sdk/commit/2cd1cc76f2a308135cd60f424fe09084a34b5cb5)]: + - @firebase/component@0.5.15 + - @firebase/logger@0.3.3 + - @firebase/util@1.6.1 + +## 0.20.2 + +### Patch Changes + +- [`63ac2ed28`](https://github.com/firebase/firebase-js-sdk/commit/63ac2ed28f237950290a7af2dcdcf1518ddaee4b) - Add missing field to `firebase` claim in token result typing + +* [`88517b591`](https://github.com/firebase/firebase-js-sdk/commit/88517b59179410e43d5d5129a1fefc355cd1d4eb) [#6289](https://github.com/firebase/firebase-js-sdk/pull/6289) - Propagate customData in FirebaseError when the user is disabled. + +## 0.20.1 + +### Patch Changes + +- [`07cf0f1c9`](https://github.com/firebase/firebase-js-sdk/commit/07cf0f1c9033373bf1d3a8a1958385f177506c6c) [#6248](https://github.com/firebase/firebase-js-sdk/pull/6248) (fixes [#6246](https://github.com/firebase/firebase-js-sdk/issues/6246)) - Add `@internal` tags to fix public typings file. + +## 0.20.0 + +### Minor Changes + +- [`1ac3c9d41`](https://github.com/firebase/firebase-js-sdk/commit/1ac3c9d41e8f69a94c64c6e0caf5f1a159b7dc3c) [#6151](https://github.com/firebase/firebase-js-sdk/pull/6151) - Add `beforeAuthStateChanged()` middleware function which allows the user to provide callbacks that are run before an auth state change + sets a new user. + +### Patch Changes + +- Updated dependencies [[`9c5c9c36d`](https://github.com/firebase/firebase-js-sdk/commit/9c5c9c36da80b98b73cfd60ef2e2965087e9f801)]: + - @firebase/util@1.6.0 + - @firebase/component@0.5.14 + +## 0.19.12 + +### Patch Changes + +- Updated dependencies [[`e9e5f6b3c`](https://github.com/firebase/firebase-js-sdk/commit/e9e5f6b3ca9d61323b22f87986d9959f5297ec59)]: + - @firebase/util@1.5.2 + - @firebase/component@0.5.13 + +## 0.19.11 + +### Patch Changes + +- Updated dependencies [[`3198d58dc`](https://github.com/firebase/firebase-js-sdk/commit/3198d58dcedbf7583914dbcc76984f6f7df8d2ef)]: + - @firebase/util@1.5.1 + - @firebase/component@0.5.12 + +## 0.19.10 + +### Patch Changes + +- [`7405e7d59`](https://github.com/firebase/firebase-js-sdk/commit/7405e7d593b40c9945c32ffbe66ac6fb11e2991e) [#6033](https://github.com/firebase/firebase-js-sdk/pull/6033) - Heartbeat + +- Updated dependencies [[`2d672cead`](https://github.com/firebase/firebase-js-sdk/commit/2d672cead167187cb714cd89b638c0884ba58f03)]: + - @firebase/util@1.5.0 + - @firebase/component@0.5.11 + +## 0.19.9 + +### Patch Changes + +- [`3a8d4c1d1`](https://github.com/firebase/firebase-js-sdk/commit/3a8d4c1d1a5e5fda5906b1feb96324efb68739cd) [#6007](https://github.com/firebase/firebase-js-sdk/pull/6007) - Update chromedriver version number (dev dependency) + +## 0.19.8 + +### Patch Changes + +- [`af9234866`](https://github.com/firebase/firebase-js-sdk/commit/af923486662bc9449cca122b55840b045c9b4a5a) [#5938](https://github.com/firebase/firebase-js-sdk/pull/5938) (fixes [#917](https://github.com/firebase/firebase-js-sdk/issues/917)) - Fix bug where `user.providerData` field was being improperly initialized + +## 0.19.7 + +### Patch Changes + +- [`4983f4d5a`](https://github.com/firebase/firebase-js-sdk/commit/4983f4d5a0dc385c5b3e042ace44c8204d3cce81) [#5923](https://github.com/firebase/firebase-js-sdk/pull/5923) - Fix errors in compatibility layer when cookies are fully disabled in Chrome + +* [`d612d6f6e`](https://github.com/firebase/firebase-js-sdk/commit/d612d6f6e4d3113d45427b7df68459c0a3e31a1f) [#5928](https://github.com/firebase/firebase-js-sdk/pull/5928) - Upgrade `node-fetch` dependency due to a security issue. + +- [`e04b7452b`](https://github.com/firebase/firebase-js-sdk/commit/e04b7452bae10e6525cfb9c551f76a1aa98f9078) [#5924](https://github.com/firebase/firebase-js-sdk/pull/5924) (fixes [#5922](https://github.com/firebase/firebase-js-sdk/issues/5922)) - Add missing PhoneMultiFactorInfo public interface + +* [`2820674b8`](https://github.com/firebase/firebase-js-sdk/commit/2820674b848e918ab164e7d0ec9d5b838bbfa6e0) [#5927](https://github.com/firebase/firebase-js-sdk/pull/5927) - Prevent React Native from logging a warning about deprecation of `AsyncStorage` if the developer has provided the non-deprecated version. + +## 0.19.6 + +### Patch Changes + +- [`67b6decbb`](https://github.com/firebase/firebase-js-sdk/commit/67b6decbb9b5ee806d4109b9b6c188c4933e1270) [#5908](https://github.com/firebase/firebase-js-sdk/pull/5908) - Add cordova and react-native paths to auth package.json exports field. + +* [`922e9ed9a`](https://github.com/firebase/firebase-js-sdk/commit/922e9ed9a68c130aefa0cdb9b27720b73011c397) [#5892](https://github.com/firebase/firebase-js-sdk/pull/5892) (fixes [#5874](https://github.com/firebase/firebase-js-sdk/issues/5874)) - Fix error code thrown when the network times out + +## 0.19.5 + +### Patch Changes + +- [`e3a5248fc`](https://github.com/firebase/firebase-js-sdk/commit/e3a5248fc8536fe2ca6d97483aa7e1b3f737dd17) [#5811](https://github.com/firebase/firebase-js-sdk/pull/5811) (fixes [#5791](https://github.com/firebase/firebase-js-sdk/issues/5791)) - Fix persistence selection in compatibility layer in worker scripts + +- Updated dependencies [[`3b481f572`](https://github.com/firebase/firebase-js-sdk/commit/3b481f572456e1eab3435bfc25717770d95a8c49)]: + - @firebase/util@1.4.3 + - @firebase/component@0.5.10 + +## 0.19.4 + +### Patch Changes + +- [`a777385d6`](https://github.com/firebase/firebase-js-sdk/commit/a777385d67653cdcc3b839149dde867f32b48369) [#5799](https://github.com/firebase/firebase-js-sdk/pull/5799) - Add X-Firebase-gmpid header to requests + +* [`dc6b447ba`](https://github.com/firebase/firebase-js-sdk/commit/dc6b447bac4e899a0c4741ec18bf19e2ae66731a) [#5777](https://github.com/firebase/firebase-js-sdk/pull/5777) (fixes [#5720](https://github.com/firebase/firebase-js-sdk/issues/5720)) - Fix errors during Auth initialization when the network is unavailable + +## 0.19.3 + +### Patch Changes + +- [`1583a8202`](https://github.com/firebase/firebase-js-sdk/commit/1583a82022bfd404e94f28d1786e596d6b5a9f43) [#5715](https://github.com/firebase/firebase-js-sdk/pull/5715) - Fix Provider.credentialFromResult documentation snippets + +## 0.19.2 + +### Patch Changes + +- [`3281315fa`](https://github.com/firebase/firebase-js-sdk/commit/3281315fae9c6f535f9d5052ee17d60861ea569a) [#5708](https://github.com/firebase/firebase-js-sdk/pull/5708) (fixes [#1487](https://github.com/firebase/firebase-js-sdk/issues/1487)) - Update build scripts to work with the exports field + +* [`dbd54f7c9`](https://github.com/firebase/firebase-js-sdk/commit/dbd54f7c9ef0b5d78d491e26d816084a478bdf04) [#5700](https://github.com/firebase/firebase-js-sdk/pull/5700) (fixes [#5631](https://github.com/firebase/firebase-js-sdk/issues/5631)) - Fix lighthouse issues related to the embedded iframe used to perform OAuth sign in. + +* Updated dependencies [[`3281315fa`](https://github.com/firebase/firebase-js-sdk/commit/3281315fae9c6f535f9d5052ee17d60861ea569a)]: + - @firebase/component@0.5.9 + - @firebase/logger@0.3.2 + - @firebase/util@1.4.2 + +## 0.19.1 + +### Patch Changes + +- [`31bd6f27f`](https://github.com/firebase/firebase-js-sdk/commit/31bd6f27f965a561f814bad1110a43849a6a9cbf) [#5689](https://github.com/firebase/firebase-js-sdk/pull/5689) - Add SAMLAuthProvider to the compatability layer (it was missing before) + +* [`2322b6023`](https://github.com/firebase/firebase-js-sdk/commit/2322b6023c628cd9f4f4172767c17d215dd91684) [#5693](https://github.com/firebase/firebase-js-sdk/pull/5693) - Add exports field to all packages + +- [`0765b5e19`](https://github.com/firebase/firebase-js-sdk/commit/0765b5e19c3e949bb33233ee52c8e33f01418e54) [#5686](https://github.com/firebase/firebase-js-sdk/pull/5686) (fixes [#5685](https://github.com/firebase/firebase-js-sdk/issues/5685)) - Fix bug that caused onAuthStateChanged to be fired twice + +- Updated dependencies [[`2322b6023`](https://github.com/firebase/firebase-js-sdk/commit/2322b6023c628cd9f4f4172767c17d215dd91684)]: + - @firebase/component@0.5.8 + - @firebase/logger@0.3.1 + - @firebase/util@1.4.1 + +## 0.19.0 + +### Minor Changes + +- [`b6f30c24f`](https://github.com/firebase/firebase-js-sdk/commit/b6f30c24fdf096ac4e8bdba32b9c1380903a7507) [#5617](https://github.com/firebase/firebase-js-sdk/pull/5617) (fixes [#5610](https://github.com/firebase/firebase-js-sdk/issues/5610)) - Fix behavior on subsequent calls to `getRedirectResult()` + +### Patch Changes + +- [`69ff8eb54`](https://github.com/firebase/firebase-js-sdk/commit/69ff8eb549e49de51cae11a04bce023bb6e1fc02) [#5616](https://github.com/firebase/firebase-js-sdk/pull/5616) - Fix the public `AuthError` typing, and update the `MultiFactorError` implementation to follow the new standard (all fields listed under `customData`) + +* [`2429ac105`](https://github.com/firebase/firebase-js-sdk/commit/2429ac105b0aeb15eb8c362665448c209887bada) [#5633](https://github.com/firebase/firebase-js-sdk/pull/5633) (fixes [#5631](https://github.com/firebase/firebase-js-sdk/issues/5631)) - Add the attribute `aria-hidden="true"` to the embedded iframe + +- [`4594d3fd6`](https://github.com/firebase/firebase-js-sdk/commit/4594d3fd6c7f7680b877aa2017ba35084ef6af96) [#5673](https://github.com/firebase/firebase-js-sdk/pull/5673) - Export Phone sign in functionality in React Native entrypoint (except for RecaptchaVerifier) + +* [`6dacc2400`](https://github.com/firebase/firebase-js-sdk/commit/6dacc2400fdcf4432ed1977ca1eb148da6db3fc5) [#5635](https://github.com/firebase/firebase-js-sdk/pull/5635) (fixes [#5618](https://github.com/firebase/firebase-js-sdk/issues/5618)) - Make the library resilient against localStorage and sessionStorage permissions errors + +## 0.18.3 + +### Patch Changes + +- [`93795c780`](https://github.com/firebase/firebase-js-sdk/commit/93795c7801d6b28ccbbe5855fd2f3fc377b1db5f) [#5596](https://github.com/firebase/firebase-js-sdk/pull/5596) - report build variants for packages + +## 0.18.2 + +### Patch Changes + +- [`1b0e7af13`](https://github.com/firebase/firebase-js-sdk/commit/1b0e7af130c59b867e84b3f2615248fedad5b83d) [#5564](https://github.com/firebase/firebase-js-sdk/pull/5564) - Calls to `connectAuthEmulator` with the `disableWarnings` flag set to true will no longer cause a `console.info` warning to be printed + +* [`e1d551ddb`](https://github.com/firebase/firebase-js-sdk/commit/e1d551ddb29db0f1fdf25c986cfcae6804bc8e79) [#5574](https://github.com/firebase/firebase-js-sdk/pull/5574) (fixes [#5553](https://github.com/firebase/firebase-js-sdk/issues/5553)) - Fix bug in the `OAuthProvider.prototype.credential` method that was preventing the `rawNonce` field from being populated in the returned `OAuthCredential`. + +- [`f7d8324a1`](https://github.com/firebase/firebase-js-sdk/commit/f7d8324a188f013f7875cf6c35fc4beb2c78c0ae) [#5562](https://github.com/firebase/firebase-js-sdk/pull/5562) - Attempt to fix bug in compatability layer in Safari ("Right side of assignment cannot be destructured") + +* [`e456d00a7`](https://github.com/firebase/firebase-js-sdk/commit/e456d00a7d054b2e95476562a087f2b12301e800) [#5577](https://github.com/firebase/firebase-js-sdk/pull/5577) - Fix bug where `user.tenantId` wasn't being carried over in `updateCurrentUser` function + +## 0.18.1 + +### Patch Changes + +- [`49b0406ab`](https://github.com/firebase/firebase-js-sdk/commit/49b0406abb9b211c5b75325b0383539ac03358d1) [#5542](https://github.com/firebase/firebase-js-sdk/pull/5542) (fixes [#5541](https://github.com/firebase/firebase-js-sdk/issues/5541)) - Fix incorrectly-cased parameter in out-of-band request that was causing incorrect behavior in some cases + +## 0.18.0 + +### Minor Changes + +- [`4d2a54fb0`](https://github.com/firebase/firebase-js-sdk/commit/4d2a54fb0611ab1987ad415c265440b9bbbc28c6) [#5527](https://github.com/firebase/firebase-js-sdk/pull/5527) - Update all persistences to map to `inMemoryPersistence` in Node, to avoid errors with server-side rendering + +### Patch Changes + +- [`a5d87bc5c`](https://github.com/firebase/firebase-js-sdk/commit/a5d87bc5c5d6360d5fa2386fe351937463bc45b8) [#5511](https://github.com/firebase/firebase-js-sdk/pull/5511) - Fix bug with the user `emailVerified` field persistence across tabs + +* [`07b88e6e8`](https://github.com/firebase/firebase-js-sdk/commit/07b88e6e80f60525c66bf330d28160dbef2d0a2c) [#5487](https://github.com/firebase/firebase-js-sdk/pull/5487) - Add missing phone FACTOR_ID static property to the PhoneMultiFactorGenerator class + +- [`c2362214a`](https://github.com/firebase/firebase-js-sdk/commit/c2362214ad6154ce013d3815a6f1ccd061679f66) [#5522](https://github.com/firebase/firebase-js-sdk/pull/5522) - Fix wrongly-typed tenantId fields in requests to some endpoints + +- Updated dependencies [[`a99943fe3`](https://github.com/firebase/firebase-js-sdk/commit/a99943fe3bd5279761aa29d138ec91272b06df39), [`b835b4cba`](https://github.com/firebase/firebase-js-sdk/commit/b835b4cbabc4b7b180ae38b908c49205ce31a422)]: + - @firebase/logger@0.3.0 + - @firebase/util@1.4.0 + - @firebase/component@0.5.7 + +## 0.17.2 + +### Patch Changes + +- [`08ec55d6d`](https://github.com/firebase/firebase-js-sdk/commit/08ec55d6dfcc85207fbdcdde77d6508f27998603) [#5423](https://github.com/firebase/firebase-js-sdk/pull/5423) - Fix bug where custom errors from blocking functions were being dropped. + +* [`271303f3c`](https://github.com/firebase/firebase-js-sdk/commit/271303f3ca6fa47c646177a41d7a3e3f31e1d296) [#5460](https://github.com/firebase/firebase-js-sdk/pull/5460) - Remove `const enum`s from the public typing file. + +## 0.17.1 + +### Patch Changes + +- [`66596f3f8`](https://github.com/firebase/firebase-js-sdk/commit/66596f3f8c747158bf30b62d8f579f7eecf97081) [#5397](https://github.com/firebase/firebase-js-sdk/pull/5397) (fixes [#5392](https://github.com/firebase/firebase-js-sdk/issues/5392)) - Fix typings where the constructor of `OAuthProvider` was missing. + +## 0.17.0 + +### Minor Changes + +- [`cdada6c68`](https://github.com/firebase/firebase-js-sdk/commit/cdada6c68f9740d13dd6674bcb658e28e68253b6) [#5345](https://github.com/firebase/firebase-js-sdk/pull/5345) (fixes [#5015](https://github.com/firebase/firebase-js-sdk/issues/5015)) - Release modularized SDKs + +## 0.16.8 + +### Patch Changes + +- [`56a6a9d4a`](https://github.com/firebase/firebase-js-sdk/commit/56a6a9d4af2766154584a0f66d3c4d8024d74ba5) [#5071](https://github.com/firebase/firebase-js-sdk/pull/5071) (fixes [#4932](https://github.com/firebase/firebase-js-sdk/issues/4932)) - Auto initialize `auth-internal` after `auth` has been initialized. + +## 0.16.7 + +### Patch Changes + +- [`c81cf82fa`](https://github.com/firebase/firebase-js-sdk/commit/c81cf82fac14cbfaebc0e440235c3fb38af22d38) [#4966](https://github.com/firebase/firebase-js-sdk/pull/4966) (fixes [#4879](https://github.com/firebase/firebase-js-sdk/issues/4879)) - Fix bug where `linkWithPopup`, `linkWithRedirect`, `reauthenticateWithPopup`, and `reauthenticateWithRedirect` weren't correctly picking up the emulator configuration. + +## 0.16.6 + +### Patch Changes + +- [`de68cdca2`](https://github.com/firebase/firebase-js-sdk/commit/de68cdca21c6ba5a890807857b529c2187e4adba) [#4868](https://github.com/firebase/firebase-js-sdk/pull/4868) (fixes [#4867](https://github.com/firebase/firebase-js-sdk/issues/4867)) - Ensure emulator warning text is accessible. + +## 0.16.5 + +### Patch Changes + +- Updated dependencies [[`3f370215a`](https://github.com/firebase/firebase-js-sdk/commit/3f370215aa571db6b41b92a7d8a9aaad2ea0ecd0)]: + - @firebase/auth-types@0.10.3 + +## 0.16.4 + +### Patch Changes + +- Updated dependencies [[`4ab5a9ce5`](https://github.com/firebase/firebase-js-sdk/commit/4ab5a9ce5b6256a95d745f6dc40a5e5ddd2301f2)]: + - @firebase/auth-types@0.10.2 + +## 0.16.3 + +### Patch Changes + +- [`73bb561e1`](https://github.com/firebase/firebase-js-sdk/commit/73bb561e18ea42286a54d28648636bf1ac7fcfe0) [#4357](https://github.com/firebase/firebase-js-sdk/pull/4357) (fixes [#4174](https://github.com/firebase/firebase-js-sdk/issues/4174)) - Decode UTF-8 in ID Token. Fix #4174. + +## 0.16.2 + +### Patch Changes + +- [`92a7f4345`](https://github.com/firebase/firebase-js-sdk/commit/92a7f434536051bedd00bc1be7e774174378aa7d) [#4280](https://github.com/firebase/firebase-js-sdk/pull/4280) - Add the `useEmulator()` function and `emulatorConfig` to the `firebase` package externs + ## 0.16.1 ### Patch Changes diff --git a/packages/auth/CONTRIBUTING.md b/packages/auth/CONTRIBUTING.md deleted file mode 100644 index 6220996aa5d..00000000000 --- a/packages/auth/CONTRIBUTING.md +++ /dev/null @@ -1,86 +0,0 @@ -# @firebase/auth - -This is the authentication component for the Firebase JS SDK. It has a peer -dependency on the [`@firebase/app`](https://npm.im/@firebase/app) package on NPM. - -This package is included by default in the [`firebase`](https://npm.im/firebase) wrapper -package. - -## Table of Contents - -1. [Developer Setup](#developer-setup) - -## Developer Setup - -### Dependencies - -To set up a development environment to build Firebase-auth from source, you must -have the following installed: -- Node.js (>= 6.0.0) -- npm (should be included with Node.js) -- Java Runtime Environment - -In order to run the tests, you must also have: -- Python (2.7) - -Download the Firebase source and its dependencies with: - -```bash -git clone https://github.com/firebase/firebase-js-sdk.git -cd firebase-js-sdk -yarn install -``` - -### Building Firebase-auth - -To build the library, run: -```bash -cd packages/auth -yarn build -``` - -This will create output files in the `dist/` folder. - -### Running unit tests. - -All unit tests can be run on the command line (via Chrome and Firefox) with: - -```bash -yarn test -``` - -Alternatively, the unit tests can be run manually by running - -```bash -yarn run serve -``` - -Then, all unit tests can be run at: http://localhost:4000/buildtools/all_tests.html -You can also run tests individually by accessing each HTML file under -`generated/tests`, for example: http://localhost:4000/generated/tests/test/auth_test.html - -### Run tests using SauceLabs - -*You need a [SauceLabs](https://saucelabs.com/) account to run tests on -SauceLabs.* - -Go to your SauceLab account, under "My Account", and copy paste the access key. -Now export the following variables, *in two Terminal windows*: - -```bash -export SAUCE_USERNAME= -export SAUCE_ACCESS_KEY= -``` - - Then, in one Terminal window, start SauceConnect: - - ```bash -./buildtools/sauce_connect.sh -``` - -Take note of the "Tunnel Identifier" value logged in the terminal, at the top. In -the other terminal that has the exported variables, run the tests: - -```bash -yarn test -- --saucelabs --tunnelIdentifier= -``` diff --git a/packages/auth/LICENSE b/packages/auth/LICENSE deleted file mode 100644 index d6456956733..00000000000 --- a/packages/auth/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/auth/README.md b/packages/auth/README.md index 41b0a7864ea..74e52968aa4 100644 --- a/packages/auth/README.md +++ b/packages/auth/README.md @@ -3,3 +3,137 @@ This is the Firebase Authentication component of the Firebase JS SDK. **This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** + +## Testing + +The modular Auth SDK has both unit tests and integration tests, along with a +host of npm scripts to run these tests. The most important commands are: + +| Command | Description | +| ------- | ----------- | +| `yarn test` | This will run lint, unit tests, and integration tests against the live environment| +| `yarn test:` | Runs all browser tests, unit and integration | +| `yarn test::unit` | Runs only \ unit tests | +| `yarn test::unit:debug` | Runs \ unit tests, auto-watching for file system changes | +| `yarn test::integration` | Runs only integration tests against the live environment | +| `yarn test::integration:local` | Runs all headless \ integration tests against the emulator (more below) | +| `yarn test:browser:integration:prodbackend` | Runs TOTP MFA integration tests against the backend (more below) | + +Where \ is "browser" or "node". There are also cordova tests, but they +are not broken into such granular details. Check out `package.json` for more. + +### Integration testing with the emulator + +To test against the emulator, set up the Auth emulator +([instructions](https://firebase.google.com/docs/emulator-suite/connect_and_prototype)). +The easiest way to run these tests is to use the `firebase emulators:exec` +command +([documentation](https://firebase.google.com/docs/emulator-suite/install_and_configure#startup)). +You can also manually start the emulator separately, and then point the tests +to it by setting the `GCLOUD_PROJECT` and `FIREBASE_AUTH_EMULATOR_HOST` +environmental variables. In addition to the commands listed above, the below +commands also run various tests: + + * `yarn test:integration:local` — Executes Node and browser emulator + integration tests, as well as the Selenium WebDriver tests + + * `yarn test:webdriver` — Executes only the Selenium WebDriver + integration tests + +Note - The webdriver tests require the Chrome webdriver to match the version of Chrome running locally. +In the CI environment, this is ensured using the environment variable [here.](https://github.com/firebase/firebase-js-sdk/blob/6e80a678fe0c31046860554cec0459a2be34d22b/.github/workflows/test-changed-auth.yml#L7) +When running locally, change the chromedriver version in [package.json](https://github.com/firebase/firebase-js-sdk/blob/6e80a678fe0c31046860554cec0459a2be34d22b/packages/auth/package.json#L124) to match your local Chrome version and run `yarn install`. + + +For example, to run all integration and WebDriver tests against the emulator, +you would simply execute the following command: + +```sh +firebase emulators:exec --project foo-bar --only auth "yarn test:integration:local" +``` + +### Integration testing with the production backend + +Currently, MFA TOTP, password policy, and reCAPTCHA Enterprise phone verification tests only run +against the production backend (since they are not supported on the emulator yet). +Running against the backend also makes it a more reliable end-to-end test. + +#### TOTP + +The TOTP tests require the following email/password combination to exist in the project, so if you are running this test against your test project, please create this user: + +'totpuser-donotdelete@test.com', 'password' + +You also need to verify this email address, in order to use MFA. This can be done with a curl command like this: + +``` +curl -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Content-Type: application/json" -H "X-Goog-User-Project: ${PROJECT_ID}" -X POST https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode -d '{ + "email": "totpuser-donotdelete@test.com", + "requestType": "VERIFY_EMAIL", + "returnOobLink": true, + }' +``` + +#### Password policy + +The password policy tests require a tenant configured with a password policy that requires all options to exist in the project. + +If you are running this test against your test project, please create the tenant and configure the policy with the following curl command: + +``` +curl -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Content-Type: application/json" -H "X-Goog-User-Project: ${PROJECT_ID}" -X POST https://identitytoolkit.googleapis.com/v2/projects/${PROJECT_ID}/tenants -d '{ + "displayName": "passpol-tenant", + "passwordPolicyConfig": { + "passwordPolicyEnforcementState": "ENFORCE", + "passwordPolicyVersions": [ + { + "customStrengthOptions": { + "minPasswordLength": 8, + "maxPasswordLength": 24, + "containsLowercaseCharacter": true, + "containsUppercaseCharacter": true, + "containsNumericCharacter": true, + "containsNonAlphanumericCharacter": true + } + } + ] + } + }' +``` + +Replace the tenant ID `passpol-tenant-d7hha` in [test/integration/flows/password_policy.test.ts](https://github.com/firebase/firebase-js-sdk/blob/main/packages/auth/test/integration/flows/password_policy.test.ts) with the ID for the newly created tenant. The tenant ID can be found at the end of the `name` property in the response and is in the format `passpol-tenant-xxxxx`. + +#### reCAPTCHA Enterprise phone verification + +The reCAPTCHA Enterprise phone verification tests require reCAPTCHA Enterprise to be enabled and +the following fictional phone number to be configured and in the project. + +If you are running this +test against your project, please [add this test phone number](https://firebase.google.com/docs/auth/web/phone-auth#create-fictional-phone-numbers-and-verification-codes): + +'+1 555-555-1000', SMS code: '123456' + +Follow [this guide](https://cloud.google.com/identity-platform/docs/recaptcha-enterprise) to enable reCAPTCHA +Enterprise, then use the following curl command to set reCAPTCHA Enterprise to ENFORCE for phone provider: + +``` +curl -H "Authorization: Bearer $(gcloud auth print-access-token)" -H "Content-Type: application/json" -H "X-Goog-User-Project: $ +{PROJECT_ID}" -X POST https://identitytoolkit.googleapis.com/v2/projects/${PROJECT_ID}/config?updateMask=recaptchaConfig.phoneEnforcementState,recaptchaConfig.useSmsBotScore,recaptchaConfig.useSmsTollFraudProtection -d ' +{ + "name": "projects/{PROJECT_ID}", + "recaptchaConfig": { + "phoneEnforcementState": "ENFORCE", + "useSmsBotScore": "true", + "useSmsTollFraudProtection": "true", + }, +}' +``` + +### Selenium Webdriver tests + +These tests assume that you have both Firefox and Chrome installed on your +computer and in your `$PATH`. The tests will error out if this is not the case. +The WebDriver tests talk to the emulator, but unlike the headless integration +tests, these run in a browser robot environment; the assertions themselves run +in Node. When you run these tests a small Express server will be started to +serve the static files the browser robot uses. diff --git a/packages/auth/STYLEGUIDE.md b/packages/auth/STYLEGUIDE.md deleted file mode 100644 index 4fd244b6c19..00000000000 --- a/packages/auth/STYLEGUIDE.md +++ /dev/null @@ -1,34 +0,0 @@ -# Firebase-auth Web Style Guide - -Here are the style rules to follow for Firebase-auth: - -## #1 Be consistent with the rest of the codebase - -This is the number one rule and should help determine what to do in most cases. - -## #2 Respect Google JavaScript style guide - -The style guide accessible -[here](https://google.github.io/styleguide/javascriptguide.xml) has to be fully -respected. - -## #3 Follow these grammar rules - -- Functions descriptions have to start with a verb using the third person of the -singular. - - *Ex: `/** Tests the validity of the input. */`* -- Inline comments within procedures should always use the imperative. - - *Ex: `// Check whether the value is true.`* -- Acronyms have to be uppercased in comments. - - *Ex: `// IP, DOM, CORS, URL...`* - - *Exception: Identity Provider = IdP* -- Acronyms have to be capitalized (but not uppercased) in variable names. - - *Ex: `redirectUrl()`, `signInIdp()`* -- Never use login/log in in comments. Use “sign-in” if it’s a noun, “sign in” if -it’s a verb. The same goes for the variable name. Never use `login`; always use -`signIn`. - - *Ex: `// The sign-in method.`* - - *Ex: `// Signs in the user.`* -- Always start an inline comment with a capital (unless referring to the name of -a variable/function), and end it with a period. - - *Ex: `// This is a valid inline comment.`* diff --git a/packages/auth/api-extractor.json b/packages/auth/api-extractor.json new file mode 100644 index 00000000000..85e6f846d5c --- /dev/null +++ b/packages/auth/api-extractor.json @@ -0,0 +1,9 @@ +{ + "extends": "../../config/api-extractor.json", + "mainEntryPointFilePath": "/dist/esm/index.d.ts", + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/.d.ts", + "betaTrimmedFilePath": "/dist/-public.d.ts" + } +} diff --git a/packages/auth/buildtools/all_tests.html b/packages/auth/buildtools/all_tests.html deleted file mode 100644 index 83de0a0e247..00000000000 --- a/packages/auth/buildtools/all_tests.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - FirebaseUI Auth Tests - - - - - - -

Firebase Auth Tests

-
- - - diff --git a/packages/auth/buildtools/common.py b/packages/auth/buildtools/common.py deleted file mode 100644 index c5a473ba4e7..00000000000 --- a/packages/auth/buildtools/common.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2016 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS-IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Common methods and constants for generating test files.""" -import os - - -# The directory in which test HTML files are generated. -TESTS_BASE_PATH = "./generated/tests/" - - -def cd_to_firebaseauth_root(): - """Changes the current directory to the firebase-auth root directory. - This method assumes that this script is in the buildtools/ directory, which is - a direct child of the root directory. - This allows us to avoid writing to the wrong files. - """ - root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - os.chdir(root_dir) - - -def get_files_with_suffix(root, suffix): - """Yields file names under a directory with a given suffix. - Args: - root: The path to the directory where we wish to search. - suffix: The suffix we wish to search for. - Yields: - The paths to files under the directory that have the given suffix. - """ - for root, _, files in os.walk(root): - for file_name in files: - if file_name.endswith(suffix): - yield os.path.join(root, file_name) diff --git a/packages/auth/buildtools/gen_all_tests_js.py b/packages/auth/buildtools/gen_all_tests_js.py deleted file mode 100644 index ced11b5d9de..00000000000 --- a/packages/auth/buildtools/gen_all_tests_js.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2016 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS-IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Generates the all_tests.js file. -all_tests.js tells all_tests.html the paths to the files to test. -Usage: -$ python ./buildtools/gen_all_tests_js.py > generated/all_tests.js -""" - -import common - - -def main(): - common.cd_to_firebaseauth_root() - print "var allTests = [" - _print_test_files_under_root(common.TESTS_BASE_PATH) - print "];" - # The following is required in the context of protractor. - print "if (typeof module !== 'undefined' && module.exports) {" - print " module.exports = allTests;" - print "}" - - -def _print_test_files_under_root(root): - """Prints all test HTML files found under a given directory (recursively). - Args: - root: The path to the directory. - """ - for file_name in common.get_files_with_suffix(root, "_test.html"): - print " '%s'," % file_name[2:] # Ignore the beginning './'. - - -if __name__ == "__main__": - main() diff --git a/packages/auth/buildtools/gen_test_html.py b/packages/auth/buildtools/gen_test_html.py deleted file mode 100644 index 90a2362c268..00000000000 --- a/packages/auth/buildtools/gen_test_html.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright 2016 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS-IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Generates *_test.html files from *_test.js files. -This modifies files in place and will overwrite existing *_test.html files. -Usage: -$ python ./buildtools/gen_test_html.py -""" - -from collections import namedtuple -import os -import re -from string import Template -import common - - -# Stores the paths of files related to a test file -# (e.g. *_test.html, *_test_dom.html) -RelatedPaths = namedtuple("RelatedPaths", ["html", "dom"]) - - -# The root-level directories containing JS tests. -DIRECTORIES_WITH_TESTS = ["test"] - - -def main(): - common.cd_to_firebaseauth_root() - template_data = _read_file("./buildtools/test_template.html") - template = Template(template_data) - for directory in DIRECTORIES_WITH_TESTS: - for js_path in common.get_files_with_suffix(directory, "_test.js"): - _gen_html(js_path, template) - - -def _gen_html(js_path, template): - """Generates a Closure test HTML wrapper file and saves it to the filesystem. - Args: - js_path: The path to the JS test (*_test.js) file. - template: The template for the HTML wrapper. - """ - try: - related_paths = _get_related_paths_from_js_path(js_path) - js_data = _read_file(js_path) - dom = (_read_file(related_paths.dom) - if os.path.isfile(related_paths.dom) else "") - package = _extract_closure_package(js_data) - generated_html = template.substitute(package=package, dom=dom) - - _write_file(related_paths.html, generated_html) - - except: # pylint: disable=bare-except - print "HTML generation failed for: %s" % js_path - - -def _get_related_paths_from_js_path(js_path): - """Converts the JS test file path to paths of related files. - For example, ./path/to/foo_test.js becomes - ./generated/tests/path/to/foo_test.html - ./path/to/foo_test_dom.html - Args: - js_path: The path to the JS test (*_test.js) file. - Returns: - The paths to the related files, as a RelatedPaths. - """ - base_name = os.path.splitext(js_path)[0] - return RelatedPaths(common.TESTS_BASE_PATH + base_name + ".html", - base_name + "_dom.html") - - -def _extract_closure_package(js_data): - """Extracts the package name that is goog.provide()d in the JS file. - Args: - js_data: The contents of a JS test (*_test.js) file. - Returns: - The closure package goog.provide()d by the file. - Raises: - ValueError: The JS does not contain a goog.provide(). - """ - matches = re.search(r"goog\.provide\('(.+)'\);", js_data) - if matches is None: - raise ValueError("goog.provide() not found in file") - return matches.group(1) - - -def _read_file(path): - """Reads a file into a string. - Args: - path: The path to a file. - Returns: - The contents of the file. - """ - with open(path) as f: - return f.read() - - -def _write_file(path, contents): - """Writes a string to file, overwriting existing content. - Intermediate directories are created if not present. - Args: - path: The path to a file. - contents: The string to write to the file. - """ - dir_name = os.path.dirname(path) - if not os.path.exists(dir_name): - os.makedirs(dir_name) - with open(path, "w") as f: - f.write(contents) - - -if __name__ == "__main__": - main() diff --git a/packages/auth/buildtools/generate_test_files.sh b/packages/auth/buildtools/generate_test_files.sh deleted file mode 100755 index cd5e56f151d..00000000000 --- a/packages/auth/buildtools/generate_test_files.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -# Copyright 2016 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS-IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Generates the temporary files needed for tests to run, putting them in the -# generated/ directory. -# -# Usage: -# $ buildtools/generate_test_files.sh - -# CD to the root firebase-auth directory, which should be the parent directory of -# buildtools/. -cd "$(dirname $(dirname "$0"))" -mkdir -p generated - -echo "Generating dependency file..." -python ../../node_modules/google-closure-library/closure/bin/build/depswriter.py \ - --root_with_prefix="test ../../../../test" \ - --root_with_prefix="src ../../../../src" \ - > generated/deps.js - -echo "Generating test HTML files..." -python ./buildtools/gen_test_html.py -python ./buildtools/gen_all_tests_js.py > generated/all_tests.js - -echo "Done." diff --git a/packages/auth/buildtools/run_demo.sh b/packages/auth/buildtools/run_demo.sh deleted file mode 100755 index 0f5feaafb3c..00000000000 --- a/packages/auth/buildtools/run_demo.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -# Copyright 2017 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS-IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Runs the server for the demo page. -# -# Usage: -# $ buildtools/run_demo.sh - -# CD to the root packages/auth directory, which should be the parent directory of -# buildtools/. -cd "$(dirname $(dirname "$0"))" -# Go back to repo root and build all binaries needed for the demo app. -cd ../.. -yarn build:release -# Go back to Auth package. -cd packages/auth -# Make dist directory if not already there. -mkdir -p demo/public/dist -# Copy app, auth and database binaries to demo dist directory. -cp ../firebase/firebase-app.js demo/public/dist/firebase-app.js -cp ../firebase/firebase-auth.js demo/public/dist/firebase-auth.js -cp ../firebase/firebase-database.js demo/public/dist/firebase-database.js -# Serve demo app. -cd demo -`yarn bin`/firebase emulators:start diff --git a/packages/auth/buildtools/run_tests.sh b/packages/auth/buildtools/run_tests.sh deleted file mode 100755 index 2661ae73320..00000000000 --- a/packages/auth/buildtools/run_tests.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/bin/bash -# Copyright 2016 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS-IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Prepares the setup for running unit tests. It starts a Selenium Webdriver. -# creates a local webserver to serve test files, and run protractor. -# -# Usage: -# -# ./buildtools/run_tests.sh [--saucelabs [--tunnelIdentifier=]] -# -# Can take up to two arguments: -# --saucelabs: Use SauceLabs instead of local Chrome and Firefox. -# --tunnelIdentifier=: when using SauceLabs, specify the tunnel -# identifier. Otherwise, uses the environment variable TRAVIS_JOB_NUMBER. -# -# Prefer to use the `npm test` command as explained below. -# -# Run locally with Chrome & Firefox: -# $ npm test -# It will start a local Selenium Webdriver server as well as the HTTP server -# that serves test files. -# -# Run locally using SauceLabs: -# Go to your SauceLab account, under "My Account", and copy paste the -# access key. Now export the following variables: -# $ export SAUCE_USERNAME= -# $ export SAUCE_ACCESS_KEY= -# Then, start SauceConnect: -# $ ./buildtools/sauce_connect.sh -# Take note of the "Tunnel Identifier" value logged in the terminal. -# Run the tests: -# $ npm run -- --saucelabs --tunnelIdentifier= -# This will start the HTTP Server locally, and connect through SauceConnect -# to SauceLabs remote browsers instances. -# -# Travis will run `npm test -- --saucelabs`. - -# Since yarn workspaces might hoist our packages, we have to fallback to -# ../../node_modules when we want to execute a script. -declare -a nodeModulesBasedirs=( - "./node_modules/.bin" - "../../node_modules/.bin" -) - -# Tries to resolve the first argument as an npm executable, taking hoisting into -# account. If case of a successful resolution, executes the binary, passing -# the rest of given arguments to an invocation. -function evalModule { - for basedir in "${nodeModulesBasedirs[@]}" - do - if [ -f "$basedir/$1" ]; then - eval "$basedir/$1 ${@:2}" - ret=$? - if [ $ret -ne 0 ]; then - exit $ret - fi - break - fi - done -} - -cd "$(dirname $(dirname "$0"))" - -function killServer () { - if [ "$seleniumStarted" = true ]; then - echo "Stopping Selenium..." - evalModule webdriver-manager shutdown - evalModule webdriver-manager clean - # Selenium is not getting shutdown. Send a kill signal. - lsof -t -i :4444 | xargs kill - fi - echo "Killing HTTP Server..." - kill $serverPid -} - -# Start the local webserver. -evalModule gulp "serve &" -serverPid=$! -echo "Local HTTP Server started with PID $serverPid." - -trap killServer EXIT - -# If --saucelabs option is passed, forward it to the protractor command adding -# the second argument that is required for local SauceLabs test run. -if [[ $1 = "--saucelabs" ]]; then - seleniumStarted=false - sleep 2 - echo "Using SauceLabs." - # $2 contains the tunnelIdentifier argument if specified, otherwise is empty. - evalModule protractor protractor.conf.js --saucelabs $2 -else - echo "Using Chrome and Firefox." - evalModule webdriver-manager clean - # Updates Selenium Webdriver. - evalModule webdriver-manager update - # Start Selenium Webdriver. - evalModule webdriver-manager start &>/dev/null & - seleniumStarted=true - echo "Selenium Server started." - # Wait for servers to come up. - sleep 10 - evalModule protractor protractor.conf.js -fi diff --git a/packages/auth/buildtools/sauce_connect.sh b/packages/auth/buildtools/sauce_connect.sh deleted file mode 100755 index a75d2671af9..00000000000 --- a/packages/auth/buildtools/sauce_connect.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash -# Copyright 2016 Google Inc. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS-IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# -# Download and install SauceConnect under Linux 64-bit. To be used when testing -# with SauceLabs locally. See the instructions in protractor.conf.js file. -# -# It should not be used on Travis. Travis already handles SauceConnect. -# -# Script copied from the Closure Library repository: -# https://github.com/google/closure-library/blob/master/scripts/ci/sauce_connect.sh -# - -# Setup and start Sauce Connect locally. -CONNECT_URL="https://saucelabs.com/downloads/sc-4.4.1-linux.tar.gz" -CONNECT_DIR="/tmp/sauce-connect-$RANDOM" -CONNECT_DOWNLOAD="sc-latest-linux.tar.gz" - -BROWSER_PROVIDER_READY_FILE="/tmp/sauce-connect-ready" - -# Get Connect and start it. -mkdir -p $CONNECT_DIR -cd $CONNECT_DIR -curl $CONNECT_URL -o $CONNECT_DOWNLOAD 2> /dev/null 1> /dev/null -mkdir sauce-connect -tar --extract --file=$CONNECT_DOWNLOAD --strip-components=1 \ - --directory=sauce-connect > /dev/null -rm $CONNECT_DOWNLOAD - -function removeFiles() { - echo "Removing SauceConnect files..." - rm -rf $CONNECT_DIR -} - -trap removeFiles EXIT - -# This will be used by Protractor to connect to SauceConnect. -TUNNEL_IDENTIFIER="tunnelId-$RANDOM" -echo "" -echo "=========================================================================" -echo " Tunnel Identifier to pass to Protractor:" -echo " $TUNNEL_IDENTIFIER" -echo "=========================================================================" -echo "" -echo "" - -echo "Starting Sauce Connect..." - -# Start SauceConnect. -sauce-connect/bin/sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY \ - -i $TUNNEL_IDENTIFIER diff --git a/packages/auth/buildtools/test_template.html b/packages/auth/buildtools/test_template.html deleted file mode 100644 index 096a3f18aa1..00000000000 --- a/packages/auth/buildtools/test_template.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - $dom - - diff --git a/packages/auth/cordova/api-extractor.json b/packages/auth/cordova/api-extractor.json new file mode 100644 index 00000000000..e76c159a8ef --- /dev/null +++ b/packages/auth/cordova/api-extractor.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../config/api-extractor.json", + "mainEntryPointFilePath": "/dist/cordova/index.cordova.d.ts", + "apiReport": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/cordova/.d.ts", + "publicTrimmedFilePath": "/dist/cordova/-public.d.ts" + }, + "docModel": { + "enabled": true, + "apiJsonFilePath": "/temp/subpackages/.api.json" + } +} diff --git a/packages/auth/cordova/demo/.gitignore b/packages/auth/cordova/demo/.gitignore new file mode 100644 index 00000000000..24a9d84d538 --- /dev/null +++ b/packages/auth/cordova/demo/.gitignore @@ -0,0 +1,5 @@ +platforms/ +plugins/ +ul_web_hooks/ +src/config.js +config.xml \ No newline at end of file diff --git a/packages/auth/cordova/demo/README.md b/packages/auth/cordova/demo/README.md new file mode 100644 index 00000000000..cf72e9ec6d3 --- /dev/null +++ b/packages/auth/cordova/demo/README.md @@ -0,0 +1,119 @@ +# Cordova Auth Demo App +This package contains a demo of the various Firebase Auth features bundled in +an Apache Cordova app. + +## Dev Setup + +Follow [this guide](https://cordova.apache.org/docs/en/10.x/guide/cli/) to run +set up Cordova CLI. tl;dr: + +```bash +npm install -g cordova +cordova requirements +``` + +### Preparing the deps + +At this point you should have a basic environment set up that can support +Cordova. Now you'll need to update some config files to make everything work. +First, read through steps 1 - 5, 7 in the +[Firebase Auth Cordova docs page](https://firebase.google.com/docs/auth/web/cordova) +so that you get a sense of the values you need to add / adjust. + +Make a copy of `sample-config.xml` to `config.xml`, and replace the values +outlined in curly braces. Notably, you'll need the package name / bundle ID +of a registered app in the Firebase Console. You'll also need the Auth Domain +that comes from the Web config. + +Next, you'll need to make a copy of `src/sample-config.js` to `src/config.js`, +and you'll need to supply a Firebase JS SDK configuration in the form of that +file. + +Once all this is done, you can get the Cordova setup ready in this specific +project: + +```bash +cordova prepare +``` + +Work through any issues until no more errors are printed. + +## Building and running the demo + +The app consists of a bundled script, `www/dist/bundle.js`. This script is built +from the `./src` directory. To change it, modify the source code in +`src` and then rebuild the bundle: + +```bash +# Build the deps the first time, and subsequent times if changing the core SDK +npm run build:deps +npm run build:js +``` + +### Android + +You can now build and test the app on Android Emulator: + +```bash +cordova build android +cordova emulate android + +# Or +cordova run android +``` + +TODO: Any tips or gotchas? + +### iOS + +```bash +cordova build ios +cordova emulate ios +``` + +Please ignore the command-line output mentioning `logPath` -- that file +[will not actually exist](https://github.com/ios-control/ios-sim/issues/167) and +won't contain JavaScript console logs. The Simulator app itself does not +expose console logs either (System Log won't help). + +The recommended way around this is to use the remote debugger in Safari. Launch +the Safari app on the same MacBook, and then go to Safari menu > Preferences > +Advanced and check the "Show Develop menu in menu bar" option. Then, you should +be able to see the Simulator under the Safari `Developer` menu and choose the +app web view (Hello World > Hello World). This only works when the Simulator has +already started and is running the Cordova app. This gives you full access to +console logs AND uncaught errors in the JavaScript code. + +WARNING: This may not catch any JavaScript errors during initialization (or +before the debugger was opened). If nothing seems working, try clicking the +Reload Page button in the top-left corner of the inspector, which will reload +the whole web view and hopefully catch the error this time. + +#### Xcode + +If you really want to, you can also start the Simulator via Xcode. Note that +this will only give you access to console log but WON'T show JavaScript errors +-- for which you still need the Safari remote debugger. + +```bash +cordova build ios +open ./platforms/ios/HelloWorld.xcworkspace/ +``` + +Select/add a Simulator through the nav bar and click on "Run" to start it. You +can then find console logs in the corresponding Xcode panel (not in the +Simulator window itself). + +If you go this route, +[DO NOT edit files in Xcode IDE](https://cordova.apache.org/docs/en/10.x/guide/platforms/ios/index.html#open-a-project-within-xcode). +Instead, edit files in the `www` folder and run `cordova build ios` to copy the +changes over (and over). + +### Notes + +You may need to update the cordova-universal-links-plugin `manifestWriter.js` +to point to the correct Android manifest. For example: + +```js +var pathToManifest = path.join(cordovaContext.opts.projectRoot, 'platforms', 'android', 'app', 'src', 'main', 'AndroidManifest.xml'); +``` diff --git a/packages/auth/cordova/demo/package.json b/packages/auth/cordova/demo/package.json new file mode 100644 index 00000000000..9ece12f5dfb --- /dev/null +++ b/packages/auth/cordova/demo/package.json @@ -0,0 +1,52 @@ +{ + "name": "cordova-auth-demo", + "displayName": "Cordova Auth Demo", + "version": "1.0.0", + "scripts": { + "build:js": "rollup -c", + "build:deps": "lerna run --scope @firebase/'{app,auth/cordova}' --include-dependencies build" + }, + "keywords": [ + "ecosystem:cordova" + ], + "devDependencies": { + "cordova-android": "9.1.0", + "cordova-ios": "6.2.0", + "cordova-plugin-whitelist": "1.3.5", + "cordova-universal-links-plugin": "1.2.1", + "rollup": "2.79.1" + }, + "cordova": { + "plugins": { + "cordova-plugin-whitelist": {}, + "cordova-plugin-buildinfo": {}, + "cordova-universal-links-plugin": {}, + "cordova-plugin-browsertab": {}, + "cordova-plugin-inappbrowser": {}, + "cordova-plugin-customurlscheme": { + "URL_SCHEME": "com.example.hello", + "ANDROID_SCHEME": " ", + "ANDROID_HOST": " ", + "ANDROID_PATHPREFIX": "/" + } + }, + "platforms": [ + "ios", + "android" + ] + }, + "dependencies": { + "@firebase/app": "0.7.24", + "@firebase/auth": "0.20.1", + "@firebase/logger": "0.3.2", + "@firebase/util": "1.6.0", + "cordova-plugin-browsertab": "0.2.0", + "cordova-plugin-buildinfo": "4.0.0", + "cordova-plugin-compat": "1.2.0", + "cordova-plugin-customurlscheme": "5.0.2", + "cordova-plugin-inappbrowser": "4.1.0", + "cordova-universal-links-plugin-fix": "^1.2.1", + "lerna": "^4.0.0", + "tslib": "^2.1.0" + } +} diff --git a/packages/auth/cordova/demo/rollup.config.js b/packages/auth/cordova/demo/rollup.config.js new file mode 100644 index 00000000000..21654b783d2 --- /dev/null +++ b/packages/auth/cordova/demo/rollup.config.js @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import resolve from '@rollup/plugin-node-resolve'; +import strip from '@rollup/plugin-strip'; + +/** + * Browser Build + */ +const esmBuild = { + input: 'src/index.js', + output: [{ file: 'www/dist/bundle.js', format: 'esm', sourcemap: true }], + plugins: [ + strip({ functions: ['debugAssert.*'] }), + resolve({ mainFields: ['module', 'main'] }) + ] +}; + +export default esmBuild; diff --git a/packages/auth/cordova/demo/sample-config.xml b/packages/auth/cordova/demo/sample-config.xml new file mode 100644 index 00000000000..839fccb8f8e --- /dev/null +++ b/packages/auth/cordova/demo/sample-config.xml @@ -0,0 +1,33 @@ + + + + + Cordova Auth Demo + + + + + + + + + + + + + + diff --git a/packages/auth/cordova/demo/src/index.js b/packages/auth/cordova/demo/src/index.js new file mode 100644 index 00000000000..42014cbf7e4 --- /dev/null +++ b/packages/auth/cordova/demo/src/index.js @@ -0,0 +1,1568 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/** + * @license + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview This code is for the most part copied over from the packages/auth/demo + * package. + */ + +import { initializeApp } from '@firebase/app'; +import { + applyActionCode, + browserLocalPersistence, + browserSessionPersistence, + confirmPasswordReset, + createUserWithEmailAndPassword, + EmailAuthProvider, + fetchSignInMethodsForEmail, + indexedDBLocalPersistence, + initializeAuth, + getAuth, + inMemoryPersistence, + isSignInWithEmailLink, + linkWithCredential, + multiFactor, + reauthenticateWithCredential, + sendEmailVerification, + sendPasswordResetEmail, + sendSignInLinkToEmail, + signInAnonymously, + signInWithCredential, + signInWithCustomToken, + signInWithEmailAndPassword, + unlink, + updateEmail, + updatePassword, + updateProfile, + verifyPasswordResetCode, + getMultiFactorResolver, + OAuthProvider, + GoogleAuthProvider, + FacebookAuthProvider, + TwitterAuthProvider, + GithubAuthProvider, + signInWithRedirect, + linkWithRedirect, + reauthenticateWithRedirect, + getRedirectResult, + cordovaPopupRedirectResolver +} from '@firebase/auth/dist/cordova'; + +import { config } from './config'; +import { + alertError, + alertNotImplemented, + alertSuccess, + clearLogs, + log, + logAtLevel_ +} from './logging'; + +let app = null; +let auth = null; +let currentTab = null; +let lastUser = null; +let applicationVerifier = null; +let multiFactorErrorResolver = null; +let selectedMultiFactorHint = null; +let recaptchaSize = 'normal'; +let webWorker = null; + +// The corresponding Font Awesome icons for each provider. +const providersIcons = { + 'google.com': 'fa-google', + 'facebook.com': 'fa-facebook-official', + 'twitter.com': 'fa-twitter-square', + 'github.com': 'fa-github', + 'yahoo.com': 'fa-yahoo', + 'phone': 'fa-phone' +}; + +/** + * Returns the active user (i.e. currentUser or lastUser). + * @return {!firebase.User} + */ +function activeUser() { + const type = $('input[name=toggle-user-selection]:checked').val(); + if (type === 'lastUser') { + return lastUser; + } else { + return auth.currentUser; + } +} + +/** + * Refreshes the current user data in the UI, displaying a user info box if + * a user is signed in, or removing it. + */ +function refreshUserData() { + if (activeUser()) { + const user = activeUser(); + $('.profile').show(); + $('body').addClass('user-info-displayed'); + $('div.profile-email,span.profile-email').text(user.email || 'No Email'); + $('div.profile-phone,span.profile-phone').text( + user.phoneNumber || 'No Phone' + ); + $('div.profile-uid,span.profile-uid').text(user.uid); + $('div.profile-name,span.profile-name').text(user.displayName || 'No Name'); + $('input.profile-name').val(user.displayName); + $('input.photo-url').val(user.photoURL); + if (user.photoURL != null) { + let photoURL = user.photoURL; + // Append size to the photo URL for Google hosted images to avoid requesting + // the image with its original resolution (using more bandwidth than needed) + // when it is going to be presented in smaller size. + if ( + photoURL.indexOf('googleusercontent.com') !== -1 || + photoURL.indexOf('ggpht.com') !== -1 + ) { + photoURL = photoURL + '?sz=' + $('img.profile-image').height(); + } + $('img.profile-image').attr('src', photoURL).show(); + } else { + $('img.profile-image').hide(); + } + $('.profile-email-verified').toggle(user.emailVerified); + $('.profile-email-not-verified').toggle(!user.emailVerified); + $('.profile-anonymous').toggle(user.isAnonymous); + // Display/Hide providers icons. + $('.profile-providers').empty(); + if (user['providerData'] && user['providerData'].length) { + const providersCount = user['providerData'].length; + for (let i = 0; i < providersCount; i++) { + addProviderIcon(user['providerData'][i]['providerId']); + } + } + // Show enrolled second factors if available for the active user. + showMultiFactorStatus(user); + // Change color. + if (user === auth.currentUser) { + $('#user-info').removeClass('last-user'); + $('#user-info').addClass('current-user'); + } else { + $('#user-info').removeClass('current-user'); + $('#user-info').addClass('last-user'); + } + } else { + $('.profile').slideUp(); + $('body').removeClass('user-info-displayed'); + $('input.profile-data').val(''); + } +} + +/** + * Sets last signed in user and updates UI. + * @param {?firebase.User} user The last signed in user. + */ +function setLastUser(user) { + lastUser = user; + if (user) { + // Displays the toggle. + $('#toggle-user').show(); + $('#toggle-user-placeholder').hide(); + } else { + $('#toggle-user').hide(); + $('#toggle-user-placeholder').show(); + } +} + +/** + * Add a provider icon to the profile info. + * @param {string} providerId The providerId of the provider. + */ +function addProviderIcon(providerId) { + const pElt = $('') + .addClass('fa ' + providersIcons[providerId]) + .attr('title', providerId) + .data({ + 'toggle': 'tooltip', + 'placement': 'bottom' + }); + $('.profile-providers').append(pElt); + pElt.tooltip(); +} + +/** + * Updates the active user's multi-factor enrollment status. + * @param {!firebase.User} activeUser The corresponding user. + */ +function showMultiFactorStatus(activeUser) { + mfaUser = multiFactor(activeUser); + const enrolledFactors = (mfaUser && mfaUser.enrolledFactors) || []; + const $listGroup = $('#user-info .dropdown-menu.enrolled-second-factors'); + // Hide the drop down menu initially. + $listGroup.empty().parent().hide(); + if (enrolledFactors.length) { + // If enrolled factors are available, show the drop down menu. + $listGroup.parent().show(); + // Populate the enrolled factors. + showMultiFactors( + $listGroup, + enrolledFactors, + // On row click, do nothing. This is needed to prevent the drop down + // menu from closing. + e => { + e.preventDefault(); + e.stopPropagation(); + }, + // On delete click unenroll the selected factor. + function (e) { + e.preventDefault(); + // Get the corresponding second factor index. + const index = parseInt($(this).attr('data-index'), 10); + // Get the second factor info. + const info = enrolledFactors[index]; + // Get the display name. If not available, use uid. + const label = info && (info.displayName || info.uid); + if (label) { + $('#enrolled-factors-drop-down').removeClass('open'); + mfaUser.unenroll(info).then(() => { + refreshUserData(); + alertSuccess('Multi-factor successfully unenrolled.'); + }, onAuthError); + } + } + ); + } +} + +/** + * Updates the UI when the user is successfully authenticated. + * @param {!firebase.User} user User authenticated. + */ +function onAuthSuccess(user) { + console.log(user); + alertSuccess('User authenticated, id: ' + user.uid); + refreshUserData(); +} + +/** + * Displays an error message when the authentication failed. + * @param {!Error} error Error message to display. + */ +function onAuthError(error) { + logAtLevel_(error, 'error'); + if (error.code === 'auth/multi-factor-auth-required') { + // Handle second factor sign-in. + handleMultiFactorSignIn(getMultiFactorResolver(auth, error)); + } else { + alertError('Error: ' + error.code); + } +} + +/** + * Changes the UI when the user has been signed out. + */ +function signOut() { + log('User successfully signed out.'); + alertSuccess('User successfully signed out.'); + refreshUserData(); +} + +/** + * Saves the new language code provided in the language code input field. + */ +function onSetLanguageCode() { + const languageCode = $('#language-code').val() || null; + try { + auth.languageCode = languageCode; + alertSuccess('Language code changed to "' + languageCode + '".'); + } catch (error) { + alertError('Error: ' + error.code); + } +} + +/** + * Switches Auth instance language to device language. + */ +function onUseDeviceLanguage() { + auth.useDeviceLanguage(); + $('#language-code').val(auth.languageCode); + alertSuccess('Using device language "' + auth.languageCode + '".'); +} + +/** + * Changes the Auth state persistence to the specified one. + */ +function onSetPersistence() { + const type = $('#persistence-type').val(); + let persistence; + switch (type) { + case 'local': + persistence = browserLocalPersistence; + break; + case 'session': + persistence = browserSessionPersistence; + break; + case 'indexedDB': + persistence = indexedDBLocalPersistence; + break; + case 'none': + persistence = inMemoryPersistence; + break; + default: + alertError('Unexpected persistence type: ' + type); + } + try { + auth.setPersistence(persistence).then( + () => { + log('Persistence state change to "' + type + '".'); + alertSuccess('Persistence state change to "' + type + '".'); + }, + error => { + alertError('Error: ' + error.code); + } + ); + } catch (error) { + alertError('Error: ' + error.code); + } +} + +/** + * Signs up a new user with an email and a password. + */ +function onSignUp() { + const email = $('#signup-email').val(); + const password = $('#signup-password').val(); + createUserWithEmailAndPassword(auth, email, password).then( + onAuthUserCredentialSuccess, + onAuthError + ); +} + +/** + * Signs in a user with an email and a password. + */ +function onSignInWithEmailAndPassword() { + const email = $('#signin-email').val(); + const password = $('#signin-password').val(); + signInWithEmailAndPassword(auth, email, password).then( + onAuthUserCredentialSuccess, + onAuthError + ); +} + +/** + * Signs in a user with an email link. + */ +function onSignInWithEmailLink() { + const email = $('#sign-in-with-email-link-email').val(); + const link = $('#sign-in-with-email-link-link').val() || undefined; + if (isSignInWithEmailLink(auth, link)) { + signInWithEmailLink(auth, email, link).then(onAuthSuccess, onAuthError); + } else { + alertError('Sign in link is invalid'); + } +} + +/** + * Links a user with an email link. + */ +function onLinkWithEmailLink() { + const email = $('#link-with-email-link-email').val(); + const link = $('#link-with-email-link-link').val() || undefined; + const credential = EmailAuthProvider.credentialWithLink(email, link); + linkWithCredential(activeUser(), credential).then( + onAuthUserCredentialSuccess, + onAuthError + ); +} + +/** + * Re-authenticate a user with email link credential. + */ +function onReauthenticateWithEmailLink() { + const email = $('#link-with-email-link-email').val(); + const link = $('#link-with-email-link-link').val() || undefined; + const credential = EmailAuthProvider.credentialWithLink(email, link); + reauthenticateWithCredential(activeUser(), credential).then(result => { + logAdditionalUserInfo(result); + refreshUserData(); + alertSuccess('User reauthenticated!'); + }, onAuthError); +} + +/** + * Signs in with a custom token. + * @param {DOMEvent} _event HTML DOM event returned by the listener. + */ +function onSignInWithCustomToken(_event) { + // The token can be directly specified on the html element. + const token = $('#user-custom-token').val(); + + signInWithCustomToken(auth, token).then( + onAuthUserCredentialSuccess, + onAuthError + ); +} + +/** + * Signs in anonymously. + */ +function onSignInAnonymously() { + signInAnonymously(auth).then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Signs in with a generic IdP credential. + */ +function onSignInWithGenericIdPCredential() { + alertNotImplemented(); + // var providerId = $('#signin-generic-idp-provider-id').val(); + // var idToken = $('#signin-generic-idp-id-token').val() || undefined; + // var rawNonce = $('#signin-generic-idp-raw-nonce').val() || undefined; + // var accessToken = $('#signin-generic-idp-access-token').val() || undefined; + // var provider = new OAuthProvider(providerId); + // signInWithCredential( + // auth, + // provider.credential({ + // idToken: idToken, + // accessToken: accessToken, + // rawNonce: rawNonce, + // })).then(onAuthUserCredentialSuccess, onAuthError); +} + +/** @return {!Object} The Action Code Settings object. */ +function getActionCodeSettings() { + const actionCodeSettings = {}; + const url = $('#continueUrl').val(); + const apn = $('#apn').val(); + const amv = $('#amv').val(); + const ibi = $('#ibi').val(); + const installApp = $('input[name=install-app]:checked').val() === 'Yes'; + const handleCodeInApp = + $('input[name=handle-in-app]:checked').val() === 'Yes'; + if (url || apn || ibi) { + actionCodeSettings['url'] = url; + if (apn) { + actionCodeSettings['android'] = { + 'packageName': apn, + 'installApp': !!installApp, + 'minimumVersion': amv || undefined + }; + } + if (ibi) { + actionCodeSettings['iOS'] = { + 'bundleId': ibi + }; + } + actionCodeSettings['handleCodeInApp'] = handleCodeInApp; + } + return actionCodeSettings; +} + +/** Reset action code settings form. */ +function onActionCodeSettingsReset() { + $('#continueUrl').val(''); + $('#apn').val(''); + $('#amv').val(''); + $('#ibi').val(''); +} + +/** + * Changes the user's email. + */ +function onChangeEmail() { + const email = $('#changed-email').val(); + updateEmail(activeUser(), email).then(() => { + refreshUserData(); + alertSuccess('Email changed!'); + }, onAuthError); +} + +/** + * Changes the user's password. + */ +function onChangePassword() { + const password = $('#changed-password').val(); + updatePassword(activeUser(), password).then(() => { + refreshUserData(); + alertSuccess('Password changed!'); + }, onAuthError); +} + +/** + * Changes the user's password. + */ +function onUpdateProfile() { + const displayName = $('#display-name').val(); + const photoURL = $('#photo-url').val(); + updateProfile(activeUser(), { + displayName, + photoURL + }).then(() => { + refreshUserData(); + alertSuccess('Profile updated!'); + }, onAuthError); +} + +/** + * Sends sign in with email link to the user. + */ +function onSendSignInLinkToEmail() { + const email = $('#sign-in-with-email-link-email').val(); + sendSignInLinkToEmail(auth, email, getActionCodeSettings()).then(() => { + alertSuccess('Email sent!'); + }, onAuthError); +} + +/** + * Sends sign in with email link to the user and pass in current url. + */ +function onSendSignInLinkToEmailCurrentUrl() { + const email = $('#sign-in-with-email-link-email').val(); + const actionCodeSettings = { + 'url': window.location.href, + 'handleCodeInApp': true + }; + + sendSignInLinkToEmail(auth, email, actionCodeSettings).then(() => { + if ('localStorage' in window && window['localStorage'] !== null) { + window.localStorage.setItem( + 'emailForSignIn', + // Save the email and the timestamp. + JSON.stringify({ + email, + timestamp: new Date().getTime() + }) + ); + } + alertSuccess('Email sent!'); + }, onAuthError); +} + +/** + * Sends email link to link the user. + */ +function onSendLinkEmailLink() { + const email = $('#link-with-email-link-email').val(); + sendSignInLinkToEmail(auth, email, getActionCodeSettings()).then(() => { + alertSuccess('Email sent!'); + }, onAuthError); +} + +/** + * Sends password reset email to the user. + */ +function onSendPasswordResetEmail() { + const email = $('#password-reset-email').val(); + sendPasswordResetEmail(auth, email, getActionCodeSettings()).then(() => { + alertSuccess('Email sent!'); + }, onAuthError); +} + +/** + * Verifies the password reset code entered by the user. + */ +function onVerifyPasswordResetCode() { + const code = $('#password-reset-code').val(); + verifyPasswordResetCode(auth, code).then(() => { + alertSuccess('Password reset code is valid!'); + }, onAuthError); +} + +/** + * Confirms the password reset with the code and password supplied by the user. + */ +function onConfirmPasswordReset() { + const code = $('#password-reset-code').val(); + const password = $('#password-reset-password').val(); + confirmPasswordReset(auth, code, password).then(() => { + alertSuccess('Password has been changed!'); + }, onAuthError); +} + +/** + * Gets the list of possible sign in methods for the given email address. + */ +function onFetchSignInMethodsForEmail() { + const email = $('#fetch-sign-in-methods-email').val(); + fetchSignInMethodsForEmail(auth, email).then(signInMethods => { + log('Sign in methods for ' + email + ' :'); + log(signInMethods); + if (signInMethods.length === 0) { + alertSuccess('Sign In Methods for ' + email + ': N/A'); + } else { + alertSuccess( + 'Sign In Methods for ' + email + ': ' + signInMethods.join(', ') + ); + } + }, onAuthError); +} + +/** + * Fetches and logs the user's providers data. + */ +function onGetProviderData() { + log('Providers data:'); + log(activeUser()['providerData']); +} + +/** + * Links a signed in user with an email and password account. + */ +function onLinkWithEmailAndPassword() { + const email = $('#link-email').val(); + const password = $('#link-password').val(); + linkWithCredential( + activeUser(), + EmailAuthProvider.credential(email, password) + ).then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Links with a generic IdP credential. + */ +function onLinkWithGenericIdPCredential() { + alertNotImplemented(); + // var providerId = $('#link-generic-idp-provider-id').val(); + // var idToken = $('#link-generic-idp-id-token').val() || undefined; + // var rawNonce = $('#link-generic-idp-raw-nonce').val() || undefined; + // var accessToken = $('#link-generic-idp-access-token').val() || undefined; + // var provider = new OAuthProvider(providerId); + // activeUser().linkWithCredential( + // provider.credential({ + // idToken: idToken, + // accessToken: accessToken, + // rawNonce: rawNonce, + // })).then(onAuthUserCredentialSuccess, onAuthError); +} + +/** + * Unlinks the specified provider. + */ +function onUnlinkProvider() { + const providerId = $('#unlinked-provider-id').val(); + unlink(activeUser(), providerId).then(_user => { + alertSuccess('Provider unlinked from user.'); + refreshUserData(); + }, onAuthError); +} + +/** + * Sends email verification to the user. + */ +function onSendEmailVerification() { + sendEmailVerification(activeUser(), getActionCodeSettings()).then(() => { + alertSuccess('Email verification sent!'); + }, onAuthError); +} + +/** + * Confirms the email verification code given. + */ +function onApplyActionCode() { + var code = $('#email-verification-code').val(); + applyActionCode(auth, code).then(function () { + alertSuccess('Email successfully verified!'); + refreshUserData(); + }, onAuthError); +} + +/** + * Gets or refreshes the ID token. + * @param {boolean} forceRefresh Whether to force the refresh of the token + * or not. + */ +function getIdToken(forceRefresh) { + if (activeUser() == null) { + alertError('No user logged in.'); + return; + } + if (activeUser().getIdToken) { + activeUser() + .getIdToken(forceRefresh) + .then(alertSuccess, () => { + log('No token'); + }); + } else { + activeUser() + .getToken(forceRefresh) + .then(alertSuccess, () => { + log('No token'); + }); + } +} + +/** + * Gets or refreshes the ID token result. + * @param {boolean} forceRefresh Whether to force the refresh of the token + * or not + */ +function getIdTokenResult(forceRefresh) { + if (activeUser() == null) { + alertError('No user logged in.'); + return; + } + activeUser() + .getIdTokenResult(forceRefresh) + .then(idTokenResult => { + alertSuccess(JSON.stringify(idTokenResult)); + }, onAuthError); +} + +/** + * Triggers the retrieval of the ID token result. + */ +function onGetIdTokenResult() { + getIdTokenResult(false); +} + +/** + * Triggers the refresh of the ID token result. + */ +function onRefreshTokenResult() { + getIdTokenResult(true); +} + +/** + * Triggers the retrieval of the ID token. + */ +function onGetIdToken() { + getIdToken(false); +} + +/** + * Triggers the refresh of the ID token. + */ +function onRefreshToken() { + getIdToken(true); +} + +/** + * Signs out the user. + */ +function onSignOut() { + setLastUser(auth.currentUser); + auth.signOut().then(signOut, onAuthError); +} + +/** + * Handles multi-factor sign-in completion. + * @param {!MultiFactorResolver} resolver The multi-factor error + * resolver. + */ +function handleMultiFactorSignIn(resolver) { + // Save multi-factor error resolver. + multiFactorErrorResolver = resolver; + // Populate 2nd factor options from resolver. + const $listGroup = $('#multiFactorModal div.enrolled-second-factors'); + // Populate the list of 2nd factors in the list group specified. + showMultiFactors( + $listGroup, + multiFactorErrorResolver.hints, + // On row click, select the corresponding second factor to complete + // sign-in with. + function (e) { + e.preventDefault(); + // Remove all other active entries. + $listGroup.find('a').removeClass('active'); + // Mark current entry as active. + $(this).addClass('active'); + // Select current factor. + onSelectMultiFactorHint(parseInt($(this).attr('data-index'), 10)); + }, + // Do not show delete option + null + ); + // Hide phone form (other second factor types could be supported). + $('#multi-factor-phone').addClass('hidden'); + // Show second factor recovery dialog. + $('#multiFactorModal').modal(); +} + +/** + * Displays the list of multi-factors in the provided list group. + * @param {!jQuery} $listGroup The list group where the enrolled + * factors will be displayed. + * @param {!Array} multiFactorInfo The list of + * multi-factors to display. + * @param {?function(!jQuery.Event)} onClick The click handler when a second + * factor is clicked. + * @param {?function(!jQuery.Event)} onDelete The click handler when a second + * factor is delete. If not provided, no delete button is shown. + */ +function showMultiFactors($listGroup, multiFactorInfo, onClick, onDelete) { + // Append entry to list. + $listGroup.empty(); + $.each(multiFactorInfo, i => { + // Append entry to list. + const info = multiFactorInfo[i]; + const displayName = info.displayName || 'N/A'; + const $a = $('
') + .addClass('list-group-item') + .addClass('list-group-item-action') + // Set index on entry. + .attr('data-index', i) + .appendTo($listGroup); + $a.append($('

').text(info.uid)); + $a.append($('').text(info.factorId)); + $a.append($('

').text(displayName)); + if (info.phoneNumber) { + $a.append($('').text(info.phoneNumber)); + } + // Check if a delete button is to be displayed. + if (onDelete) { + const $deleteBtn = $( + '' + + '' + + '' + ); + // Append delete button to row. + $a.append($deleteBtn); + // Add delete button click handler. + $a.find('button.delete-factor').click(onDelete); + } + // On entry click. + if (onClick) { + $a.click(onClick); + } + }); +} + +/** + * Handles the user selection of second factor to complete sign-in with. + * @param {number} index The selected multi-factor hint index. + */ +function onSelectMultiFactorHint(index) { + // Hide all forms for handling each type of second factors. + // Currently only phone is supported. + $('#multi-factor-phone').addClass('hidden'); + if ( + !multiFactorErrorResolver || + typeof multiFactorErrorResolver.hints[index] === 'undefined' + ) { + return; + } + + if (multiFactorErrorResolver.hints[index].factorId === 'phone') { + // Save selected second factor. + selectedMultiFactorHint = multiFactorErrorResolver.hints[index]; + // Show options for phone 2nd factor. + // Get reCAPTCHA ready. + clearApplicationVerifier(); + makeApplicationVerifier('send-2fa-phone-code'); + // Show sign-in with phone second factor menu. + $('#multi-factor-phone').removeClass('hidden'); + // Clear all input. + $('#multi-factor-sign-in-verification-id').val(''); + $('#multi-factor-sign-in-verification-code').val(''); + } else { + // 2nd factor not found or not supported by app. + alertError('Selected 2nd factor is not supported!'); + } +} + +/** + * Adds a new row to insert an OAuth custom parameter key/value pair. + * @param {!jQuery.Event} _event The jQuery event object. + */ +function onPopupRedirectAddCustomParam(_event) { + // Form container. + let html = '

'; + // OAuth parameter key input. + html += + ''; + // OAuth parameter value input. + html += + ''; + // Button to remove current key/value pair. + html += ''; + html += ''; + // Create jQuery node. + const $node = $(html); + // Add button click event listener to remove item. + $node.find('button').on('click', function (e) { + // Remove button click event listener. + $(this).off('click'); + // Get row container and remove it. + $(this).closest('form.customParamItem').remove(); + e.preventDefault(); + }); + // Append constructed row to parameter list container. + $('#popup-redirect-custom-parameters').append($node); +} + +/** + * Performs the corresponding popup/redirect action for a generic provider. + */ +function onPopupRedirectGenericProviderClick() { + var providerId = $('#popup-redirect-generic-providerid').val(); + var provider = new OAuthProvider(providerId); + signInWithPopupRedirect(provider); +} + +/** + * Performs the corresponding popup/redirect action for a SAML provider. + */ +function onPopupRedirectSamlProviderClick() { + alertNotImplemented(); + // var providerId = $('#popup-redirect-saml-providerid').val(); + // var provider = new SAMLAuthProvider(providerId); + // signInWithPopupRedirect(provider); +} + +/** + * Performs the corresponding popup/redirect action based on user's selection. + * @param {!jQuery.Event} _event The jQuery event object. + */ +function onPopupRedirectProviderClick(_event) { + const providerId = $(event.currentTarget).data('provider'); + let provider = null; + switch (providerId) { + case 'google.com': + provider = new GoogleAuthProvider(); + break; + case 'facebook.com': + provider = new FacebookAuthProvider(); + break; + case 'github.com': + provider = new GithubAuthProvider(); + break; + case 'twitter.com': + provider = new TwitterAuthProvider(); + break; + default: + return; + } + signInWithPopupRedirect(provider); +} + +/** + * Performs a popup/redirect action based on a given provider and the user's + * selections. + * @param {!AuthProvider} provider The provider with which to + * sign in. + */ +function signInWithPopupRedirect(provider) { + let action = $('input[name=popup-redirect-action]:checked').val(); + let type = $('input[name=popup-redirect-type]:checked').val(); + let method = null; + let inst = null; + + switch (action) { + case 'link': + if (!activeUser()) { + alertError('No user logged in.'); + return; + } + inst = activeUser(); + method = type === 'popup' ? alertNotImplemented() : linkWithRedirect; + break; + case 'reauthenticate': + if (!activeUser()) { + alertError('No user logged in.'); + return; + } + inst = activeUser(); + method = + type === 'popup' ? alertNotImplemented() : reauthenticateWithRedirect; + break; + default: + inst = auth; + method = type === 'popup' ? alertNotImplemented() : signInWithRedirect; + } + + // Get custom OAuth parameters. + const customParameters = {}; + // For each entry. + $('form.customParamItem').each(function (_index) { + // Get parameter key. + const key = $(this).find('input.customParamKey').val(); + // Get parameter value. + const value = $(this).find('input.customParamValue').val(); + // Save to list if valid. + if (key && value) { + customParameters[key] = value; + } + }); + console.log('customParameters: ', customParameters); + // For older jscore versions that do not support this. + if (provider.setCustomParameters) { + // Set custom parameters on current provider. + provider.setCustomParameters(customParameters); + } + + // Add scopes for providers who do have scopes available (i.e. not Twitter). + if (provider.addScope) { + // String.prototype.trim not available in IE8. + const scopes = $.trim($('#scopes').val()).split(/\s*,\s*/); + for (let i = 0; i < scopes.length; i++) { + provider.addScope(scopes[i]); + } + } + console.log('Provider:'); + console.log(provider); + method(inst, provider, cordovaPopupRedirectResolver) + .then(() => getRedirectResult(auth)) + .then(response => { + console.log('Popup response:'); + console.log(response); + alertSuccess(action + ' with ' + provider['providerId'] + ' successful!'); + logAdditionalUserInfo(response); + onAuthSuccess(activeUser()); + }, onAuthError); +} + +/** + * Displays user credential result. + * @param {!UserCredential} result The UserCredential result + * object. + */ +function onAuthUserCredentialSuccess(result) { + onAuthSuccess(result.user); + logAdditionalUserInfo(result); +} + +/** + * Displays redirect result. + */ +function onGetRedirectResult() { + getRedirectResult(auth, cordovaPopupRedirectResolver).then(function ( + response + ) { + log('Redirect results:'); + if (response.credential) { + log('Credential:'); + log(response.credential); + } else { + log('No credential'); + } + if (response.user) { + log("User's id:"); + log(response.user.uid); + } else { + log('No user'); + } + logAdditionalUserInfo(response); + console.log(response); + }, + onAuthError); +} + +/** + * Logs additional user info returned by a sign-in event, if available. + * @param {!Object} response + */ +function logAdditionalUserInfo(response) { + if (response?.additionalUserInfo) { + if (response.additionalUserInfo.username) { + log( + response.additionalUserInfo['providerId'] + + ' username: ' + + response.additionalUserInfo.username + ); + } + if (response.additionalUserInfo.profile) { + log(response.additionalUserInfo['providerId'] + ' profile information:'); + log(JSON.stringify(response.additionalUserInfo.profile, null, 2)); + } + if (typeof response.additionalUserInfo.isNewUser !== 'undefined') { + log( + response.additionalUserInfo['providerId'] + + ' isNewUser: ' + + response.additionalUserInfo.isNewUser + ); + } + if (response.credential) { + log('credential: ' + JSON.stringify(response.credential.toJSON())); + } + } +} + +/** + * Deletes the user account. + */ +function onDelete() { + activeUser() + ['delete']() + .then(() => { + log('User successfully deleted.'); + alertSuccess('User successfully deleted.'); + refreshUserData(); + }, onAuthError); +} + +/** + * Gets a specific query parameter from the current URL. + * @param {string} name Name of the parameter. + * @return {string} The query parameter requested. + */ +function getParameterByName(name) { + const url = window.location.href; + name = name.replace(/[\[\]]/g, '\\$&'); + const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); + const results = regex.exec(url); + if (!results) { + return null; + } + if (!results[2]) { + return ''; + } + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +} + +/** + * Detects if an action code is passed in the URL, and populates accordingly + * the input field for the confirm email verification process. + */ +function populateActionCodes() { + let emailForSignIn = null; + let signInTime = 0; + if ('localStorage' in window && window['localStorage'] !== null) { + try { + // Try to parse as JSON first using new storage format. + const emailForSignInData = JSON.parse( + window.localStorage.getItem('emailForSignIn') + ); + emailForSignIn = emailForSignInData['email'] || null; + signInTime = emailForSignInData['timestamp'] || 0; + } catch (e) { + // JSON parsing failed. This means the email is stored in the old string + // format. + emailForSignIn = window.localStorage.getItem('emailForSignIn'); + } + if (emailForSignIn) { + // Clear old codes. Old format codes should be cleared immediately. + if (new Date().getTime() - signInTime >= 1 * 24 * 3600 * 1000) { + // Remove email from storage. + window.localStorage.removeItem('emailForSignIn'); + } + } + } + const actionCode = getParameterByName('oobCode'); + if (actionCode != null) { + const mode = getParameterByName('mode'); + if (mode === 'verifyEmail') { + $('#email-verification-code').val(actionCode); + } else if (mode === 'resetPassword') { + $('#password-reset-code').val(actionCode); + } else if (mode === 'signIn') { + if (emailForSignIn) { + $('#sign-in-with-email-link-email').val(emailForSignIn); + $('#sign-in-with-email-link-link').val(window.location.href); + onSignInWithEmailLink(); + // Remove email from storage as the code is only usable once. + window.localStorage.removeItem('emailForSignIn'); + } + } else { + $('#email-verification-code').val(actionCode); + $('#password-reset-code').val(actionCode); + } + } +} + +/** + * Provides basic Database checks for authenticated and unauthenticated access. + * The Database node being tested has the following rule: + * "users": { + * "$user_id": { + * ".read": "$user_id === auth.uid", + * ".write": "$user_id === auth.uid" + * } + * } + * This applies when Real-time database service is available. + */ +function checkDatabaseAuthAccess() { + const randomString = Math.floor(Math.random() * 10000000).toString(); + let dbRef; + let dbPath; + let errMessage; + // Run this check only when Database module is available. + if ( + typeof firebase !== 'undefined' && + typeof firebase.database !== 'undefined' + ) { + if (lastUser && !auth.currentUser) { + dbPath = 'users/' + lastUser.uid; + // After sign out, confirm read/write access to users/$user_id blocked. + dbRef = firebase.database().ref(dbPath); + dbRef + .set({ + 'test': randomString + }) + .then(() => { + alertError( + 'Error: Unauthenticated write to Database node ' + + dbPath + + ' unexpectedly succeeded!' + ); + }) + .catch(error => { + errMessage = error.message.toLowerCase(); + // Permission denied error should be thrown. + if (errMessage.indexOf('permission_denied') === -1) { + alertError('Error: ' + error.code); + return; + } + dbRef + .once('value') + .then(() => { + alertError( + 'Error: Unauthenticated read to Database node ' + + dbPath + + ' unexpectedly succeeded!' + ); + }) + .catch(error => { + errMessage = error.message.toLowerCase(); + // Permission denied error should be thrown. + if (errMessage.indexOf('permission_denied') === -1) { + alertError('Error: ' + error.code); + return; + } + log( + 'Unauthenticated read/write to Database node ' + + dbPath + + ' failed as expected!' + ); + }); + }); + } else if (auth.currentUser) { + dbPath = 'users/' + auth.currentUser.uid; + // Confirm read/write access to users/$user_id allowed. + dbRef = firebase.database().ref(dbPath); + dbRef + .set({ + 'test': randomString + }) + .then(() => { + return dbRef.once('value'); + }) + .then(snapshot => { + if (snapshot.val().test === randomString) { + // read/write successful. + log( + 'Authenticated read/write to Database node ' + + dbPath + + ' succeeded!' + ); + } else { + throw new Error( + 'Authenticated read/write to Database node ' + dbPath + ' failed!' + ); + } + // Clean up: clear that node's content. + return dbRef.remove(); + }) + .catch(error => { + alertError('Error: ' + error.code); + }); + } + } +} + +/** Copy current user of auth to tempAuth. */ +function onCopyActiveUser() { + tempAuth.updateCurrentUser(activeUser()).then( + () => { + alertSuccess('Copied active user to temp Auth'); + }, + error => { + alertError('Error: ' + error.code); + } + ); +} + +/** Copy last user to auth. */ +function onCopyLastUser() { + // If last user is null, NULL_USER error will be thrown. + auth.updateCurrentUser(lastUser).then( + () => { + alertSuccess('Copied last user to Auth'); + }, + error => { + alertError('Error: ' + error.code); + } + ); +} + +/** Applies selected auth settings change. */ +function onApplyAuthSettingsChange() { + try { + auth.settings.appVerificationDisabledForTesting = + $('input[name=enable-app-verification]:checked').val() === 'No'; + alertSuccess('Auth settings changed'); + } catch (error) { + alertError('Error: ' + error.code); + } +} + +/** + * Initiates the application by setting event listeners on the various buttons. + */ +function initApp() { + log('Initializing app...'); + app = initializeApp(config); + auth = getAuth(app); + + tempApp = initializeApp( + { + apiKey: config.apiKey, + authDomain: config.authDomain + }, + `${auth.name}-temp` + ); + tempAuth = initializeAuth(tempApp, { + persistence: inMemoryPersistence, + popupRedirectResolver: cordovaPopupRedirectResolver + }); + + // Listen to reCAPTCHA config togglers. + initRecaptchaToggle(size => { + clearApplicationVerifier(); + recaptchaSize = size; + }); + + // The action code for email verification or password reset + // can be passed in the url address as a parameter, and for convenience + // this preloads the input field. + populateActionCodes(); + + // Allows to login the user if previously logged in. + if (auth.onIdTokenChanged) { + auth.onIdTokenChanged(user => { + refreshUserData(); + if (user) { + user.getIdTokenResult(false).then( + idTokenResult => { + log(JSON.stringify(idTokenResult)); + }, + () => { + log('No token.'); + } + ); + } else { + log('No user logged in.'); + } + }); + } + + if (auth.onAuthStateChanged) { + auth.onAuthStateChanged(user => { + if (user) { + log('user state change detected: ' + user.uid); + } else { + log('user state change detected: no user'); + } + // Check Database Auth access. + checkDatabaseAuthAccess(); + }); + } + + if (tempAuth.onAuthStateChanged) { + tempAuth.onAuthStateChanged(user => { + if (user) { + log('user state change on temp Auth detect: ' + JSON.stringify(user)); + alertSuccess('user state change on temp Auth detect: ' + user.uid); + } + }); + } + + /** + * @fileoverview Utilities for Auth test app features. + */ + + /** + * Initializes the widget for toggling reCAPTCHA size. + * @param {function(string):void} callback The callback to call when the + * size toggler is changed, which takes in the new reCAPTCHA size. + */ + function initRecaptchaToggle(callback) { + // Listen to recaptcha config togglers. + const $recaptchaConfigTogglers = $('.toggleRecaptcha'); + $recaptchaConfigTogglers.click(function (e) { + // Remove currently active option. + $recaptchaConfigTogglers.removeClass('active'); + // Set currently selected option. + $(this).addClass('active'); + // Get the current reCAPTCHA setting label. + const size = $(e.target).text().toLowerCase(); + callback(size); + }); + } + + // Install servicerWorker if supported. + if ('serviceWorker' in navigator) { + navigator.serviceWorker + .register('/service-worker.js') + .then(reg => { + // Registration worked. + console.log('Registration succeeded. Scope is ' + reg.scope); + }) + .catch(error => { + // Registration failed. + console.log('Registration failed with ' + error.message); + }); + } + + if (window.Worker) { + webWorker = new Worker('/web-worker.js'); + /** + * Handles the incoming message from the web worker. + * @param {!Object} e The message event received. + */ + webWorker.onmessage = function (e) { + console.log('User data passed through web worker: ', e.data); + switch (e.data.type) { + case 'GET_USER_INFO': + alertSuccess( + 'User data passed through web worker: ' + JSON.stringify(e.data) + ); + break; + case 'RUN_TESTS': + if (e.data.status === 'success') { + alertSuccess('Web worker tests ran successfully!'); + } else { + alertError('Error: ' + JSON.stringify(e.data.error)); + } + break; + default: + return; + } + }; + } + + /** + * Asks the web worker, if supported in current browser, to return the user info + * corresponding to the currentUser as seen within the worker. + */ + function onGetCurrentUserDataFromWebWorker() { + if (webWorker) { + webWorker.postMessage({ type: 'GET_USER_INFO' }); + } else { + alertError( + 'Error: Web workers are not supported in the current browser!' + ); + } + } + + // We check for redirect result to refresh user's data. + getRedirectResult(auth, cordovaPopupRedirectResolver).then(function ( + response + ) { + refreshUserData(); + logAdditionalUserInfo(response); + }, + onAuthError); + + // Bootstrap tooltips. + $('[data-toggle="tooltip"]').tooltip(); + + // Auto submit the choose library type form. + $('#library-form').on('change', 'input.library-option', () => { + $('#library-form').submit(); + }); + + // To clear the logs in the page. + $('.clear-logs').click(clearLogs); + + // Disables JS forms. + $('form.no-submit').on('submit', () => { + return false; + }); + + // Keeps track of the current tab opened. + $('#tab-menu a').click(event => { + currentTab = $(event.currentTarget).attr('href'); + }); + + // Toggles user. + $('input[name=toggle-user-selection]').change(refreshUserData); + + // Actions listeners. + $('#sign-up-with-email-and-password').click(onSignUp); + $('#sign-in-with-email-and-password').click(onSignInWithEmailAndPassword); + $('.sign-in-with-custom-token').click(onSignInWithCustomToken); + $('#sign-in-anonymously').click(onSignInAnonymously); + $('#sign-in-with-generic-idp-credential').click( + onSignInWithGenericIdPCredential + ); + $('#sign-in-with-email-link').click(onSignInWithEmailLink); + $('#link-with-email-link').click(onLinkWithEmailLink); + $('#reauth-with-email-link').click(onReauthenticateWithEmailLink); + + $('#change-email').click(onChangeEmail); + $('#change-password').click(onChangePassword); + $('#update-profile').click(onUpdateProfile); + + $('#send-sign-in-link-to-email').click(onSendSignInLinkToEmail); + $('#send-sign-in-link-to-email-current-url').click( + onSendSignInLinkToEmailCurrentUrl + ); + $('#send-link-email-link').click(onSendLinkEmailLink); + + $('#send-password-reset-email').click(onSendPasswordResetEmail); + $('#verify-password-reset-code').click(onVerifyPasswordResetCode); + $('#confirm-password-reset').click(onConfirmPasswordReset); + + $('#get-provider-data').click(onGetProviderData); + $('#link-with-email-and-password').click(onLinkWithEmailAndPassword); + $('#link-with-generic-idp-credential').click(onLinkWithGenericIdPCredential); + $('#unlink-provider').click(onUnlinkProvider); + + $('#send-email-verification').click(onSendEmailVerification); + $('#confirm-email-verification').click(onApplyActionCode); + $('#get-token-result').click(onGetIdTokenResult); + $('#refresh-token-result').click(onRefreshTokenResult); + $('#get-token').click(onGetIdToken); + $('#refresh-token').click(onRefreshToken); + $('#get-token-worker').click(onGetCurrentUserDataFromWebWorker); + $('#sign-out').click(onSignOut); + + $('.popup-redirect-provider').click(onPopupRedirectProviderClick); + $('#popup-redirect-generic').click(onPopupRedirectGenericProviderClick); + $('#popup-redirect-get-redirect-result').click(onGetRedirectResult); + $('#popup-redirect-add-custom-parameter').click( + onPopupRedirectAddCustomParam + ); + $('#popup-redirect-saml').click(onPopupRedirectSamlProviderClick); + + $('#action-code-settings-reset').click(onActionCodeSettingsReset); + + $('#delete').click(onDelete); + + $('#set-persistence').click(onSetPersistence); + + $('#set-language-code').click(onSetLanguageCode); + $('#use-device-language').click(onUseDeviceLanguage); + + $('#fetch-sign-in-methods-for-email').click(onFetchSignInMethodsForEmail); + + $('#copy-active-user').click(onCopyActiveUser); + $('#copy-last-user').click(onCopyLastUser); + + $('#apply-auth-settings-change').click(onApplyAuthSettingsChange); +} + +document.addEventListener( + 'deviceready', + function () { + initApp(); + }, + false +); diff --git a/packages-exp/auth-exp/demo/src/logging.js b/packages/auth/cordova/demo/src/logging.js similarity index 100% rename from packages-exp/auth-exp/demo/src/logging.js rename to packages/auth/cordova/demo/src/logging.js diff --git a/packages-exp/auth-exp/demo/src/sample-config.js b/packages/auth/cordova/demo/src/sample-config.js similarity index 100% rename from packages-exp/auth-exp/demo/src/sample-config.js rename to packages/auth/cordova/demo/src/sample-config.js diff --git a/packages/auth/cordova/demo/www/index.html b/packages/auth/cordova/demo/www/index.html new file mode 100644 index 00000000000..5816e27c4a0 --- /dev/null +++ b/packages/auth/cordova/demo/www/index.html @@ -0,0 +1,680 @@ + + + + + + + Headless App + + + + + + + + + + + +
+ +
+ + + + + +
+
+ + +
+
+ + +
+
+ + + [anonymous] / + uid: + +
+
+ + / + + + + + + + +
+ + + +
+
+
+
+ + +
+ +
+
+
+ + + +
+
+ + +
Auth State Persistence
+
+ + +
+ + +
Language code
+
+ + + +
+ + +
Sign Up
+
+ + + +
+ + + +
Sign In
+
+ + + +
+
+ + +
+ +
+ + + + + +
+ + +
Sign In with Email Link
+
+ + + +
+
+ + +
+ + +
Password Reset
+
+ + +
+
+ + + + +
+ +
Fetch Sign In Methods
+
+ + +
+ + +
Update Current User
+
+ +
+
+ +
+
+
+ +
Update Profile
+
+ + +
+
+ + +
+
+ + + +
+ + + +
Linking/Unlinking
+ +
+ + + +
+
+ + + + + +
+ +
+ + + + + +
+ +
+ + +
+ + +
Enroll Second Factor
+ + + +
Other Actions
+ +
+ + +
+ + + + + + + +
Delete account
+ +
+ +
+
Web
+
+ +
+
Android
+
+
+ +
+ + +
+ +
+
+
iOS
+
+
+ +
+
+
+
+ + +
+ +
+
+
+

+              
+            
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/auth/cordova/demo/www/style.css b/packages/auth/cordova/demo/www/style.css new file mode 100644 index 00000000000..e16d82e1154 --- /dev/null +++ b/packages/auth/cordova/demo/www/style.css @@ -0,0 +1,207 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + body { + padding-top: 70px; +} +body.user-info-displayed { + padding-top: 120px; +} + +@media (min-width: 768px) { + body { + padding-bottom: 100px; + } +} +@media (max-width: 767px) { + body { + padding-bottom: 15px; + } +} + +#tab-menu { + margin-bottom: 15px; +} + +#user-info { + display: none; + padding: 10px 15px; + position: fixed; + top: 50px; + width: 100%; + z-index: 1000; +} + +#toggle-user-placeholder { + margin-bottom: 15px; +} + +#user-info.current-user, +#toggle-user-placeholder .current-user, +#toggle-user label.active.current-user { + background-color: #d6e9c6; + border: 1px solid #dff0d8; + color: #3c763d; +} + +#user-info.last-user, +#toggle-user label.active.last-user { + background-color: #d9edf7; + border: 1px solid #bce8f1; + color: #31708f; +} + +#recaptcha-container { + border: 5px solid red; + bottom: 0; + position: fixed; + right: 0; + z-index: 1500; +} + +#recaptcha-container:empty { + border: none; +} + +.logs { + color: #555; + font-family: 'Courier New', Courier; + font-size: 0.9em; + word-wrap: break-word; +} + +.logs > .error { + color: #d9534f; +} + +/* Margin top for small screens when the logs are below the buttons */ +@media (max-width: 767px) { + .logs { + margin-top: 20px; + } +} + +.overlaying-alert { + bottom: 15px; + pointer-events: none; + position: fixed; + width: 100%; + word-wrap: break-word; + z-index: 1010; +} + +.actions { + margin-bottom: 15px; +} + +.group { + border-top: 1px solid #555; + color: #555; + font-size: 0.9em; + font-weight: bold; + letter-spacing: 0.2em; + margin-bottom: 15px; + padding: 2px 0 0 10px; +} + +button + .group, +div + .group, +.btn-block + .group, +.form + .group { + margin-top: 30px; +} + +.form { + text-align: center; +} + +.form-bordered { + border: 1px solid #CCC; + border-radius: 9px; + padding: 5px; +} + +button + .form, +input + .form, +.form + .form { + margin: 15px 0; +} + +.form + button, +.form-control + .btn-block, +.form-control + .form-control { + margin-top: 5px; +} + +/* Bootstrap .hidden adds the !important which invalidates jQuery .show() */ +.hidden, +.hide, +.profile, +.overlaying-alert > .alert, +#toggle-user { + display: none; +} + +.profile { + line-height: 30px; +} + +@media (max-width: 767px) { + .profile { + /* Use a smaller line height for small screens so user information doesn't + take up half the screen. */ + line-height: normal; + } +} + +.profile-uid { + font-family: 'Courier New', Courier; +} + +.profile-image { + float: left; + height: 30px; + margin-right: 10px; +} + +.profile-email-not-verified { + color: #d9534f; +} + +.profile-providers { + color: #333; +} + +.profile-providers > i { + margin: 0 5px; +} + +.radio-block { + margin-bottom: 15px; + width: 100%; +} + +.radio-block > label { + width: 50%; +} + +/** Overrides default drop down menu styles for enrolled factors display. */ +.enrolled-second-factors { + left: initial; + min-width: 400px; + right: 0px; + width: 100%; +} diff --git a/packages/auth/cordova/package.json b/packages/auth/cordova/package.json new file mode 100644 index 00000000000..aef216664f4 --- /dev/null +++ b/packages/auth/cordova/package.json @@ -0,0 +1,7 @@ +{ + "name": "@firebase/auth-cordova", + "description": "A Cordova-specific build of the Firebase Auth JS SDK", + "browser": "../dist/cordova/index.js", + "module": "../dist/cordova/index.js", + "typings": "../dist/cordova/auth-cordova-public.d.ts" +} \ No newline at end of file diff --git a/packages/auth/demo/.eslintignore b/packages/auth/demo/.eslintignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages-exp/auth-exp/demo/.eslintrc.js b/packages/auth/demo/.eslintrc.js similarity index 100% rename from packages-exp/auth-exp/demo/.eslintrc.js rename to packages/auth/demo/.eslintrc.js diff --git a/packages/auth/demo/.gitignore b/packages/auth/demo/.gitignore index dbb58ffbfa3..798f66cee47 100644 --- a/packages/auth/demo/.gitignore +++ b/packages/auth/demo/.gitignore @@ -1,66 +1,6 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -firebase-debug.log* -firebase-debug.*.log* - -# Firebase cache -.firebase/ - -# Firebase config - -# Uncomment this if you'd like others to create their own Firebase project. -# For a team working on the same Firebase project(s), it is recommended to leave -# it commented so all members can deploy to the same project(s) in .firebaserc. -# .firebaserc - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env +src/config.js +.firebaserc +.firebase +public/service-worker.* +public/web-worker.* +public/index.js* \ No newline at end of file diff --git a/packages/auth/demo/README.md b/packages/auth/demo/README.md index 12ff0f9ab58..c83177a5e05 100644 --- a/packages/auth/demo/README.md +++ b/packages/auth/demo/README.md @@ -1,6 +1,8 @@ -# Firebase-Auth for web - Auth Demo +# Firebase-Auth for web - Auth Demo (Auth Next) -## Prerequisite +## Prerequisites + +You need all the prerequisites mentioned in the [top-level README.](https://github.com/firebase/firebase-js-sdk#prerequisites) You need to have created a Firebase Project in the [Firebase Console](https://firebase.google.com/console/) as well as configured a web app. @@ -30,22 +32,36 @@ firebase use --add Select the project you have created in the prerequisite, and type in `default` or any other name as the alias to use for this project. -Copy `public/sample-config.js` to `public/config.js`: +Copy `src/sample-config.js` to `src/config.js`: ```bash -cp public/sample-config.js public/config.js +cp src/sample-config.js src/config.js ``` -Then copy and paste the Web snippet config found in the console (either by clicking "Add Firebase to -your web app" button in your Project overview, or clicking the "Web setup" button in the Auth page) +Then copy and paste the Web snippet config found in the console (Project Settings -> Your apps -> SDK setup and Configuration) in the `config.js` file. ## Deploy -### Option 1: Compile and use local Firebase Auth files +Before deploying, you may need to build the auth package: +```bash +cd auth/demo +yarn +yarn build:deps +``` + +This can take some time, and you only need to do it if you've modified the auth package. + +You can optionally clear the cache and rebuild using: + +```bash +cd auth/demo +rm -rf node_modules yarn.lock +yarn +yarn build:deps +``` -To deploy the demo app, run the following command in the root directory of Firebase Auth (use `cd ..` -first if you are still in the `demo/` folder): +To run the app locally, simply issue the following command in the `auth/demo` directory: ```bash yarn run demo @@ -54,28 +70,106 @@ yarn run demo This will compile all the files needed to run Firebase Auth, and start a Firebase server locally at [http://localhost:5000](http://localhost:5000). -### Option 2: Use CDN hosted Firebase files +The demo opens a page like this: + +![image](https://user-images.githubusercontent.com/35932340/153662957-41ba6a82-ea15-4084-ad3a-9fd41083efd3.png) + + +This is a developer view of all the supported auth flows. Make sure that the auth flow you are testing is already enabled in your firebase project. +For example, if you are testing “Sign up with email/password”, your project should allow email/password as a provider. +If not, you will see an “auth/operation-not-allowed” error message. + +You can check the enabled providers on the firebase console. + +![image](https://user-images.githubusercontent.com/35932340/153662750-c0faf417-07b4-4f0e-93ab-5e0b82e3c793.png) -If you would prefer to use a CDN instead of locally compiled Firebase Auth files, you can instead -locate the following in the `` tag of `public/index.html`: -```html - - - +## Running against Auth Emulator + +The demo page by default runs against the actual Auth Backend. To run against the Auth Emulator with mocked endpoints, do the following: + +1. (Optional) If you are running against local changes to the Auth Emulator (see [Firebase CLI Contributing Guide](https://github.com/firebase/firebase-tools/blob/master/CONTRIBUTING.md)), make sure that your version of `firebase-tools` is executing against your `npm link`’d repository and that you've built your local emulator changes with `npm run build`. + +2. From `auth/demo`, locally initialize a Firebase project by running `firebase init`. When asked "Which Firebase features do you want to set up for this directory?", select "Emulators". When asked "Which Firebase emulators do you want to set up?", select "Authentication Emulator". This will create a `firebase.json` file. + +3. Change the constants `USE_AUTH_EMULATOR` and `AUTH_EMULATOR_URL` in `auth/demo/src/index.js` to `true` and `http://localhost:{port}` where the auth `port` can be found in the `firebase.json` file. + +4. To run the app locally and against the Auth Emulator, simply issue the following command in the `auth/demo` directory: + +```bash +yarn run demo:emulator ``` -Then replace that with the public CDN: +## Running against Auth Staging endpoint + +Modify the configured endpoint to staging by following the changes in this branch: + +https://github.com/firebase/firebase-js-sdk/compare/use-staging + +## Running against local changes to auth package + -```html - +By default, the demo runs against the local firebase-auth implementation in packages/auth/src. +This can be modified to point to a released version using: + +``` +// packages/auth/demo/package.json +- "@firebase/auth": "file:..", ++ "@firebase/auth": "0.18.0", ``` -Finally, ensure you are in the `demo/` folder (and not the root directory of Firebase Auth package), -and run: +## Troubleshooting + +### Errors about dependency not being installed, example `lerna: command not found` + + Ensure that you run `yarn` to install dependencies. + +### `Failed to get Firebase project . Please make sure the project exists and your account has permission to access it.` + +Logout, re-login and launch the demo ```bash -yarn run demo +firebase logout && firebase login && yarn demo +``` + +### `Failed to list firebase projects` when running `firebase use --add` + +Logout, re-login and add the project. + +```bash +firebase logout && firebase login && firebase use --add ``` -This will start a Firebase server locally at [http://localhost:5000](http://localhost:5000). +### `Access to localhost was denied` when accessing the demo app via http://localhost:5000 + +Most likely this means a different process is binding to localhost:5000. +You can access the demo app via http://127.0.0.1:5000 or use a different port using `yarn demo --port 5002` + +Note - If you use 127.0.0.1 in your browser, you need to allowlist it as a domain for sign in, as shown below. + +![image](https://user-images.githubusercontent.com/35932340/153659058-d669055f-b587-4bc2-9f32-323149df50c3.png) + +### `hosting: Port 5000 is not open on localhost, could not start Hosting Emulator.` + +This can happen when you run `yarn run demo:emulator` if port 5000 is taken. +Modify auth/demo/firebase.json with a custom port. Pick any port, this example picks 5091. + +``` +{ + //... + "emulators": { + "hosting": { + "host": "", + "port": "5091" + } + } +} +``` + +### Error message about functions + +`The Cloud Functions emulator requires the module "firebase-admin" to be installed. This package is in your package.json, but it's not available. You probably need to run "npm install" in your functions directory. +i functions: Your functions could not be parsed due to an issue with your node_modules (see above) +` + +Run `npm install` inside the auth/demo/functions directory as mentioned in the error message. diff --git a/packages/auth/demo/firebase.json b/packages/auth/demo/firebase.json index c44bbb73ccf..300ada0abc9 100644 --- a/packages/auth/demo/firebase.json +++ b/packages/auth/demo/firebase.json @@ -4,28 +4,11 @@ }, "hosting": { "public": "public", - "rewrites": [ - { - "source": "/checkIfAuthenticated", - "function": "checkIfAuthenticated" - } - ] - }, - "emulators": { - "auth": { - "port": 9099 - }, - "functions": { - "port": 5001 - }, - "database": { - "port": 9000 - }, - "hosting": { - "port": 5000 - }, - "ui": { - "enabled": true - } + "rewrites": [ + { + "source": "/checkIfAuthenticated", + "function": "checkIfAuthenticated" + } + ] } } diff --git a/packages/auth/demo/functions/index.js b/packages/auth/demo/functions/index.js index 58ceaeb5f1a..f3a9091bf20 100644 --- a/packages/auth/demo/functions/index.js +++ b/packages/auth/demo/functions/index.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,14 +30,16 @@ exports.checkIfAuthenticated = functions.https.onRequest((req, res) => { const idToken = req.get('x-id-token'); res.setHeader('Content-Type', 'application/json'); if (idToken) { - admin.auth().verifyIdToken(idToken) - .then((decodedIdToken) => { - res.status(200).send(JSON.stringify({uid: decodedIdToken.sub})); - }) - .catch((error) => { - res.status(400).send(JSON.stringify({error: error.code})); - }); + admin + .auth() + .verifyIdToken(idToken) + .then(decodedIdToken => { + res.status(200).send(JSON.stringify({ uid: decodedIdToken.sub })); + }) + .catch(error => { + res.status(400).send(JSON.stringify({ error: error.code })); + }); } else { - res.status(403).send(JSON.stringify({error: 'Unauthorized access'})); + res.status(403).send(JSON.stringify({ error: 'Unauthorized access' })); } }); diff --git a/packages/auth/demo/functions/package.json b/packages/auth/demo/functions/package.json index 50d1685786f..275924c8ed2 100644 --- a/packages/auth/demo/functions/package.json +++ b/packages/auth/demo/functions/package.json @@ -9,8 +9,11 @@ "logs": "firebase functions:log" }, "dependencies": { - "firebase-admin": "9.4.2", - "firebase-functions": "3.13.0" + "firebase-admin": "11.11.1", + "firebase-functions": "3.24.1" }, - "private": true + "private": true, + "engines": { + "node": "10" + } } diff --git a/packages/auth/demo/functions/yarn.lock b/packages/auth/demo/functions/yarn.lock index 4f0eb3608b6..db07d180dab 100644 --- a/packages/auth/demo/functions/yarn.lock +++ b/packages/auth/demo/functions/yarn.lock @@ -2,152 +2,158 @@ # yarn lockfile v1 -"@firebase/app-types@0.6.1": - version "0.6.1" - resolved "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz#dcbd23030a71c0c74fc95d4a3f75ba81653850e9" - integrity sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg== +"@babel/parser@^7.20.15": + version "7.24.0" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz#26a3d1ff49031c53a97d03b604375f028746a9ac" + integrity sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg== + +"@fastify/busboy@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz#9c6db24a55f8b803b5222753b24fe3aea2ba9ca3" + integrity sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q== + dependencies: + text-decoding "^1.0.0" + +"@firebase/app-types@0.9.0": + version "0.9.0" + resolved "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz#35b5c568341e9e263b29b3d2ba0e9cfc9ec7f01e" + integrity sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q== -"@firebase/auth-interop-types@0.1.5": - version "0.1.5" - resolved "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz#9fc9bd7c879f16b8d1bb08373a0f48c3a8b74557" - integrity sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw== +"@firebase/auth-interop-types@0.2.1": + version "0.2.1" + resolved "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.1.tgz#78884f24fa539e34a06c03612c75f222fcc33742" + integrity sha512-VOaGzKp65MY6P5FI84TfYKBXEPi6LmOCSMMzys6o2BN2LOsqy7pCuZCup7NYnfbk5OkkQKzvIfHOzTm0UDpkyg== -"@firebase/component@0.1.21": - version "0.1.21" - resolved "https://registry.npmjs.org/@firebase/component/-/component-0.1.21.tgz#56062eb0d449dc1e7bbef3c084a9b5fa48c7c14d" - integrity sha512-kd5sVmCLB95EK81Pj+yDTea8pzN2qo/1yr0ua9yVi6UgMzm6zAeih73iVUkaat96MAHy26yosMufkvd3zC4IKg== +"@firebase/component@0.6.4": + version "0.6.4" + resolved "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz#8981a6818bd730a7554aa5e0516ffc9b1ae3f33d" + integrity sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA== dependencies: - "@firebase/util" "0.3.4" - tslib "^1.11.1" + "@firebase/util" "1.9.3" + tslib "^2.1.0" -"@firebase/database-types@0.6.1", "@firebase/database-types@^0.6.1": - version "0.6.1" - resolved "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.6.1.tgz#cf1cfc03e617ed4c2561703781f85ba4c707ff65" - integrity sha512-JtL3FUbWG+bM59iYuphfx9WOu2Mzf0OZNaqWiQ7lJR8wBe7bS9rIm9jlBFtksB7xcya1lZSQPA/GAy2jIlMIkA== - dependencies: - "@firebase/app-types" "0.6.1" - -"@firebase/database@^0.8.1": - version "0.8.1" - resolved "https://registry.npmjs.org/@firebase/database/-/database-0.8.1.tgz#a7bc1c01052d35817a242c21bfe09ab29ee485a3" - integrity sha512-/1HhR4ejpqUaM9Cn3KSeNdQvdlehWIhdfTVWFxS73ZlLYf7ayk9jITwH10H3ZOIm5yNzxF67p/U7Z/0IPhgWaQ== - dependencies: - "@firebase/auth-interop-types" "0.1.5" - "@firebase/component" "0.1.21" - "@firebase/database-types" "0.6.1" - "@firebase/logger" "0.2.6" - "@firebase/util" "0.3.4" - faye-websocket "0.11.3" - tslib "^1.11.1" - -"@firebase/logger@0.2.6": - version "0.2.6" - resolved "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz#3aa2ca4fe10327cabf7808bd3994e88db26d7989" - integrity sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw== - -"@firebase/util@0.3.4": +"@firebase/database-compat@^0.3.4": version "0.3.4" - resolved "https://registry.npmjs.org/@firebase/util/-/util-0.3.4.tgz#e389d0e0e2aac88a5235b06ba9431db999d4892b" - integrity sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ== + resolved "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz#4e57932f7a5ba761cd5ac946ab6b6ab3f660522c" + integrity sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg== + dependencies: + "@firebase/component" "0.6.4" + "@firebase/database" "0.14.4" + "@firebase/database-types" "0.10.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + tslib "^2.1.0" + +"@firebase/database-types@0.10.4", "@firebase/database-types@^0.10.4": + version "0.10.4" + resolved "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz#47ba81113512dab637abace61cfb65f63d645ca7" + integrity sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ== + dependencies: + "@firebase/app-types" "0.9.0" + "@firebase/util" "1.9.3" + +"@firebase/database@0.14.4": + version "0.14.4" + resolved "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz#9e7435a16a540ddfdeb5d99d45618e6ede179aa6" + integrity sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ== + dependencies: + "@firebase/auth-interop-types" "0.2.1" + "@firebase/component" "0.6.4" + "@firebase/logger" "0.4.0" + "@firebase/util" "1.9.3" + faye-websocket "0.11.4" + tslib "^2.1.0" + +"@firebase/logger@0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz#15ecc03c452525f9d47318ad9491b81d1810f113" + integrity sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA== dependencies: - tslib "^1.11.1" + tslib "^2.1.0" -"@google-cloud/common@^3.3.0": - version "3.4.0" - resolved "https://registry.npmjs.org/@google-cloud/common/-/common-3.4.0.tgz#8951d0dc94c9dfd8af2b49ed125984dc71f1de6b" - integrity sha512-bVMQlK4aZEeopo2oJwDUJiBhPVjRRQHfFCCv9JowmKS3L//PBHNDJzC/LxJixGZEU3fh3YXkUwm67JZ5TBCCNQ== +"@firebase/util@1.9.3": + version "1.9.3" + resolved "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz#45458dd5cd02d90e55c656e84adf6f3decf4b7ed" + integrity sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA== dependencies: - "@google-cloud/projectify" "^2.0.0" - "@google-cloud/promisify" "^2.0.0" - arrify "^2.0.1" - duplexify "^4.1.1" - ent "^2.2.0" - extend "^3.0.2" - google-auth-library "^6.0.0" - retry-request "^4.1.1" - teeny-request "^7.0.0" + tslib "^2.1.0" -"@google-cloud/firestore@^4.5.0": - version "4.7.1" - resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.7.1.tgz#d170c72ecda6286ebab460765511103362da7b21" - integrity sha512-Qici+WKB6uRdDS1S3CaxGrIaCl4Bck70DYSzA5dZFkTU03Jj5DKXC4PYeUkfCAiB4haj7tzx+2ye7rhLxPclhQ== +"@google-cloud/firestore@^6.6.0": + version "6.8.0" + resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz#d8c852844c381afaf62592796606c10e178400b5" + integrity sha512-JRpk06SmZXLGz0pNx1x7yU3YhkUXheKgH5hbDZ4kMsdhtfV5qPLJLRI4wv69K0cZorIk+zTMOwptue7hizo0eA== dependencies: fast-deep-equal "^3.1.1" functional-red-black-tree "^1.0.1" - google-gax "^2.9.2" + google-gax "^3.5.7" + protobufjs "^7.2.5" -"@google-cloud/paginator@^3.0.0": - version "3.0.5" - resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz#9d6b96c421a89bd560c1bc2c197c7611ef21db6c" - integrity sha512-N4Uk4BT1YuskfRhKXBs0n9Lg2YTROZc6IMpkO/8DIHODtm5s3xY8K5vVBo23v/2XulY3azwITQlYWgT4GdLsUw== +"@google-cloud/paginator@^3.0.7": + version "3.0.7" + resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b" + integrity sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ== dependencies: arrify "^2.0.0" extend "^3.0.2" -"@google-cloud/projectify@^2.0.0": - version "2.0.1" - resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz#13350ee609346435c795bbfe133a08dfeab78d65" - integrity sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ== - -"@google-cloud/promisify@^2.0.0": - version "2.0.3" - resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz#f934b5cdc939e3c7039ff62b9caaf59a9d89e3a8" - integrity sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw== - -"@google-cloud/storage@^5.3.0": - version "5.3.0" - resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.3.0.tgz#cf86683911cce68829e46de544abb41947d29da2" - integrity sha512-3t5UF3SZ14Bw2kcBHubCai6EIugU2GnQOstYWVSFuoO8IJ94RAaIOPq/dtexvQbUTpBTAGpd5smVR9WPL1mJVw== - dependencies: - "@google-cloud/common" "^3.3.0" - "@google-cloud/paginator" "^3.0.0" - "@google-cloud/promisify" "^2.0.0" - arrify "^2.0.0" +"@google-cloud/projectify@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-3.0.0.tgz#302b25f55f674854dce65c2532d98919b118a408" + integrity sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA== + +"@google-cloud/promisify@^3.0.0": + version "3.0.1" + resolved "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-3.0.1.tgz#8d724fb280f47d1ff99953aee0c1669b25238c2e" + integrity sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA== + +"@google-cloud/storage@^6.9.5": + version "6.12.0" + resolved "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz#a5d3093cc075252dca5bd19a3cfda406ad3a9de1" + integrity sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw== + dependencies: + "@google-cloud/paginator" "^3.0.7" + "@google-cloud/projectify" "^3.0.0" + "@google-cloud/promisify" "^3.0.0" + abort-controller "^3.0.0" + async-retry "^1.3.3" compressible "^2.0.12" - concat-stream "^2.0.0" - date-and-time "^0.14.0" - duplexify "^3.5.0" + duplexify "^4.0.0" + ent "^2.2.0" extend "^3.0.2" - gaxios "^3.0.0" - gcs-resumable-upload "^3.1.0" - hash-stream-validation "^0.2.2" - mime "^2.2.0" + fast-xml-parser "^4.2.2" + gaxios "^5.0.0" + google-auth-library "^8.0.1" + mime "^3.0.0" mime-types "^2.0.8" - onetime "^5.1.0" p-limit "^3.0.1" - pumpify "^2.0.0" - snakeize "^0.1.0" - stream-events "^1.0.1" - xdg-basedir "^4.0.0" - -"@grpc/grpc-js@~1.1.1": - version "1.1.7" - resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.7.tgz#d3d71c6da95397e2d63895ccc4a05e7572f7b7e6" - integrity sha512-EuxMstI0u778dp0nk6Fe3gHXYPeV6FYsWOe0/QFwxv1NQ6bc5Wl/0Yxa4xl9uBlKElL6AIxuASmSfu7KEJhqiw== - dependencies: - "@grpc/proto-loader" "^0.6.0-pre14" - "@types/node" "^12.12.47" - google-auth-library "^6.0.0" - semver "^6.2.0" - -"@grpc/proto-loader@^0.5.1": - version "0.5.5" - resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz#6725e7a1827bdf8e92e29fbf4e9ef0203c0906a9" - integrity sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ== + retry-request "^5.0.0" + teeny-request "^8.0.0" + uuid "^8.0.0" + +"@grpc/grpc-js@~1.8.0": + version "1.8.21" + resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.21.tgz#d282b122c71227859bf6c5866f4c40f4a2696513" + integrity sha512-KeyQeZpxeEBSqFVTi3q2K7PiPXmgBfECc4updA1ejCLjYmoAlvvM3ZMp5ztTDUCUQmoY3CpDxvchjO1+rFkoHg== dependencies: - lodash.camelcase "^4.3.0" - protobufjs "^6.8.6" + "@grpc/proto-loader" "^0.7.0" + "@types/node" ">=12.12.47" -"@grpc/proto-loader@^0.6.0-pre14": - version "0.6.0-pre9" - resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.0-pre9.tgz#0c6fe42f6c5ef9ce1b3cef7be64d5b09d6fe4d6d" - integrity sha512-oM+LjpEjNzW5pNJjt4/hq1HYayNeQT+eGrOPABJnYHv7TyNPDNzkQ76rDYZF86X5swJOa4EujEMzQ9iiTdPgww== +"@grpc/proto-loader@^0.7.0": + version "0.7.10" + resolved "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz#6bf26742b1b54d0a473067743da5d3189d06d720" + integrity sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ== dependencies: - "@types/long" "^4.0.1" lodash.camelcase "^4.3.0" - long "^4.0.0" - protobufjs "^6.9.0" - yargs "^15.3.1" + long "^5.0.0" + protobufjs "^7.2.4" + yargs "^17.7.2" + +"@jsdoc/salty@^0.2.1": + version "0.2.7" + resolved "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.7.tgz#98ddce519fd95d7bee605a658fabf6e8cbf7556d" + integrity sha512-mh8LbS9d4Jq84KLw8pzho7XC2q2/IJGiJss3xwRoLD1A+EE16SjN4PfaG4jRCzKegTFLlN0Zd8SdUPE6XdoPFg== + dependencies: + lodash "^4.17.21" "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -202,39 +208,49 @@ resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== "@types/body-parser@*": - version "1.19.0" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" - integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + version "1.19.1" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c" + integrity sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg== dependencies: "@types/connect" "*" "@types/node" "*" -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - "@types/connect@*": - version "3.4.33" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" - integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + version "3.4.35" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== dependencies: "@types/node" "*" +"@types/cors@^2.8.5": + version "2.8.12" + resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" + integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== + "@types/express-serve-static-core@*": - version "4.17.13" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" - integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== + version "4.17.24" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07" + integrity sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.43" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" + integrity sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" + "@types/send" "*" "@types/express@4.17.3": version "4.17.3" @@ -245,53 +261,102 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" -"@types/long@^4.0.0", "@types/long@^4.0.1": +"@types/express@^4.17.17": + version "4.17.21" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/glob@*": + version "8.1.0" + resolved "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" + integrity sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w== + dependencies: + "@types/minimatch" "^5.1.2" + "@types/node" "*" + +"@types/jsonwebtoken@^9.0.2": + version "9.0.6" + resolved "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz#d1af3544d99ad992fb6681bbe60676e06b032bd3" + integrity sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw== + dependencies: + "@types/node" "*" + +"@types/linkify-it@*": + version "3.0.5" + resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz#1e78a3ac2428e6d7e6c05c1665c242023a4601d8" + integrity sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw== + +"@types/long@^4.0.0": version "4.0.1" resolved "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== -"@types/mime@*": - version "2.0.3" - resolved "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" - integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== +"@types/markdown-it@^12.2.3": + version "12.2.3" + resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51" + integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ== + dependencies: + "@types/linkify-it" "*" + "@types/mdurl" "*" -"@types/node@*": - version "14.11.2" - resolved "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" - integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== +"@types/mdurl@*": + version "1.0.5" + resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz#3e0d2db570e9fb6ccb2dc8fde0be1d79ac810d39" + integrity sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA== -"@types/node@^10.10.0": - version "10.17.35" - resolved "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56" - integrity sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA== +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== -"@types/node@^12.12.47": - version "12.12.62" - resolved "https://registry.npmjs.org/@types/node/-/node-12.12.62.tgz#733923d73669188d35950253dd18a21570085d2b" - integrity sha512-qAfo81CsD7yQIM9mVyh6B/U47li5g7cfpVQEDMfQeF8pSZVwzbhwU3crc0qG4DmpsebpJPR49AKOExQyJ05Cpg== +"@types/minimatch@^5.1.2": + version "5.1.2" + resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== -"@types/node@^13.7.0": - version "13.13.21" - resolved "https://registry.npmjs.org/@types/node/-/node-13.13.21.tgz#e48d3c2e266253405cf404c8654d1bcf0d333e5c" - integrity sha512-tlFWakSzBITITJSxHV4hg4KvrhR/7h3xbJdSFbYJBVzKubrASbnnIFuSgolUh7qKGo/ZeJPKUfbZ0WS6Jp14DQ== +"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "16.9.6" + resolved "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz#040a64d7faf9e5d9e940357125f0963012e66f04" + integrity sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ== "@types/qs@*": - version "6.9.5" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" - integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== + version "6.9.7" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== "@types/range-parser@*": - version "1.2.3" - resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" - integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + version "1.2.4" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/rimraf@^3.0.2": + version "3.0.2" + resolved "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8" + integrity sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ== + dependencies: + "@types/glob" "*" + "@types/node" "*" + +"@types/send@*": + version "0.17.4" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" "@types/serve-static@*": - version "1.13.5" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" - integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== + version "1.13.10" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" + integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== dependencies: - "@types/express-serve-static-core" "*" - "@types/mime" "*" + "@types/mime" "^1" + "@types/node" "*" abort-controller@^3.0.0: version "3.0.0" @@ -308,46 +373,77 @@ accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + agent-base@6: - version "6.0.1" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" - integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== + version "6.0.2" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^4.0.0: - version "4.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "@types/color-name" "^1.1.1" color-convert "^2.0.1" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= -arrify@^2.0.0, arrify@^2.0.1: +arrify@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + base64-js@^1.3.0: - version "1.3.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + version "1.5.1" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== bignumber.js@^9.0.0: version "9.0.1" resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + body-parser@1.19.0: version "1.19.0" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -364,34 +460,46 @@ body-parser@1.19.0: raw-body "2.4.0" type-is "~1.6.17" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - bytes@3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +catharsis@^0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" + integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== + dependencies: + lodash "^4.17.15" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== dependencies: string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" color-convert@^2.0.1: version "2.0.1" @@ -412,28 +520,6 @@ compressible@^2.0.12: dependencies: mime-db ">= 1.43.0 < 2" -concat-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" - integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.0.2" - typedarray "^0.0.6" - -configstore@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - content-disposition@0.5.3: version "0.5.3" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" @@ -456,11 +542,6 @@ cookie@0.4.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - cors@^2.8.5: version "2.8.5" resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -469,16 +550,6 @@ cors@^2.8.5: object-assign "^4" vary "^1" -crypto-random-string@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" - integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== - -date-and-time@^0.14.0: - version "0.14.1" - resolved "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.1.tgz#969634697b78956fb66b8be6fb0f39fbd631f2f6" - integrity sha512-M4RggEH5OF2ZuCOxgOU67R6Z9ohjKbxGvAQz48vj53wLmL0bAgumkBvycR32f30pK+Og9pIR+RFDyChbaE4oLA== - debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -487,16 +558,23 @@ debug@2.6.9: ms "2.0.0" debug@4, debug@^4.1.1: - version "4.2.0" - resolved "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + version "4.3.2" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: ms "2.1.2" -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +debug@^4.3.4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== depd@~1.1.2: version "1.1.2" @@ -508,34 +586,10 @@ destroy@~1.0.4: resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= -dicer@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" - integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== - dependencies: - streamsearch "0.1.2" - -dot-prop@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.0.0" - -duplexify@^3.5.0: - version "3.7.1" - resolved "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -duplexify@^4.0.0, duplexify@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz#7027dc374f157b122a8ae08c2d3ea4d2d953aa61" - integrity sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA== +duplexify@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" + integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw== dependencies: end-of-stream "^1.4.1" inherits "^2.0.3" @@ -564,7 +618,7 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -576,11 +630,72 @@ ent@^2.2.0: resolved "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escodegen@^1.13.0: + version "1.14.3" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +espree@^9.0.0: + version "9.6.1" + resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + etag@~1.8.1: version "1.8.1" resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" @@ -637,15 +752,27 @@ fast-deep-equal@^3.1.1: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-text-encoding@^1.0.0: +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== -faye-websocket@0.11.3: - version "0.11.3" - resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" - integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== +fast-xml-parser@^4.2.2: + version "4.3.5" + resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.5.tgz#e2f2a2ae8377e9c3dc321b151e58f420ca7e5ccc" + integrity sha512-sWvP1Pl8H03B8oFJpFR3HE31HUfwtX7Rlf9BNsvdpujD4n7WMhfmu8h9wOV2u+c1k0ZilTADhPqypzx2J690ZQ== + dependencies: + strnum "^1.0.5" + +faye-websocket@0.11.4: + version "0.11.4" + resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== dependencies: websocket-driver ">=0.5.1" @@ -662,192 +789,150 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -firebase-admin@9.4.2: - version "9.4.2" - resolved "https://registry.npmjs.org/firebase-admin/-/firebase-admin-9.4.2.tgz#190d5d7ca5e3f251d99503feb6e05e7ab1623851" - integrity sha512-mRnBJbW6BAz6DJkZ0GOUTkmnmCrwVzMreMc6O+RXWukFydOzi5Xr6TKSiPKxoOQw41r9IluP2AZ3Qzvlx2SR+g== - dependencies: - "@firebase/database" "^0.8.1" - "@firebase/database-types" "^0.6.1" - "@types/node" "^10.10.0" - dicer "^0.3.0" - jsonwebtoken "^8.5.1" - node-forge "^0.10.0" +firebase-admin@11.10.1: + version "11.10.1" + resolved "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.10.1.tgz#5f0f83a44627e89938d350c5e4bbac76596c963e" + integrity sha512-atv1E6GbuvcvWaD3eHwrjeP5dAVs+EaHEJhu9CThMzPY6In8QYDiUR6tq5SwGl4SdA/GcAU0nhwWc/FSJsAzfQ== + dependencies: + "@fastify/busboy" "^1.2.1" + "@firebase/database-compat" "^0.3.4" + "@firebase/database-types" "^0.10.4" + "@types/node" ">=12.12.47" + jsonwebtoken "^9.0.0" + jwks-rsa "^3.0.1" + node-forge "^1.3.1" + uuid "^9.0.0" optionalDependencies: - "@google-cloud/firestore" "^4.5.0" - "@google-cloud/storage" "^5.3.0" + "@google-cloud/firestore" "^6.6.0" + "@google-cloud/storage" "^6.9.5" -firebase-functions@3.13.0: - version "3.13.0" - resolved "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.13.0.tgz#66278dbeb45f343a179814f2b1d95b383beec5e7" - integrity sha512-tnltJL5KlGtbeBD9scsVjoKTSTMeo6EAy1gsdOfRlrwAu6idgLRKYVdmw0YymS8N7SwJ3CXo+3fuvSSihKhXbA== +firebase-functions@3.24.1: + version "3.24.1" + resolved "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.24.1.tgz#50d13274c4ae96b2308a67e9fc76f1a74cff690d" + integrity sha512-GYhoyOV0864HFMU1h/JNBXYNmDk2MlbvU7VO/5qliHX6u/6vhSjTJjlyCG4leDEI8ew8IvmkIC5QquQ1U8hAuA== dependencies: + "@types/cors" "^2.8.5" "@types/express" "4.17.3" cors "^2.8.5" express "^4.17.1" lodash "^4.17.14" + node-fetch "^2.6.7" -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fresh@0.5.2: version "0.5.2" resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -gaxios@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/gaxios/-/gaxios-3.2.0.tgz#11b6f0e8fb08d94a10d4d58b044ad3bec6dd486a" - integrity sha512-+6WPeVzPvOshftpxJwRi2Ozez80tn/hdtOUag7+gajDHRJvAblKxTFSSMPtr2hmnLy7p0mvYz0rMXLBl8pSO7Q== - dependencies: - abort-controller "^3.0.0" - extend "^3.0.2" - https-proxy-agent "^5.0.0" - is-stream "^2.0.0" - node-fetch "^2.3.0" - -gaxios@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/gaxios/-/gaxios-4.0.1.tgz#bc7b205a89d883452822cc75e138620c35e3291e" - integrity sha512-jOin8xRZ/UytQeBpSXFqIzqU7Fi5TqgPNLlUsSB8kjJ76+FiGBfImF8KJu++c6J4jOldfJUtt0YmkRj2ZpSHTQ== +gaxios@^5.0.0, gaxios@^5.0.1: + version "5.1.3" + resolved "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz#f7fa92da0fe197c846441e5ead2573d4979e9013" + integrity sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA== dependencies: - abort-controller "^3.0.0" extend "^3.0.2" https-proxy-agent "^5.0.0" is-stream "^2.0.0" - node-fetch "^2.3.0" - -gcp-metadata@^4.1.0: - version "4.2.0" - resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.0.tgz#3b424355ccdc240ee07c5791e2fd6a60a283d89a" - integrity sha512-vQZD57cQkqIA6YPGXM/zc+PIZfNRFdukWGsGZ5+LcJzesi5xp6Gn7a02wRJi4eXPyArNMIYpPET4QMxGqtlk6Q== - dependencies: - gaxios "^3.0.0" - json-bigint "^1.0.0" + node-fetch "^2.6.9" -gcp-metadata@^4.2.0: - version "4.2.1" - resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz#31849fbcf9025ef34c2297c32a89a1e7e9f2cd62" - integrity sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw== +gcp-metadata@^5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz#6f45eb473d0cb47d15001476b48b663744d25408" + integrity sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w== dependencies: - gaxios "^4.0.0" + gaxios "^5.0.0" json-bigint "^1.0.0" -gcs-resumable-upload@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.1.tgz#67c766a0555d6a352f9651b7603337207167d0de" - integrity sha512-RS1osvAicj9+MjCc6jAcVL1Pt3tg7NK2C2gXM5nqD1Gs0klF2kj5nnAFSBy97JrtslMIQzpb7iSuxaG8rFWd2A== - dependencies: - abort-controller "^3.0.0" - configstore "^5.0.0" - extend "^3.0.2" - gaxios "^3.0.0" - google-auth-library "^6.0.0" - pumpify "^2.0.0" - stream-events "^1.0.4" - -get-caller-file@^2.0.1: +get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -google-auth-library@^6.0.0: - version "6.1.0" - resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.0.tgz#db3bbe2b3add5783442c84efdcb1c061721c1bfb" - integrity sha512-GbalszIADE1YPWhUyfFMrkLhFHnlAgoRcqGVW+MsLDPsuaOB5MRPk7NNafPDv9SherNE4EKzcYuxMJjaxzXMOw== +glob@^8.0.0: + version "8.1.0" + resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: - arrify "^2.0.0" - base64-js "^1.3.0" - ecdsa-sig-formatter "^1.0.11" - fast-text-encoding "^1.0.0" - gaxios "^3.0.0" - gcp-metadata "^4.1.0" - gtoken "^5.0.0" - jws "^4.0.0" - lru-cache "^6.0.0" + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" -google-auth-library@^6.1.3: - version "6.1.3" - resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.1.3.tgz#39d868140b70d0c4b32c6f6d8f4ccc1400d84dca" - integrity sha512-m9mwvY3GWbr7ZYEbl61isWmk+fvTmOt0YNUfPOUY2VH8K5pZlAIWJjxEi0PqR3OjMretyiQLI6GURMrPSwHQ2g== +google-auth-library@^8.0.1, google-auth-library@^8.0.2: + version "8.9.0" + resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz#15a271eb2ec35d43b81deb72211bd61b1ef14dd0" + integrity sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg== dependencies: arrify "^2.0.0" base64-js "^1.3.0" ecdsa-sig-formatter "^1.0.11" fast-text-encoding "^1.0.0" - gaxios "^4.0.0" - gcp-metadata "^4.2.0" - gtoken "^5.0.4" + gaxios "^5.0.0" + gcp-metadata "^5.3.0" + gtoken "^6.1.0" jws "^4.0.0" lru-cache "^6.0.0" -google-gax@^2.9.2: - version "2.9.2" - resolved "https://registry.npmjs.org/google-gax/-/google-gax-2.9.2.tgz#780b2c0fc031c864007e1e198a9b90c7e946cca0" - integrity sha512-Pve4osEzNKpBZqFXMfGKBbKCtgnHpUe5IQMh5Ou+Xtg8nLcba94L3gF0xgM5phMdGRRqJn0SMjcuEVmOYu7EBg== +google-gax@^3.5.7: + version "3.6.1" + resolved "https://registry.npmjs.org/google-gax/-/google-gax-3.6.1.tgz#02c78fc496f5adf86f2ca9145545f4b6575f6118" + integrity sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w== dependencies: - "@grpc/grpc-js" "~1.1.1" - "@grpc/proto-loader" "^0.5.1" + "@grpc/grpc-js" "~1.8.0" + "@grpc/proto-loader" "^0.7.0" "@types/long" "^4.0.0" + "@types/rimraf" "^3.0.2" abort-controller "^3.0.0" duplexify "^4.0.0" - google-auth-library "^6.1.3" + fast-text-encoding "^1.0.3" + google-auth-library "^8.0.2" is-stream-ended "^0.1.4" node-fetch "^2.6.1" - protobufjs "^6.9.0" - retry-request "^4.0.0" + object-hash "^3.0.0" + proto3-json-serializer "^1.0.0" + protobufjs "7.2.4" + protobufjs-cli "1.1.1" + retry-request "^5.0.0" -google-p12-pem@^3.0.0, google-p12-pem@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" - integrity sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA== +google-p12-pem@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz#82841798253c65b7dc2a4e5fe9df141db670172a" + integrity sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ== dependencies: - node-forge "^0.10.0" - -graceful-fs@^4.1.2: - version "4.2.4" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + node-forge "^1.3.1" -gtoken@^5.0.0: - version "5.0.3" - resolved "https://registry.npmjs.org/gtoken/-/gtoken-5.0.3.tgz#b76ef8e9a2fed6fef165e47f7d05b60c498e4d05" - integrity sha512-Nyd1wZCMRc2dj/mAD0LlfQLcAO06uKdpKJXvK85SGrF5+5+Bpfil9u/2aw35ltvEHjvl0h5FMKN5knEU+9JrOg== - dependencies: - gaxios "^3.0.0" - google-p12-pem "^3.0.0" - jws "^4.0.0" - mime "^2.2.0" +graceful-fs@^4.1.9: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -gtoken@^5.0.4: - version "5.1.0" - resolved "https://registry.npmjs.org/gtoken/-/gtoken-5.1.0.tgz#4ba8d2fc9a8459098f76e7e8fd7beaa39fda9fe4" - integrity sha512-4d8N6Lk8TEAHl9vVoRVMh9BNOKWVgl2DdNtr3428O75r3QFrF/a5MMu851VmK0AA8+iSvbwRv69k5XnMLURGhg== +gtoken@^6.1.0: + version "6.1.2" + resolved "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz#aeb7bdb019ff4c3ba3ac100bbe7b6e74dce0e8bc" + integrity sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ== dependencies: - gaxios "^4.0.0" - google-p12-pem "^3.0.3" + gaxios "^5.0.1" + google-p12-pem "^4.0.0" jws "^4.0.0" - mime "^2.2.0" -hash-stream-validation@^0.2.2: - version "0.2.4" - resolved "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" - integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== http-errors@1.7.2: version "1.7.2" @@ -872,16 +957,16 @@ http-errors@~1.7.2: toidentifier "1.0.0" http-parser-js@>=0.5.1: - version "0.5.2" - resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" - integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== + version "0.5.3" + resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" + integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== -http-proxy-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== dependencies: - "@tootallnate/once" "1" + "@tootallnate/once" "2" agent-base "6" debug "4" @@ -900,21 +985,24 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -925,30 +1013,48 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - is-stream-ended@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +jose@^4.14.6: + version "4.15.5" + resolved "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz#6475d0f467ecd3c630a1b5dadd2735a7288df706" + integrity sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg== + +js2xmlparser@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a" + integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== + dependencies: + xmlcreate "^2.0.4" + +jsdoc@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.2.tgz#a1273beba964cf433ddf7a70c23fd02c3c60296e" + integrity sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg== + dependencies: + "@babel/parser" "^7.20.15" + "@jsdoc/salty" "^0.2.1" + "@types/markdown-it" "^12.2.3" + bluebird "^3.7.2" + catharsis "^0.9.0" + escape-string-regexp "^2.0.0" + js2xmlparser "^4.0.2" + klaw "^3.0.0" + markdown-it "^12.3.2" + markdown-it-anchor "^8.4.1" + marked "^4.0.10" + mkdirp "^1.0.4" + requizzle "^0.2.3" + strip-json-comments "^3.1.0" + underscore "~1.13.2" json-bigint@^1.0.0: version "1.0.0" @@ -957,10 +1063,10 @@ json-bigint@^1.0.0: dependencies: bignumber.js "^9.0.0" -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== +jsonwebtoken@^9.0.0: + version "9.0.2" + resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== dependencies: jws "^3.2.2" lodash.includes "^4.3.0" @@ -971,7 +1077,7 @@ jsonwebtoken@^8.5.1: lodash.isstring "^4.0.1" lodash.once "^4.0.0" ms "^2.1.1" - semver "^5.6.0" + semver "^7.5.4" jwa@^1.4.1: version "1.4.1" @@ -991,6 +1097,18 @@ jwa@^2.0.0: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwks-rsa@^3.0.1: + version "3.1.0" + resolved "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz#50406f23e38c9b2682cd437f824d7d61aa983171" + integrity sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg== + dependencies: + "@types/express" "^4.17.17" + "@types/jsonwebtoken" "^9.0.2" + debug "^4.3.4" + jose "^4.14.6" + limiter "^1.1.5" + lru-memoizer "^2.2.0" + jws@^3.2.2: version "3.2.2" resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" @@ -1007,18 +1125,43 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== +klaw@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146" + integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== + dependencies: + graceful-fs "^4.1.9" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== dependencies: - p-locate "^4.1.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + +limiter@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== + +linkify-it@^3.0.1: + version "3.0.3" + resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -1054,15 +1197,15 @@ lodash.once@^4.0.0: resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -lodash@^4.17.14: - version "4.17.20" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -long@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.0.0: + version "5.2.3" + resolved "https://registry.npmjs.org/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== lru-cache@^6.0.0: version "6.0.0" @@ -1071,12 +1214,47 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== +lru-cache@~4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" + integrity sha1-HRdnnAac2l0ECZGgnbwsDbN35V4= + dependencies: + pseudomap "^1.0.1" + yallist "^2.0.0" + +lru-memoizer@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz#b9d90c91637b4b1a423ef76f3156566691293df8" + integrity sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw== + dependencies: + lodash.clonedeep "^4.5.0" + lru-cache "~4.0.0" + +markdown-it-anchor@^8.4.1: + version "8.6.7" + resolved "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634" + integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== + +markdown-it@^12.3.2: + version "12.3.2" + resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== dependencies: - semver "^6.0.0" + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + +marked@^4.0.10: + version "4.3.0" + resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" + integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== + +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== media-typer@0.3.0: version "0.3.0" @@ -1093,37 +1271,49 @@ methods@~1.1.2: resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +mime-db@1.49.0: + version "1.49.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" + integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== "mime-db@>= 1.43.0 < 2": - version "1.45.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== + version "1.50.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f" + integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== mime-types@^2.0.8, mime-types@~2.1.24: - version "2.1.27" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + version "2.1.32" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" + integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== dependencies: - mime-db "1.44.0" + mime-db "1.49.0" mime@1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.2.0: - version "2.4.6" - resolved "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" - integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +minimist@^1.2.0: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== ms@2.0.0: version "2.0.0" @@ -1135,31 +1325,50 @@ ms@2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@2.1.2, ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + negotiator@0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -node-fetch@^2.3.0, node-fetch@^2.6.1: - version "2.6.1" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@^2.6.1, node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" -node-forge@^0.10.0: - version "0.10.0" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" - integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== +node-fetch@^2.6.9: + version "2.7.0" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== object-assign@^4: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -1167,70 +1376,74 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" -onetime@^5.1.0: - version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== dependencies: - p-try "^2.0.0" + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" p-limit@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" - integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== - dependencies: - p-try "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + yocto-queue "^0.1.0" parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + +proto3-json-serializer@^1.0.0: + version "1.1.1" + resolved "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz#1b5703152b6ce811c5cdcc6468032caf53521331" + integrity sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw== + dependencies: + protobufjs "^7.0.0" -protobufjs@^6.8.6, protobufjs@^6.9.0: - version "6.10.1" - resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz#e6a484dd8f04b29629e9053344e3970cccf13cd2" - integrity sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ== +protobufjs-cli@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/protobufjs-cli/-/protobufjs-cli-1.1.1.tgz#f531201b1c8c7772066aa822bf9a08318b24a704" + integrity sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA== + dependencies: + chalk "^4.0.0" + escodegen "^1.13.0" + espree "^9.0.0" + estraverse "^5.1.0" + glob "^8.0.0" + jsdoc "^4.0.0" + minimist "^1.2.0" + semver "^7.1.2" + tmp "^0.2.1" + uglify-js "^3.7.7" + +protobufjs@7.2.4: + version "7.2.4" + resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz#3fc1ec0cdc89dd91aef9ba6037ba07408485c3ae" + integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -1242,34 +1455,39 @@ protobufjs@^6.8.6, protobufjs@^6.9.0: "@protobufjs/path" "^1.1.2" "@protobufjs/pool" "^1.1.0" "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.1" - "@types/node" "^13.7.0" - long "^4.0.0" + "@types/node" ">=13.7.0" + long "^5.0.0" -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== +protobufjs@^7.0.0, protobufjs@^7.2.4, protobufjs@^7.2.5: + version "7.2.6" + resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz#4a0ccd79eb292717aacf07530a07e0ed20278215" + integrity sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw== dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.1" + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== +proxy-addr@~2.0.5: + version "2.0.7" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" + forwarded "0.2.0" + ipaddr.js "1.9.1" -pumpify@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" - integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw== - dependencies: - duplexify "^4.1.1" - inherits "^2.0.3" - pump "^3.0.0" +pseudomap@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= qs@6.7.0: version "6.7.0" @@ -1291,20 +1509,7 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -readable-stream@^2.0.0: - version "2.3.7" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - 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" - -readable-stream@^3.0.2, readable-stream@^3.1.1: +readable-stream@^3.1.1: version "3.6.0" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -1318,19 +1523,27 @@ require-directory@^2.1.1: resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +requizzle@^0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" + integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== + dependencies: + lodash "^4.17.21" -retry-request@^4.0.0, retry-request@^4.1.1: - version "4.1.3" - resolved "https://registry.npmjs.org/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" - integrity sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ== +retry-request@^5.0.0: + version "5.0.2" + resolved "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz#143d85f90c755af407fcc46b7166a4ba520e44da" + integrity sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ== dependencies: debug "^4.1.1" + extend "^3.0.2" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +retry@0.13.1: + version "0.13.1" + resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +safe-buffer@5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -1345,15 +1558,12 @@ safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semver@^5.6.0: - version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.2.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.1.2, semver@^7.5.4: + version "7.6.0" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + dependencies: + lru-cache "^6.0.0" send@0.17.1: version "0.17.1" @@ -1384,32 +1594,22 @@ serve-static@1.14.1: parseurl "~1.3.3" send "0.17.1" -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -snakeize@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d" - integrity sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0= +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -stream-events@^1.0.1, stream-events@^1.0.4, stream-events@^1.0.5: +stream-events@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== @@ -1421,19 +1621,14 @@ stream-shift@^1.0.0: resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== -streamsearch@0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" - integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" string_decoder@^1.1.1: version "1.3.0" @@ -1442,45 +1637,77 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - safe-buffer "~5.1.0" + ansi-regex "^5.0.1" -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" +strip-json-comments@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== stubs@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls= -teeny-request@^7.0.0: - version "7.0.1" - resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz#bdd41fdffea5f8fbc0d29392cb47bec4f66b2b4c" - integrity sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw== +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +teeny-request@^8.0.0: + version "8.0.3" + resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.3.tgz#5cb9c471ef5e59f2fca8280dc3c5909595e6ca24" + integrity sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww== dependencies: - http-proxy-agent "^4.0.0" + http-proxy-agent "^5.0.0" https-proxy-agent "^5.0.0" node-fetch "^2.6.1" stream-events "^1.0.5" - uuid "^8.0.0" + uuid "^9.0.0" + +text-decoding@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz#38a5692d23b5c2b12942d6e245599cb58b1bc52f" + integrity sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA== + +tmp@^0.2.1: + version "0.2.3" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== toidentifier@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -tslib@^1.11.1: - version "1.13.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + +tslib@^2.1.0: + version "2.4.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" @@ -1490,31 +1717,27 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +uglify-js@^3.7.7: + version "3.17.4" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" + integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== -unique-string@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" - integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== - dependencies: - crypto-random-string "^2.0.0" +underscore@~1.13.2: + version "1.13.6" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" + integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -1525,15 +1748,25 @@ utils-merge@1.0.1: integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= uuid@^8.0.0: - version "8.3.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" - integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + version "8.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuid@^9.0.0: + version "9.0.1" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + websocket-driver@>=0.5.1: version "0.7.4" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" @@ -1548,15 +1781,23 @@ websocket-extensions@>=0.1.1: resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" string-width "^4.1.0" @@ -1567,52 +1808,45 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" +xmlcreate@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" + integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +yallist@^2.0.0: + version "2.1.2" + resolved "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^15.3.1: - version "15.4.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/packages/auth/demo/package.json b/packages/auth/demo/package.json new file mode 100644 index 00000000000..3d9c38bbc5c --- /dev/null +++ b/packages/auth/demo/package.json @@ -0,0 +1,53 @@ +{ + "name": "@firebase/auth-demo", + "version": "0.1.0", + "private": true, + "description": "Demo for Auth TS SDK", + "author": "Firebase (https://firebase.google.com/)", + "browser": "public/index.js", + "webworker": "public/web-worker.js", + "serviceworker": "public/service-worker.js", + "scripts": { + "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../../.gitignore'", + "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../../.gitignore'", + "demo": "rollup -c && firebase serve", + "demo:emulator": "rollup -c && firebase emulators:start", + "build": "rollup -c", + "build:deps": "lerna run --scope @firebase/'{app,auth}' --include-dependencies build", + "dev": "rollup -c -w" + }, + "dependencies": { + "@firebase/app": "*", + "@firebase/auth": "file:..", + "@firebase/logger": "*", + "@firebase/util": "*", + "tslib": "^2.1.0" + }, + "license": "Apache-2.0", + "devDependencies": { + "@rollup/plugin-strip": "2.1.0", + "rollup": "2.79.1", + "@rollup/plugin-json": "4.1.0", + "rollup-plugin-replace": "2.2.0", + "@rollup/plugin-terser": "0.4.4", + "rollup-plugin-typescript2": "0.31.2", + "rollup-plugin-uglify": "6.0.4", + "@rollup/plugin-node-resolve": "16.0.0", + "lerna": "4.0.0" + }, + "repository": { + "directory": "packages/auth/demo", + "type": "git", + "url": "git+https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "typings": "dist/index.d.ts", + "nyc": { + "extension": [ + ".ts" + ], + "reportDir": "./coverage/node" + } +} diff --git a/packages/auth/demo/public/common.js b/packages/auth/demo/public/common.js index d5def6f2290..9224926af95 100644 --- a/packages/auth/demo/public/common.js +++ b/packages/auth/demo/public/common.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ * @fileoverview Utilities for Auth test app features. */ - /** * Initializes the widget for toggling reCAPTCHA size. * @param {function(string):void} callback The callback to call when the @@ -28,7 +27,7 @@ function initRecaptchaToggle(callback) { // Listen to recaptcha config togglers. var $recaptchaConfigTogglers = $('.toggleRecaptcha'); - $recaptchaConfigTogglers.click(function(e) { + $recaptchaConfigTogglers.click(function (e) { // Remove currently active option. $recaptchaConfigTogglers.removeClass('active'); // Set currently selected option. @@ -41,14 +40,16 @@ function initRecaptchaToggle(callback) { // Install servicerWorker if supported. if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('/service-worker.js', {scope: '/'}) - .then(function(reg) { - // Registration worked. - console.log('Registration succeeded. Scope is ' + reg.scope); - }).catch(function(error) { - // Registration failed. - console.log('Registration failed with ' + error.message); - }); + navigator.serviceWorker + .register('/service-worker.js', { scope: '/' }) + .then(function (reg) { + // Registration worked. + console.log('Registration succeeded. Scope is ' + reg.scope); + }) + .catch(function (error) { + // Registration failed. + console.log('Registration failed with ' + error.message); + }); } var webWorker = null; @@ -58,12 +59,13 @@ if (window.Worker) { * Handles the incoming message from the web worker. * @param {!Object} e The message event received. */ - webWorker.onmessage = function(e) { - console.log('User data passed through web worker: ', e.data); + webWorker.onmessage = function (e) { + console.log('User data passed through web worker: ', e.data); switch (e.data.type) { case 'GET_USER_INFO': alertSuccess( - 'User data passed through web worker: ' + JSON.stringify(e.data)); + 'User data passed through web worker: ' + JSON.stringify(e.data) + ); break; case 'RUN_TESTS': if (e.data.status == 'success') { @@ -84,7 +86,7 @@ if (window.Worker) { */ function onGetCurrentUserDataFromWebWorker() { if (webWorker) { - webWorker.postMessage({type: 'GET_USER_INFO'}); + webWorker.postMessage({ type: 'GET_USER_INFO' }); } else { alertError('Error: Web workers are not supported in the current browser!'); } diff --git a/packages/auth/demo/public/index.html b/packages/auth/demo/public/index.html index 3d518d16854..78b14b8dd92 100644 --- a/packages/auth/demo/public/index.html +++ b/packages/auth/demo/public/index.html @@ -1,4 +1,20 @@ + + @@ -9,12 +25,7 @@ - - - - - - + + +
Set Tenant
+
+ + +
+ + +
Recaptcha Configs
+ +
Sign Up
- + +
+ +
+ +
+
+ These requirements are not currently enforced by the backend. Basic requirements are still enforced +
+
+ Password must be at least 6 characters. +
+
+ Password must be at most 4096 characters. +
+
+ Password must contain a lowercase letter. +
+
+ Password must contain an uppercase letter. +
+
+ Password must contain a numeric character. +
+
+ Password must contain a non-alphanumeric character. +
+
@@ -251,12 +310,49 @@
- + +
+ +
+ +
+
+ These requirements are not currently enforced by the backend. Basic requirements are still enforced. +
+
+ Existing passwords must meet these requirements. +
+
+ Password must be at least 6 characters. +
+
+ Password must be at most 4096 characters. +
+
+ Password must contain a lowercase letter. +
+
+ Password must contain an uppercase letter. +
+
+ Password must contain a numeric character. +
+
+ Password must contain a non-alphanumeric character. +
+
+
- + + class="form-control input-group" /> +
+ +
+ +
+
+ These requirements are not currently enforced by the backend. Basic requirements are still enforced. +
+
+ Password must be at least 6 characters. +
+
+ Password must be at most 4096 characters. +
+
+ Password must contain a lowercase letter. +
+
+ Password must contain an uppercase letter. +
+
+ Password must contain a numeric character. +
+
+ Password must contain a non-alphanumeric character. +
+
+
Fetch Sign In Methods
@@ -488,6 +615,12 @@ Phone +
  • + + TOTP + +
  • @@ -505,7 +638,27 @@ class="form-control" placeholder="Display Name" /> + + +
    +
    +
    + +
    + + + +
    + + +
    @@ -611,7 +764,7 @@
    +
    Mobile link
    +
    + +
    @@ -734,6 +892,17 @@
    + +