├── .clocignore ├── .gitignore ├── .idea ├── .gitignore ├── backupfire-firebase.iml ├── modules.xml ├── prettier.xml └── vcs.xml ├── .prettierrc ├── .tool-versions ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── babel.config.js ├── examples ├── js │ ├── .firebaserc │ ├── .gitignore │ └── functions │ │ ├── .gitignore │ │ ├── firebase.json │ │ ├── index.js │ │ └── package.json └── ts │ ├── .firebaserc │ ├── .gitignore │ ├── firebase.json │ └── functions │ ├── .gitignore │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── extension ├── CHANGELOG.md ├── LICENSE ├── README.md ├── extension.yaml └── functions │ ├── package-lock.json │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── _lib │ ├── asyncMiddleware │ │ └── index.ts │ ├── exceptions │ │ └── index.ts │ ├── logging │ │ └── index.ts │ └── operation │ │ └── index.ts ├── firestore │ ├── _lib │ │ └── client │ │ │ └── index.ts │ ├── backup │ │ └── index.ts │ ├── collections │ │ └── index.ts │ ├── index.ts │ └── status │ │ └── index.ts ├── index.ts ├── options.ts ├── storage │ └── index.ts ├── types.ts ├── users │ ├── index.ts │ └── test.ts └── version.ts ├── test ├── extension │ ├── .firebaserc.example │ ├── firebase.json │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── yarn.lock ├── lib │ ├── commonjs.js │ └── ts.ts └── server │ ├── .env.example │ ├── .firebaserc.example │ ├── firebase.json │ ├── index.js │ ├── package-lock.json │ ├── package.json │ ├── scripts │ └── seedUsers.js │ └── tsconfig.json ├── tsconfig.json └── types └── firebase-tools.d.ts /.clocignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | secrets 3 | lib -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules 3 | 4 | /lib 5 | /secrets 6 | .envrc 7 | 8 | # Extension 9 | /extension/functions/lib 10 | /extension/firebase-debug.log 11 | 12 | # Test projects 13 | /test/*/build 14 | /test/*/.firebaserc 15 | /test/*/.env 16 | /test/*/secrets 17 | 18 | # Firebase 19 | firebase-debug.log 20 | 21 | # VS Code 22 | /.vscode/*.log -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/backupfire-firebase.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 18.16.0 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "workbench.colorCustomizations": { 4 | "activityBar.activeBackground": "#f49300", 5 | "activityBar.activeBorder": "#bfffe6", 6 | "activityBar.background": "#f49300", 7 | "activityBar.foreground": "#15202b", 8 | "activityBar.inactiveForeground": "#15202b99", 9 | "activityBarBadge.background": "#bfffe6", 10 | "activityBarBadge.foreground": "#15202b", 11 | "statusBar.background": "#c17400", 12 | "statusBar.foreground": "#e7e7e7", 13 | "statusBarItem.hoverBackground": "#f49300", 14 | "titleBar.activeBackground": "#c17400", 15 | "titleBar.activeForeground": "#e7e7e7", 16 | "titleBar.inactiveBackground": "#c1740099", 17 | "titleBar.inactiveForeground": "#e7e7e799", 18 | "sash.hoverBorder": "#f49300", 19 | "statusBarItem.remoteBackground": "#c17400", 20 | "statusBarItem.remoteForeground": "#e7e7e7", 21 | "commandCenter.border": "#e7e7e799" 22 | }, 23 | "peacock.remoteColor": "#c17400" 24 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning]. 5 | This change log follows the format documented in [Keep a CHANGELOG]. 6 | 7 | [semantic versioning]: https://semver.org 8 | [keep a changelog]: https://keepachangelog.com 9 | 10 | ## 1.9.1 - 2023-06-09 11 | 12 | ### Fixed 13 | 14 | - Fixed the agent incorrectly retrieving the env config from `functions.config()`. 15 | 16 | ## 1.9.0 - 2023-06-09 17 | 18 | ### Changed 19 | 20 | - Replaced `console.log` calls with `functions.logger` and added more debug logs around retrieving the env config. 21 | 22 | ## 1.8.1 - 2023-03-15 23 | 24 | ### Fixed 25 | 26 | - Excluded collection groups from exported collections when responding to the controller. 27 | 28 | ## 1.8.0 - 2023-03-15 29 | 30 | ### Added 31 | 32 | - Respond with collection groups back to the controller to display collection groups on the backup page. 33 | 34 | ## 1.7.0 - 2023-03-10 35 | 36 | ### Changed 37 | 38 | - Upgraded firebase-functions to the latest version (from 3 to 4). 39 | 40 | ### Fixed 41 | 42 | - Fixed the extension compatibility with the latest firebase-functions. 43 | 44 | ## 1.6.0 - 2023-03-10 45 | 46 | ### Changed 47 | 48 | - Upgraded firebase-tools. 49 | 50 | - Set the function `invoker` to `public` to enforce Firebase adding the permission to `allUsers`, to avoid [random permission bug](https://github.com/firebase/firebase-tools/issues/3965#issuecomment-1006005316). 51 | 52 | ### Fixed 53 | 54 | - Fix `memory` not setting to default memory runtime option (`1Gb`). 55 | 56 | ## 1.5.0 - 2023-02-28 57 | 58 | ### Added 59 | 60 | - Added logging to the agent endpoints to help with debugging and troubleshooting. 61 | 62 | ## 1.4.0 - 2022-06-03 63 | 64 | ### Changed 65 | 66 | - Set default `memory` (`1GB`) and `timeoutSeconds` (`540`) Firebase Functions runtime options. It solves the problem with the huge users' backups that either run out of memory or timeout. 67 | 68 | - Even further improved the memory usage by the users backup. 69 | 70 | - Updated dependencies to the latest supported versions. 71 | 72 | ### Added 73 | 74 | - Added delayed users backup feature. If the delay is requested, the agent will respond with a pending backup state. When the backup is completed, the agent will notify the controller. That prevents multiple backups caused by timeouts. 75 | 76 | ## 1.3.0 - 2022-06-01 77 | 78 | ### Fixed 79 | 80 | - Fixed compatibility with `firebase-functions` tripping over `timeoutSeconds` set to `undefined` and throwing `Field 'timeout', Invalid duration format, failed to parse seconds"`. 81 | 82 | ### Changed 83 | 84 | - Dramatically decrease the memory footprint of the user backup, making `memory` usage unnecessary. 85 | 86 | - Updated dependencies to the latest supported versions. 87 | 88 | ### Added 89 | 90 | - Added list files endpoint to the agent API that will help with Realtime Database backup integration and check for the backups' status (storage class, size, etc.) 91 | 92 | - Added create storage endpoint to simplify the integration process and automatically create the backups bucket with optimal defaults. 93 | 94 | - Added support for managing Backup Fire config with `.env` by setting `BACKUPFIRE_TOKEN` and `BACKUPFIRE_PASSWORD`. 95 | 96 | ## 1.2.0 - 2021-05-20 97 | 98 | ### Added 99 | 100 | - Added support for Firebase Extension runtime. 101 | 102 | - The agent now reports the GAE runtime version. 103 | 104 | ## 1.1.0 - 2021-04-01 105 | 106 | ### Changed 107 | 108 | - Updated dependencies. 109 | 110 | ## 1.0.3 - 2021-01-30 111 | 112 | ### Fixed 113 | 114 | - Finally fixed the `memory` option. 115 | 116 | ### Added 117 | 118 | - Added `timeout` option that allows to set the agent timeout in seconds (defaults to `60`; max `540`). 119 | 120 | ## 1.0.2 - 2021-01-22 121 | 122 | ### Fixed 123 | 124 | - Fixed the `memory` option in the agent that previously wasn't properly applied. 125 | 126 | ## 1.0.1 - 2021-01-14 127 | 128 | ### Fixed 129 | 130 | - Fixed an issue with `express-jwt` failing without specifying `algorithms`. 131 | 132 | ## 1.0.0 - 2021-01-13 133 | 134 | ### Changed 135 | 136 | - Upgraded dependencies to address the dependabot alerts. 137 | 138 | - Upgraded peer dependency `firebase-admin` to `>=9`. 139 | 140 | ## 0.19.0 - 2020-10-26 141 | 142 | ### Added 143 | 144 | - Added `memory` to the agent options to allow configuring the memory limit. 145 | 146 | ## 0.18.0 - 2020-07-17 147 | 148 | ### Changed 149 | 150 | - Accurately detect if the agent code is executed in a non-Functions environment (i.e., emulator or tests code) and suppress warnings. 151 | 152 | ## 0.17.0 - 2020-07-14 153 | 154 | ### Fixed 155 | 156 | - Prevent intercepting app events and exceptions if the app's also using Sentry. 157 | 158 | ### Changed 159 | 160 | - Improve the behavior of the agent crashed during initialization. 161 | 162 | ### Added 163 | 164 | - Send more information with the ping request: agent and Node.js versions, and the current region. 165 | 166 | ## 0.16.0 - 2020-06-20 167 | 168 | - Upgraded `@google-cloud/firestore` to the latest version (v3). 169 | 170 | ## 0.15.0 - 2020-06-18 171 | 172 | ### Changed 173 | 174 | - Upgraded `firebase-tools` to the latest version (v8). 175 | 176 | ## 0.14.0 - 2020-05-27 177 | 178 | ### Changed 179 | 180 | - Unless explicitly specified, make complete Firestore database backup by default. 181 | 182 | ### Added 183 | 184 | - Added the ability to choose between complete and selective Firestore backups. 185 | 186 | - Added the ability to specify collection groups when selective Firestore backup is chosen. 187 | 188 | ## 0.13.0 - 2020-04-12 189 | 190 | ### Added 191 | 192 | - Added the ability to specify the region via `region` option. 193 | 194 | ## 0.12.0 - 2019-12-22 195 | 196 | ### Changed 197 | 198 | - Ignore the emulator environment. 199 | 200 | - Improve exceptions tracking: 201 | - Stop automatic exception tracking to prevent accidental user data leaks. 202 | - Send additional information that could help with debugging (user ID, project ID, Node.js version). 203 | - General improvements. 204 | 205 | ## 0.11.0 - 2019-12-20 206 | 207 | ### Added 208 | 209 | - Added README and license. 210 | 211 | ## 0.10.0 - 2019-11-25 212 | 213 | ### Changed 214 | 215 | - **BREAKING**: Use `module.exports` to export the agent for CommonJS. If you import the agent using `require('@backupfire/firebase').default` you'll need to drop `.default`. 216 | 217 | ## 0.9.0 - 2019-11-15 218 | 219 | ### Added 220 | 221 | - Add Firestore collections list endpoint. 222 | 223 | - Enable selective Firestore backups: 224 | - Always specify collection ids during export, to enable selective restore. 225 | - Allow specifying ignored collections during Firestore backup. 226 | 227 | ## 0.8.0 - 2019-11-14 228 | 229 | ### Added 230 | 231 | - Capture exceptions to Sentry. 232 | 233 | ## 0.7.0 - 2019-11-13 234 | 235 | ### Changed 236 | 237 | - **BREAKING**: Move firebase-admin and firebase-functions to peer dependencies. 238 | 239 | ## 0.6.0 - 2019-10-30 240 | 241 | ### Added 242 | 243 | - Protected the storage update endpoint with admin password. 244 | 245 | ## 0.5.0 - 2019-10-19 246 | 247 | ### Fixed 248 | 249 | - Fixed more incompatibilities with the Node.js v10 runtime. 250 | 251 | ## 0.4.0 - 2019-10-18 252 | 253 | ### Fixed 254 | 255 | - Make the agent work with Node.js v10 runtime. 256 | 257 | ### Changed 258 | 259 | - Always deploy the agent to `us-central1` region. 260 | 261 | ## 0.3.0 - 2019-10-18 262 | 263 | ### Added 264 | 265 | - Added `debug` option which prints debug information to the log. 266 | - Print warnings when environment configuration isn't found or necessary variables are missing in the runtime environment. 267 | 268 | ### Changed 269 | 270 | - Ignore function instances with name not equal `backupfire`. 271 | 272 | ## 0.2.0 - 2019-10-17 273 | 274 | ### Changed 275 | 276 | - Replace the Backup Fire agent with a dummy HTTP handler and print a warning when `backupfire` key isn't found in the Functions environment configuration. 277 | 278 | ## 0.1.0 - 2019-10-15 279 | 280 | Initial version. 281 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Backup Fire 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | - Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | - Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | - Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := build 2 | .PHONY: build test 3 | 4 | test: 5 | npx jest 6 | 7 | test-watch: 8 | npx jest --watch 9 | 10 | test-lib: 11 | node test/lib/commonjs.js 12 | npx tsx test/lib/ts.ts 13 | 14 | # Test projects 15 | 16 | deploy-test-server: 17 | @cd test/server && npx firebase deploy 18 | 19 | build-test-extension: 20 | @npx tsc test/extension/index.ts --esModuleInterop --outDir test/extension/build 21 | 22 | deploy-test-extension: build-test-extension 23 | @cd test/extension && npx firebase deploy 24 | 25 | # Staging & production 26 | 27 | build: 28 | @rm -rf lib 29 | @npx tsc 30 | @npx prettier "lib/**/*.[jt]s" --write --loglevel silent 31 | @cp package.json lib 32 | @cp *.md lib 33 | @rsync --archive --prune-empty-dirs --exclude '*.ts' --relative src/./ lib 34 | 35 | publish: build test-lib 36 | cd lib && npm publish --access public 37 | 38 | publish-next: build 39 | cd lib && npm publish --access public --tag next 40 | 41 | build-extension: 42 | @cd extension/functions && npm run build 43 | 44 | publish-extension: build-extension 45 | @cd extension && npx firebase ext:dev:publish backupfire/backupfire-agent 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backup Fire agent for Firebase 2 | 3 | This is open-source core of [Backup Fire](https://backupfire.dev), a service that enables automatic backup of Firestore, the Firebase's DB, and Firebase authentication data. 4 | 5 | **To setup automatic backups, sign up at [Backup Fire](https://backupfire.dev), create a project, and follow the provided instructions. The examples below are provided only as a reference.** 6 | 7 | ## Installation 8 | 9 | The library is available as [an npm package](https://www.npmjs.com/package/@backupfire/firebase). To install it, run: 10 | 11 | ```bash 12 | npm install @backupfire/firebase --save 13 | # or with yarn 14 | yarn add @backupfire/firebase 15 | ``` 16 | 17 | ## Usage 18 | 19 | **Make sure that you created and activated a project at [Backup Fire](https://backupfire.dev) first**. 20 | 21 | JavaScript: 22 | 23 | ```js 24 | // 1. Import the agent package 25 | const backupfireAgent = require('@backupfire/firebase') 26 | 27 | // 2. Create and export the agent 28 | exports.backupfire = backupfireAgent() 29 | ``` 30 | 31 | TypeScript: 32 | 33 | ```ts 34 | // 1. Import the agent package 35 | import backupfireAgent from '@backupfire/firebase' 36 | 37 | // 2. Create and export the agent 38 | export const backupfire = backupfireAgent() 39 | ``` 40 | 41 | Specify the region to deploy the agent function: 42 | 43 | ```ts 44 | import backupfireAgent from '@backupfire/firebase' 45 | 46 | export const backupfire = backupfireAgent({ 47 | // See the list of available regions: 48 | // https://firebase.google.com/docs/functions/locations 49 | region: 'europe-west3' 50 | }) 51 | ``` 52 | 53 | ## License 54 | 55 | [BSD 3-Clause © Backup Fire](./LICENSE.md) 56 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | 'power-assert' 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /examples/js/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "backup-fire-playground" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/js/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | -------------------------------------------------------------------------------- /examples/js/functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /examples/js/functions/firebase.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/js/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions') 2 | const admin = require('firebase-admin') 3 | // 1. Import the agent package 4 | const backupfireAgent = require('@backupfire/firebase') 5 | 6 | admin.initializeApp() 7 | 8 | exports.helloWorld = functions.https.onRequest((_request, response) => { 9 | response.send('Hello from Firebase!') 10 | }) 11 | 12 | // 2. Create and export the agent 13 | exports.backupfire = backupfireAgent() 14 | -------------------------------------------------------------------------------- /examples/js/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase serve --only functions", 6 | "shell": "firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "8" 13 | }, 14 | "dependencies": { 15 | "@backupfire/firebase": "^0.5.0", 16 | "firebase-admin": "^8.6.0", 17 | "firebase-functions": "^3.3.0" 18 | }, 19 | "devDependencies": { 20 | "firebase-functions-test": "^0.1.6" 21 | }, 22 | "private": true 23 | } 24 | -------------------------------------------------------------------------------- /examples/ts/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "backup-fire-playground" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/ts/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | -------------------------------------------------------------------------------- /examples/ts/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/ts/functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Typescript v1 declaration files 6 | typings/ 7 | 8 | node_modules/ -------------------------------------------------------------------------------- /examples/ts/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "build": "tsc", 5 | "serve": "npm run build && firebase serve --only functions", 6 | "shell": "npm run build && firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "8" 13 | }, 14 | "main": "lib/index.js", 15 | "dependencies": { 16 | "@backupfire/firebase": "^0.5.0", 17 | "firebase-admin": "^8.6.0", 18 | "firebase-functions": "^3.3.0" 19 | }, 20 | "devDependencies": { 21 | "typescript": "^3.2.2", 22 | "firebase-functions-test": "^0.1.6" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /examples/ts/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions' 2 | import * as admin from 'firebase-admin' 3 | // 1. Import the agent package. 4 | // Make sure that you have esModuleInterop set to true in compilerOptions in your tsconfig.json! 5 | import backupfireAgent from '@backupfire/firebase' 6 | 7 | admin.initializeApp() 8 | 9 | export const helloWorld = functions.https.onRequest((_request, response) => { 10 | response.send('Hello from Firebase!') 11 | }) 12 | 13 | // 2. Create and export the agent 14 | export const backupfire = backupfireAgent() 15 | -------------------------------------------------------------------------------- /examples/ts/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017" 10 | }, 11 | "compileOnSave": true, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /extension/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning]. 5 | This change log follows the format documented in [Keep a CHANGELOG]. 6 | 7 | [semantic versioning]: https://semver.org 8 | [keep a changelog]: https://keepachangelog.com 9 | 10 | ## 1.8.1 - 2023-03-15 11 | 12 | ### Fixed 13 | 14 | - Excluded collection groups from exported collections when responding to the controller. 15 | 16 | ## 1.8.0 - 2023-03-15 17 | 18 | ### Added 19 | 20 | - Respond with collection groups back to the controller to display collection groups on the backup page. 21 | 22 | ## 1.7.0 - 2023-03-10 23 | 24 | ### Fixed 25 | 26 | - Fixed compatibility with the latest firebase-functions. 27 | 28 | ## 1.6.2 - 2023-03-10 29 | 30 | ### Fixed 31 | 32 | - Rollback the runtime version back to v16 as v18 is not yet supported by the dependencies. 33 | 34 | ## 1.6.1 - 2023-03-10 35 | 36 | ### Fixed 37 | 38 | - Removed unsupported `invoker`, which unfortunately voids the related update from `v1.6.0`. 39 | 40 | ## v1.6.0 - 2023-03-10 41 | 42 | ### Changed 43 | 44 | - Upgraded the Node.js runtime from v16 to v18. 45 | 46 | - Bumped the default RAM to 1Gb RAM and set the timeout to 9 minutes. It solves the problem with the huge users' backups that either run out of memory or timeout. 47 | 48 | - Set function invoker to public, to avoid [random permission bug](https://github.com/firebase/firebase-tools/issues/3965#issuecomment-1006005316). 49 | 50 | - Upgraded firebase-functions and firebase-admin. 51 | 52 | ## v1.5.0 - 2023-02-28 53 | 54 | ### Changed 55 | 56 | - The version now match the agent npm package version. 57 | 58 | ### Added 59 | 60 | - Added logging to the agent endpoints to help with debugging and troubleshooting. 61 | 62 | ## v1.1.0 - 2022-08-03 63 | 64 | ### Changed 65 | 66 | - Dramatically decrease the memory footprint of the user backup, making `memory` usage unnecessary. 67 | 68 | - Updated dependencies to the latest supported versions. 69 | 70 | ### Added 71 | 72 | - Added support for more Firestore location regions. 73 | 74 | - Added list files endpoint to the agent API that will help with Realtime Database backup integration and check for the backups' status (storage class, size, etc.) 75 | 76 | - Added create storage endpoint to simplify the integration process and automatically create the backups bucket with optimal defaults. 77 | 78 | ## v1.0.0 - 2021-09-27 79 | 80 | ### Fixed 81 | 82 | - Fixed missing `datastore.viewer` role that allows fetching the list of collections needed to perform selective backups. 83 | 84 | ## v0.2.0 - 2021-05-20 85 | 86 | Initial version. 87 | -------------------------------------------------------------------------------- /extension/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Backup Fire 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /extension/README.md: -------------------------------------------------------------------------------- 1 | # Backup Fire agent extension for Firebase 2 | 3 | This is open-source core of [Backup Fire](https://backupfire.dev) packed as a Firebase extension, a service that enables automatic backup of Firestore, the Firebase's DB, and Firebase authentication data. 4 | 5 | **To setup automatic backups, sign up at [Backup Fire](https://backupfire.dev), create a project, and follow the provided instructions. The examples below are provided only as a reference.** 6 | 7 | ## Installation 8 | 9 | To install the agent extension, please execute the command below in the project directory. You can also specify the project ID using the argument: `--project=PROJECT_ID`. 10 | 11 | ```bash 12 | firebase ext:install backupfire/backupfire-agent 13 | # or with project ID: 14 | firebase ext:install backupfire/backupfire-agent --project=PROJECT_ID 15 | ``` 16 | 17 | During the installation you'll need to pick: 18 | 19 | - Region (pick the same region where your Firestore is deployed). 20 | 21 | - Agent token. 22 | 23 | - Admin password. 24 | 25 | To get the agent token you'll need to sign up and create a project at [Backup Fire](https://backupfire.dev). 26 | 27 | ## License 28 | 29 | [Apache-2.0 © Backup Fire](./LICENSE) 30 | -------------------------------------------------------------------------------- /extension/extension.yaml: -------------------------------------------------------------------------------- 1 | name: backupfire-agent 2 | version: 1.8.1 3 | specVersion: v1beta 4 | 5 | displayName: Backup Fire Agent 6 | description: Backup your Firestore and Firebase Authentication data 7 | 8 | license: Apache-2.0 9 | 10 | sourceUrl: https://github.com/backupfire/backupfire-firebase/tree/master/extension 11 | releaseNotesUrl: https://github.com/backupfire/backupfire-firebase/blob/master/extension/CHANGELOG.md 12 | 13 | author: 14 | authorName: Sasha Koss 15 | url: https://github.com/kossnocorp 16 | 17 | billingRequired: true 18 | 19 | params: 20 | - param: LOCATION 21 | label: Where's your Firestore is deployed? 22 | description: >- 23 | Where's your Firestore is deployed? You can find it on the bottom of this 24 | page: https://console.firebase.google.com/project/_/firestore. Pick 25 | project and search for "Cloud Firestore location:" label. 26 | type: select 27 | options: 28 | - label: Iowa (us-central1) / United States (nam5) 29 | value: us-central1 30 | - label: Oregon (us-west1) 31 | value: us-west1 32 | - label: Los Angeles (us-west2) 33 | value: us-west2 34 | - label: Salt Lake City (us-west3) 35 | value: us-west3 36 | - label: Las Vegas (us-west4) 37 | value: us-west4 38 | - label: Montréal (northamerica-northeast1) 39 | value: northamerica-northeast1 40 | - label: South Carolina (us-east1) 41 | value: us-east1 42 | - label: Northern Virginia (us-east4) 43 | value: us-east4 44 | - label: São Paulo (southamerica-east1) 45 | value: southamerica-east1 46 | - label: Belgium (europe-west1) / Europe (eur3) 47 | value: europe-west1 48 | - label: London (europe-west2) 49 | value: europe-west2 50 | - label: Frankfurt (europe-west3) 51 | value: europe-west3 52 | - label: Warsaw (europe-central2) 53 | value: europe-central2 54 | - label: Zürich (europe-west6) 55 | value: europe-west6 56 | - label: Mumbai (asia-south1) 57 | value: asia-south1 58 | - label: Singapore (asia-southeast1) 59 | value: asia-southeast1 60 | - label: Jakarta (asia-southeast2) 61 | value: asia-southeast2 62 | - label: Hong Kong (asia-east2) 63 | value: asia-east2 64 | - label: Taiwan (asia-east1) 65 | value: asia-east1 66 | - label: Tokyo (asia-northeast1) 67 | value: asia-northeast1 68 | - label: Osaka (asia-northeast2) 69 | value: asia-northeast2 70 | - label: Seoul (asia-northeast3) 71 | value: asia-northeast3 72 | - label: Sydney (australia-southeast1) 73 | value: australia-southeast1 74 | default: us-central1 75 | required: true 76 | immutable: true 77 | 78 | - param: BACKUPFIRE_TOKEN 79 | label: Agent token 80 | description: >- 81 | The token is used to secure a connection between the controller 82 | (BackupFire) and the agent (this extension). Used to perform backups, 83 | retrieve meta information, etc. 84 | type: string 85 | required: true 86 | 87 | - param: BACKUPFIRE_PASSWORD 88 | label: Admin password 89 | description: >- 90 | The admin password is used as an extra layer of protection and ensures 91 | that only you can perform destructive operations such as retention policy 92 | change. The password is not stored on the Backup Fire side. 93 | type: string 94 | required: true 95 | 96 | roles: 97 | - role: datastore.importExportAdmin 98 | reason: Allows to export Firestore data 99 | - role: datastore.viewer 100 | reason: Allows to fetch the list of collections needed to perform selective backups 101 | - role: firebaseauth.admin 102 | reason: Allows to export Firebase Authentication data 103 | - role: storage.admin 104 | reason: Allows to adjust retention policy 105 | 106 | resources: 107 | - name: backupfire 108 | type: firebaseextensions.v1beta.function 109 | properties: 110 | location: ${LOCATION} 111 | runtime: nodejs16 112 | timeout: 540s 113 | availableMemoryMb: 1024 114 | httpsTrigger: {} 115 | -------------------------------------------------------------------------------- /extension/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@backupfire/firebase-extension", 3 | "version": "1.8.1", 4 | "description": "Backup Fire Firebase extension", 5 | "homepage": "https://backupfire.dev", 6 | "repository": "https://github.com/backupfire/backupfire-firebase", 7 | "license": "Apache-2.0", 8 | "author": "Sasha Koss ", 9 | "main": "lib/index.js", 10 | "dependencies": { 11 | "@backupfire/firebase": "^1.8.1", 12 | "firebase-admin": "^11.5.0", 13 | "firebase-functions": "^4.2.1" 14 | }, 15 | "devDependencies": { 16 | "typescript": "^4.9.5" 17 | }, 18 | "scripts": { 19 | "build": "npm run clean && npm run compile", 20 | "clean": "rm -rf lib", 21 | "compile": "tsc" 22 | }, 23 | "private": true 24 | } 25 | -------------------------------------------------------------------------------- /extension/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import backupfire from '@backupfire/firebase' 2 | import * as admin from 'firebase-admin' 3 | 4 | admin.initializeApp() 5 | 6 | exports.backupfire = backupfire() 7 | -------------------------------------------------------------------------------- /extension/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./lib" 6 | }, 7 | "include": ["./src"], 8 | "exclude": [] 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src/'] 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@backupfire/firebase", 3 | "version": "1.9.1", 4 | "description": "Backup Fire Firebase agent", 5 | "keywords": [ 6 | "backup Firebase database", 7 | "backup Firestore", 8 | "backup Firebase Firestore", 9 | "backup Firebase authentication", 10 | "backup Firebase users", 11 | "backup Firebase data", 12 | "backup Google Firebase", 13 | "backup", 14 | "Backup Fire", 15 | "Firebase", 16 | "Firestore", 17 | "Firebase database", 18 | "Firebase authentication", 19 | "Firebase users" 20 | ], 21 | "main": "index.js", 22 | "homepage": "https://backupfire.dev", 23 | "repository": "https://github.com/backupfire/backupfire-firebase", 24 | "license": "BSD-3-Clause", 25 | "author": "Sasha Koss ", 26 | "devDependencies": { 27 | "@babel/core": "^7.13.14", 28 | "@babel/preset-env": "^7.13.12", 29 | "@babel/preset-typescript": "^7.13.0", 30 | "@types/body-parser": "^1.19.0", 31 | "@types/cors": "^2.8.10", 32 | "@types/express": "^4.17.13", 33 | "@types/express-jwt": "^6.0.4", 34 | "@types/jest": "^26.0.22", 35 | "@types/jsonwebtoken": "^8.5.8", 36 | "@types/mz": "^2.7.3", 37 | "@types/node": "^18.15.0", 38 | "@types/node-fetch": "^2.5.8", 39 | "@types/serve-static": "^1.13.10", 40 | "@types/sinon": "^9.0.11", 41 | "babel-loader": "^8.2.2", 42 | "babel-preset-power-assert": "^3.0.0", 43 | "esbuild": "^0.14.38", 44 | "firebase-admin": "^10.2.0", 45 | "firebase-functions": "^4.2.1", 46 | "jest": "^26", 47 | "power-assert": "^1.6.1", 48 | "prettier": "^2.2.1", 49 | "sinon": "^10.0.0", 50 | "tsx": "^3.12.3", 51 | "typescript": "^4.9.5" 52 | }, 53 | "dependencies": { 54 | "@google-cloud/firestore": "^4.15.1", 55 | "@google-cloud/storage": "^6.0.1", 56 | "@sentry/node": "^6.19.7", 57 | "body-parser": "^1.20.0", 58 | "cors": "^2.8.5", 59 | "express": "^4.18.1", 60 | "express-jwt": "^7.7.5", 61 | "firebase-tools": "^11.24.1", 62 | "googleapis": "^100.0.0", 63 | "jsonwebtoken": "^8.5.1", 64 | "node-fetch": "^2.6.7" 65 | }, 66 | "peerDependencies": { 67 | "firebase-admin": ">=9.6", 68 | "firebase-functions": ">=3.13.2" 69 | }, 70 | "resolutions": { 71 | "@types/serve-static": "1.13.10" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/_lib/asyncMiddleware/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | export default function asyncMiddleware( 4 | fn: express.RequestHandler 5 | ): express.RequestHandler { 6 | return (request, response, next) => { 7 | Promise.resolve(fn(request, response, next)).catch(next) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/_lib/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | import { Hub, Integrations, NodeClient } from '@sentry/node' 2 | import { ErrorRequestHandler } from 'express' 3 | import * as functions from 'firebase-functions' 4 | import { BackupFireHTTPSHandler } from '../../types' 5 | import version from '../../version' 6 | 7 | let client: NodeClient 8 | let hub: Hub 9 | 10 | export function initExceptionsTracker() { 11 | client = new NodeClient({ 12 | dsn: 'https://18820ae312bc46c4af3b672248d8a361@sentry.io/1819926', 13 | release: version, 14 | integrations: [new Integrations.FunctionToString()], 15 | defaultIntegrations: false, 16 | }) 17 | hub = new Hub(client) 18 | } 19 | 20 | export const captureException: Hub['captureException'] = (...args) => 21 | hub?.captureException(...args) 22 | 23 | export const configureExceptionsScope: Hub['configureScope'] = (...args) => 24 | hub?.configureScope(...args) 25 | 26 | export const flushExceptions: NodeClient['flush'] = (...args) => 27 | client?.flush(...args) 28 | 29 | export const exceptionHandlerMiddleware: ErrorRequestHandler = ( 30 | err, 31 | request, 32 | _response, 33 | next 34 | ) => { 35 | configureExceptionsScope((scope) => { 36 | scope.setUser({ ip_address: request.ip }) 37 | scope.setContext('request', request as any) 38 | }) 39 | captureException(err) 40 | next(err) 41 | } 42 | 43 | export function createCrashedApp(err: any): BackupFireHTTPSHandler { 44 | return (_request: functions.Request, response: functions.Response) => { 45 | const eventId = captureException(err) 46 | response.status(500).send({ 47 | message: `The Backup Fire agent has failed to initialiaze (see event ${eventId})`, 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/_lib/logging/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The {@link maskString} function options. 3 | */ 4 | export interface MaskStringOptions { 5 | /** If can display few chars */ 6 | critical?: boolean 7 | } 8 | 9 | /** 10 | * The function masks passed input string or returns a string describing 11 | * the type of the input. 12 | * 13 | * @param input - the value to mask 14 | * @param options - the masking options 15 | * @returns the masked string or a string describing the type of the input 16 | */ 17 | export function maskString( 18 | input: unknown, 19 | options?: MaskStringOptions 20 | ): string { 21 | if (typeof input !== 'string') { 22 | return `Type: ${typeof input}` 23 | } 24 | 25 | // Unless specified explicitly, mask all chars and display random length 26 | if (options?.critical !== false) { 27 | return '*'.repeat(8) 28 | } 29 | 30 | const length = input.length 31 | let unmaskedChars: number 32 | 33 | if (length <= 1) { 34 | unmaskedChars = 0 35 | } else if (length <= 4) { 36 | unmaskedChars = 1 37 | } else if (length <= 8) { 38 | unmaskedChars = 2 39 | } else { 40 | unmaskedChars = 3 41 | } 42 | 43 | const maskedPart = '*'.repeat(length - unmaskedChars) 44 | const unmaskedPart = input.slice(length - unmaskedChars) 45 | 46 | return `${maskedPart}${unmaskedPart}` 47 | } 48 | -------------------------------------------------------------------------------- /src/_lib/operation/index.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express' 2 | import { OperationStatus } from '../../firestore/status' 3 | 4 | export type FirestoreStatusResponse = 5 | | { 6 | state: 'completed' 7 | data: { 8 | usersCount: number | undefined 9 | size: string 10 | } 11 | } 12 | | { 13 | state: 'completed' | 'pending' 14 | data: { id: string; status: OperationStatus } 15 | } 16 | 17 | export type UsersStatusResponse = 18 | | { 19 | state: 'pending' 20 | } 21 | | { 22 | state: 'completed' 23 | data: { 24 | usersCount: number | undefined 25 | size: string 26 | } 27 | } 28 | | { 29 | state: 'failed' 30 | data: { 31 | reason: string 32 | } 33 | } 34 | 35 | export default function operationResponse( 36 | response: Response, 37 | payload: UsersStatusResponse | FirestoreStatusResponse 38 | ) { 39 | response.status(payload.state === 'failed' ? 400 : 200).send(payload) 40 | } 41 | -------------------------------------------------------------------------------- /src/firestore/_lib/client/index.ts: -------------------------------------------------------------------------------- 1 | import firestore from '@google-cloud/firestore' 2 | 3 | const client = new firestore.v1.FirestoreAdminClient() 4 | export default client 5 | -------------------------------------------------------------------------------- /src/firestore/backup/index.ts: -------------------------------------------------------------------------------- 1 | import { FirestoreBackupRequestBody } from '..' 2 | import firestore from '@google-cloud/firestore' 3 | 4 | export async function backupFirestore( 5 | projectId: string, 6 | collectionIds: string[] | undefined, 7 | options: FirestoreBackupRequestBody 8 | ) { 9 | const client = new firestore.v1.FirestoreAdminClient() 10 | 11 | const databaseName = client.databasePath(projectId, '(default)') 12 | 13 | // https://googleapis.dev/nodejs/firestore/latest/v1.FirestoreAdminClient.html#exportDocuments 14 | const [operation] = await client.exportDocuments({ 15 | name: databaseName, 16 | outputUriPrefix: `gs://${options.storageId}/${options.path}`, 17 | collectionIds, 18 | }) 19 | 20 | return operation.name 21 | } 22 | -------------------------------------------------------------------------------- /src/firestore/collections/index.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin' 2 | 3 | export async function getCollections() { 4 | const collections = await admin.firestore().listCollections() 5 | return collections.map(collection => { 6 | return collection.path 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/firestore/index.ts: -------------------------------------------------------------------------------- 1 | import operationResponse from '../_lib/operation' 2 | import asyncMiddleware from '../_lib/asyncMiddleware' 3 | import { backupFirestore } from './backup' 4 | import { checkFirestoreBackupStatus } from './status' 5 | import { Response } from 'express' 6 | import { getCollections } from './collections' 7 | import * as functions from 'firebase-functions' 8 | 9 | export interface FirestoreBackupOptions { 10 | bucketsAllowlist?: string[] 11 | projectId: string 12 | } 13 | 14 | export type FirestoreBackupRequestBody = 15 | | FirestoreBackupRequestBodyComplete 16 | | FirestoreBackupRequestBodySelective 17 | 18 | export interface FirestoreBackupRequestBodyBase { 19 | storageId: string 20 | path: string 21 | } 22 | 23 | export interface FirestoreBackupRequestBodyComplete 24 | extends FirestoreBackupRequestBodyBase { 25 | mode: 'complete' 26 | } 27 | 28 | export interface FirestoreBackupRequestBodySelective 29 | extends FirestoreBackupRequestBodyBase { 30 | mode: 'selective' 31 | ignoreCollections?: string[] 32 | collectionGroups?: string[] 33 | } 34 | 35 | export function backupFirestoreMiddleware({ 36 | bucketsAllowlist, 37 | projectId, 38 | }: FirestoreBackupOptions) { 39 | return asyncMiddleware(async (request, response) => { 40 | // TODO: Validate body 41 | const body = request.body as FirestoreBackupRequestBody 42 | 43 | if (body.mode === 'selective') { 44 | functions.logger.info('Requested Firestore backup', { 45 | // NOTE: Do not ...body here to avoid logging sensitive data 46 | mode: body.mode, 47 | ignoreCollections: body.ignoreCollections, 48 | collectionGroups: body.collectionGroups, 49 | bucketsAllowlist, 50 | projectId, 51 | }) 52 | 53 | // Get all root-level collections 54 | const allCollections = await getCollections() 55 | const { ignoreCollections, collectionGroups } = body 56 | const exportedCollections = ( 57 | ignoreCollections 58 | ? allCollections.filter((coll) => !ignoreCollections.includes(coll)) 59 | : allCollections 60 | ).concat(collectionGroups || []) 61 | 62 | functions.logger.info('Initiating selective Firestore backup', { 63 | exportedCollections, 64 | ignoreCollections, 65 | }) 66 | 67 | // Request selective Firestore backup 68 | const id = await backupFirestore(projectId, exportedCollections, body) 69 | 70 | if (id) { 71 | return respondWithStatus(response, id, { 72 | exportedCollections: exportedCollections.filter( 73 | (coll) => !collectionGroups?.includes(coll) 74 | ), 75 | ignoredCollections: ignoreCollections || [], 76 | exportedCollectionGroups: collectionGroups || [], 77 | }) 78 | } else { 79 | return respondWithMissingId(response) 80 | } 81 | } else { 82 | functions.logger.info('Requested Firestore backup', { 83 | // NOTE: Do not ...body here to avoid logging sensitive data 84 | mode: body.mode, 85 | bucketsAllowlist, 86 | projectId, 87 | }) 88 | 89 | // NOTE: Back-to-back logging here is to reflect the selective backup logging 90 | functions.logger.info('Initiating complete Firestore backup') 91 | 92 | // Request complete Firestore backup 93 | const id = await backupFirestore(projectId, undefined, body) 94 | 95 | if (id) { 96 | return respondWithStatus(response, id) 97 | } else { 98 | return respondWithMissingId(response) 99 | } 100 | } 101 | }) 102 | } 103 | 104 | export interface FirestoreCheckBackupStatusRequestOptions { 105 | id: string 106 | } 107 | 108 | export function checkFirestoreBackupStatusMiddleware() { 109 | return asyncMiddleware(async (request, response) => { 110 | // TODO: Validate query 111 | const query = 112 | request.query as unknown as FirestoreCheckBackupStatusRequestOptions 113 | 114 | functions.logger.info('Requested Firestore backup status') 115 | 116 | return respondWithStatus(response, query.id) 117 | }) 118 | } 119 | 120 | async function respondWithStatus( 121 | response: Response, 122 | id: string, 123 | extraData: object = {} 124 | ) { 125 | functions.logger.info('Checking the backup status', { id }) 126 | 127 | const status = await checkFirestoreBackupStatus(id) 128 | const state = status.done ? 'completed' : 'pending' 129 | 130 | functions.logger.info('Responding with the backup status', { id, state }) 131 | 132 | operationResponse(response, { 133 | state, 134 | data: Object.assign({ id, status }, extraData), 135 | }) 136 | } 137 | 138 | function respondWithMissingId(response: Response) { 139 | functions.logger.info('Responding with missing id error') 140 | 141 | operationResponse(response, { 142 | state: 'failed', 143 | data: { 144 | reason: 145 | 'Firestore backup failed to initiate: the operation response has no id.', 146 | }, 147 | }) 148 | } 149 | 150 | export function getCollectionsMiddleware() { 151 | return asyncMiddleware(async (_request, response) => { 152 | functions.logger.info('Requested Firestore collections') 153 | 154 | const collections = await getCollections() 155 | 156 | functions.logger.info('Responding with Firestore collections', { 157 | collections, 158 | }) 159 | 160 | response.send(collections) 161 | }) 162 | } 163 | -------------------------------------------------------------------------------- /src/firestore/status/index.ts: -------------------------------------------------------------------------------- 1 | import { google } from 'googleapis' 2 | import { GaxiosPromise } from 'gaxios' 3 | 4 | type OperationProgressWork = { 5 | estimatedWork: string // number as string 6 | completedWork: string // same as above 7 | } 8 | 9 | type OperationState = 10 | | 'STATE_UNSPECIFIED' 11 | | 'INITIALIZING' 12 | | 'PROCESSING' 13 | | 'CANCELLING' 14 | | 'FINALIZING' 15 | | 'SUCCESSFUL' 16 | | 'FAILED' 17 | | 'CANCELLED' 18 | 19 | type GetOperationResponse = GaxiosPromise<{ 20 | name: string // 'projects/backup-fire-staging/databases/(default)/operations/ASA2MTAwMTYyNDEyChp0bHVhZmVkBxJsYXJ0bmVjc3Utc2Jvai1uaW1kYRQKLRI' 21 | metadata: { 22 | '@type': string // 'type.googleapis.com/google.firestore.admin.v1.ExportDocumentsMetadata' 23 | startTime: string // '2019-09-19T08:00:00.717192Z' 24 | endTime: string // '2019-09-19T08:00:48.955036Z' 25 | operationState: OperationState 26 | progressDocuments: OperationProgressWork 27 | progressBytes: OperationProgressWork 28 | collectionIds?: string[] 29 | outputUriPrefix: string // 'gs://backup-fire-staging.appspot.com/2019-09-19T08:00:00_29822' 30 | } 31 | done: boolean 32 | response: { 33 | '@type': 'type.googleapis.com/google.firestore.admin.v1.ExportDocumentsResponse' 34 | outputUriPrefix: string // 'gs://backup-fire-staging.appspot.com/2019-09-19T08:00:00_29822' 35 | } 36 | }> 37 | 38 | type OperationStatusProgress = { 39 | estimatedWork: number 40 | completedWork: number 41 | } 42 | 43 | export type OperationStatus = { 44 | startTime: string 45 | endTime?: string 46 | done?: boolean 47 | progressDocuments?: OperationStatusProgress 48 | progressBytes?: OperationStatusProgress 49 | } 50 | 51 | export async function checkFirestoreBackupStatus( 52 | id: string 53 | ): Promise { 54 | const auth = new google.auth.GoogleAuth({ 55 | scopes: [ 56 | 'https://www.googleapis.com/auth/cloud-platform', 57 | 'https://www.googleapis.com/auth/datastore' 58 | ] 59 | }) 60 | const authClient = await auth.getClient() 61 | const firestore = google.firestore({ 62 | version: 'v1', 63 | auth: authClient 64 | }) 65 | const response = await (firestore.projects.databases.operations.get({ 66 | name: id 67 | }) as GetOperationResponse) 68 | 69 | const { 70 | done, 71 | metadata: { startTime, endTime, progressDocuments, progressBytes } 72 | } = response.data 73 | 74 | return { 75 | startTime, 76 | endTime, 77 | done, 78 | progressDocuments: progressDocuments && parseProgress(progressDocuments), 79 | progressBytes: progressBytes && parseProgress(progressBytes) 80 | } 81 | } 82 | 83 | function parseProgress( 84 | progress: OperationProgressWork 85 | ): OperationStatusProgress { 86 | return { 87 | estimatedWork: parseInt(progress.estimatedWork), 88 | completedWork: parseInt(progress.completedWork) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser' 2 | import cors from 'cors' 3 | import express from 'express' 4 | import { expressjwt as jwt } from 'express-jwt' 5 | import * as functions from 'firebase-functions' 6 | import fetch from 'node-fetch' 7 | import { format } from 'url' 8 | import { 9 | backupFirestoreMiddleware, 10 | checkFirestoreBackupStatusMiddleware, 11 | getCollectionsMiddleware, 12 | } from './firestore' 13 | import { 14 | defaultControllerDomain, 15 | defaultMemory, 16 | defaultRegion, 17 | defaultTimeout, 18 | } from './options' 19 | import { 20 | createStorageMiddleware, 21 | listFilesMiddleware, 22 | storageListMiddleware, 23 | updateStorageMiddleware, 24 | } from './storage' 25 | import { 26 | AgentOptions, 27 | BackupFireEnvConfig, 28 | BackupFireHTTPSHandler, 29 | BackupFireOptions, 30 | RuntimeEnvironment, 31 | } from './types' 32 | import { backupUsersMiddleware } from './users' 33 | import version from './version' 34 | import { 35 | configureExceptionsScope, 36 | createCrashedApp, 37 | exceptionHandlerMiddleware, 38 | initExceptionsTracker, 39 | } from './_lib/exceptions' 40 | import { maskString } from './_lib/logging' 41 | 42 | export enum BackupFireConfig { 43 | Token = 'BACKUPFIRE_TOKEN', 44 | Password = 'BACKUPFIRE_PASSWORD', 45 | Domain = 'BACKUPFIRE_DOMAIN', 46 | Allowlist = 'BACKUPFIRE_ALLOWLIST', 47 | } 48 | 49 | // Fallback for CommonJS 50 | module.exports = backupFire 51 | 52 | /** 53 | * Creates Backup Fire Firebase Functions HTTPS handler. 54 | * 55 | * @param options - The Backup Fire agent options 56 | */ 57 | export default function backupFire(agentOptions?: AgentOptions) { 58 | initExceptionsTracker() 59 | 60 | try { 61 | // Use dummy handler if it's emulator or not deployed to Functions 62 | if (isEmulator() || !isDeployedToFunctions()) 63 | return dummyHandler({ 64 | region: agentOptions?.region, 65 | memory: agentOptions?.memory, 66 | timeout: agentOptions?.timeout, 67 | }) 68 | 69 | // Derive Backup Fire options from environment configuration 70 | const envConfig = getEnvConfig() 71 | 72 | // If options aren't set, use dummy handler instead 73 | if (!envConfig) { 74 | functions.logger.warn( 75 | `Warning: the Backup Fire configuration is missing, either set BACKUPFIRE_TOKEN and BACKUPFIRE_PASSWORD or set backupfire.token and backupfire.password as env config values. Running a dummy HTTP handler instead of the Backup Fire agent...` 76 | ) 77 | return dummyHandler({ region: agentOptions?.region }) 78 | } 79 | 80 | const options: BackupFireOptions = Object.assign( 81 | { 82 | controllerDomain: envConfig.domain, 83 | controllerToken: envConfig.token, 84 | adminPassword: envConfig.password, 85 | bucketsAllowlist: envConfig.allowlist?.split(','), 86 | debug: envConfig.debug === 'true', 87 | }, 88 | agentOptions 89 | ) 90 | 91 | // Get runtime environment (Firebase project ID, region, etc) 92 | 93 | const runtimeEnv = getRuntimeEnv(options) 94 | 95 | // If the function name isn't backupfire, use dummy handler 96 | if (runtimeEnv.functionName !== 'backupfire') { 97 | if (options.debug) 98 | functions.logger.debug( 99 | `The function name isn't "backupfire" (${runtimeEnv.functionName}). Running a dummy HTTP handler instead of the Backup Fire agent...` 100 | ) 101 | return dummyHandler(options) 102 | } 103 | 104 | // If some of the variables are missing, use dummy handler 105 | if (!isCompleteRuntimeEnv(runtimeEnv)) { 106 | functions.logger.warn( 107 | 'Warning: runtime environment is incomplete:', 108 | prettyJSON(runtimeEnv) 109 | ) 110 | functions.logger.warn( 111 | 'Running a dummy HTTP handler instead of the Backup Fire agent...' 112 | ) 113 | return dummyHandler(options) 114 | } 115 | 116 | // Set additional context 117 | configureExceptionsScope((scope) => { 118 | scope.setUser({ id: envConfig.token }) 119 | scope.setTag('project_id', runtimeEnv.projectId) 120 | scope.setTag('node_version', process.version) 121 | }) 122 | 123 | if (options.debug) { 124 | functions.logger.debug( 125 | 'Initializing Backup Fire agent with options:', 126 | prettyJSON(options) 127 | ) 128 | functions.logger.debug('Runtime environment:', prettyJSON(runtimeEnv)) 129 | } 130 | 131 | // Send the initialization ping to the controller 132 | sendInitializationPing(options, runtimeEnv) 133 | 134 | return httpsHandler({ 135 | handler: createApp(runtimeEnv, options), 136 | agentOptions, 137 | runtimeEnv, 138 | }) 139 | } catch (err) { 140 | return httpsHandler({ 141 | handler: createCrashedApp(err), 142 | agentOptions, 143 | }) 144 | } 145 | } 146 | 147 | /** 148 | * Creates [Express] app that serves as an agent that provide API to 149 | * the Backup Fire controller. 150 | * 151 | * @param runtimeEnv - The runtime environment variables 152 | * @param options - The Backup Fire agent options 153 | * 154 | * [Express]: https://expressjs.com/ 155 | */ 156 | export function createApp( 157 | runtimeEnv: RuntimeEnvironment, 158 | options: BackupFireOptions 159 | ): BackupFireHTTPSHandler { 160 | // Create Express app that would be mounted as a function 161 | const app = express() 162 | 163 | // Protect Backup Fire API with token authorization 164 | app.use(jwt({ secret: options.controllerToken, algorithms: ['HS256'] })) 165 | 166 | // Parse JSON body 167 | app.use(bodyParser.json()) 168 | 169 | // Allow requests from web 170 | app.use(cors({ origin: true })) 171 | 172 | const globalOptions = { bucketsAllowlist: options.bucketsAllowlist } 173 | 174 | // Backup Firestore 175 | app.post( 176 | '/firestore', 177 | backupFirestoreMiddleware({ 178 | projectId: runtimeEnv.projectId, 179 | ...globalOptions, 180 | }) 181 | ) 182 | // Check Firestore backup status 183 | app.get('/firestore/status', checkFirestoreBackupStatusMiddleware()) 184 | 185 | // List collections 186 | app.get('/firestore/collections', getCollectionsMiddleware()) 187 | 188 | // Backup Firebase users 189 | app.post( 190 | '/users', 191 | backupUsersMiddleware({ 192 | projectId: runtimeEnv.projectId, 193 | controllerToken: options.controllerToken, 194 | controllerDomain: options.controllerDomain, 195 | agentURL: agentURL(runtimeEnv), 196 | ...globalOptions, 197 | }) 198 | ) 199 | 200 | // List storage 201 | app.get('/storage', storageListMiddleware(globalOptions)) 202 | // Create storage 203 | app.post('/storage', createStorageMiddleware(globalOptions)) 204 | // Update storage 205 | app.put( 206 | '/storage/:storageId', 207 | updateStorageMiddleware({ 208 | adminPassword: options.adminPassword, 209 | ...globalOptions, 210 | }) 211 | ) 212 | // List files in the storage 213 | app.get('/storage/:storageId/files', listFilesMiddleware(globalOptions)) 214 | 215 | app.use(exceptionHandlerMiddleware) 216 | 217 | return app 218 | } 219 | 220 | interface HTTPSHandlerProps { 221 | handler: BackupFireHTTPSHandler 222 | agentOptions: AgentOptions | undefined 223 | runtimeEnv?: RuntimeEnvironment 224 | } 225 | 226 | function httpsHandler({ 227 | handler, 228 | agentOptions, 229 | runtimeEnv, 230 | }: HTTPSHandlerProps) { 231 | if (runtimeEnv?.extensionId) { 232 | return functions.https.onRequest(handler) 233 | } else { 234 | return functions 235 | .runWith({ 236 | ...getRuntimeOptions(agentOptions), 237 | secrets: Object.values(BackupFireConfig), 238 | // Sometimes Firebase Functions doesn't set the invoker correctly: 239 | // See: https://github.com/firebase/firebase-tools/issues/3965#issuecomment-1006005316 240 | invoker: 'public', 241 | }) 242 | .region(agentOptions?.region || defaultRegion) 243 | .https.onRequest(handler) 244 | } 245 | } 246 | 247 | function sendInitializationPing( 248 | options: BackupFireOptions, 249 | runtimeEnv: RuntimeEnvironment 250 | ) { 251 | // TODO: Report failure if the request fails 252 | const pingURL = format({ 253 | hostname: options.controllerDomain || defaultControllerDomain, 254 | protocol: 'https', 255 | pathname: '/ping', 256 | query: { 257 | agentVersion: version, 258 | nodeVersion: process.version, 259 | region: runtimeEnv.region, 260 | token: options.controllerToken, 261 | projectId: runtimeEnv.projectId, 262 | runtime: runtimeEnv.region, 263 | agentURL: agentURL(runtimeEnv), 264 | }, 265 | }) 266 | return fetch(pingURL) 267 | } 268 | 269 | function agentURL(runtimeEnv: RuntimeEnvironment) { 270 | const { region, projectId, functionName, extensionId } = runtimeEnv 271 | return `https://${region}-${projectId}.cloudfunctions.net/${ 272 | extensionId ? `ext-${extensionId}-${functionName}` : functionName 273 | }` 274 | } 275 | 276 | function isEmulator() { 277 | return process.env.FUNCTIONS_EMULATOR === 'true' 278 | } 279 | 280 | function isDeployedToFunctions() { 281 | // Detect if the agent is deployed to Firebase Functions 282 | // See: https://cloud.google.com/functions/docs/env-var#environment_variables_set_automatically 283 | return !!process.env.FUNCTION_NAME || !!process.env.FUNCTION_TARGET 284 | } 285 | 286 | function getRuntimeEnv( 287 | options: BackupFireOptions 288 | ): Partial | RuntimeEnvironment { 289 | const extensionId = process.env.EXT_INSTANCE_ID 290 | 291 | return { 292 | region: extensionId 293 | ? process.env.LOCATION 294 | : options.region || defaultRegion, 295 | // Node.js v8 runtime sets GCP_PROJECT, while v10 uses depricated GCLOUD_PROJECT 296 | projectId: process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT, 297 | // Node.js v8 runtime uses FUNCTION_NAME, v10 — FUNCTION_TARGET 298 | // See: https://cloud.google.com/functions/docs/env-var#environment_variables_set_automatically 299 | functionName: process.env.FUNCTION_NAME || process.env.FUNCTION_TARGET, 300 | extensionId, 301 | } 302 | } 303 | 304 | function getEnvConfig(): BackupFireEnvConfig | undefined { 305 | functions.logger.debug('Retrieving the env config') 306 | 307 | const token = process.env[BackupFireConfig.Token] 308 | const password = process.env[BackupFireConfig.Password] 309 | const domain = process.env[BackupFireConfig.Domain] 310 | const allowlist = process.env[BackupFireConfig.Allowlist] 311 | 312 | const envConfig = functions.config().backupfire as 313 | | BackupFireEnvConfig 314 | | undefined 315 | 316 | functions.logger.debug('The env variables found', { 317 | token: maskString(token, { critical: false }), 318 | password: maskString(password), 319 | domain, 320 | allowlist, 321 | }) 322 | 323 | if (envConfig) { 324 | functions.logger.debug('The Firebase env config found', { 325 | password: maskString(envConfig.password), 326 | token: maskString(envConfig.token, { critical: false }), 327 | }) 328 | } else { 329 | functions.logger.debug('The Firebase env config is not found') 330 | } 331 | 332 | // First, check if the env contains the token & password 333 | if (token && password) { 334 | functions.logger.debug('Using env variable values') 335 | return { token, password, domain, allowlist } 336 | } 337 | // Otherwise, return the env config value 338 | else { 339 | functions.logger.debug('Using Firebase env config values') 340 | return envConfig 341 | } 342 | } 343 | 344 | function isCompleteRuntimeEnv( 345 | runtimeEnv: Partial | RuntimeEnvironment 346 | ): runtimeEnv is RuntimeEnvironment { 347 | return ( 348 | !!runtimeEnv.functionName && !!runtimeEnv.projectId && !!runtimeEnv.region 349 | ) 350 | } 351 | 352 | function dummyHandler( 353 | options: Pick 354 | ) { 355 | const runtimeOptions: functions.RuntimeOptions = {} 356 | if (options?.memory) runtimeOptions.memory = options.memory 357 | if (options?.timeout) runtimeOptions.timeoutSeconds = options.timeout 358 | 359 | return functions 360 | .runWith(getRuntimeOptions(options)) 361 | .region(options.region || defaultRegion) 362 | .https.onRequest((_req, resp) => { 363 | resp.end() 364 | }) 365 | } 366 | 367 | function prettyJSON(obj: any) { 368 | return JSON.stringify(obj, null, 2) 369 | } 370 | 371 | /** 372 | * 373 | * @param agentOptions - TODO 374 | * @returns 375 | */ 376 | function getRuntimeOptions( 377 | agentOptions: AgentOptions | undefined 378 | ): functions.RuntimeOptions { 379 | const options: functions.RuntimeOptions = { 380 | // Always assign timeout and memory to runtime options. Unless the user 381 | // defines custom values, we want to use the default timeout of 9 minutes 382 | // and 1Gb memory to make sure the user backups are completed regardless 383 | // of how many there are. 384 | timeoutSeconds: agentOptions?.timeout || defaultTimeout, 385 | memory: agentOptions?.memory || defaultMemory, 386 | } 387 | 388 | if (agentOptions?.memory) options.memory = agentOptions.memory 389 | 390 | return options 391 | } 392 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | export const defaultControllerDomain = 'backupfire.dev' 2 | 3 | export const defaultRegion = 'us-central1' 4 | 5 | /** 6 | * The default function timeout - 9 minutes. It ensures that the user backups 7 | * are completed regardless of how many there are. 8 | * 9 | * Unlike the memory runtime option, timeout doesn't affect the function 10 | * instance price, so it's safe to set it to max. 11 | */ 12 | export const defaultTimeout = 540 13 | 14 | /** 15 | * The default function memory. With the increased timeout, it ensures 16 | * the users' backup completion. 17 | * 18 | * Internal testing shows that 1GB is the sweet spot. It's still cheap to run 19 | * and gives room to process huge backups. 20 | */ 21 | export const defaultMemory = '1GB' 22 | -------------------------------------------------------------------------------- /src/storage/index.ts: -------------------------------------------------------------------------------- 1 | import asyncMiddleware from '../_lib/asyncMiddleware' 2 | import { Storage as CloudStorage, Bucket } from '@google-cloud/storage' 3 | import * as functions from 'firebase-functions' 4 | 5 | export interface StorageOptions { 6 | bucketsAllowlist?: string[] 7 | } 8 | 9 | export function storageListMiddleware({ bucketsAllowlist }: StorageOptions) { 10 | return asyncMiddleware(async (request, response) => { 11 | functions.logger.info('Requested the storage list', { bucketsAllowlist }) 12 | 13 | const storage = new CloudStorage() 14 | const [buckets] = await storage.getBuckets() 15 | const storageList: Storage[] = buckets.map(bucketAsStorage) 16 | 17 | functions.logger.info('Responding with the storage list', { storageList }) 18 | 19 | response.send(storageList) 20 | }) 21 | } 22 | 23 | interface StorageRetentionData { 24 | removeOldBackups: boolean 25 | keepBackupsValue: number 26 | keepBackupsUnit: KeepBackupsUnit 27 | } 28 | 29 | type KeepBackupsUnit = 'years' | 'months' | 'days' 30 | 31 | export interface CreateStorageRequestBody 32 | extends Partial { 33 | storageId: string 34 | location: string 35 | storageClass?: 'standard' | 'nearline' | 'coldline' | 'archive' 36 | } 37 | 38 | export function createStorageMiddleware({ bucketsAllowlist }: StorageOptions) { 39 | return asyncMiddleware(async (request, response) => { 40 | const body = request.body as CreateStorageRequestBody 41 | 42 | functions.logger.info('Requested a bucket creation', { 43 | // NOTE: Do not ...body here to avoid logging sensitive data 44 | storageId: body.storageId, 45 | location: body.location, 46 | storageClass: body.storageClass, 47 | removeOldBackups: body.removeOldBackups, 48 | keepBackupsValue: body.keepBackupsValue, 49 | keepBackupsUnit: body.keepBackupsUnit, 50 | bucketsAllowlist, 51 | }) 52 | 53 | const [bucket] = await new CloudStorage().createBucket(body.storageId, { 54 | [body.storageClass || 'nearline']: true, 55 | location: body.location, 56 | }) 57 | 58 | functions.logger.info('Bucket created', { bucket: bucket.name }) 59 | 60 | if ( 61 | body.removeOldBackups && 62 | body.keepBackupsValue && 63 | body.keepBackupsUnit 64 | ) { 65 | const deleteAge = keepBackupInDays( 66 | body.keepBackupsValue, 67 | body.keepBackupsUnit 68 | ) 69 | 70 | if (body.removeOldBackups) { 71 | functions.logger.info('Setting the bucket lifecycle', { deleteAge }) 72 | 73 | await bucket.addLifecycleRule( 74 | { 75 | action: { type: 'Delete' }, 76 | condition: { age: deleteAge }, 77 | }, 78 | { append: false } 79 | ) 80 | } else { 81 | functions.logger.info('Clearing the bucket lifecycle') 82 | 83 | await bucket.setMetadata({ lifecycle: null }) 84 | } 85 | } 86 | 87 | const [bucketData] = await bucket.get() 88 | const storage = bucketAsStorage(bucketData) 89 | 90 | functions.logger.info('Responding with the storage object', { storage }) 91 | 92 | response.send(storage) 93 | }) 94 | } 95 | 96 | export interface PasswordedStorageOptions extends StorageOptions { 97 | adminPassword: string 98 | } 99 | 100 | interface UpdateStorageRequestBody extends StorageRetentionData { 101 | password: string 102 | } 103 | 104 | export function updateStorageMiddleware({ 105 | bucketsAllowlist, 106 | adminPassword, 107 | }: PasswordedStorageOptions) { 108 | return asyncMiddleware(async (request, response) => { 109 | // TODO: Validate the payload 110 | const storageId = request.params.storageId as string 111 | const body = request.body as UpdateStorageRequestBody 112 | 113 | functions.logger.info('Requested a bucket update', { 114 | // NOTE: Do not ...body here to avoid logging sensitive data 115 | removeOldBackups: body.removeOldBackups, 116 | keepBackupsValue: body.keepBackupsValue, 117 | keepBackupsUnit: body.keepBackupsUnit, 118 | bucketsAllowlist, 119 | }) 120 | 121 | if (body.password !== adminPassword) { 122 | functions.logger.info( 123 | 'Admin password is incorrect, responding with an error' 124 | ) 125 | 126 | response.status(403).send({ friendlyMessage: 'Wrong admin password' }) 127 | return 128 | } 129 | 130 | const bucket = new CloudStorage().bucket(storageId) 131 | const deleteAge = keepBackupInDays( 132 | body.keepBackupsValue, 133 | body.keepBackupsUnit 134 | ) 135 | 136 | if (body.removeOldBackups) { 137 | functions.logger.info('Setting the bucket lifecycle', { deleteAge }) 138 | 139 | await bucket.addLifecycleRule( 140 | { 141 | action: { type: 'Delete' }, 142 | condition: { age: deleteAge }, 143 | }, 144 | { append: false } 145 | ) 146 | } else { 147 | functions.logger.info('Clearing the bucket lifecycle') 148 | 149 | await bucket.setMetadata({ lifecycle: null }) 150 | } 151 | 152 | const [bucketData] = await bucket.get() 153 | const storage = bucketAsStorage(bucketData) 154 | 155 | functions.logger.info('Responding with the storage object', { storage }) 156 | 157 | response.send(storage) 158 | }) 159 | } 160 | 161 | export interface ListFilesRequestQuery { 162 | path: string 163 | maxResults: string | undefined 164 | startOffset: string | undefined 165 | endOffset: string | undefined 166 | prefix: string | undefined 167 | } 168 | 169 | export function listFilesMiddleware({ bucketsAllowlist }: StorageOptions) { 170 | return asyncMiddleware(async (request, response) => { 171 | const storageId = request.params.storageId as string 172 | const query = request.query as unknown as ListFilesRequestQuery 173 | 174 | functions.logger.info('Requested bucket files list', { 175 | // NOTE: Do not ...query here to avoid logging sensitive data 176 | storageId, 177 | path: query.path, 178 | maxResults: query.maxResults, 179 | startOffset: query.startOffset, 180 | endOffset: query.endOffset, 181 | prefix: query.prefix, 182 | bucketsAllowlist, 183 | }) 184 | 185 | const bucket = new CloudStorage().bucket(storageId) 186 | const [files] = await bucket.getFiles({ 187 | prefix: query.path, 188 | maxResults: query.maxResults ? parseInt(query.maxResults) : undefined, 189 | startOffset: query.startOffset, 190 | endOffset: query.endOffset, 191 | autoPaginate: !query.endOffset && !query.startOffset, 192 | }) 193 | 194 | const result = files.map((file) => ({ 195 | name: file.name, 196 | createdAt: file.metadata.timeCreated, 197 | updatedAt: file.metadata.updated, 198 | size: parseInt(file.metadata.size), 199 | contentType: file.metadata.contentType, // 'application/octet-stream' 200 | storageClass: file.metadata.storageClass, // 'STANDARD' 201 | storageClassUpdatedAt: new Date(file.metadata.timeStorageClassUpdated), 202 | etag: file.metadata.etag, 203 | })) 204 | 205 | response.send(result) 206 | }) 207 | } 208 | 209 | function bucketAsStorage(bucket: Bucket): Storage { 210 | const { 211 | metadata: { 212 | name, 213 | id, 214 | location, 215 | storageClass, 216 | locationType, 217 | projectNumber, 218 | timeCreated, 219 | updated, 220 | lifecycle, 221 | }, 222 | } = bucket 223 | 224 | return { 225 | name, 226 | id, 227 | location, 228 | projectNumber, 229 | storageClass, 230 | locationType, 231 | createdAt: new Date(timeCreated), 232 | updatedAt: new Date(updated), 233 | lifecycle: lifecycle && lifecycle.rule, 234 | } 235 | } 236 | 237 | export function keepBackupInDays(value: number, unit: KeepBackupsUnit) { 238 | // https://cloud.google.com/storage/docs/bucket-lock#retention-periods 239 | switch (unit) { 240 | case 'days': 241 | return value 242 | 243 | case 'months': 244 | return 31 * value 245 | 246 | case 'years': 247 | return Math.floor(365.25 * value) 248 | } 249 | } 250 | 251 | export interface Storage { 252 | name: string 253 | id: string 254 | location: StorageLocation 255 | storageClass: StorageClass 256 | locationType: StorageLocationType 257 | projectNumber: string 258 | createdAt: Date 259 | updatedAt: Date 260 | lifecycle: LifecycleRule[] 261 | } 262 | 263 | interface LifecycleRule { 264 | action: { type: string; storageClass?: string } | string 265 | condition: { [key: string]: boolean | Date | number | string } 266 | storageClass?: string 267 | } 268 | 269 | export type StorageLocation = 270 | | 'US' 271 | | 'EU' 272 | | 'ASIA' 273 | | 'NORTHAMERICA-NORTHEAST1' 274 | | 'EUR4' 275 | | 'NAM4' 276 | | 'US-CENTRAL1' 277 | | 'US-EAST1' 278 | | 'US-EAST4' 279 | | 'US-WEST1' 280 | | 'US-WEST2' 281 | | 'SOUTHAMERICA-EAST1' 282 | | 'EUROPE-NORTH1' 283 | | 'EUROPE-WEST1' 284 | | 'EUROPE-WEST2' 285 | | 'EUROPE-WEST3' 286 | | 'EUROPE-WEST4' 287 | | 'EUROPE-WEST6' 288 | | 'ASIA-EAST1' 289 | | 'ASIA-EAST2' 290 | | 'ASIA-NORTHEAST1' 291 | | 'ASIA-NORTHEAST2' 292 | | 'ASIA-SOUTH1' 293 | | 'ASIA-SOUTHEAST1' 294 | | 'AUSTRALIA-SOUTHEAST1' 295 | 296 | export type StorageClass = 297 | | 'STANDARD' 298 | | 'NEARLINE' /* min 30 days */ 299 | | 'COLDLINE' /* min 90 days */ 300 | | 'MULTI-REGIONAL' 301 | | 'REGIONAL' 302 | | 'DURABLE_REDUCED_AVAILABILITY' 303 | 304 | export type StorageLocationType = 'region' | 'dual-region' | 'multi-region' 305 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Region = typeof import('firebase-functions').region extends ( 2 | region: infer RegionType 3 | ) => any 4 | ? RegionType 5 | : never 6 | 7 | export type Memory = typeof import('firebase-functions').VALID_MEMORY_OPTIONS[number] 8 | 9 | /** 10 | * Backup Fire agent options. 11 | */ 12 | export interface BackupFireOptions { 13 | /** 14 | * The Google Cloud region id where to deploy the Firebase function. 15 | */ 16 | region?: Region 17 | 18 | /** 19 | * The agent function memory limit, defaults to "256MB". 20 | */ 21 | memory?: Memory 22 | 23 | /** 24 | * The agent function timeout in seconds, defaults to 60. 25 | */ 26 | timeout?: number 27 | 28 | /** 29 | * The controller app domain, defaults to backupfire.dev. 30 | */ 31 | controllerDomain?: string 32 | 33 | /** 34 | * The controller access token that allows to securely communicate with 35 | * the controller. 36 | */ 37 | controllerToken: string 38 | 39 | /** 40 | * The admin password which protects the agent from unauthorized commands 41 | * from the controller. 42 | */ 43 | adminPassword: string 44 | 45 | /** 46 | * The list of buckets where the data can be backed up. It protects the agent 47 | * from malformed backup commands from the controller. 48 | */ 49 | bucketsAllowlist?: string[] 50 | 51 | /** 52 | * Make the agent print debug messages to the log. 53 | */ 54 | debug?: boolean 55 | } 56 | 57 | // TODO: Split options definition to the ones coming from the environment config 58 | // and the user-defined agent options. 59 | export type AgentOptions = Pick< 60 | BackupFireOptions, 61 | 'region' | 'controllerDomain' | 'debug' | 'memory' | 'timeout' 62 | > 63 | 64 | export interface BackupFireEnvConfig { 65 | domain?: string 66 | token: string 67 | password: string 68 | allowlist?: string 69 | debug?: string 70 | } 71 | 72 | export interface RuntimeEnvironment { 73 | region: string 74 | projectId: string 75 | functionName: string 76 | extensionId: string | undefined 77 | runtime: string | undefined 78 | } 79 | 80 | export type BackupFireHTTPSHandler = ( 81 | request: import('firebase-functions').Request, 82 | response: import('firebase-functions').Response 83 | ) => any 84 | -------------------------------------------------------------------------------- /src/users/index.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin' 2 | import * as tools from 'firebase-tools' 3 | import fs from 'fs' 4 | import fetch from 'node-fetch' 5 | import { tmpdir } from 'os' 6 | import { parse, resolve } from 'path' 7 | import { format } from 'url' 8 | import { promisify } from 'util' 9 | import { defaultControllerDomain } from '../options' 10 | import asyncMiddleware from '../_lib/asyncMiddleware' 11 | import operationResponse, { UsersStatusResponse } from '../_lib/operation' 12 | import * as functions from 'firebase-functions' 13 | 14 | const unlink = promisify(fs.unlink) 15 | 16 | export interface UsersBackupOptions { 17 | bucketsAllowlist?: string[] 18 | projectId: string 19 | controllerDomain?: string 20 | controllerToken: string 21 | agentURL: string 22 | } 23 | 24 | export interface UsersBackupRequestBody { 25 | storageId: string 26 | path: string 27 | delay?: { 28 | backupId: string 29 | state: 'delay' | 'backup' 30 | } 31 | } 32 | 33 | export function backupUsersMiddleware({ 34 | bucketsAllowlist, 35 | projectId, 36 | controllerDomain, 37 | controllerToken, 38 | agentURL, 39 | }: UsersBackupOptions) { 40 | return asyncMiddleware(async (request, response) => { 41 | // TODO: Validate options 42 | const body = request.body as UsersBackupRequestBody 43 | 44 | functions.logger.info('Requested users backup', { 45 | // NOTE: Do not ...body here to avoid logging sensitive data 46 | storageId: body.storageId, 47 | path: body.path, 48 | backupId: body.delay?.backupId, 49 | state: body.delay?.state, 50 | bucketsAllowlist, 51 | projectId, 52 | }) 53 | 54 | if (body.delay?.state === 'delay') { 55 | functions.logger.info('Delaying users backup') 56 | 57 | // NOTE: Trigger backup, but do not wait for the result 58 | fetch(agentURL + request.path, { 59 | method: 'POST', 60 | headers: { 61 | // Authorization can't be missing as we verify it 62 | Authorization: request.header('Authorization')!, 63 | 'Content-Type': 'application/json', 64 | }, 65 | body: JSON.stringify({ 66 | storageId: body.storageId, 67 | path: body.path, 68 | delay: { 69 | state: 'backup', 70 | backupId: body.delay.backupId, 71 | }, 72 | }), 73 | }) 74 | 75 | functions.logger.info('Responding with pending status') 76 | 77 | operationResponse(response, { state: 'pending' }) 78 | } else { 79 | functions.logger.info('Initiating users backup') 80 | 81 | const backupResponse = await backupUsers(projectId, body) 82 | 83 | functions.logger.info('Got the users backup response', { 84 | // NOTE: Do not ...body here to avoid logging sensitive data 85 | state: backupResponse.state, 86 | ...(backupResponse.state === 'pending' 87 | ? {} 88 | : backupResponse.state === 'completed' 89 | ? { 90 | usersCount: backupResponse.data.usersCount, 91 | size: backupResponse.data.size, 92 | } 93 | : { 94 | reason: backupResponse.data.reason, 95 | }), 96 | }) 97 | 98 | if (body.delay) { 99 | const reportURL = format({ 100 | hostname: controllerDomain || defaultControllerDomain, 101 | protocol: 'https', 102 | pathname: '/reportBackup', 103 | }) 104 | 105 | functions.logger.info('Reporting users backup to the controller', { 106 | reportURL, 107 | }) 108 | 109 | await fetch(reportURL, { 110 | method: 'POST', 111 | headers: { 'Content-Type': 'application/json' }, 112 | body: JSON.stringify({ 113 | token: controllerToken, 114 | backupId: body.delay.backupId, 115 | type: 'users', 116 | ...backupResponse, 117 | }), 118 | }) 119 | } 120 | 121 | functions.logger.info('Responding with the backup status') 122 | 123 | operationResponse(response, backupResponse) 124 | } 125 | }) 126 | } 127 | 128 | async function backupUsers( 129 | projectId: string, 130 | options: UsersBackupRequestBody 131 | ): Promise { 132 | // Create bucket 133 | const bucket = admin.storage().bucket(options.storageId) 134 | 135 | // Create temporary file path 136 | const path = tmpPath(options.path) 137 | 138 | functions.logger.info('Exporting users to a temporary file', { path }) 139 | 140 | // Export users to a temporary file 141 | await tools.auth.export(path, { project: projectId }) 142 | 143 | // Calculate users in the backup and 144 | // upload the users backup to the storage 145 | 146 | functions.logger.info( 147 | 'Uploading users backup to the storage and counting the users' 148 | ) 149 | 150 | const [usersCount, size] = await Promise.all([ 151 | calculateUsers(path).then((count) => { 152 | functions.logger.info('Got the users count', { count }) 153 | return count 154 | }), 155 | 156 | bucket 157 | .upload(path, { destination: options.path }) 158 | .then(([file]) => file.metadata.size as string) 159 | .then((size) => { 160 | functions.logger.info('Uploaded the users backup to the storage', { 161 | size, 162 | }) 163 | return size 164 | }), 165 | ]) 166 | 167 | functions.logger.info('Removing the temporary file') 168 | 169 | // Remove the temporary file 170 | await unlink(path) 171 | 172 | return { state: 'completed', data: { usersCount, size } } 173 | } 174 | 175 | /** 176 | * Calculates the number of users in the backup. 177 | * @param path - the backup path 178 | * @returns the number of users in the backup 179 | */ 180 | export async function calculateUsers(path: string) { 181 | let usersCount = 0 182 | let lookingForEnding: string | null = null 183 | 184 | for await (const chunk of generateFileChunks(path, 10000000 /* 10MB */)) { 185 | const text: string = chunk.toString() 186 | 187 | if (lookingForEnding) { 188 | if (text.slice(0, lookingForEnding.length) === lookingForEnding) 189 | usersCount++ 190 | lookingForEnding = null 191 | } 192 | 193 | usersCount += text.match(/"localId"/g)?.length || 0 194 | 195 | const ending = text.match(/"(l(o(c(a(l(I(d(")?)?)?)?)?)?)?)?$/) 196 | if (ending) lookingForEnding = '"localId"'.slice(ending[0].length) 197 | } 198 | 199 | return usersCount 200 | } 201 | 202 | /** 203 | * Calculates the number of users in the file stream. 204 | * @param usersStream - the backup file stream 205 | * @returns the number of users in the stream 206 | */ 207 | export async function calculateUsersInSteam(usersStream: fs.ReadStream) { 208 | let usersCount = 0 209 | let lookingForEnding: string | null = null 210 | 211 | for await (const data of usersStream) { 212 | const text: string = data.toString() 213 | 214 | if (lookingForEnding) { 215 | if (text.slice(0, lookingForEnding.length) === lookingForEnding) 216 | usersCount++ 217 | lookingForEnding = null 218 | } 219 | 220 | usersCount += text.match(/"localId"/g)?.length || 0 221 | 222 | const ending = text.match(/"(l(o(c(a(l(I(d(")?)?)?)?)?)?)?)?$/) 223 | if (ending) lookingForEnding = '"localId"'.slice(ending[0].length) 224 | } 225 | 226 | return usersCount 227 | } 228 | 229 | /** 230 | * Genenerates chunks of data from the given file. 231 | * 232 | * The code is based on the article by Kasper Moskwiak (https://github.com/kmoskwiak): https://betterprogramming.pub/a-memory-friendly-way-of-reading-files-in-node-js-a45ad0cc7bb6 233 | * 234 | * @param path - the file path to read 235 | * @param size - the chunk size 236 | */ 237 | async function* generateFileChunks(path: string, size: number) { 238 | const sharedBuffer = Buffer.alloc(size) 239 | const stats = fs.statSync(path) 240 | const file = fs.openSync(path, 'r') 241 | 242 | let bytesRead = 0 // How many bytes were read 243 | let end = size 244 | 245 | for (let chunk = 0; chunk < Math.ceil(stats.size / size); chunk++) { 246 | await readFileBytes(file, sharedBuffer) 247 | 248 | bytesRead = (chunk + 1) * size 249 | // When we reach the end of file, we have to calculate how many bytes were 250 | // actually read. 251 | if (bytesRead > stats.size) end = size - (bytesRead - stats.size) 252 | 253 | yield sharedBuffer.slice(0, end) 254 | } 255 | } 256 | 257 | /** 258 | * Reads the file bytes into the shared buffer. 259 | * 260 | * The code is based on the article by Kasper Moskwiak (https://github.com/kmoskwiak): https://betterprogramming.pub/a-memory-friendly-way-of-reading-files-in-node-js-a45ad0cc7bb6 261 | * 262 | * @param file - the file descriptor 263 | * @param buffer - the shared buffer to use 264 | * @returns promise to file read 265 | */ 266 | function readFileBytes(file: number, buffer: Buffer) { 267 | return new Promise((resolve, reject) => { 268 | fs.read(file, buffer, 0, buffer.length, null, (error) => { 269 | if (error) return reject(error) 270 | resolve(void 0) 271 | }) 272 | }) 273 | } 274 | 275 | /** 276 | * Generates temporary path on the FS from passed path in a bucket 277 | * @param path - The path to backup in a bucket 278 | */ 279 | function tmpPath(path: string) { 280 | const { base: fileName } = parse(path) 281 | return resolve(tmpdir(), fileName) 282 | } 283 | -------------------------------------------------------------------------------- /src/users/test.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs/promises' 2 | import { fs } from 'mz' 3 | import { tmpdir } from 'os' 4 | import { resolve } from 'path' 5 | import { calculateUsers, calculateUsersInSteam } from '.' 6 | 7 | describe('users', () => { 8 | describe('calculateUsers', () => { 9 | let backupPath: string 10 | beforeEach(() => { 11 | backupPath = resolve(tmpdir(), Date.now() + '.json') 12 | return writeFile( 13 | backupPath, 14 | JSON.stringify([{ localId: 1 }, { localId: 2 }, { localId: 3 }]) 15 | ) 16 | }) 17 | 18 | it('calculates the number of users in the backup', async () => { 19 | const result = await calculateUsers(backupPath) 20 | expect(result).toBe(3) 21 | }) 22 | }) 23 | 24 | describe('calculateUsersInSteam', () => { 25 | it('calculates the number of users in the stream', async () => { 26 | const stream = ([ 27 | '[{"localId":1},{', 28 | '"localId":2},{"localId":3}]' 29 | ] as unknown) as fs.ReadStream 30 | const result = await calculateUsersInSteam(stream) 31 | expect(result).toBe(3) 32 | }) 33 | 34 | it('considers chunks split', async () => { 35 | const stream = ([ 36 | '[{"localId":1},{', 37 | '"loca', 38 | 'lId":2},{"localId":3}]' 39 | ] as unknown) as fs.ReadStream 40 | const result = await calculateUsersInSteam(stream) 41 | expect(result).toBe(3) 42 | }) 43 | 44 | it('skips incomplete symbols when calculating split', async () => { 45 | const stream = ([ 46 | '[{"localId":1},{', 47 | '"loca', 48 | // It must be 'lId":2},{"localId":3}]' 49 | // to calculate split 50 | 'Id":2},{"localId":3}]' 51 | ] as unknown) as fs.ReadStream 52 | const result = await calculateUsersInSteam(stream) 53 | expect(result).toBe(2) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | let version = 'dev' 5 | try { 6 | const packageStr = fs.readFileSync( 7 | path.resolve(__dirname, './package.json'), 8 | 'utf8' 9 | ) 10 | const packageJson = JSON.parse(packageStr) 11 | if (typeof packageJson.version === 'string') 12 | version = `v${packageJson.version}` 13 | } catch (_err) {} 14 | 15 | export default version 16 | -------------------------------------------------------------------------------- /test/extension/.firebaserc.example: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "backup-fire-playground-ext" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/extension/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "source": "." 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/extension/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions' 2 | 3 | export const helloWorld = functions.https.onRequest((_request, response) => { 4 | response.send('Hello from Firebase!') 5 | }) 6 | -------------------------------------------------------------------------------- /test/extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backupfire-firebase-test-server-ext", 3 | "version": "1.0.0", 4 | "description": "Backup Fire test server", 5 | "main": "./build/index.js", 6 | "author": "Sasha Koss ", 7 | "license": "MIT", 8 | "engines": { 9 | "node": "14" 10 | }, 11 | "dependencies": { 12 | "firebase-admin": "^9.6.0", 13 | "firebase-functions": "^3.13.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["test/extension/**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /test/extension/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@firebase/app-types@0.6.2": 6 | version "0.6.2" 7 | resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.6.2.tgz#8578cb1061a83ced4570188be9e225d54e0f27fb" 8 | integrity sha512-2VXvq/K+n8XMdM4L2xy5bYp2ZXMawJXluUIDzUBvMthVR+lhxK4pfFiqr1mmDbv9ydXvEAuFsD+6DpcZuJcSSw== 9 | 10 | "@firebase/auth-interop-types@0.1.6": 11 | version "0.1.6" 12 | resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz#5ce13fc1c527ad36f1bb1322c4492680a6cf4964" 13 | integrity sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g== 14 | 15 | "@firebase/component@0.5.0": 16 | version "0.5.0" 17 | resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.5.0.tgz#f5b577d6c6f78d0f12fdc45046108921507f49c9" 18 | integrity sha512-v18csWtXb0ri+3m7wuGLY/UDgcb89vuMlZGQ//+7jEPLIQeLbylvZhol1uzW9WzoOpxMxOS2W5qyVGX36wZvEA== 19 | dependencies: 20 | "@firebase/util" "1.1.0" 21 | tslib "^2.1.0" 22 | 23 | "@firebase/database-types@0.7.2", "@firebase/database-types@^0.7.2": 24 | version "0.7.2" 25 | resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.7.2.tgz#449c4b36ec59a1ad9089797b540e2ba1c0d4fcbf" 26 | integrity sha512-cdAd/dgwvC0r3oLEDUR+ULs1vBsEvy0b27nlzKhU6LQgm9fCDzgaH9nFGv8x+S9dly4B0egAXkONkVoWcOAisg== 27 | dependencies: 28 | "@firebase/app-types" "0.6.2" 29 | 30 | "@firebase/database@^0.10.0": 31 | version "0.10.1" 32 | resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.10.1.tgz#1e8c64519552f225a4a88e7dae09ecf29447f2be" 33 | integrity sha512-umT0kynJKc5VpVBOg3+YTDzdJORssh+QqPjoHfbSvtmgZizNiV8mgmKRcDhlVM6CisPb6v5xBn9l8JbK/WRQ1Q== 34 | dependencies: 35 | "@firebase/auth-interop-types" "0.1.6" 36 | "@firebase/component" "0.5.0" 37 | "@firebase/database-types" "0.7.2" 38 | "@firebase/logger" "0.2.6" 39 | "@firebase/util" "1.1.0" 40 | faye-websocket "0.11.3" 41 | tslib "^2.1.0" 42 | 43 | "@firebase/logger@0.2.6": 44 | version "0.2.6" 45 | resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.2.6.tgz#3aa2ca4fe10327cabf7808bd3994e88db26d7989" 46 | integrity sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw== 47 | 48 | "@firebase/util@1.1.0": 49 | version "1.1.0" 50 | resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.1.0.tgz#add2d57d0b2307a932520abdee303b66be0ac8b0" 51 | integrity sha512-lfuSASuPKNdfebuFR8rjFamMQUPH9iiZHcKS755Rkm/5gRT0qC7BMhCh3ZkHf7NVbplzIc/GhmX2jM+igDRCag== 52 | dependencies: 53 | tslib "^2.1.0" 54 | 55 | "@google-cloud/common@^3.6.0": 56 | version "3.6.0" 57 | resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-3.6.0.tgz#c2f6da5f79279a4a9ac7c71fc02d582beab98e8b" 58 | integrity sha512-aHIFTqJZmeTNO9md8XxV+ywuvXF3xBm5WNmgWeeCK+XN5X+kGW0WEX94wGwj+/MdOnrVf4dL2RvSIt9J5yJG6Q== 59 | dependencies: 60 | "@google-cloud/projectify" "^2.0.0" 61 | "@google-cloud/promisify" "^2.0.0" 62 | arrify "^2.0.1" 63 | duplexify "^4.1.1" 64 | ent "^2.2.0" 65 | extend "^3.0.2" 66 | google-auth-library "^7.0.2" 67 | retry-request "^4.1.1" 68 | teeny-request "^7.0.0" 69 | 70 | "@google-cloud/firestore@^4.5.0": 71 | version "4.11.1" 72 | resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-4.11.1.tgz#03e12e5721383165efc09e208143378f3ea681d6" 73 | integrity sha512-iNsCGYwKBxYZS+TpkUAJLGkGko2QtWaf11JDNx6kvqOVN0359qSnZlF1SWFTvm26ZsKyX6uR4oAiFmmjfXTlCg== 74 | dependencies: 75 | fast-deep-equal "^3.1.1" 76 | functional-red-black-tree "^1.0.1" 77 | google-gax "^2.12.0" 78 | protobufjs "^6.8.6" 79 | 80 | "@google-cloud/paginator@^3.0.0": 81 | version "3.0.5" 82 | resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.5.tgz#9d6b96c421a89bd560c1bc2c197c7611ef21db6c" 83 | integrity sha512-N4Uk4BT1YuskfRhKXBs0n9Lg2YTROZc6IMpkO/8DIHODtm5s3xY8K5vVBo23v/2XulY3azwITQlYWgT4GdLsUw== 84 | dependencies: 85 | arrify "^2.0.0" 86 | extend "^3.0.2" 87 | 88 | "@google-cloud/projectify@^2.0.0": 89 | version "2.0.1" 90 | resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-2.0.1.tgz#13350ee609346435c795bbfe133a08dfeab78d65" 91 | integrity sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ== 92 | 93 | "@google-cloud/promisify@^2.0.0": 94 | version "2.0.3" 95 | resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-2.0.3.tgz#f934b5cdc939e3c7039ff62b9caaf59a9d89e3a8" 96 | integrity sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw== 97 | 98 | "@google-cloud/storage@^5.3.0": 99 | version "5.8.5" 100 | resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-5.8.5.tgz#2cf1e2e0ef8ca552abc4450301fef3fea4900ef6" 101 | integrity sha512-i0gB9CRwQeOBYP7xuvn14M40LhHCwMjceBjxE4CTvsqL519sVY5yVKxLiAedHWGwUZHJNRa7Q2CmNfkdRwVNPg== 102 | dependencies: 103 | "@google-cloud/common" "^3.6.0" 104 | "@google-cloud/paginator" "^3.0.0" 105 | "@google-cloud/promisify" "^2.0.0" 106 | arrify "^2.0.0" 107 | async-retry "^1.3.1" 108 | compressible "^2.0.12" 109 | date-and-time "^1.0.0" 110 | duplexify "^4.0.0" 111 | extend "^3.0.2" 112 | gaxios "^4.0.0" 113 | gcs-resumable-upload "^3.1.4" 114 | get-stream "^6.0.0" 115 | hash-stream-validation "^0.2.2" 116 | mime "^2.2.0" 117 | mime-types "^2.0.8" 118 | onetime "^5.1.0" 119 | p-limit "^3.0.1" 120 | pumpify "^2.0.0" 121 | snakeize "^0.1.0" 122 | stream-events "^1.0.1" 123 | xdg-basedir "^4.0.0" 124 | 125 | "@grpc/grpc-js@~1.3.0": 126 | version "1.3.2" 127 | resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.3.2.tgz#eae97e6daf5abd49a7818aadeca0744dfb1ebca1" 128 | integrity sha512-UXepkOKCATJrhHGsxt+CGfpZy9zUn1q9mop5kfcXq1fBkTePxVNPOdnISlCbJFlCtld+pSLGyZCzr9/zVprFKA== 129 | dependencies: 130 | "@types/node" ">=12.12.47" 131 | 132 | "@grpc/proto-loader@^0.6.1": 133 | version "0.6.2" 134 | resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.2.tgz#412575f3ff5ef0a9b79d4ea12c08cba5601041cb" 135 | integrity sha512-q2Qle60Ht2OQBCp9S5hv1JbI4uBBq6/mqSevFNK3ZEgRDBCAkWqZPUhD/K9gXOHrHKluliHiVq2L9sw1mVyAIg== 136 | dependencies: 137 | "@types/long" "^4.0.1" 138 | lodash.camelcase "^4.3.0" 139 | long "^4.0.0" 140 | protobufjs "^6.10.0" 141 | yargs "^16.1.1" 142 | 143 | "@panva/asn1.js@^1.0.0": 144 | version "1.0.0" 145 | resolved "https://registry.yarnpkg.com/@panva/asn1.js/-/asn1.js-1.0.0.tgz#dd55ae7b8129e02049f009408b97c61ccf9032f6" 146 | integrity sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw== 147 | 148 | "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": 149 | version "1.1.2" 150 | resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" 151 | integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= 152 | 153 | "@protobufjs/base64@^1.1.2": 154 | version "1.1.2" 155 | resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" 156 | integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== 157 | 158 | "@protobufjs/codegen@^2.0.4": 159 | version "2.0.4" 160 | resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" 161 | integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== 162 | 163 | "@protobufjs/eventemitter@^1.1.0": 164 | version "1.1.0" 165 | resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" 166 | integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= 167 | 168 | "@protobufjs/fetch@^1.1.0": 169 | version "1.1.0" 170 | resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" 171 | integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= 172 | dependencies: 173 | "@protobufjs/aspromise" "^1.1.1" 174 | "@protobufjs/inquire" "^1.1.0" 175 | 176 | "@protobufjs/float@^1.0.2": 177 | version "1.0.2" 178 | resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" 179 | integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= 180 | 181 | "@protobufjs/inquire@^1.1.0": 182 | version "1.1.0" 183 | resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" 184 | integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= 185 | 186 | "@protobufjs/path@^1.1.2": 187 | version "1.1.2" 188 | resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" 189 | integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= 190 | 191 | "@protobufjs/pool@^1.1.0": 192 | version "1.1.0" 193 | resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" 194 | integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= 195 | 196 | "@protobufjs/utf8@^1.1.0": 197 | version "1.1.0" 198 | resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" 199 | integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= 200 | 201 | "@tootallnate/once@1": 202 | version "1.1.2" 203 | resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" 204 | integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== 205 | 206 | "@types/body-parser@*": 207 | version "1.19.0" 208 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" 209 | integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== 210 | dependencies: 211 | "@types/connect" "*" 212 | "@types/node" "*" 213 | 214 | "@types/connect@*": 215 | version "3.4.34" 216 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901" 217 | integrity sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ== 218 | dependencies: 219 | "@types/node" "*" 220 | 221 | "@types/express-jwt@0.0.42": 222 | version "0.0.42" 223 | resolved "https://registry.yarnpkg.com/@types/express-jwt/-/express-jwt-0.0.42.tgz#4f04e1fadf9d18725950dc041808a4a4adf7f5ae" 224 | integrity sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag== 225 | dependencies: 226 | "@types/express" "*" 227 | "@types/express-unless" "*" 228 | 229 | "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": 230 | version "4.17.19" 231 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz#00acfc1632e729acac4f1530e9e16f6dd1508a1d" 232 | integrity sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA== 233 | dependencies: 234 | "@types/node" "*" 235 | "@types/qs" "*" 236 | "@types/range-parser" "*" 237 | 238 | "@types/express-unless@*": 239 | version "0.5.1" 240 | resolved "https://registry.yarnpkg.com/@types/express-unless/-/express-unless-0.5.1.tgz#4f440b905e42bbf53382b8207bc337dc5ff9fd1f" 241 | integrity sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw== 242 | dependencies: 243 | "@types/express" "*" 244 | 245 | "@types/express@*": 246 | version "4.17.11" 247 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.11.tgz#debe3caa6f8e5fcda96b47bd54e2f40c4ee59545" 248 | integrity sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg== 249 | dependencies: 250 | "@types/body-parser" "*" 251 | "@types/express-serve-static-core" "^4.17.18" 252 | "@types/qs" "*" 253 | "@types/serve-static" "*" 254 | 255 | "@types/express@4.17.3": 256 | version "4.17.3" 257 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.3.tgz#38e4458ce2067873b09a73908df488870c303bd9" 258 | integrity sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg== 259 | dependencies: 260 | "@types/body-parser" "*" 261 | "@types/express-serve-static-core" "*" 262 | "@types/serve-static" "*" 263 | 264 | "@types/long@^4.0.0", "@types/long@^4.0.1": 265 | version "4.0.1" 266 | resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" 267 | integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== 268 | 269 | "@types/mime@^1": 270 | version "1.3.2" 271 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" 272 | integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== 273 | 274 | "@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": 275 | version "15.3.0" 276 | resolved "https://registry.yarnpkg.com/@types/node/-/node-15.3.0.tgz#d6fed7d6bc6854306da3dea1af9f874b00783e26" 277 | integrity sha512-8/bnjSZD86ZfpBsDlCIkNXIvm+h6wi9g7IqL+kmFkQ+Wvu3JrasgLElfiPgoo8V8vVfnEi0QVS12gbl94h9YsQ== 278 | 279 | "@types/qs@*": 280 | version "6.9.6" 281 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1" 282 | integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA== 283 | 284 | "@types/range-parser@*": 285 | version "1.2.3" 286 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" 287 | integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== 288 | 289 | "@types/serve-static@*": 290 | version "1.13.9" 291 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e" 292 | integrity sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA== 293 | dependencies: 294 | "@types/mime" "^1" 295 | "@types/node" "*" 296 | 297 | abort-controller@^3.0.0: 298 | version "3.0.0" 299 | resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" 300 | integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== 301 | dependencies: 302 | event-target-shim "^5.0.0" 303 | 304 | accepts@~1.3.7: 305 | version "1.3.7" 306 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" 307 | integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== 308 | dependencies: 309 | mime-types "~2.1.24" 310 | negotiator "0.6.2" 311 | 312 | agent-base@6: 313 | version "6.0.2" 314 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" 315 | integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== 316 | dependencies: 317 | debug "4" 318 | 319 | ansi-regex@^5.0.0: 320 | version "5.0.0" 321 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" 322 | integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== 323 | 324 | ansi-styles@^4.0.0: 325 | version "4.3.0" 326 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 327 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 328 | dependencies: 329 | color-convert "^2.0.1" 330 | 331 | array-flatten@1.1.1: 332 | version "1.1.1" 333 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 334 | integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= 335 | 336 | arrify@^2.0.0, arrify@^2.0.1: 337 | version "2.0.1" 338 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" 339 | integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== 340 | 341 | async-retry@^1.3.1: 342 | version "1.3.1" 343 | resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55" 344 | integrity sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA== 345 | dependencies: 346 | retry "0.12.0" 347 | 348 | base64-js@^1.3.0: 349 | version "1.5.1" 350 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 351 | integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 352 | 353 | bignumber.js@^9.0.0: 354 | version "9.0.1" 355 | resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" 356 | integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== 357 | 358 | body-parser@1.19.0: 359 | version "1.19.0" 360 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" 361 | integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== 362 | dependencies: 363 | bytes "3.1.0" 364 | content-type "~1.0.4" 365 | debug "2.6.9" 366 | depd "~1.1.2" 367 | http-errors "1.7.2" 368 | iconv-lite "0.4.24" 369 | on-finished "~2.3.0" 370 | qs "6.7.0" 371 | raw-body "2.4.0" 372 | type-is "~1.6.17" 373 | 374 | buffer-equal-constant-time@1.0.1: 375 | version "1.0.1" 376 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 377 | integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= 378 | 379 | bytes@3.1.0: 380 | version "3.1.0" 381 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" 382 | integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== 383 | 384 | cliui@^7.0.2: 385 | version "7.0.4" 386 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" 387 | integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== 388 | dependencies: 389 | string-width "^4.2.0" 390 | strip-ansi "^6.0.0" 391 | wrap-ansi "^7.0.0" 392 | 393 | color-convert@^2.0.1: 394 | version "2.0.1" 395 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 396 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 397 | dependencies: 398 | color-name "~1.1.4" 399 | 400 | color-name@~1.1.4: 401 | version "1.1.4" 402 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 403 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 404 | 405 | compressible@^2.0.12: 406 | version "2.0.18" 407 | resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" 408 | integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== 409 | dependencies: 410 | mime-db ">= 1.43.0 < 2" 411 | 412 | configstore@^5.0.0: 413 | version "5.0.1" 414 | resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" 415 | integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== 416 | dependencies: 417 | dot-prop "^5.2.0" 418 | graceful-fs "^4.1.2" 419 | make-dir "^3.0.0" 420 | unique-string "^2.0.0" 421 | write-file-atomic "^3.0.0" 422 | xdg-basedir "^4.0.0" 423 | 424 | content-disposition@0.5.3: 425 | version "0.5.3" 426 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" 427 | integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== 428 | dependencies: 429 | safe-buffer "5.1.2" 430 | 431 | content-type@~1.0.4: 432 | version "1.0.4" 433 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 434 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 435 | 436 | cookie-signature@1.0.6: 437 | version "1.0.6" 438 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 439 | integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= 440 | 441 | cookie@0.4.0: 442 | version "0.4.0" 443 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" 444 | integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== 445 | 446 | cors@^2.8.5: 447 | version "2.8.5" 448 | resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" 449 | integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== 450 | dependencies: 451 | object-assign "^4" 452 | vary "^1" 453 | 454 | crypto-random-string@^2.0.0: 455 | version "2.0.0" 456 | resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" 457 | integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== 458 | 459 | date-and-time@^1.0.0: 460 | version "1.0.0" 461 | resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-1.0.0.tgz#0062394bdf6f44e961f0db00511cb19cdf3cc0a5" 462 | integrity sha512-477D7ypIiqlXBkxhU7YtG9wWZJEQ+RUpujt2quTfgf4+E8g5fNUkB0QIL0bVyP5/TKBg8y55Hfa1R/c4bt3dEw== 463 | 464 | debug@2.6.9: 465 | version "2.6.9" 466 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 467 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 468 | dependencies: 469 | ms "2.0.0" 470 | 471 | debug@4, debug@^4.1.0, debug@^4.1.1: 472 | version "4.3.1" 473 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" 474 | integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== 475 | dependencies: 476 | ms "2.1.2" 477 | 478 | depd@~1.1.2: 479 | version "1.1.2" 480 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 481 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= 482 | 483 | destroy@~1.0.4: 484 | version "1.0.4" 485 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 486 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= 487 | 488 | dicer@^0.3.0: 489 | version "0.3.0" 490 | resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" 491 | integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== 492 | dependencies: 493 | streamsearch "0.1.2" 494 | 495 | dot-prop@^5.2.0: 496 | version "5.3.0" 497 | resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" 498 | integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== 499 | dependencies: 500 | is-obj "^2.0.0" 501 | 502 | duplexify@^4.0.0, duplexify@^4.1.1: 503 | version "4.1.1" 504 | resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.1.tgz#7027dc374f157b122a8ae08c2d3ea4d2d953aa61" 505 | integrity sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA== 506 | dependencies: 507 | end-of-stream "^1.4.1" 508 | inherits "^2.0.3" 509 | readable-stream "^3.1.1" 510 | stream-shift "^1.0.0" 511 | 512 | ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: 513 | version "1.0.11" 514 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" 515 | integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== 516 | dependencies: 517 | safe-buffer "^5.0.1" 518 | 519 | ee-first@1.1.1: 520 | version "1.1.1" 521 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 522 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= 523 | 524 | emoji-regex@^8.0.0: 525 | version "8.0.0" 526 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 527 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 528 | 529 | encodeurl@~1.0.2: 530 | version "1.0.2" 531 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 532 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= 533 | 534 | end-of-stream@^1.1.0, end-of-stream@^1.4.1: 535 | version "1.4.4" 536 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 537 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== 538 | dependencies: 539 | once "^1.4.0" 540 | 541 | ent@^2.2.0: 542 | version "2.2.0" 543 | resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" 544 | integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= 545 | 546 | escalade@^3.1.1: 547 | version "3.1.1" 548 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" 549 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== 550 | 551 | escape-html@~1.0.3: 552 | version "1.0.3" 553 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 554 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= 555 | 556 | etag@~1.8.1: 557 | version "1.8.1" 558 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 559 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= 560 | 561 | event-target-shim@^5.0.0: 562 | version "5.0.1" 563 | resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" 564 | integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== 565 | 566 | express@^4.17.1: 567 | version "4.17.1" 568 | resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" 569 | integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== 570 | dependencies: 571 | accepts "~1.3.7" 572 | array-flatten "1.1.1" 573 | body-parser "1.19.0" 574 | content-disposition "0.5.3" 575 | content-type "~1.0.4" 576 | cookie "0.4.0" 577 | cookie-signature "1.0.6" 578 | debug "2.6.9" 579 | depd "~1.1.2" 580 | encodeurl "~1.0.2" 581 | escape-html "~1.0.3" 582 | etag "~1.8.1" 583 | finalhandler "~1.1.2" 584 | fresh "0.5.2" 585 | merge-descriptors "1.0.1" 586 | methods "~1.1.2" 587 | on-finished "~2.3.0" 588 | parseurl "~1.3.3" 589 | path-to-regexp "0.1.7" 590 | proxy-addr "~2.0.5" 591 | qs "6.7.0" 592 | range-parser "~1.2.1" 593 | safe-buffer "5.1.2" 594 | send "0.17.1" 595 | serve-static "1.14.1" 596 | setprototypeof "1.1.1" 597 | statuses "~1.5.0" 598 | type-is "~1.6.18" 599 | utils-merge "1.0.1" 600 | vary "~1.1.2" 601 | 602 | extend@^3.0.2: 603 | version "3.0.2" 604 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 605 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 606 | 607 | fast-deep-equal@^3.1.1: 608 | version "3.1.3" 609 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 610 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 611 | 612 | fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3: 613 | version "1.0.3" 614 | resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" 615 | integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== 616 | 617 | faye-websocket@0.11.3: 618 | version "0.11.3" 619 | resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" 620 | integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== 621 | dependencies: 622 | websocket-driver ">=0.5.1" 623 | 624 | finalhandler@~1.1.2: 625 | version "1.1.2" 626 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" 627 | integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== 628 | dependencies: 629 | debug "2.6.9" 630 | encodeurl "~1.0.2" 631 | escape-html "~1.0.3" 632 | on-finished "~2.3.0" 633 | parseurl "~1.3.3" 634 | statuses "~1.5.0" 635 | unpipe "~1.0.0" 636 | 637 | firebase-admin@^9.6.0: 638 | version "9.8.0" 639 | resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-9.8.0.tgz#d54d1acdd2e1117e59b7d74cb467ef3d18f3aa7a" 640 | integrity sha512-v8B1qU8McZZT2hlLZ018TKz2FoKlfFkZq9mOIyzN7wJUOlAywqQX0JyqNpVGyPeU+B+77ojlvmkGTNXt2OFkgw== 641 | dependencies: 642 | "@firebase/database" "^0.10.0" 643 | "@firebase/database-types" "^0.7.2" 644 | "@types/node" ">=12.12.47" 645 | dicer "^0.3.0" 646 | jsonwebtoken "^8.5.1" 647 | jwks-rsa "^2.0.2" 648 | node-forge "^0.10.0" 649 | optionalDependencies: 650 | "@google-cloud/firestore" "^4.5.0" 651 | "@google-cloud/storage" "^5.3.0" 652 | 653 | firebase-functions@^3.13.2: 654 | version "3.14.1" 655 | resolved "https://registry.yarnpkg.com/firebase-functions/-/firebase-functions-3.14.1.tgz#3ac5bc70989365874f41d06bca3b42a233dd6039" 656 | integrity sha512-hL/qm+i5i1qKYmAFMlQ4mwRngDkP+3YT3F4E4Nd5Hj2QKeawBdZiMGgEt6zqTx08Zq04vHiSnSM0z75UJRSg6Q== 657 | dependencies: 658 | "@types/express" "4.17.3" 659 | cors "^2.8.5" 660 | express "^4.17.1" 661 | lodash "^4.17.14" 662 | 663 | forwarded@~0.1.2: 664 | version "0.1.2" 665 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 666 | integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= 667 | 668 | fresh@0.5.2: 669 | version "0.5.2" 670 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 671 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= 672 | 673 | functional-red-black-tree@^1.0.1: 674 | version "1.0.1" 675 | resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" 676 | integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= 677 | 678 | gaxios@^4.0.0: 679 | version "4.2.1" 680 | resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.2.1.tgz#7463d3a06f56ddbffa745a242d2b4933b88b2ada" 681 | integrity sha512-s+rTywpw6CmfB8r9TXYkpix7YFeuRjnR/AqhaJrQqsNhsAqej+IAiCc3hadzQH3gHyWth30tvYjxH8EVjQt/8Q== 682 | dependencies: 683 | abort-controller "^3.0.0" 684 | extend "^3.0.2" 685 | https-proxy-agent "^5.0.0" 686 | is-stream "^2.0.0" 687 | node-fetch "^2.3.0" 688 | 689 | gcp-metadata@^4.2.0: 690 | version "4.2.1" 691 | resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.2.1.tgz#31849fbcf9025ef34c2297c32a89a1e7e9f2cd62" 692 | integrity sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw== 693 | dependencies: 694 | gaxios "^4.0.0" 695 | json-bigint "^1.0.0" 696 | 697 | gcs-resumable-upload@^3.1.4: 698 | version "3.1.4" 699 | resolved "https://registry.yarnpkg.com/gcs-resumable-upload/-/gcs-resumable-upload-3.1.4.tgz#2e591889efb02247af26868de300b398346b17b5" 700 | integrity sha512-5dyDfHrrVcIskiw/cPssVD4HRiwoHjhk1Nd6h5W3pQ/qffDvhfy4oNCr1f3ZXFPwTnxkCbibsB+73oOM+NvmJQ== 701 | dependencies: 702 | abort-controller "^3.0.0" 703 | configstore "^5.0.0" 704 | extend "^3.0.2" 705 | gaxios "^4.0.0" 706 | google-auth-library "^7.0.0" 707 | pumpify "^2.0.0" 708 | stream-events "^1.0.4" 709 | 710 | get-caller-file@^2.0.5: 711 | version "2.0.5" 712 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 713 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 714 | 715 | get-stream@^6.0.0: 716 | version "6.0.1" 717 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" 718 | integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== 719 | 720 | google-auth-library@^7.0.0, google-auth-library@^7.0.2: 721 | version "7.0.4" 722 | resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.0.4.tgz#610cb010de71435dca47dfbe8dc7fbff23055d2c" 723 | integrity sha512-o8irYyeijEiecTXeoEe8UKNEzV1X+uhR4b2oNdapDMZixypp0J+eHimGOyx5Joa3UAeokGngdtDLXtq9vDqG2Q== 724 | dependencies: 725 | arrify "^2.0.0" 726 | base64-js "^1.3.0" 727 | ecdsa-sig-formatter "^1.0.11" 728 | fast-text-encoding "^1.0.0" 729 | gaxios "^4.0.0" 730 | gcp-metadata "^4.2.0" 731 | gtoken "^5.0.4" 732 | jws "^4.0.0" 733 | lru-cache "^6.0.0" 734 | 735 | google-gax@^2.12.0: 736 | version "2.13.0" 737 | resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-2.13.0.tgz#404bb9df62c3a0a414e2f5339eda4d751f540304" 738 | integrity sha512-aKNJy2+Vv2I7flyNYbwpq0aYBHp6Qv32HZn+wr6ZhZ8xlSCLS9K9k7izfh2nd1rCJQcsqB6KMxHV0Vwny6Rc1g== 739 | dependencies: 740 | "@grpc/grpc-js" "~1.3.0" 741 | "@grpc/proto-loader" "^0.6.1" 742 | "@types/long" "^4.0.0" 743 | abort-controller "^3.0.0" 744 | duplexify "^4.0.0" 745 | fast-text-encoding "^1.0.3" 746 | google-auth-library "^7.0.2" 747 | is-stream-ended "^0.1.4" 748 | node-fetch "^2.6.1" 749 | object-hash "^2.1.1" 750 | protobufjs "^6.10.2" 751 | retry-request "^4.0.0" 752 | 753 | google-p12-pem@^3.0.3: 754 | version "3.0.3" 755 | resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" 756 | integrity sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA== 757 | dependencies: 758 | node-forge "^0.10.0" 759 | 760 | graceful-fs@^4.1.2: 761 | version "4.2.6" 762 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" 763 | integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== 764 | 765 | gtoken@^5.0.4: 766 | version "5.2.1" 767 | resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.2.1.tgz#4dae1fea17270f457954b4a45234bba5fc796d16" 768 | integrity sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw== 769 | dependencies: 770 | gaxios "^4.0.0" 771 | google-p12-pem "^3.0.3" 772 | jws "^4.0.0" 773 | 774 | hash-stream-validation@^0.2.2: 775 | version "0.2.4" 776 | resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" 777 | integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== 778 | 779 | http-errors@1.7.2: 780 | version "1.7.2" 781 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" 782 | integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== 783 | dependencies: 784 | depd "~1.1.2" 785 | inherits "2.0.3" 786 | setprototypeof "1.1.1" 787 | statuses ">= 1.5.0 < 2" 788 | toidentifier "1.0.0" 789 | 790 | http-errors@~1.7.2: 791 | version "1.7.3" 792 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" 793 | integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== 794 | dependencies: 795 | depd "~1.1.2" 796 | inherits "2.0.4" 797 | setprototypeof "1.1.1" 798 | statuses ">= 1.5.0 < 2" 799 | toidentifier "1.0.0" 800 | 801 | http-parser-js@>=0.5.1: 802 | version "0.5.3" 803 | resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" 804 | integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== 805 | 806 | http-proxy-agent@^4.0.0: 807 | version "4.0.1" 808 | resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" 809 | integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== 810 | dependencies: 811 | "@tootallnate/once" "1" 812 | agent-base "6" 813 | debug "4" 814 | 815 | https-proxy-agent@^5.0.0: 816 | version "5.0.0" 817 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" 818 | integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== 819 | dependencies: 820 | agent-base "6" 821 | debug "4" 822 | 823 | iconv-lite@0.4.24: 824 | version "0.4.24" 825 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 826 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 827 | dependencies: 828 | safer-buffer ">= 2.1.2 < 3" 829 | 830 | imurmurhash@^0.1.4: 831 | version "0.1.4" 832 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 833 | integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= 834 | 835 | inherits@2.0.3: 836 | version "2.0.3" 837 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 838 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 839 | 840 | inherits@2.0.4, inherits@^2.0.3: 841 | version "2.0.4" 842 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 843 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 844 | 845 | ipaddr.js@1.9.1: 846 | version "1.9.1" 847 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 848 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 849 | 850 | is-fullwidth-code-point@^3.0.0: 851 | version "3.0.0" 852 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 853 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 854 | 855 | is-obj@^2.0.0: 856 | version "2.0.0" 857 | resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" 858 | integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== 859 | 860 | is-stream-ended@^0.1.4: 861 | version "0.1.4" 862 | resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" 863 | integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== 864 | 865 | is-stream@^2.0.0: 866 | version "2.0.0" 867 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" 868 | integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== 869 | 870 | is-typedarray@^1.0.0: 871 | version "1.0.0" 872 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 873 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 874 | 875 | jose@^2.0.5: 876 | version "2.0.5" 877 | resolved "https://registry.yarnpkg.com/jose/-/jose-2.0.5.tgz#29746a18d9fff7dcf9d5d2a6f62cb0c7cd27abd3" 878 | integrity sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA== 879 | dependencies: 880 | "@panva/asn1.js" "^1.0.0" 881 | 882 | json-bigint@^1.0.0: 883 | version "1.0.0" 884 | resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" 885 | integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== 886 | dependencies: 887 | bignumber.js "^9.0.0" 888 | 889 | jsonwebtoken@^8.5.1: 890 | version "8.5.1" 891 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" 892 | integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== 893 | dependencies: 894 | jws "^3.2.2" 895 | lodash.includes "^4.3.0" 896 | lodash.isboolean "^3.0.3" 897 | lodash.isinteger "^4.0.4" 898 | lodash.isnumber "^3.0.3" 899 | lodash.isplainobject "^4.0.6" 900 | lodash.isstring "^4.0.1" 901 | lodash.once "^4.0.0" 902 | ms "^2.1.1" 903 | semver "^5.6.0" 904 | 905 | jwa@^1.4.1: 906 | version "1.4.1" 907 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" 908 | integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== 909 | dependencies: 910 | buffer-equal-constant-time "1.0.1" 911 | ecdsa-sig-formatter "1.0.11" 912 | safe-buffer "^5.0.1" 913 | 914 | jwa@^2.0.0: 915 | version "2.0.0" 916 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" 917 | integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== 918 | dependencies: 919 | buffer-equal-constant-time "1.0.1" 920 | ecdsa-sig-formatter "1.0.11" 921 | safe-buffer "^5.0.1" 922 | 923 | jwks-rsa@^2.0.2: 924 | version "2.0.3" 925 | resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-2.0.3.tgz#4059f25e27f1d9cb5681dd12a98e46f8aa39fcbd" 926 | integrity sha512-/rkjXRWAp0cS00tunsHResw68P5iTQru8+jHufLNv3JHc4nObFEndfEUSuPugh09N+V9XYxKUqi7QrkmCHSSSg== 927 | dependencies: 928 | "@types/express-jwt" "0.0.42" 929 | debug "^4.1.0" 930 | jose "^2.0.5" 931 | limiter "^1.1.5" 932 | lru-memoizer "^2.1.2" 933 | 934 | jws@^3.2.2: 935 | version "3.2.2" 936 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" 937 | integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== 938 | dependencies: 939 | jwa "^1.4.1" 940 | safe-buffer "^5.0.1" 941 | 942 | jws@^4.0.0: 943 | version "4.0.0" 944 | resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" 945 | integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== 946 | dependencies: 947 | jwa "^2.0.0" 948 | safe-buffer "^5.0.1" 949 | 950 | limiter@^1.1.5: 951 | version "1.1.5" 952 | resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" 953 | integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== 954 | 955 | lodash.camelcase@^4.3.0: 956 | version "4.3.0" 957 | resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" 958 | integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= 959 | 960 | lodash.clonedeep@^4.5.0: 961 | version "4.5.0" 962 | resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" 963 | integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= 964 | 965 | lodash.includes@^4.3.0: 966 | version "4.3.0" 967 | resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 968 | integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= 969 | 970 | lodash.isboolean@^3.0.3: 971 | version "3.0.3" 972 | resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" 973 | integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= 974 | 975 | lodash.isinteger@^4.0.4: 976 | version "4.0.4" 977 | resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" 978 | integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= 979 | 980 | lodash.isnumber@^3.0.3: 981 | version "3.0.3" 982 | resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" 983 | integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= 984 | 985 | lodash.isplainobject@^4.0.6: 986 | version "4.0.6" 987 | resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" 988 | integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= 989 | 990 | lodash.isstring@^4.0.1: 991 | version "4.0.1" 992 | resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" 993 | integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= 994 | 995 | lodash.once@^4.0.0: 996 | version "4.1.1" 997 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 998 | integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= 999 | 1000 | lodash@^4.17.14: 1001 | version "4.17.21" 1002 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 1003 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 1004 | 1005 | long@^4.0.0: 1006 | version "4.0.0" 1007 | resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" 1008 | integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== 1009 | 1010 | lru-cache@^6.0.0: 1011 | version "6.0.0" 1012 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 1013 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 1014 | dependencies: 1015 | yallist "^4.0.0" 1016 | 1017 | lru-cache@~4.0.0: 1018 | version "4.0.2" 1019 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.0.2.tgz#1d17679c069cda5d040991a09dbc2c0db377e55e" 1020 | integrity sha1-HRdnnAac2l0ECZGgnbwsDbN35V4= 1021 | dependencies: 1022 | pseudomap "^1.0.1" 1023 | yallist "^2.0.0" 1024 | 1025 | lru-memoizer@^2.1.2: 1026 | version "2.1.4" 1027 | resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.1.4.tgz#b864d92b557f00b1eeb322156a0409cb06dafac6" 1028 | integrity sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ== 1029 | dependencies: 1030 | lodash.clonedeep "^4.5.0" 1031 | lru-cache "~4.0.0" 1032 | 1033 | make-dir@^3.0.0: 1034 | version "3.1.0" 1035 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" 1036 | integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== 1037 | dependencies: 1038 | semver "^6.0.0" 1039 | 1040 | media-typer@0.3.0: 1041 | version "0.3.0" 1042 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 1043 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= 1044 | 1045 | merge-descriptors@1.0.1: 1046 | version "1.0.1" 1047 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 1048 | integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= 1049 | 1050 | methods@~1.1.2: 1051 | version "1.1.2" 1052 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 1053 | integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= 1054 | 1055 | mime-db@1.47.0, "mime-db@>= 1.43.0 < 2": 1056 | version "1.47.0" 1057 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c" 1058 | integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw== 1059 | 1060 | mime-types@^2.0.8, mime-types@~2.1.24: 1061 | version "2.1.30" 1062 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.30.tgz#6e7be8b4c479825f85ed6326695db73f9305d62d" 1063 | integrity sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg== 1064 | dependencies: 1065 | mime-db "1.47.0" 1066 | 1067 | mime@1.6.0: 1068 | version "1.6.0" 1069 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 1070 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 1071 | 1072 | mime@^2.2.0: 1073 | version "2.5.2" 1074 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" 1075 | integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== 1076 | 1077 | mimic-fn@^2.1.0: 1078 | version "2.1.0" 1079 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" 1080 | integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== 1081 | 1082 | ms@2.0.0: 1083 | version "2.0.0" 1084 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 1085 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 1086 | 1087 | ms@2.1.1: 1088 | version "2.1.1" 1089 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 1090 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== 1091 | 1092 | ms@2.1.2: 1093 | version "2.1.2" 1094 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 1095 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 1096 | 1097 | ms@^2.1.1: 1098 | version "2.1.3" 1099 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 1100 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 1101 | 1102 | negotiator@0.6.2: 1103 | version "0.6.2" 1104 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" 1105 | integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== 1106 | 1107 | node-fetch@^2.3.0, node-fetch@^2.6.1: 1108 | version "2.6.1" 1109 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" 1110 | integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== 1111 | 1112 | node-forge@^0.10.0: 1113 | version "0.10.0" 1114 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" 1115 | integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== 1116 | 1117 | object-assign@^4: 1118 | version "4.1.1" 1119 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 1120 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 1121 | 1122 | object-hash@^2.1.1: 1123 | version "2.1.1" 1124 | resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09" 1125 | integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ== 1126 | 1127 | on-finished@~2.3.0: 1128 | version "2.3.0" 1129 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 1130 | integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= 1131 | dependencies: 1132 | ee-first "1.1.1" 1133 | 1134 | once@^1.3.1, once@^1.4.0: 1135 | version "1.4.0" 1136 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 1137 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 1138 | dependencies: 1139 | wrappy "1" 1140 | 1141 | onetime@^5.1.0: 1142 | version "5.1.2" 1143 | resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" 1144 | integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== 1145 | dependencies: 1146 | mimic-fn "^2.1.0" 1147 | 1148 | p-limit@^3.0.1: 1149 | version "3.1.0" 1150 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" 1151 | integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== 1152 | dependencies: 1153 | yocto-queue "^0.1.0" 1154 | 1155 | parseurl@~1.3.3: 1156 | version "1.3.3" 1157 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 1158 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 1159 | 1160 | path-to-regexp@0.1.7: 1161 | version "0.1.7" 1162 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 1163 | integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= 1164 | 1165 | protobufjs@^6.10.0, protobufjs@^6.10.2, protobufjs@^6.8.6: 1166 | version "6.11.2" 1167 | resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" 1168 | integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== 1169 | dependencies: 1170 | "@protobufjs/aspromise" "^1.1.2" 1171 | "@protobufjs/base64" "^1.1.2" 1172 | "@protobufjs/codegen" "^2.0.4" 1173 | "@protobufjs/eventemitter" "^1.1.0" 1174 | "@protobufjs/fetch" "^1.1.0" 1175 | "@protobufjs/float" "^1.0.2" 1176 | "@protobufjs/inquire" "^1.1.0" 1177 | "@protobufjs/path" "^1.1.2" 1178 | "@protobufjs/pool" "^1.1.0" 1179 | "@protobufjs/utf8" "^1.1.0" 1180 | "@types/long" "^4.0.1" 1181 | "@types/node" ">=13.7.0" 1182 | long "^4.0.0" 1183 | 1184 | proxy-addr@~2.0.5: 1185 | version "2.0.6" 1186 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" 1187 | integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== 1188 | dependencies: 1189 | forwarded "~0.1.2" 1190 | ipaddr.js "1.9.1" 1191 | 1192 | pseudomap@^1.0.1: 1193 | version "1.0.2" 1194 | resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" 1195 | integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= 1196 | 1197 | pump@^3.0.0: 1198 | version "3.0.0" 1199 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 1200 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== 1201 | dependencies: 1202 | end-of-stream "^1.1.0" 1203 | once "^1.3.1" 1204 | 1205 | pumpify@^2.0.0: 1206 | version "2.0.1" 1207 | resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" 1208 | integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw== 1209 | dependencies: 1210 | duplexify "^4.1.1" 1211 | inherits "^2.0.3" 1212 | pump "^3.0.0" 1213 | 1214 | qs@6.7.0: 1215 | version "6.7.0" 1216 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" 1217 | integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== 1218 | 1219 | range-parser@~1.2.1: 1220 | version "1.2.1" 1221 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 1222 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 1223 | 1224 | raw-body@2.4.0: 1225 | version "2.4.0" 1226 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" 1227 | integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== 1228 | dependencies: 1229 | bytes "3.1.0" 1230 | http-errors "1.7.2" 1231 | iconv-lite "0.4.24" 1232 | unpipe "1.0.0" 1233 | 1234 | readable-stream@^3.1.1: 1235 | version "3.6.0" 1236 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" 1237 | integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== 1238 | dependencies: 1239 | inherits "^2.0.3" 1240 | string_decoder "^1.1.1" 1241 | util-deprecate "^1.0.1" 1242 | 1243 | require-directory@^2.1.1: 1244 | version "2.1.1" 1245 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 1246 | integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= 1247 | 1248 | retry-request@^4.0.0, retry-request@^4.1.1: 1249 | version "4.1.3" 1250 | resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" 1251 | integrity sha512-QnRZUpuPNgX0+D1xVxul6DbJ9slvo4Rm6iV/dn63e048MvGbUZiKySVt6Tenp04JqmchxjiLltGerOJys7kJYQ== 1252 | dependencies: 1253 | debug "^4.1.1" 1254 | 1255 | retry@0.12.0: 1256 | version "0.12.0" 1257 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" 1258 | integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= 1259 | 1260 | safe-buffer@5.1.2: 1261 | version "5.1.2" 1262 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 1263 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 1264 | 1265 | safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@~5.2.0: 1266 | version "5.2.1" 1267 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 1268 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 1269 | 1270 | "safer-buffer@>= 2.1.2 < 3": 1271 | version "2.1.2" 1272 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 1273 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 1274 | 1275 | semver@^5.6.0: 1276 | version "5.7.1" 1277 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 1278 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 1279 | 1280 | semver@^6.0.0: 1281 | version "6.3.0" 1282 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" 1283 | integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== 1284 | 1285 | send@0.17.1: 1286 | version "0.17.1" 1287 | resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" 1288 | integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== 1289 | dependencies: 1290 | debug "2.6.9" 1291 | depd "~1.1.2" 1292 | destroy "~1.0.4" 1293 | encodeurl "~1.0.2" 1294 | escape-html "~1.0.3" 1295 | etag "~1.8.1" 1296 | fresh "0.5.2" 1297 | http-errors "~1.7.2" 1298 | mime "1.6.0" 1299 | ms "2.1.1" 1300 | on-finished "~2.3.0" 1301 | range-parser "~1.2.1" 1302 | statuses "~1.5.0" 1303 | 1304 | serve-static@1.14.1: 1305 | version "1.14.1" 1306 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" 1307 | integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== 1308 | dependencies: 1309 | encodeurl "~1.0.2" 1310 | escape-html "~1.0.3" 1311 | parseurl "~1.3.3" 1312 | send "0.17.1" 1313 | 1314 | setprototypeof@1.1.1: 1315 | version "1.1.1" 1316 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" 1317 | integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== 1318 | 1319 | signal-exit@^3.0.2: 1320 | version "3.0.3" 1321 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" 1322 | integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== 1323 | 1324 | snakeize@^0.1.0: 1325 | version "0.1.0" 1326 | resolved "https://registry.yarnpkg.com/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d" 1327 | integrity sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0= 1328 | 1329 | "statuses@>= 1.5.0 < 2", statuses@~1.5.0: 1330 | version "1.5.0" 1331 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 1332 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= 1333 | 1334 | stream-events@^1.0.1, stream-events@^1.0.4, stream-events@^1.0.5: 1335 | version "1.0.5" 1336 | resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" 1337 | integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== 1338 | dependencies: 1339 | stubs "^3.0.0" 1340 | 1341 | stream-shift@^1.0.0: 1342 | version "1.0.1" 1343 | resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" 1344 | integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== 1345 | 1346 | streamsearch@0.1.2: 1347 | version "0.1.2" 1348 | resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" 1349 | integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= 1350 | 1351 | string-width@^4.1.0, string-width@^4.2.0: 1352 | version "4.2.2" 1353 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" 1354 | integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== 1355 | dependencies: 1356 | emoji-regex "^8.0.0" 1357 | is-fullwidth-code-point "^3.0.0" 1358 | strip-ansi "^6.0.0" 1359 | 1360 | string_decoder@^1.1.1: 1361 | version "1.3.0" 1362 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 1363 | integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== 1364 | dependencies: 1365 | safe-buffer "~5.2.0" 1366 | 1367 | strip-ansi@^6.0.0: 1368 | version "6.0.0" 1369 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" 1370 | integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== 1371 | dependencies: 1372 | ansi-regex "^5.0.0" 1373 | 1374 | stubs@^3.0.0: 1375 | version "3.0.0" 1376 | resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" 1377 | integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls= 1378 | 1379 | teeny-request@^7.0.0: 1380 | version "7.0.1" 1381 | resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-7.0.1.tgz#bdd41fdffea5f8fbc0d29392cb47bec4f66b2b4c" 1382 | integrity sha512-sasJmQ37klOlplL4Ia/786M5YlOcoLGQyq2TE4WHSRupbAuDaQW0PfVxV4MtdBtRJ4ngzS+1qim8zP6Zp35qCw== 1383 | dependencies: 1384 | http-proxy-agent "^4.0.0" 1385 | https-proxy-agent "^5.0.0" 1386 | node-fetch "^2.6.1" 1387 | stream-events "^1.0.5" 1388 | uuid "^8.0.0" 1389 | 1390 | toidentifier@1.0.0: 1391 | version "1.0.0" 1392 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 1393 | integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== 1394 | 1395 | tslib@^2.1.0: 1396 | version "2.2.0" 1397 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" 1398 | integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== 1399 | 1400 | type-is@~1.6.17, type-is@~1.6.18: 1401 | version "1.6.18" 1402 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 1403 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 1404 | dependencies: 1405 | media-typer "0.3.0" 1406 | mime-types "~2.1.24" 1407 | 1408 | typedarray-to-buffer@^3.1.5: 1409 | version "3.1.5" 1410 | resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" 1411 | integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== 1412 | dependencies: 1413 | is-typedarray "^1.0.0" 1414 | 1415 | unique-string@^2.0.0: 1416 | version "2.0.0" 1417 | resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" 1418 | integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== 1419 | dependencies: 1420 | crypto-random-string "^2.0.0" 1421 | 1422 | unpipe@1.0.0, unpipe@~1.0.0: 1423 | version "1.0.0" 1424 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1425 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 1426 | 1427 | util-deprecate@^1.0.1: 1428 | version "1.0.2" 1429 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1430 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 1431 | 1432 | utils-merge@1.0.1: 1433 | version "1.0.1" 1434 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1435 | integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= 1436 | 1437 | uuid@^8.0.0: 1438 | version "8.3.2" 1439 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" 1440 | integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== 1441 | 1442 | vary@^1, vary@~1.1.2: 1443 | version "1.1.2" 1444 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1445 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 1446 | 1447 | websocket-driver@>=0.5.1: 1448 | version "0.7.4" 1449 | resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" 1450 | integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== 1451 | dependencies: 1452 | http-parser-js ">=0.5.1" 1453 | safe-buffer ">=5.1.0" 1454 | websocket-extensions ">=0.1.1" 1455 | 1456 | websocket-extensions@>=0.1.1: 1457 | version "0.1.4" 1458 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" 1459 | integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== 1460 | 1461 | wrap-ansi@^7.0.0: 1462 | version "7.0.0" 1463 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 1464 | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 1465 | dependencies: 1466 | ansi-styles "^4.0.0" 1467 | string-width "^4.1.0" 1468 | strip-ansi "^6.0.0" 1469 | 1470 | wrappy@1: 1471 | version "1.0.2" 1472 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1473 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 1474 | 1475 | write-file-atomic@^3.0.0: 1476 | version "3.0.3" 1477 | resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" 1478 | integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== 1479 | dependencies: 1480 | imurmurhash "^0.1.4" 1481 | is-typedarray "^1.0.0" 1482 | signal-exit "^3.0.2" 1483 | typedarray-to-buffer "^3.1.5" 1484 | 1485 | xdg-basedir@^4.0.0: 1486 | version "4.0.0" 1487 | resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" 1488 | integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== 1489 | 1490 | y18n@^5.0.5: 1491 | version "5.0.8" 1492 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" 1493 | integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== 1494 | 1495 | yallist@^2.0.0: 1496 | version "2.1.2" 1497 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" 1498 | integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= 1499 | 1500 | yallist@^4.0.0: 1501 | version "4.0.0" 1502 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 1503 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 1504 | 1505 | yargs-parser@^20.2.2: 1506 | version "20.2.7" 1507 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" 1508 | integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== 1509 | 1510 | yargs@^16.1.1: 1511 | version "16.2.0" 1512 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" 1513 | integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== 1514 | dependencies: 1515 | cliui "^7.0.2" 1516 | escalade "^3.1.1" 1517 | get-caller-file "^2.0.5" 1518 | require-directory "^2.1.1" 1519 | string-width "^4.2.0" 1520 | y18n "^5.0.5" 1521 | yargs-parser "^20.2.2" 1522 | 1523 | yocto-queue@^0.1.0: 1524 | version "0.1.0" 1525 | resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 1526 | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 1527 | -------------------------------------------------------------------------------- /test/lib/commonjs.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const backupfire = require('../../lib') 3 | 4 | assert( 5 | typeof backupfire === 'function', 6 | "CommonJS require does't work as expected" 7 | ) 8 | -------------------------------------------------------------------------------- /test/lib/ts.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import backupfire from '../../lib' 3 | 4 | assert(typeof backupfire === 'function') 5 | -------------------------------------------------------------------------------- /test/server/.env.example: -------------------------------------------------------------------------------- 1 | BACKUPFIRE_TOKEN=TODO 2 | BACKUPFIRE_PASSWORD=TODO -------------------------------------------------------------------------------- /test/server/.firebaserc.example: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "backup-fire-playground" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/server/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "source": "." 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/server/index.js: -------------------------------------------------------------------------------- 1 | const backupfireAgent = require('@backupfire/firebase') 2 | const admin = require('firebase-admin') 3 | 4 | admin.initializeApp() 5 | 6 | exports.backupfire = backupfireAgent({ 7 | controllerDomain: 'staging.backupfire.dev' 8 | }) 9 | -------------------------------------------------------------------------------- /test/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backupfire-firebase-test-server", 3 | "version": "1.0.0", 4 | "description": "Backup Fire test server", 5 | "main": "index.js", 6 | "author": "Sasha Koss ", 7 | "license": "MIT", 8 | "engines": { 9 | "node": "16" 10 | }, 11 | "dependencies": { 12 | "@backupfire/firebase": "^1.8.0", 13 | "firebase-admin": "^10.2.0", 14 | "firebase-functions": "^3.21.2", 15 | "firebase-tools": "^11.0.1" 16 | }, 17 | "scripts": { 18 | "seedUsers": "env GOOGLE_APPLICATION_CREDENTIALS=secrets/backup-fire-playground-alt.json node scripts/seedUsers.js" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/server/scripts/seedUsers.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin') 2 | 3 | admin.initializeApp() 4 | 5 | const number = parseInt(process.env.NUMBER) 6 | if (isNaN(number)) 7 | throw new Error('The NUMBER environment variable must be a number') 8 | 9 | const auth = admin.auth() 10 | 11 | async function main() { 12 | for (let bunch = 0; bunch < number; bunch++) { 13 | console.log(`=== Creating users bunch #${bunch} ===`) 14 | 15 | await Promise.all( 16 | new Array(100).fill(undefined).map((_, i) => { 17 | const email = `test${bunch}${i}${Date.now()}@backupfire.dev` 18 | 19 | console.log(`...creating user #${bunch}/${i} (${email})`) 20 | 21 | return auth.createUser({ 22 | email, 23 | password: Date.now().toString(), 24 | displayName: 'Sasha Clone', 25 | photoURL: 26 | 'https://pbs.twimg.com/profile_images/979030533719064576/rD33B86M_400x400.jpg', 27 | }) 28 | }) 29 | ) 30 | } 31 | } 32 | 33 | main() 34 | -------------------------------------------------------------------------------- /test/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["test/server/**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "declaration": true, 5 | "target": "es2018", 6 | "module": "commonjs", 7 | "sourceMap": true, 8 | "outDir": "lib", 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true 14 | }, 15 | "exclude": [ 16 | "**/test.ts", 17 | "lib", 18 | "node_modules", 19 | "test", 20 | "examples", 21 | "extension" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /types/firebase-tools.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'firebase-tools' { 2 | export const auth: { 3 | export: (path: string, options: { project: string }) => Promise 4 | } 5 | } 6 | --------------------------------------------------------------------------------