├── .eslintignore ├── .eslintrc.js ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── README.md │ └── node-test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── mocha.opts ├── package-lock.json ├── package.json ├── scripts ├── assets │ └── functions_to_test.js ├── build │ ├── Dockerfile │ └── cloudbuild.yaml ├── clean.ts ├── creds-private.json.enc ├── creds-public.json.enc ├── firepit-builder │ ├── .dockerignore │ ├── Dockerfile │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── pipeline.js ├── lint-changed-files.ts ├── publish.sh ├── publish │ ├── .gitignore │ ├── cloudbuild.yaml │ ├── deploy_key.enc │ ├── hub.enc │ ├── npmrc.enc │ └── twitter.json.enc ├── set-default-credentials.sh ├── test-functions-config.js ├── test-functions-deploy.js ├── test-hosting.sh ├── test-project │ ├── database.rules.json │ ├── firebase.json │ ├── functions │ │ ├── package-lock.json │ │ └── package.json │ └── public │ │ ├── 404.html │ │ └── index.html ├── test-triggers-end-to-end.sh ├── triggers-end-to-end-tests │ ├── .firebaserc │ ├── .gitignore │ ├── README.md │ ├── firebase.json │ ├── functions │ │ ├── .gitignore │ │ ├── index.js │ │ └── package.json │ ├── package.json │ └── run.spec.js └── tweet.js ├── src ├── accountExporter.js ├── accountImporter.js ├── api.js ├── appdistribution │ ├── client.ts │ └── distribution.ts ├── archiveDirectory.js ├── auth.js ├── bin │ └── firebase.js ├── checkDupHostingKeys.js ├── checkFirebaseSDKVersion.js ├── checkValidTargetFilters.js ├── command.ts ├── commands │ ├── appdistribution-distribute.ts │ ├── apps-create.ts │ ├── apps-list.ts │ ├── apps-sdkconfig.ts │ ├── auth-export.js │ ├── auth-import.js │ ├── database-get.js │ ├── database-instances-create.ts │ ├── database-instances-list.ts │ ├── database-profile.js │ ├── database-push.js │ ├── database-remove.js │ ├── database-rules-canary.ts │ ├── database-rules-get.ts │ ├── database-rules-list.ts │ ├── database-rules-release.ts │ ├── database-rules-stage.ts │ ├── database-set.js │ ├── database-settings-get.ts │ ├── database-settings-set.ts │ ├── database-update.js │ ├── deploy.js │ ├── emulators-exec.ts │ ├── emulators-start.ts │ ├── experimental-functions-shell.js │ ├── ext-configure.ts │ ├── ext-info.ts │ ├── ext-install.ts │ ├── ext-list.ts │ ├── ext-uninstall.ts │ ├── ext-update.ts │ ├── ext.ts │ ├── firestore-delete.js │ ├── firestore-indexes-list.ts │ ├── functions-config-clone.js │ ├── functions-config-get.js │ ├── functions-config-legacy.js │ ├── functions-config-set.js │ ├── functions-config-unset.js │ ├── functions-delete.js │ ├── functions-log.ts │ ├── functions-shell.js │ ├── help.js │ ├── hosting-disable.js │ ├── index.js │ ├── init.js │ ├── list.js │ ├── login-ci.js │ ├── login.js │ ├── logout.js │ ├── open.ts │ ├── projects-addfirebase.ts │ ├── projects-create.ts │ ├── projects-list.ts │ ├── serve.js │ ├── setup-emulators-database.js │ ├── setup-emulators-firestore.js │ ├── setup-emulators-pubsub.ts │ ├── setup-web.js │ ├── target-apply.js │ ├── target-clear.js │ ├── target-remove.js │ ├── target.js │ ├── tools-migrate.js │ └── use.js ├── config.js ├── configstore.js ├── database │ ├── listRemote.ts │ ├── metadata.ts │ ├── remove.ts │ ├── removeRemote.ts │ └── settings.ts ├── deploy │ ├── database │ │ ├── index.js │ │ ├── prepare.js │ │ └── release.js │ ├── firestore │ │ ├── deploy.ts │ │ ├── index.ts │ │ ├── prepare.ts │ │ └── release.ts │ ├── functions │ │ ├── createOrUpdateSchedulesAndTopics.ts │ │ ├── deploy.js │ │ ├── index.js │ │ ├── prepare.js │ │ ├── release.js │ │ └── validate.ts │ ├── hosting │ │ ├── convertConfig.js │ │ ├── deploy.js │ │ ├── hashcache.js │ │ ├── index.js │ │ ├── prepare.js │ │ ├── release.js │ │ └── uploader.js │ ├── index.js │ ├── lifecycleHooks.js │ └── storage │ │ ├── deploy.ts │ │ ├── index.ts │ │ ├── prepare.ts │ │ └── release.ts ├── deploymentTool.js ├── detectProjectRoot.js ├── emulator │ ├── commandUtils.ts │ ├── constants.ts │ ├── controller.ts │ ├── databaseEmulator.ts │ ├── download.ts │ ├── emulatorLogger.ts │ ├── emulatorServer.ts │ ├── events │ │ └── types.ts │ ├── firestoreEmulator.ts │ ├── functionsEmulator.ts │ ├── functionsEmulatorRuntime.ts │ ├── functionsEmulatorShared.ts │ ├── functionsEmulatorShell.ts │ ├── functionsEmulatorUtils.ts │ ├── functionsRuntimeWorker.ts │ ├── hostingEmulator.ts │ ├── pubsubEmulator.ts │ ├── registry.ts │ ├── types.ts │ └── workQueue.ts ├── ensureApiEnabled.ts ├── ensureCloudResourceLocation.ts ├── ensureDefaultCredentials.js ├── error.ts ├── errorOut.ts ├── extensions │ ├── askUserForConsent.js │ ├── askUserForParam.ts │ ├── checkProjectBilling.js │ ├── extensionsApi.ts │ ├── extensionsHelper.ts │ ├── generateInstanceId.ts │ ├── listExtensions.ts │ ├── paramHelper.ts │ ├── populatePostinstall.ts │ ├── resolveSource.ts │ ├── rolesHelper.ts │ ├── updateHelper.ts │ └── utils.ts ├── extractTriggers.js ├── fetchMOTD.js ├── fetchWebSetup.ts ├── filterTargets.js ├── firestore │ ├── delete.js │ ├── encodeFirestoreValue.ts │ ├── indexes-api.ts │ ├── indexes-sort.ts │ ├── indexes-spec.ts │ ├── indexes.ts │ ├── util.ts │ └── validator.ts ├── fsAsync.js ├── fsutils.ts ├── functionsConfig.js ├── functionsConfigClone.js ├── functionsDelete.js ├── functionsDeployHelper.js ├── functionsShellCommandAction.js ├── gcp │ ├── cloudbilling.js │ ├── cloudfunctions.js │ ├── cloudlogging.js │ ├── cloudscheduler.ts │ ├── firedata.ts │ ├── firestore.ts │ ├── iam.js │ ├── index.js │ ├── pubsub.ts │ ├── rules.ts │ ├── runtimeconfig.js │ └── storage.js ├── getInstanceId.js ├── getProjectId.js ├── getProjectNumber.js ├── handlePreviewToggles.js ├── hosting │ ├── cloudRunProxy.ts │ ├── functionsProxy.ts │ ├── implicitInit.js │ ├── initMiddleware.js │ ├── normalizedHostingConfigs.js │ └── proxy.ts ├── identifierToProjectId.js ├── index.js ├── init │ ├── features │ │ ├── database.js │ │ ├── emulators.ts │ │ ├── firestore │ │ │ ├── index.ts │ │ │ ├── indexes.ts │ │ │ └── rules.ts │ │ ├── functions │ │ │ ├── index.js │ │ │ ├── javascript.js │ │ │ ├── npm-dependencies.js │ │ │ └── typescript.js │ │ ├── hosting.js │ │ ├── index.js │ │ ├── project.ts │ │ └── storage.ts │ └── index.js ├── listFiles.ts ├── loadCJSON.js ├── localFunction.js ├── logError.js ├── logger.js ├── management │ ├── apps.ts │ └── projects.ts ├── operation-poller.ts ├── parseBoltRules.js ├── parseTriggers.js ├── pollOperations.js ├── prepareFirebaseRules.js ├── prepareFunctionsUpload.js ├── prepareUpload.js ├── previews.js ├── profileReport.js ├── profiler.js ├── projectPath.ts ├── prompt.ts ├── rc.js ├── requireAuth.js ├── requireConfig.js ├── requireInstance.js ├── requirePermissions.ts ├── responseToError.js ├── rtdb.js ├── rulesDeploy.ts ├── runtimeChoiceSelector.ts ├── scopes.js ├── serve │ ├── database.ts │ ├── firestore.ts │ ├── functions.ts │ ├── hosting.js │ ├── index.ts │ └── javaEmulators.ts ├── test │ ├── accountExporter.spec.js │ ├── accountImporter.spec.js │ ├── appdistro │ │ └── client.spec.ts │ ├── archiveDirectory.spec.ts │ ├── command.spec.ts │ ├── config.spec.js │ ├── database │ │ ├── fakeListRemote.spec.ts │ │ ├── fakeRemoveRemote.spec.ts │ │ ├── listRemote.spec.ts │ │ ├── remove.spec.ts │ │ └── removeRemote.spec.ts │ ├── deploy │ │ └── functions │ │ │ └── validate.spec.ts │ ├── emulators │ │ ├── controller.spec.ts │ │ ├── emulatorServer.spec.ts │ │ ├── fakeEmulator.ts │ │ ├── fixtures.ts │ │ ├── functionsEmulator.spec.ts │ │ ├── functionsEmulatorRuntime.spec.ts │ │ ├── functionsEmulatorUtils.spec.ts │ │ ├── functionsRuntimeWorker.spec.ts │ │ ├── registry.spec.ts │ │ └── workQueue.spec.ts │ ├── error.spec.ts │ ├── extensions │ │ ├── askUserForConsent.spec.js │ │ ├── askUserForParam.spec.ts │ │ ├── checkProjectBilling.spec.js │ │ ├── extensionsApi.spec.ts │ │ ├── extensionsHelper.spec.ts │ │ ├── generateInstanceId.spec.ts │ │ ├── listExtensions.spec.ts │ │ ├── paramHelper.spec.ts │ │ ├── populatePostinstall.spec.ts │ │ ├── resolveSource.spec.ts │ │ ├── rolesHelper.spec.ts │ │ └── updateHelper.spec.ts │ ├── extractTriggers.spec.js │ ├── firestore │ │ ├── encodeFirestoreValue.spec.ts │ │ └── indexes.spec.ts │ ├── fixtures │ │ ├── config-imports │ │ │ ├── firebase.json │ │ │ ├── hosting.json │ │ │ ├── rules.json │ │ │ └── unsupported.txt │ │ ├── dup-top-level │ │ │ ├── firebase.json │ │ │ └── rules.json │ │ ├── fbrc │ │ │ ├── .firebaserc │ │ │ ├── conflict │ │ │ │ └── .firebaserc │ │ │ ├── firebase.json │ │ │ └── invalid │ │ │ │ ├── .firebaserc │ │ │ │ └── firebase.json │ │ ├── ignores │ │ │ ├── .hiddenfile │ │ │ ├── firebase.json │ │ │ ├── ignored.txt │ │ │ ├── ignored │ │ │ │ ├── deeper │ │ │ │ │ └── index.txt │ │ │ │ ├── ignore.txt │ │ │ │ └── index.html │ │ │ ├── index.html │ │ │ └── present │ │ │ │ └── index.html │ │ ├── invalid-config │ │ │ └── firebase.json │ │ ├── profiler-data │ │ │ ├── sample-output.json │ │ │ └── sample.json │ │ ├── rulesDeploy │ │ │ ├── firebase.json │ │ │ ├── firestore.rules │ │ │ └── storage.rules │ │ └── valid-config │ │ │ └── firebase.json │ ├── fsAsync.spec.js │ ├── fsutils.spec.ts │ ├── functionsConfig.spec.js │ ├── functionsDeployHelper.spec.js │ ├── gcp │ │ └── cloudscheduler.spec.ts │ ├── helpers │ │ ├── index.js │ │ └── mocha-bootstrap.js │ ├── hosting │ │ ├── cloudRunProxy.spec.ts │ │ └── functionsProxy.spec.ts │ ├── identifierToProjectId.spec.js │ ├── init │ │ └── features │ │ │ ├── firestore.spec.ts │ │ │ ├── project.spec.ts │ │ │ └── storage.spec.ts │ ├── listFiles.spec.ts │ ├── localFunction.spec.js │ ├── management │ │ ├── apps.spec.ts │ │ └── projects.spec.ts │ ├── operation-poller.spec.ts │ ├── profilerReport.spec.js │ ├── prompt.spec.ts │ ├── rc.spec.js │ ├── rulesDeploy.spec.ts │ ├── runtimeChoiceSelector.spec.ts │ ├── throttler │ │ ├── queue.spec.ts │ │ ├── stack.spec.ts │ │ └── throttler.spec.ts │ └── utils.spec.ts ├── throttler │ ├── errors │ │ ├── retries-exhausted-error.ts │ │ ├── task-error.ts │ │ └── timeout-error.ts │ ├── queue.ts │ ├── stack.ts │ └── throttler.ts ├── track.js ├── triggerParser.js ├── types │ └── marked-terminal │ │ └── index.d.ts ├── utils.ts ├── validateJsonRules.js └── validator.js ├── standalone ├── check.js ├── config.template.js ├── firepit.js ├── package.json ├── runtime.js └── welcome.js ├── templates ├── _gitignore ├── banner.txt ├── firebase.json ├── hosting │ └── init.js ├── init │ ├── firestore │ │ ├── firestore.indexes.json │ │ └── firestore.rules │ ├── functions │ │ ├── javascript │ │ │ ├── _gitignore │ │ │ ├── eslint.json │ │ │ ├── index.js │ │ │ ├── package.lint.json │ │ │ └── package.nolint.json │ │ └── typescript │ │ │ ├── _gitignore │ │ │ ├── index.ts │ │ │ ├── package.lint.json │ │ │ ├── package.nolint.json │ │ │ ├── tsconfig.json │ │ │ └── tslint.json │ ├── hosting │ │ ├── 404.html │ │ └── index.html │ └── storage │ │ └── storage.rules ├── loginFailure.html ├── loginSuccess.html └── setup │ └── web.js ├── tsconfig.dev.json ├── tsconfig.json └── tsconfig.publish.json /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | lib 3 | node_modules 4 | standalone 5 | templates -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult [GitHub Help] for more 22 | information on using pull requests. 23 | 24 | [GitHub Help]: https://help.github.com/articles/about-pull-requests/ 25 | 26 | ## Development setup 27 | 28 | When working on the Firebase CLI, you will want to [fork the project](https://help.github.com/articles/fork-a-repo/), clone the forked repository, make sure you're using `node >= 8.16.0` and `npm >= 6.9.0`, and then use `npm link` to globally link your working directory. This allows you to use the firebase command anywhere with your work-in-progress code. 29 | 30 | ``` 31 | node --version # Make sure it is >= 8.16.0 32 | npm install -g 'npm@>=6.9.0' 33 | 34 | git clone 35 | cd firebase-tools # navigate to your local repository 36 | npm link 37 | npm test # runs linter and tests 38 | ``` 39 | 40 | Now, whenever you run the firebase command, it is executing against the code in your working directory. This is great for manual testing. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 13 | 14 | 40 | 41 | Please submit feature requests through our [support page](https://firebase.google.com/support/contact/bugs-features/). 42 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | ### Description 13 | 14 | 15 | 16 | ### Scenarios Tested 17 | 18 | 19 | 20 | ### Sample Commands 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # Github Actions 2 | 3 | This directory contains [Github Actions](https://help.github.com/en/actions) workflows 4 | used for testing. 5 | 6 | ## Workflows 7 | 8 | * `node-test.yml` - unit tests and integration tests. 9 | 10 | ## Secrets 11 | 12 | The following secrets must be defined on the project: 13 | 14 | | Name | Description | 15 | |--------------------------|-------------| 16 | | `FBTOOLS_TARGET_PROJECT` | The project ID that should be used for integration tests | 17 | | `service_account_json_base64` | A base64-encoded service account JSON file with access to the selected project | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | node_modules 3 | /coverage 4 | /.nyc_output 5 | firebase-debug.log 6 | npm-debug.log 7 | yarn.lock 8 | .npmrc 9 | 10 | .DS_Store 11 | .idea 12 | *.iml 13 | *.tgz 14 | scripts/*.json 15 | 16 | lib/ 17 | dev/ 18 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /templates 2 | /node_modules 3 | /lib/**/* 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 100, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | * Adds the ability to select an extension to install from a list of available official extensions when `firebase ext:install -i` or `firebase ext:install --interactive` is run. 2 | * Fixes a small bug that caused `false` values in the `options` object to be ignored. 3 | * Release Database Emulator v4.3.1. 4 | * Fixes a bug where unidentified commands gave an unhelpful error message (#1889). 5 | * Prevents potential false-negative permissions check errors from erroring command. 6 | * Adds `-s, --site` flag to `hosting:disable` command, allowing it to be run against the non-default site of a project. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Firebase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --require src/test/helpers/mocha-bootstrap.js 4 | --recursive 5 | --timeout=1000 6 | src/test/**/*.{ts,js} 7 | -------------------------------------------------------------------------------- /scripts/assets/functions_to_test.js: -------------------------------------------------------------------------------- 1 | var functions = require("firebase-functions"); 2 | var admin = require("firebase-admin"); 3 | admin.initializeApp(functions.config().firebase); 4 | 5 | exports.dbAction = functions.database.ref("/input/{uuid}").onCreate(function(snap, context) { 6 | console.log("Received snapshot:", snap); 7 | return snap.ref.root.child("output/" + context.params.uuid).set(snap.val()); 8 | }); 9 | 10 | exports.nested = { 11 | dbAction: functions.database.ref("/inputNested/{uuid}").onCreate(function(snap, context) { 12 | console.log("Received snap:", snap); 13 | return snap.ref.root.child("output/" + context.params.uuid).set(snap.val()); 14 | }), 15 | }; 16 | 17 | exports.httpsAction = functions.https.onRequest(function(req, res) { 18 | res.send(req.body); 19 | }); 20 | 21 | exports.pubsubAction = functions.pubsub.topic("topic1").onPublish(function(message) { 22 | console.log("Received message:", message); 23 | var uuid = message.json; 24 | return admin 25 | .database() 26 | .ref("output/" + uuid) 27 | .set(uuid); 28 | }); 29 | 30 | exports.gcsAction = functions.storage.object().onFinalize(function(object) { 31 | console.log("Received object:", object); 32 | var uuid = object.name; 33 | return admin 34 | .database() 35 | .ref("output/" + uuid) 36 | .set(uuid); 37 | }); 38 | 39 | exports.pubsubScheduleAction = functions.pubsub 40 | .schedule("every 10 minutes") 41 | .onRun(function(context) { 42 | console.log("Scheduled function triggered:", context); 43 | return true; 44 | }); 45 | -------------------------------------------------------------------------------- /scripts/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.16.0 2 | 3 | # Install dependencies 4 | RUN apt-get update && \ 5 | apt-get install -y curl git jq 6 | 7 | # Install npm at 6.10.2. 8 | RUN npm install --global npm@6.10.2 9 | 10 | # Install hub 11 | RUN curl -fsSL --output hub.tgz https://github.com/github/hub/releases/download/v2.11.2/hub-linux-amd64-2.11.2.tgz 12 | RUN tar --strip-components=2 -C /usr/bin -xf hub.tgz hub-linux-amd64-2.11.2/bin/hub 13 | -------------------------------------------------------------------------------- /scripts/build/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: 'gcr.io/cloud-builders/docker' 3 | args: ['build', '-t', 'gcr.io/$PROJECT_ID/package-builder', '.'] 4 | images: ['gcr.io/$PROJECT_ID/package-builder'] 5 | -------------------------------------------------------------------------------- /scripts/clean.ts: -------------------------------------------------------------------------------- 1 | import { removeSync } from "fs-extra"; 2 | import { resolve } from "path"; 3 | 4 | removeSync(`${resolve(__dirname, "..", "lib")}`); 5 | -------------------------------------------------------------------------------- /scripts/creds-private.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-oss-bot/firebase-tools/5e501afd85a4d65cea68b330ea9c7a1d0eca8353/scripts/creds-private.json.enc -------------------------------------------------------------------------------- /scripts/creds-public.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-oss-bot/firebase-tools/5e501afd85a4d65cea68b330ea9c7a1d0eca8353/scripts/creds-public.json.enc -------------------------------------------------------------------------------- /scripts/firepit-builder/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /scripts/firepit-builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | # Install dependencies 4 | RUN apt-get update && \ 5 | apt-get install -y wget tar 6 | 7 | # Install hub 8 | RUN curl -fsSL --output hub.tgz https://github.com/github/hub/releases/download/v2.11.2/hub-linux-amd64-2.11.2.tgz 9 | RUN tar --strip-components=2 -C /usr/bin -xf hub.tgz hub-linux-amd64-2.11.2/bin/hub 10 | 11 | # Create app directory 12 | WORKDIR /usr/src/app 13 | 14 | # Install app dependencies 15 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 16 | # where available (npm@5+) 17 | COPY package*.json ./ 18 | 19 | RUN npm install 20 | # If you are building your code for production 21 | # RUN npm ci --only=production 22 | 23 | # Bundle app source 24 | COPY . . 25 | 26 | CMD [ "node", "pipeline.js" ] 27 | -------------------------------------------------------------------------------- /scripts/firepit-builder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud_build", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "pipeline.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "private": true, 13 | "dependencies": { 14 | "shelljs": "^0.8.3", 15 | "yargs": "^13.3.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /scripts/publish/.gitignore: -------------------------------------------------------------------------------- 1 | originals 2 | -------------------------------------------------------------------------------- /scripts/publish/deploy_key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-oss-bot/firebase-tools/5e501afd85a4d65cea68b330ea9c7a1d0eca8353/scripts/publish/deploy_key.enc -------------------------------------------------------------------------------- /scripts/publish/hub.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-oss-bot/firebase-tools/5e501afd85a4d65cea68b330ea9c7a1d0eca8353/scripts/publish/hub.enc -------------------------------------------------------------------------------- /scripts/publish/npmrc.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-oss-bot/firebase-tools/5e501afd85a4d65cea68b330ea9c7a1d0eca8353/scripts/publish/npmrc.enc -------------------------------------------------------------------------------- /scripts/publish/twitter.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-oss-bot/firebase-tools/5e501afd85a4d65cea68b330ea9c7a1d0eca8353/scripts/publish/twitter.json.enc -------------------------------------------------------------------------------- /scripts/set-default-credentials.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | CWD="$(pwd)" 5 | 6 | if [[ -z $CI ]]; then 7 | echo "CI is unset, assuming local testing." 8 | export COMMIT_SHA="localtesting" 9 | export CI_JOB_ID="$(echo $RANDOM)" 10 | else 11 | echo "CI=${CI}, setting GOOGLE_APPLICATION_CREDENTIALS" 12 | export GOOGLE_APPLICATION_CREDENTIALS="${CWD}/scripts/service-account.json" 13 | fi 14 | 15 | echo "Application Default Credentials: ${GOOGLE_APPLICATION_CREDENTIALS}" -------------------------------------------------------------------------------- /scripts/test-project/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": true, 4 | ".write": true 5 | } 6 | } -------------------------------------------------------------------------------- /scripts/test-project/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "public" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/test-project/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Firebase Functions", 4 | "dependencies": { 5 | "firebase-admin": "~8.6.0", 6 | "firebase-functions": "^3.2.0" 7 | }, 8 | "engines": { 9 | "node": "8" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /scripts/test-project/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /scripts/test-project/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /scripts/test-triggers-end-to-end.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # To run this command locally: 3 | # LOCAL=true FBTOOLS_TARGET_PROJECT={{YOUR_PROJECT}} ./scripts/test-triggers-end-to-end.sh 4 | set -xe 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | 7 | source $DIR/set-default-credentials.sh 8 | 9 | FIREBASE_CLI="./lib/bin/firebase.js" 10 | 11 | if ! [ -x $FIREBASE_CLI ]; 12 | then 13 | echo "marking $FIREBASE_CLI user/group executable" 14 | chmod ug+x $FIREBASE_CLI 15 | fi; 16 | 17 | cd ./scripts/triggers-end-to-end-tests 18 | 19 | npm install && npm test 20 | -------------------------------------------------------------------------------- /scripts/triggers-end-to-end-tests/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "fir-tools-testing" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /scripts/triggers-end-to-end-tests/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | database-debug.log* 9 | firestore-debug.log* 10 | pubsub-debug.log* 11 | 12 | # Firebase cache 13 | .firebase/ 14 | 15 | # Firebase config 16 | 17 | # Uncomment this if you'd like others to create their own Firebase project. 18 | # For a team working on the same Firebase project(s), it is recommended to leave 19 | # it commented so all members can deploy to the same project(s) in .firebaserc. 20 | # .firebaserc 21 | 22 | # Runtime data 23 | pids 24 | *.pid 25 | *.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | lib-cov 30 | 31 | # Coverage directory used by tools like istanbul 32 | coverage 33 | 34 | # nyc test coverage 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 38 | .grunt 39 | 40 | # Bower dependency directory (https://bower.io/) 41 | bower_components 42 | 43 | # node-waf configuration 44 | .lock-wscript 45 | 46 | # Compiled binary addons (http://nodejs.org/api/addons.html) 47 | build/Release 48 | 49 | # Dependency directories 50 | node_modules/ 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | -------------------------------------------------------------------------------- /scripts/triggers-end-to-end-tests/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": {}, 3 | "firestore": { 4 | "rules": "firestore.rules" 5 | }, 6 | "functions": {}, 7 | "emulators": { 8 | "database": { 9 | "port": 9000 10 | }, 11 | "firestore": { 12 | "port": 9001 13 | }, 14 | "functions": { 15 | "port": 9002 16 | }, 17 | "pubsub": { 18 | "port": 8085 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/triggers-end-to-end-tests/functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .eslintrc 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /scripts/triggers-end-to-end-tests/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 | "@google-cloud/pubsub": "^1.1.5", 16 | "firebase-admin": "^8.0.0", 17 | "firebase-functions": "^3.3.0" 18 | }, 19 | "devDependencies": { 20 | "firebase-functions-test": "^0.1.6" 21 | }, 22 | "private": true 23 | } 24 | -------------------------------------------------------------------------------- /scripts/triggers-end-to-end-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "triggers-end-to-end-tests", 3 | "version": "1.0.0", 4 | "description": "End-to-end tests for emulator cloud function triggers", 5 | "main": "run.spec.js", 6 | "scripts": { 7 | "test": "./node_modules/mocha/bin/mocha run.spec.js --reporter spec --exit" 8 | }, 9 | "dependencies": { 10 | "async": "^3.0.1", 11 | "grpc": "^1.21.0", 12 | "mocha": "^5.0.5" 13 | }, 14 | "author": "", 15 | "license": "ISC" 16 | } 17 | -------------------------------------------------------------------------------- /scripts/tweet.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const Twitter = require("twitter"); 5 | 6 | function printUsage() { 7 | console.error( 8 | ` 9 | Usage: tweet.js 10 | 11 | Credentials must be stored in "twitter.json" in this directory. 12 | 13 | Arguments: 14 | - version: Version of module that was released. e.g. "1.2.3" 15 | ` 16 | ); 17 | process.exit(1); 18 | } 19 | 20 | function getUrl(version) { 21 | return `https://github.com/firebase/firebase-tools/releases/tag/v${version}`; 22 | } 23 | 24 | if (process.argv.length !== 3) { 25 | console.error("Missing arguments."); 26 | printUsage(); 27 | } 28 | 29 | const version = process.argv.pop(); 30 | if (!version.match(/^\d+\.\d+\.\d+$/)) { 31 | console.error(`Version "${version}" not a version number.`); 32 | printUsage(); 33 | } 34 | 35 | if (!fs.existsSync(`${__dirname}/twitter.json`)) { 36 | console.error("Missing credentials."); 37 | printUsage(); 38 | } 39 | const creds = require("./twitter.json"); 40 | 41 | const client = new Twitter(creds); 42 | 43 | client.post( 44 | "statuses/update", 45 | { status: `v${version} of @Firebase CLI is available. Release notes: ${getUrl(version)}` }, 46 | (err) => { 47 | if (err) { 48 | console.error(err); 49 | process.exit(1); 50 | } 51 | } 52 | ); 53 | -------------------------------------------------------------------------------- /src/appdistribution/distribution.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs-extra"; 2 | import { FirebaseError } from "../error"; 3 | import * as crypto from "crypto"; 4 | 5 | export enum DistributionFileType { 6 | IPA = "ipa", 7 | APK = "apk", 8 | } 9 | 10 | /** 11 | * Object representing an APK or IPa file. Used for uploading app distributions. 12 | */ 13 | export class Distribution { 14 | private readonly fileType: DistributionFileType; 15 | 16 | constructor(private readonly path: string) { 17 | if (!path) { 18 | throw new FirebaseError("must specify a distribution file"); 19 | } 20 | 21 | const distributionType = path.split(".").pop(); 22 | if ( 23 | distributionType !== DistributionFileType.IPA && 24 | distributionType !== DistributionFileType.APK 25 | ) { 26 | throw new FirebaseError("unsupported distribution file format, should be .ipa or .apk"); 27 | } 28 | 29 | if (!fs.existsSync(path)) { 30 | throw new FirebaseError( 31 | `File ${path} does not exist: verify that file points to a distribution` 32 | ); 33 | } 34 | 35 | this.path = path; 36 | this.fileType = distributionType; 37 | } 38 | 39 | fileSize(): number { 40 | return fs.statSync(this.path).size; 41 | } 42 | 43 | readStream(): fs.ReadStream { 44 | return fs.createReadStream(this.path); 45 | } 46 | 47 | platform(): string { 48 | switch (this.fileType) { 49 | case DistributionFileType.IPA: 50 | return "ios"; 51 | case DistributionFileType.APK: 52 | return "android"; 53 | default: 54 | throw new FirebaseError("Unsupported distribution file format, should be .ipa or .apk"); 55 | } 56 | } 57 | 58 | async releaseHash(): Promise { 59 | return new Promise((resolve) => { 60 | const hash = crypto.createHash("sha1"); 61 | const stream = this.readStream(); 62 | stream.on("data", (data) => hash.update(data)); 63 | stream.on("end", () => { 64 | return resolve(hash.digest("hex")); 65 | }); 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/checkDupHostingKeys.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | 5 | var utils = require("./utils"); 6 | var clc = require("cli-color"); 7 | var Config = require("./config"); 8 | var { FirebaseError } = require("./error"); 9 | var logger = require("./logger"); 10 | 11 | module.exports = function(options) { 12 | return new Promise(function(resolve, reject) { 13 | var src = options.config._src; 14 | var legacyKeys = Config.LEGACY_HOSTING_KEYS; 15 | 16 | var hasLegacyKeys = _.reduce( 17 | legacyKeys, 18 | function(result, key) { 19 | return result || _.has(src, key); 20 | }, 21 | false 22 | ); 23 | 24 | if (hasLegacyKeys && _.has(src, ["hosting"])) { 25 | utils.logWarning( 26 | clc.bold.yellow("hosting: ") + 27 | "We found a " + 28 | clc.bold("hosting") + 29 | " key inside " + 30 | clc.bold("firebase.json") + 31 | " as well as hosting configuration keys that are not nested inside the " + 32 | clc.bold("hosting") + 33 | " key." 34 | ); 35 | logger.info("\n\nPlease run " + clc.bold("firebase tools:migrate") + " to fix this issue."); 36 | logger.info( 37 | "Please note that this will overwrite any configuration keys nested inside the " + 38 | clc.bold("hosting") + 39 | " key with configuration keys at the root level of " + 40 | clc.bold("firebase.json.") 41 | ); 42 | reject( 43 | new FirebaseError("Hosting key and legacy hosting keys are both present in firebase.json.") 44 | ); 45 | } else { 46 | resolve(); 47 | } 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /src/checkValidTargetFilters.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | 5 | var { FirebaseError } = require("./error"); 6 | 7 | module.exports = function(options) { 8 | function numFilters(targetTypes) { 9 | return _.filter(options.only, function(opt) { 10 | var optChunks = opt.split(":"); 11 | return _.includes(targetTypes, optChunks[0]) && optChunks.length > 1; 12 | }).length; 13 | } 14 | function targetContainsFilter(targetTypes) { 15 | return numFilters(targetTypes) > 1; 16 | } 17 | function targetDoesNotContainFilter(targetTypes) { 18 | return numFilters(targetTypes) === 0; 19 | } 20 | 21 | return new Promise(function(resolve, reject) { 22 | if (!options.only) { 23 | return resolve(); 24 | } 25 | if (options.except) { 26 | return reject( 27 | new FirebaseError("Cannot specify both --only and --except", { 28 | exit: 1, 29 | }) 30 | ); 31 | } 32 | if (targetContainsFilter(["database", "storage", "hosting"])) { 33 | return reject( 34 | new FirebaseError( 35 | "Filters specified with colons (e.g. --only functions:func1,functions:func2) are only supported for functions", 36 | { exit: 1 } 37 | ) 38 | ); 39 | } 40 | if (targetContainsFilter(["functions"]) && targetDoesNotContainFilter(["functions"])) { 41 | return reject( 42 | new FirebaseError( 43 | 'Cannot specify "--only functions" and "--only functions:" at the same time', 44 | { exit: 1 } 45 | ) 46 | ); 47 | } 48 | return resolve(); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/commands/auth-export.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var clc = require("cli-color"); 4 | var fs = require("fs"); 5 | var os = require("os"); 6 | 7 | var { Command } = require("../command"); 8 | var accountExporter = require("../accountExporter"); 9 | var getProjectId = require("../getProjectId"); 10 | var logger = require("../logger"); 11 | var { requirePermissions } = require("../requirePermissions"); 12 | 13 | var MAX_BATCH_SIZE = 1000; 14 | 15 | var validateOptions = accountExporter.validateOptions; 16 | var serialExportUsers = accountExporter.serialExportUsers; 17 | 18 | module.exports = new Command("auth:export [dataFile]") 19 | .description("Export accounts from your Firebase project into a data file") 20 | .option( 21 | "--format ", 22 | "Format of exported data (csv, json). Ignored if [dataFile] has format extension." 23 | ) 24 | .before(requirePermissions, ["firebaseauth.users.get"]) 25 | .action(function(dataFile, options) { 26 | var projectId = getProjectId(options); 27 | var checkRes = validateOptions(options, dataFile); 28 | if (!checkRes.format) { 29 | return checkRes; 30 | } 31 | var exportOptions = checkRes; 32 | var writeStream = fs.createWriteStream(dataFile); 33 | if (exportOptions.format === "json") { 34 | writeStream.write('{"users": [' + os.EOL); 35 | } 36 | exportOptions.writeStream = writeStream; 37 | exportOptions.batchSize = MAX_BATCH_SIZE; 38 | logger.info("Exporting accounts to " + clc.bold(dataFile)); 39 | return serialExportUsers(projectId, exportOptions).then(function() { 40 | if (exportOptions.format === "json") { 41 | writeStream.write("]}"); 42 | } 43 | writeStream.end(); 44 | // Ensure process ends only when all data have been flushed 45 | // to the output file 46 | return new Promise(function(resolve, reject) { 47 | writeStream.on("finish", resolve); 48 | writeStream.on("close", resolve); 49 | writeStream.on("error", reject); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/commands/database-instances-create.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | import logger = require("../logger"); 3 | import { requirePermissions } from "../requirePermissions"; 4 | import getProjectNumber = require("../getProjectNumber"); 5 | import firedata = require("../gcp/firedata"); 6 | 7 | export default new Command("database:instances:create ") 8 | .description("create a realtime database instance") 9 | .before(requirePermissions, ["firebasedatabase.instances.create"]) 10 | .action(async (instanceName: string, options: any) => { 11 | const projectNumber = await getProjectNumber(options); 12 | const instance = await firedata.createDatabaseInstance(projectNumber, instanceName); 13 | logger.info(`created database instance ${instance.instance}`); 14 | return instance; 15 | }); 16 | -------------------------------------------------------------------------------- /src/commands/database-instances-list.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | import logger = require("../logger"); 3 | import { requirePermissions } from "../requirePermissions"; 4 | import getProjectNumber = require("../getProjectNumber"); 5 | import firedata = require("../gcp/firedata"); 6 | 7 | export default new Command("database:instances:list") 8 | .description("list realtime database instances") 9 | .before(requirePermissions, ["firebasedatabase.instances.list"]) 10 | .action(async (options: any) => { 11 | const projectNumber = await getProjectNumber(options); 12 | const instances = await firedata.listDatabaseInstances(projectNumber); 13 | for (const instance of instances) { 14 | logger.info(instance.instance); 15 | } 16 | logger.info(`Project ${options.project} has ${instances.length} database instances`); 17 | return instances; 18 | }); 19 | -------------------------------------------------------------------------------- /src/commands/database-remove.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var { Command } = require("../command"); 4 | var requireInstance = require("../requireInstance"); 5 | var { requirePermissions } = require("../requirePermissions"); 6 | var DatabaseRemove = require("../database/remove").default; 7 | var api = require("../api"); 8 | 9 | var utils = require("../utils"); 10 | var { prompt } = require("../prompt"); 11 | var clc = require("cli-color"); 12 | var _ = require("lodash"); 13 | 14 | module.exports = new Command("database:remove ") 15 | .description("remove data from your Firebase at the specified path") 16 | .option("-y, --confirm", "pass this option to bypass confirmation prompt") 17 | .option( 18 | "--instance ", 19 | "use the database .firebaseio.com (if omitted, use default database instance)" 20 | ) 21 | .before(requirePermissions, ["firebasedatabase.instances.update"]) 22 | .before(requireInstance) 23 | .action(function(path, options) { 24 | if (!_.startsWith(path, "/")) { 25 | return utils.reject("Path must begin with /", { exit: 1 }); 26 | } 27 | 28 | return prompt(options, [ 29 | { 30 | type: "confirm", 31 | name: "confirm", 32 | default: false, 33 | message: 34 | "You are about to remove all data at " + 35 | clc.cyan(utils.addSubdomain(api.realtimeOrigin, options.instance) + path) + 36 | ". Are you sure?", 37 | }, 38 | ]).then(function() { 39 | if (!options.confirm) { 40 | return utils.reject("Command aborted.", { exit: 1 }); 41 | } 42 | var removeOps = new DatabaseRemove(options.instance, path); 43 | return removeOps.execute().then(function() { 44 | utils.logSuccess("Data removed successfully"); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/commands/database-rules-canary.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | import * as logger from "../logger"; 3 | import * as requireInstance from "../requireInstance"; 4 | import { requirePermissions } from "../requirePermissions"; 5 | import * as metadata from "../database/metadata"; 6 | 7 | export default new Command("database:rules:canary ") 8 | .description("mark a staged ruleset as the canary ruleset") 9 | .option( 10 | "--instance ", 11 | "use the database .firebaseio.com (if omitted, uses default database instance)" 12 | ) 13 | .before(requirePermissions, ["firebasedatabase.instances.update"]) 14 | .before(requireInstance) 15 | .action(async (rulesetId: string, options: any) => { 16 | const oldLabels = await metadata.getRulesetLabels(options.instance); 17 | const newLabels = { 18 | stable: oldLabels.stable, 19 | canary: rulesetId, 20 | }; 21 | await metadata.setRulesetLabels(options.instance, newLabels); 22 | return newLabels; 23 | }); 24 | -------------------------------------------------------------------------------- /src/commands/database-rules-get.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | import * as logger from "../logger"; 3 | import * as requireInstance from "../requireInstance"; 4 | import { requirePermissions } from "../requirePermissions"; 5 | import * as metadata from "../database/metadata"; 6 | 7 | export default new Command("database:rules:get ") 8 | .description("get a realtime database ruleset by id") 9 | .option( 10 | "--instance ", 11 | "use the database .firebaseio.com (if omitted, uses default database instance)" 12 | ) 13 | .before(requirePermissions, ["firebasedatabase.instances.get"]) 14 | .before(requireInstance) 15 | .action(async (rulesetId: metadata.RulesetId, options: any) => { 16 | const ruleset = await metadata.getRuleset(options.instance, rulesetId); 17 | logger.info(`Ruleset ${ruleset.id} was created at ${ruleset.createdAt}`); 18 | logger.info(ruleset.source); 19 | return ruleset; 20 | }); 21 | -------------------------------------------------------------------------------- /src/commands/database-rules-list.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | import * as logger from "../logger"; 3 | import * as requireInstance from "../requireInstance"; 4 | import { requirePermissions } from "../requirePermissions"; 5 | import * as metadata from "../database/metadata"; 6 | 7 | export default new Command("database:rules:list") 8 | .description("list realtime database rulesets") 9 | .option( 10 | "--instance ", 11 | "use the database .firebaseio.com (if omitted, uses default database instance)" 12 | ) 13 | .before(requirePermissions, ["firebasedatabase.instances.get"]) 14 | .before(requireInstance) 15 | .action(async (options: any) => { 16 | const labeled = await metadata.getRulesetLabels(options.instance); 17 | const rulesets = await metadata.listAllRulesets(options.instance); 18 | for (const ruleset of rulesets) { 19 | const labels = []; 20 | if (ruleset.id == labeled.stable) { 21 | labels.push("stable"); 22 | } 23 | if (ruleset.id == labeled.canary) { 24 | labels.push("canary"); 25 | } 26 | logger.info(`${ruleset.id} ${ruleset.createdAt} ${labels.join(",")}`); 27 | } 28 | logger.info("Labels:"); 29 | logger.info(` stable: ${labeled.stable}`); 30 | if (labeled.canary) { 31 | logger.info(` canary: ${labeled.canary}`); 32 | } 33 | return { rulesets, labeled }; 34 | }); 35 | -------------------------------------------------------------------------------- /src/commands/database-rules-release.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | import * as logger from "../logger"; 3 | import * as requireInstance from "../requireInstance"; 4 | import { requirePermissions } from "../requirePermissions"; 5 | import * as metadata from "../database/metadata"; 6 | 7 | export default new Command("database:rules:release ") 8 | .description("mark a staged ruleset as the stable ruleset") 9 | .option( 10 | "--instance ", 11 | "use the database .firebaseio.com (if omitted, uses default database instance)" 12 | ) 13 | .before(requirePermissions, ["firebasedatabase.instances.update"]) 14 | .before(requireInstance) 15 | .action(async (rulesetId: string, options: any) => { 16 | const oldLabels = await metadata.getRulesetLabels(options.instance); 17 | const newLabels = { 18 | stable: rulesetId, 19 | canary: oldLabels.canary, 20 | }; 21 | await metadata.setRulesetLabels(options.instance, newLabels); 22 | return newLabels; 23 | }); 24 | -------------------------------------------------------------------------------- /src/commands/database-rules-stage.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | import * as logger from "../logger"; 3 | import * as requireInstance from "../requireInstance"; 4 | import { requirePermissions } from "../requirePermissions"; 5 | import * as metadata from "../database/metadata"; 6 | import * as fs from "fs-extra"; 7 | import * as path from "path"; 8 | 9 | export default new Command("database:rules:stage") 10 | .description("create a new realtime database ruleset") 11 | .option( 12 | "--instance ", 13 | "use the database .firebaseio.com (if omitted, uses default database instance)" 14 | ) 15 | .before(requirePermissions, ["firebasedatabase.instances.update"]) 16 | .before(requireInstance) 17 | .action(async (options: any) => { 18 | const filepath = options.config.data.database.rules; 19 | logger.info(`staging ruleset from ${filepath}`); 20 | const source = fs.readFileSync(path.resolve(filepath), "utf8"); 21 | const rulesetId = await metadata.createRuleset(options.instance, source); 22 | logger.info(`staged ruleset ${rulesetId}`); 23 | return rulesetId; 24 | }); 25 | -------------------------------------------------------------------------------- /src/commands/emulators-start.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | import * as controller from "../emulator/controller"; 3 | import * as commandUtils from "../emulator/commandUtils"; 4 | import * as utils from "../utils"; 5 | 6 | module.exports = new Command("emulators:start") 7 | .before(commandUtils.beforeEmulatorCommand) 8 | .description("start the local Firebase emulators") 9 | .option(commandUtils.FLAG_ONLY, commandUtils.DESC_ONLY) 10 | .option(commandUtils.FLAG_INSPECT_FUNCTIONS, commandUtils.DESC_INSPECT_FUNCTIONS) 11 | .action(async (options: any) => { 12 | try { 13 | await controller.startAll(options); 14 | } catch (e) { 15 | await controller.cleanShutdown(); 16 | throw e; 17 | } 18 | 19 | utils.logSuccess("All emulators started, it is now safe to connect."); 20 | 21 | // Hang until explicitly killed 22 | await new Promise((res, rej) => { 23 | process.on("SIGINT", () => { 24 | controller 25 | .cleanShutdown() 26 | .then(res) 27 | .catch(res); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/commands/experimental-functions-shell.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var { Command } = require("../command"); 4 | var { requirePermissions } = require("../requirePermissions"); 5 | var action = require("../functionsShellCommandAction"); 6 | var requireConfig = require("../requireConfig"); 7 | 8 | module.exports = new Command("experimental:functions:shell") 9 | .description( 10 | "launch full Node shell with emulated functions. (Alias for `firebase functions:shell.)" 11 | ) 12 | .option("-p, --port ", "the port on which to emulate functions (default: 5000)", 5000) 13 | .before(requireConfig) 14 | .before(requirePermissions) 15 | .action(action); 16 | -------------------------------------------------------------------------------- /src/commands/ext-list.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | import * as getProjectId from "../getProjectId"; 3 | import { listExtensions } from "../extensions/listExtensions"; 4 | import { ensureExtensionsApiEnabled } from "../extensions/extensionsHelper"; 5 | import { requirePermissions } from "../requirePermissions"; 6 | 7 | module.exports = new Command("ext:list") 8 | .description("list all the extensions that are installed in your Firebase project") 9 | .before(requirePermissions, ["firebasemods.instances.list"]) 10 | .before(ensureExtensionsApiEnabled) 11 | .action((options: any) => { 12 | const projectId = getProjectId(options); 13 | return listExtensions(projectId); 14 | }); 15 | -------------------------------------------------------------------------------- /src/commands/ext.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as clc from "cli-color"; 3 | 4 | import { Command } from "../command"; 5 | import * as getProjectId from "../getProjectId"; 6 | import { logPrefix } from "../extensions/extensionsHelper"; 7 | import { listExtensions } from "../extensions/listExtensions"; 8 | import { requirePermissions } from "../requirePermissions"; 9 | import * as logger from "../logger"; 10 | import * as utils from "../utils"; 11 | import { CommanderStatic } from "commander"; 12 | 13 | module.exports = new Command("ext") 14 | .description( 15 | "display information on how to use ext commands and extensions installed to your project" 16 | ) 17 | .action(async (options: any) => { 18 | // Print out help info for all extensions commands. 19 | utils.logLabeledBullet(logPrefix, "list of extensions commands:"); 20 | const firebaseTools = require("../"); // eslint-disable-line @typescript-eslint/no-var-requires 21 | const commandNames = [ 22 | "ext:install", 23 | "ext:info", 24 | "ext:list", 25 | "ext:configure", 26 | "ext:update", 27 | "ext:uninstall", 28 | ]; 29 | 30 | _.forEach(commandNames, (commandName) => { 31 | const command: CommanderStatic = firebaseTools.getCommand(commandName); 32 | logger.info(clc.bold("\n" + command.name())); 33 | command.outputHelp(); 34 | }); 35 | logger.info(); 36 | 37 | // Print out a list of all extension instances on project, if called with a project. 38 | try { 39 | await requirePermissions(options, ["firebasemods.instances.list"]); 40 | const projectId = getProjectId(options); 41 | return listExtensions(projectId); 42 | } catch (err) { 43 | return; 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /src/commands/firestore-indexes-list.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | import * as clc from "cli-color"; 3 | import * as fsi from "../firestore/indexes"; 4 | import * as logger from "../logger"; 5 | import { requirePermissions } from "../requirePermissions"; 6 | 7 | module.exports = new Command("firestore:indexes") 8 | .description("List indexes in your project's Cloud Firestore database.") 9 | .option( 10 | "--pretty", 11 | "Pretty print. When not specified the indexes are printed in the " + 12 | "JSON specification format." 13 | ) 14 | .before(requirePermissions, ["datastore.indexes.list"]) 15 | .action(async (options: any) => { 16 | const indexApi = new fsi.FirestoreIndexes(); 17 | 18 | const indexes = await indexApi.listIndexes(options.project); 19 | const fieldOverrides = await indexApi.listFieldOverrides(options.project); 20 | 21 | const indexSpec = indexApi.makeIndexSpec(indexes, fieldOverrides); 22 | 23 | if (options.pretty) { 24 | logger.info(clc.bold.white("Compound Indexes")); 25 | indexApi.prettyPrintIndexes(indexes); 26 | 27 | if (fieldOverrides) { 28 | logger.info(); 29 | logger.info(clc.bold.white("Field Overrides")); 30 | indexApi.printFieldOverrides(fieldOverrides); 31 | } 32 | } else { 33 | logger.info(JSON.stringify(indexSpec, undefined, 2)); 34 | } 35 | 36 | return indexSpec; 37 | }); 38 | -------------------------------------------------------------------------------- /src/commands/functions-config-get.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var { Command } = require("../command"); 5 | var getProjectId = require("../getProjectId"); 6 | var logger = require("../logger"); 7 | var { requirePermissions } = require("../requirePermissions"); 8 | var functionsConfig = require("../functionsConfig"); 9 | 10 | function _materialize(projectId, path) { 11 | if (_.isUndefined(path)) { 12 | return functionsConfig.materializeAll(projectId); 13 | } 14 | var parts = path.split("."); 15 | var configId = parts[0]; 16 | var configName = _.join(["projects", projectId, "configs", configId], "/"); 17 | return functionsConfig.materializeConfig(configName, {}).then(function(result) { 18 | var query = _.chain(parts) 19 | .join(".") 20 | .value(); 21 | return query ? _.get(result, query) : result; 22 | }); 23 | } 24 | 25 | module.exports = new Command("functions:config:get [path]") 26 | .description("fetch environment config stored at the given path") 27 | .before(requirePermissions, [ 28 | "runtimeconfig.configs.list", 29 | "runtimeconfig.configs.get", 30 | "runtimeconfig.variables.list", 31 | "runtimeconfig.variables.get", 32 | ]) 33 | .before(functionsConfig.ensureApi) 34 | .action(function(path, options) { 35 | return _materialize(getProjectId(options), path).then(function(result) { 36 | logger.info(JSON.stringify(result, null, 2)); 37 | return result; 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/commands/functions-config-legacy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | 5 | var { Command } = require("../command"); 6 | var getProjectId = require("../getProjectId"); 7 | var { requirePermissions } = require("../requirePermissions"); 8 | var runtimeconfig = require("../gcp/runtimeconfig"); 9 | var functionsConfig = require("../functionsConfig"); 10 | var logger = require("../logger"); 11 | 12 | module.exports = new Command("functions:config:legacy") 13 | .description("get legacy functions config variables") 14 | .before(requirePermissions, [ 15 | "runtimeconfig.configs.list", 16 | "runtimeconfig.configs.get", 17 | "runtimeconfig.variables.list", 18 | "runtimeconfig.variables.get", 19 | ]) 20 | .action(function(options) { 21 | var projectId = getProjectId(options); 22 | var metaPath = "projects/" + projectId + "/configs/firebase/variables/meta"; 23 | return runtimeconfig.variables 24 | .get(metaPath) 25 | .then(function(result) { 26 | var metaVal = JSON.parse(result.text); 27 | if (!_.has(metaVal, "version")) { 28 | logger.info("You do not have any legacy config variables."); 29 | return null; 30 | } 31 | var latestVarPath = functionsConfig.idsToVarName(projectId, "firebase", metaVal.version); 32 | return runtimeconfig.variables.get(latestVarPath); 33 | }) 34 | .then(function(latest) { 35 | if (latest !== null) { 36 | var latestVal = JSON.parse(latest.text); 37 | logger.info(JSON.stringify(latestVal, null, 2)); 38 | return latestVal; 39 | } 40 | }) 41 | .catch(function(err) { 42 | if (_.get(err, "context.response.statusCode") === 404) { 43 | logger.info("You do not have any legacy config variables."); 44 | return null; 45 | } 46 | return Promise.reject(err); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/commands/functions-config-set.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var clc = require("cli-color"); 4 | 5 | var { Command } = require("../command"); 6 | var getProjectId = require("../getProjectId"); 7 | var { requirePermissions } = require("../requirePermissions"); 8 | var logger = require("../logger"); 9 | var utils = require("../utils"); 10 | var functionsConfig = require("../functionsConfig"); 11 | 12 | module.exports = new Command("functions:config:set [values...]") 13 | .description("set environment config with key=value syntax") 14 | .before(requirePermissions, [ 15 | "runtimeconfig.configs.list", 16 | "runtimeconfig.configs.create", 17 | "runtimeconfig.configs.get", 18 | "runtimeconfig.configs.update", 19 | "runtimeconfig.configs.delete", 20 | "runtimeconfig.variables.list", 21 | "runtimeconfig.variables.create", 22 | "runtimeconfig.variables.get", 23 | "runtimeconfig.variables.update", 24 | "runtimeconfig.variables.delete", 25 | ]) 26 | .before(functionsConfig.ensureApi) 27 | .action(function(args, options) { 28 | if (!args.length) { 29 | return utils.reject( 30 | "Must supply at least one key/value pair, e.g. " + clc.bold('app.name="My App"') 31 | ); 32 | } 33 | var projectId = getProjectId(options); 34 | var parsed = functionsConfig.parseSetArgs(args); 35 | var promises = []; 36 | 37 | parsed.forEach(function(item) { 38 | promises.push( 39 | functionsConfig.setVariablesRecursive(projectId, item.configId, item.varId, item.val) 40 | ); 41 | }); 42 | 43 | return Promise.all(promises).then(function() { 44 | utils.logSuccess("Functions config updated."); 45 | logger.info( 46 | "\nPlease deploy your functions for the change to take effect by running " + 47 | clc.bold("firebase deploy --only functions") + 48 | "\n" 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/commands/functions-config-unset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | 5 | var clc = require("cli-color"); 6 | var { Command } = require("../command"); 7 | var functionsConfig = require("../functionsConfig"); 8 | var getProjectId = require("../getProjectId"); 9 | var logger = require("../logger"); 10 | var { requirePermissions } = require("../requirePermissions"); 11 | var utils = require("../utils"); 12 | var runtimeconfig = require("../gcp/runtimeconfig"); 13 | 14 | module.exports = new Command("functions:config:unset [keys...]") 15 | .description("unset environment config at the specified path(s)") 16 | .before(requirePermissions, [ 17 | "runtimeconfig.configs.list", 18 | "runtimeconfig.configs.create", 19 | "runtimeconfig.configs.get", 20 | "runtimeconfig.configs.update", 21 | "runtimeconfig.configs.delete", 22 | "runtimeconfig.variables.list", 23 | "runtimeconfig.variables.create", 24 | "runtimeconfig.variables.get", 25 | "runtimeconfig.variables.update", 26 | "runtimeconfig.variables.delete", 27 | ]) 28 | .before(functionsConfig.ensureApi) 29 | .action(function(args, options) { 30 | if (!args.length) { 31 | return utils.reject("Must supply at least one key"); 32 | } 33 | var projectId = getProjectId(options); 34 | var parsed = functionsConfig.parseUnsetArgs(args); 35 | return Promise.all( 36 | _.map(parsed, function(item) { 37 | if (item.varId === "") { 38 | return runtimeconfig.configs.delete(projectId, item.configId); 39 | } 40 | return runtimeconfig.variables.delete(projectId, item.configId, item.varId); 41 | }) 42 | ).then(function() { 43 | utils.logSuccess("Environment updated."); 44 | logger.info( 45 | "\nPlease deploy your functions for the change to take effect by running " + 46 | clc.bold("firebase deploy --only functions") + 47 | "\n" 48 | ); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/commands/functions-shell.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var { Command } = require("../command"); 4 | var { requirePermissions } = require("../requirePermissions"); 5 | var action = require("../functionsShellCommandAction"); 6 | var requireConfig = require("../requireConfig"); 7 | 8 | module.exports = new Command("functions:shell") 9 | .description("launch full Node shell with emulated functions") 10 | .option("-p, --port ", "the port on which to emulate functions (default: 5000)", 5000) 11 | .before(requireConfig) 12 | .before(requirePermissions) 13 | .action(action); 14 | -------------------------------------------------------------------------------- /src/commands/help.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var { Command } = require("../command"); 4 | 5 | var clc = require("cli-color"); 6 | var logger = require("../logger"); 7 | var utils = require("../utils"); 8 | 9 | module.exports = new Command("help [command]") 10 | .description("display help information") 11 | .action(function(commandName) { 12 | var cmd = this.client.getCommand(commandName); 13 | if (cmd) { 14 | cmd.outputHelp(); 15 | } else if (commandName) { 16 | logger.warn(); 17 | utils.logWarning( 18 | clc.bold(commandName) + " is not a valid command. See below for valid commands" 19 | ); 20 | this.client.cli.outputHelp(); 21 | } else { 22 | this.client.cli.outputHelp(); 23 | logger.info(); 24 | logger.info( 25 | " To get help with a specific command, type", 26 | clc.bold("firebase help [command_name]") 27 | ); 28 | logger.info(); 29 | } 30 | 31 | return Promise.resolve(); 32 | }); 33 | -------------------------------------------------------------------------------- /src/commands/hosting-disable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var { Command } = require("../command"); 4 | var requireInstance = require("../requireInstance"); 5 | var { requirePermissions } = require("../requirePermissions"); 6 | var api = require("../api"); 7 | var utils = require("../utils"); 8 | var { prompt } = require("../prompt"); 9 | var clc = require("cli-color"); 10 | 11 | module.exports = new Command("hosting:disable") 12 | .description("stop serving web traffic to your Firebase Hosting site") 13 | .option("-y, --confirm", "skip confirmation") 14 | .option("-s, --site ", "the site to disable") 15 | .before(requirePermissions, ["firebasehosting.sites.update"]) 16 | .before(requireInstance) 17 | .action(function(options) { 18 | return prompt(options, [ 19 | { 20 | type: "confirm", 21 | name: "confirm", 22 | message: 23 | "Are you sure you want to disable Firebase Hosting?\n " + 24 | clc.bold.underline("This will immediately make your site inaccessible!"), 25 | }, 26 | ]) 27 | .then(function() { 28 | if (!options.confirm) { 29 | return Promise.resolve(); 30 | } 31 | 32 | return api.request("POST", `/v1beta1/sites/${options.site || options.instance}/releases`, { 33 | auth: true, 34 | data: { 35 | type: "SITE_DISABLE", 36 | }, 37 | origin: api.hostingApiOrigin, 38 | }); 39 | }) 40 | .then(function() { 41 | if (options.confirm) { 42 | utils.logSuccess( 43 | "Hosting has been disabled for " + 44 | clc.bold(options.project) + 45 | ". Deploy a new version to re-enable." 46 | ); 47 | } 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/commands/login-ci.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var { Command } = require("../command"); 4 | var clc = require("cli-color"); 5 | var utils = require("../utils"); 6 | var logger = require("../logger"); 7 | var auth = require("../auth"); 8 | 9 | module.exports = new Command("login:ci") 10 | .description("generate an access token for use in non-interactive environments") 11 | .option( 12 | "--no-localhost", 13 | "copy and paste a code instead of starting a local server for authentication" 14 | ) 15 | .action(function(options) { 16 | if (options.nonInteractive) { 17 | return utils.reject("Cannot run login:ci in non-interactive mode.", { 18 | exit: 1, 19 | }); 20 | } 21 | 22 | return auth.login(options.localhost).then(function(result) { 23 | logger.info(); 24 | utils.logSuccess( 25 | "Success! Use this token to login on a CI server:\n\n" + 26 | clc.bold(result.tokens.refresh_token) + 27 | '\n\nExample: firebase deploy --token "$FIREBASE_TOKEN"\n' 28 | ); 29 | return result; 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/commands/logout.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var { Command } = require("../command"); 4 | var configstore = require("../configstore"); 5 | var logger = require("../logger"); 6 | var clc = require("cli-color"); 7 | 8 | var utils = require("../utils"); 9 | var api = require("../api"); 10 | var auth = require("../auth"); 11 | var _ = require("lodash"); 12 | 13 | module.exports = new Command("logout") 14 | .description("log the CLI out of Firebase") 15 | .action(function(options) { 16 | var user = configstore.get("user"); 17 | var tokens = configstore.get("tokens"); 18 | var currentToken = _.get(tokens, "refresh_token"); 19 | var token = utils.getInheritedOption(options, "token") || currentToken; 20 | api.setRefreshToken(token); 21 | var next; 22 | if (token) { 23 | next = auth.logout(token); 24 | } else { 25 | next = Promise.resolve(); 26 | } 27 | 28 | var cleanup = function() { 29 | if (token || user || tokens) { 30 | var msg = "Logged out"; 31 | if (token === currentToken) { 32 | if (user) { 33 | msg += " from " + clc.bold(user.email); 34 | } 35 | } else { 36 | msg += ' token "' + clc.bold(token) + '"'; 37 | } 38 | utils.logSuccess(msg); 39 | } else { 40 | logger.info("No need to logout, not logged in"); 41 | } 42 | }; 43 | 44 | return next.then(cleanup, function() { 45 | utils.logWarning("Invalid refresh token, did not need to deauthorize"); 46 | cleanup(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/commands/projects-addfirebase.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | 3 | import { Command } from "../command"; 4 | import { FirebaseError } from "../error"; 5 | import { 6 | addFirebaseToCloudProjectAndLog, 7 | FirebaseProjectMetadata, 8 | promptAvailableProjectId, 9 | } from "../management/projects"; 10 | import * as requireAuth from "../requireAuth"; 11 | 12 | module.exports = new Command("projects:addfirebase [projectId]") 13 | .description("add Firebase resources to a Google Cloud Platform project") 14 | .before(requireAuth) 15 | .action( 16 | async (projectId: string | undefined, options: any): Promise => { 17 | if (!options.nonInteractive && !projectId) { 18 | projectId = await promptAvailableProjectId(); 19 | } 20 | 21 | if (!projectId) { 22 | throw new FirebaseError("Project ID cannot be empty"); 23 | } 24 | 25 | return addFirebaseToCloudProjectAndLog(projectId); 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /src/commands/setup-emulators-database.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { Command } = require("../command"); 4 | const downloadEmulator = require("../emulator/download"); 5 | 6 | const NAME = "database"; 7 | 8 | module.exports = new Command(`setup:emulators:${NAME}`) 9 | .description(`downloads the ${NAME} emulator`) 10 | .action((options) => { 11 | return downloadEmulator(NAME); 12 | }); 13 | -------------------------------------------------------------------------------- /src/commands/setup-emulators-firestore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { Command } = require("../command"); 4 | const downloadEmulator = require("../emulator/download"); 5 | 6 | const NAME = "firestore"; 7 | 8 | module.exports = new Command(`setup:emulators:${NAME}`) 9 | .description(`downloads the ${NAME} emulator`) 10 | .action((options) => { 11 | return downloadEmulator(NAME); 12 | }); 13 | -------------------------------------------------------------------------------- /src/commands/setup-emulators-pubsub.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../command"; 2 | const downloadEmulator = require("../emulator/download"); 3 | 4 | const EMULATOR_NAME = "pubsub"; 5 | 6 | module.exports = new Command(`setup:emulators:${EMULATOR_NAME}`) 7 | .description(`downloads the ${EMULATOR_NAME} emulator`) 8 | .action(() => { 9 | return downloadEmulator(EMULATOR_NAME); 10 | }); 11 | -------------------------------------------------------------------------------- /src/commands/setup-web.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | 5 | var { Command } = require("../command"); 6 | var { fetchWebSetup } = require("../fetchWebSetup"); 7 | var logger = require("../logger"); 8 | var { requirePermissions } = require("../requirePermissions"); 9 | 10 | var JS_TEMPLATE = fs.readFileSync(__dirname + "/../../templates/setup/web.js", "utf8"); 11 | 12 | /** 13 | * This command is deprecated in favor of `apps:sdkconfig web` command 14 | * TODO: Remove this command 15 | */ 16 | module.exports = new Command("setup:web") 17 | .description( 18 | "[DEPRECATED: use `apps:sdkconfig web`] display this project's setup information for the Firebase JS SDK" 19 | ) 20 | .before(requirePermissions, []) 21 | .action(function(options) { 22 | logger.warn( 23 | "This command is deprecated. Instead, use 'firebase apps:sdkconfig web' to get web setup information." 24 | ); 25 | return fetchWebSetup(options).then(function(config) { 26 | logger.info(JS_TEMPLATE.replace("{/*--CONFIG--*/}", JSON.stringify(config, null, 2))); 27 | return Promise.resolve(config); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/commands/target-apply.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var clc = require("cli-color"); 5 | 6 | var { Command } = require("../command"); 7 | var logger = require("../logger"); 8 | var requireConfig = require("../requireConfig"); 9 | var utils = require("../utils"); 10 | 11 | module.exports = new Command("target:apply ") 12 | .description("apply a deploy target to a resource") 13 | .before(requireConfig) 14 | .action(function(type, name, resources, options) { 15 | if (!options.project) { 16 | return utils.reject( 17 | "Must have an active project to set deploy targets. Try " + clc.bold("firebase use --add") 18 | ); 19 | } 20 | 21 | var changes = options.rc.applyTarget(options.project, type, name, resources); 22 | 23 | utils.logSuccess( 24 | "Applied " + type + " target " + clc.bold(name) + " to " + clc.bold(resources.join(", ")) 25 | ); 26 | _.forEach(changes, function(change) { 27 | utils.logWarning( 28 | "Previous target " + clc.bold(change.target) + " removed from " + clc.bold(change.resource) 29 | ); 30 | }); 31 | logger.info(); 32 | logger.info( 33 | "Updated: " + name + " (" + options.rc.target(options.project, type, name).join(",") + ")" 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /src/commands/target-clear.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var clc = require("cli-color"); 4 | 5 | var { Command } = require("../command"); 6 | var requireConfig = require("../requireConfig"); 7 | var utils = require("../utils"); 8 | 9 | module.exports = new Command("target:clear ") 10 | .description("clear all resources from a named resource target") 11 | .before(requireConfig) 12 | .action(function(type, name, options) { 13 | var existed = options.rc.clearTarget(options.project, type, name); 14 | if (existed) { 15 | utils.logSuccess("Cleared " + type + " target " + clc.bold(name)); 16 | } else { 17 | utils.logWarning("No action taken. No " + type + " target found named " + clc.bold(name)); 18 | } 19 | return Promise.resolve(existed); 20 | }); 21 | -------------------------------------------------------------------------------- /src/commands/target-remove.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var clc = require("cli-color"); 4 | 5 | var { Command } = require("../command"); 6 | var requireConfig = require("../requireConfig"); 7 | var utils = require("../utils"); 8 | 9 | module.exports = new Command("target:remove ") 10 | .description("remove a resource target") 11 | .before(requireConfig) 12 | .action(function(type, resource, options) { 13 | var name = options.rc.removeTarget(options.project, type, resource); 14 | if (name) { 15 | utils.logSuccess( 16 | "Removed " + type + " target " + clc.bold(name) + " from " + clc.bold(resource) 17 | ); 18 | } else { 19 | utils.logWarning( 20 | "No action taken. No target found for " + type + " resource " + clc.bold(resource) 21 | ); 22 | } 23 | return Promise.resolve(name); 24 | }); 25 | -------------------------------------------------------------------------------- /src/commands/target.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var clc = require("cli-color"); 5 | 6 | var { Command } = require("../command"); 7 | var logger = require("../logger"); 8 | var requireConfig = require("../requireConfig"); 9 | var utils = require("../utils"); 10 | 11 | function _logTargets(type, targets) { 12 | logger.info(clc.cyan("[ " + type + " ]")); 13 | _.forEach(targets, function(resources, name) { 14 | logger.info(name, "(" + (resources || []).join(",") + ")"); 15 | }); 16 | } 17 | 18 | module.exports = new Command("target [type]") 19 | .description("display configured deploy targets for the current project") 20 | .before(requireConfig) 21 | .action(function(type, options) { 22 | if (!options.project) { 23 | return utils.reject("No active project, cannot list deploy targets."); 24 | } 25 | 26 | logger.info("Resource targets for", clc.bold(options.project) + ":"); 27 | logger.info(); 28 | if (type) { 29 | var targets = options.rc.targets(options.project, type); 30 | _logTargets(type, targets); 31 | return Promise.resolve(targets); 32 | } 33 | 34 | var allTargets = options.rc.get(["targets", options.project], {}); 35 | _.forEach(allTargets, function(ts, tp) { 36 | _logTargets(tp, ts); 37 | }); 38 | return Promise.resolve(allTargets); 39 | }); 40 | -------------------------------------------------------------------------------- /src/configstore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Configstore = require("configstore"); 4 | var pkg = require("../package.json"); 5 | 6 | // Init a Configstore instance with an unique ID eg. package name 7 | // and optionally some default values 8 | module.exports = new Configstore(pkg.name); 9 | -------------------------------------------------------------------------------- /src/deploy/database/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | prepare: require("./prepare"), 5 | release: require("./release"), 6 | }; 7 | -------------------------------------------------------------------------------- /src/deploy/database/release.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var clc = require("cli-color"); 4 | 5 | var rtdb = require("../../rtdb"); 6 | var utils = require("../../utils"); 7 | 8 | module.exports = function(context) { 9 | if (!context.database || !context.database.deploys || !context.database.ruleFiles) { 10 | return Promise.resolve(); 11 | } 12 | 13 | var deploys = context.database.deploys; 14 | var ruleFiles = context.database.ruleFiles; 15 | 16 | utils.logBullet(clc.bold.cyan("database: ") + "releasing rules..."); 17 | return Promise.all( 18 | deploys.map(function(deploy) { 19 | return rtdb 20 | .updateRules(deploy.instance, ruleFiles[deploy.rules], { 21 | dryRun: false, 22 | }) 23 | .then(function() { 24 | utils.logSuccess( 25 | clc.bold.green("database: ") + 26 | "rules for database " + 27 | clc.bold(deploy.instance) + 28 | " released successfully" 29 | ); 30 | }); 31 | }) 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/deploy/firestore/deploy.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as clc from "cli-color"; 3 | 4 | import { FirebaseError } from "../../error"; 5 | import { FirestoreIndexes } from "../../firestore/indexes"; 6 | import logger = require("../../logger"); 7 | import utils = require("../../utils"); 8 | import { RulesDeploy, RulesetServiceType } from "../../rulesDeploy"; 9 | 10 | /** 11 | * Deploys Firestore Rules. 12 | * @param context The deploy context. 13 | */ 14 | async function deployRules(context: any): Promise { 15 | const rulesDeploy: RulesDeploy = _.get(context, "firestore.rulesDeploy"); 16 | if (!context.firestoreRules || !rulesDeploy) { 17 | return; 18 | } 19 | await rulesDeploy.createRulesets(RulesetServiceType.CLOUD_FIRESTORE); 20 | } 21 | 22 | /** 23 | * Deploys Firestore Indexes. 24 | * @param context The deploy context. 25 | * @param options The CLI options object. 26 | */ 27 | async function deployIndexes(context: any, options: any): Promise { 28 | if (!context.firestoreIndexes) { 29 | return; 30 | } 31 | 32 | const indexesFileName = _.get(context, "firestore.indexes.name"); 33 | const indexesSrc = _.get(context, "firestore.indexes.content"); 34 | if (!indexesSrc) { 35 | logger.debug("No Firestore indexes present."); 36 | return; 37 | } 38 | 39 | const indexes = indexesSrc.indexes; 40 | if (!indexes) { 41 | throw new FirebaseError(`Index file must contain an "indexes" property.`); 42 | } 43 | 44 | const fieldOverrides = indexesSrc.fieldOverrides || []; 45 | 46 | await new FirestoreIndexes().deploy(options.project, indexes, fieldOverrides); 47 | utils.logSuccess( 48 | `${clc.bold.green("firestore:")} deployed indexes in ${clc.bold(indexesFileName)} successfully` 49 | ); 50 | } 51 | 52 | /** 53 | * Deploy indexes. 54 | * @param context The deploy context. 55 | * @param options The CLI options object. 56 | */ 57 | export default async function(context: any, options: any): Promise { 58 | await Promise.all([deployRules(context), deployIndexes(context, options)]); 59 | } 60 | -------------------------------------------------------------------------------- /src/deploy/firestore/index.ts: -------------------------------------------------------------------------------- 1 | import prepare from "./prepare"; 2 | import deploy from "./deploy"; 3 | import release from "./release"; 4 | 5 | export { prepare, deploy, release }; 6 | -------------------------------------------------------------------------------- /src/deploy/firestore/release.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | 3 | import { RulesDeploy, RulesetServiceType } from "../../rulesDeploy"; 4 | 5 | /** 6 | * Releases Firestore rules. 7 | * @param context The deploy context. 8 | * @param options The CLI options object. 9 | */ 10 | export default async function(context: any, options: any): Promise { 11 | const rulesDeploy: RulesDeploy = _.get(context, "firestore.rulesDeploy"); 12 | if (!context.firestoreRules || !rulesDeploy) { 13 | return; 14 | } 15 | await rulesDeploy.release( 16 | options.config.get("firestore.rules"), 17 | RulesetServiceType.CLOUD_FIRESTORE 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/deploy/functions/deploy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var clc = require("cli-color"); 5 | 6 | var tmp = require("tmp"); 7 | var utils = require("../../utils"); 8 | var gcp = require("../../gcp"); 9 | var prepareFunctionsUpload = require("../../prepareFunctionsUpload"); 10 | 11 | var GCP_REGION = gcp.cloudfunctions.DEFAULT_REGION; 12 | 13 | tmp.setGracefulCleanup(); 14 | 15 | module.exports = function(context, options, payload) { 16 | var _uploadSource = function(source) { 17 | return gcp.cloudfunctions 18 | .generateUploadUrl(context.projectId, GCP_REGION) 19 | .then(function(uploadUrl) { 20 | _.set(context, "uploadUrl", uploadUrl); 21 | uploadUrl = _.replace(uploadUrl, "https://storage.googleapis.com", ""); 22 | return gcp.storage.upload(source, uploadUrl); 23 | }); 24 | }; 25 | if (options.config.get("functions")) { 26 | utils.logBullet( 27 | clc.cyan.bold("functions:") + 28 | " preparing " + 29 | clc.bold(options.config.get("functions.source")) + 30 | " directory for uploading..." 31 | ); 32 | 33 | return prepareFunctionsUpload(context, options).then(function(result) { 34 | payload.functions = { 35 | triggers: options.config.get("functions.triggers"), 36 | }; 37 | 38 | if (!result) { 39 | return undefined; 40 | } 41 | return _uploadSource(result) 42 | .then(function() { 43 | utils.logSuccess( 44 | clc.green.bold("functions:") + 45 | " " + 46 | clc.bold(options.config.get("functions.source")) + 47 | " folder uploaded successfully" 48 | ); 49 | }) 50 | .catch(function(err) { 51 | utils.logWarning(clc.yellow("functions:") + " Upload Error: " + err.message); 52 | return Promise.reject(err); 53 | }); 54 | }); 55 | } 56 | return Promise.resolve(); 57 | }; 58 | -------------------------------------------------------------------------------- /src/deploy/functions/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | prepare: require("./prepare"), 5 | deploy: require("./deploy"), 6 | release: require("./release"), 7 | }; 8 | -------------------------------------------------------------------------------- /src/deploy/functions/prepare.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | 5 | var ensureApiEnabled = require("../../ensureApiEnabled"); 6 | var functionsConfig = require("../../functionsConfig"); 7 | var getProjectId = require("../../getProjectId"); 8 | var validator = require("./validate"); 9 | 10 | module.exports = function(context, options, payload) { 11 | if (!options.config.has("functions")) { 12 | return Promise.resolve(); 13 | } 14 | 15 | var sourceDirName = options.config.get("functions.source"); 16 | var sourceDir = options.config.path(sourceDirName); 17 | var projectDir = options.config.projectDir; 18 | var functionNames = payload.functions; 19 | var projectId = getProjectId(options); 20 | 21 | try { 22 | validator.functionsDirectoryExists(options.cwd, sourceDirName); 23 | validator.functionNamesAreValid(functionNames); 24 | // it's annoying that we have to pass in both sourceDirName and sourceDir 25 | // but they are two different methods on the config object, so cannot get 26 | // sourceDir from sourceDirName without passing in config 27 | validator.packageJsonIsValid(sourceDirName, sourceDir, projectDir); 28 | } catch (e) { 29 | return Promise.reject(e); 30 | } 31 | 32 | return Promise.all([ 33 | ensureApiEnabled.ensure(options.project, "cloudfunctions.googleapis.com", "functions"), 34 | ensureApiEnabled.check(projectId, "runtimeconfig.googleapis.com", "runtimeconfig", true), 35 | ]) 36 | .then(function(results) { 37 | _.set(context, "runtimeConfigEnabled", results[1]); 38 | return functionsConfig.getFirebaseConfig(options); 39 | }) 40 | .then(function(result) { 41 | _.set(context, "firebaseConfig", result); 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/deploy/hosting/hashcache.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | const path = require("path"); 3 | const logger = require("../../logger"); 4 | 5 | function cachePath(cwd, name) { 6 | return path.resolve(cwd, ".firebase/hosting." + name + ".cache"); 7 | } 8 | 9 | exports.load = function(cwd, name) { 10 | try { 11 | const out = {}; 12 | const lines = fs.readFileSync(cachePath(cwd, name), { 13 | encoding: "utf8", 14 | }); 15 | lines.split("\n").forEach(function(line) { 16 | const d = line.split(","); 17 | if (d.length === 3) { 18 | out[d[0]] = { mtime: parseInt(d[1]), hash: d[2] }; 19 | } 20 | }); 21 | return out; 22 | } catch (e) { 23 | if (e.code === "ENOENT") { 24 | logger.debug("[hosting] hash cache [" + name + "] not populated"); 25 | } else { 26 | logger.debug("[hosting] hash cache [" + name + "] load error:", e.message); 27 | } 28 | return {}; 29 | } 30 | }; 31 | 32 | exports.dump = function(cwd, name, data) { 33 | let st = ""; 34 | let count = 0; 35 | for (const [path, d] of data) { 36 | count++; 37 | st += path + "," + d.mtime + "," + d.hash + "\n"; 38 | } 39 | try { 40 | fs.outputFileSync(cachePath(cwd, name), st, { encoding: "utf8" }); 41 | logger.debug("[hosting] hash cache [" + name + "] stored for", count, "files"); 42 | } catch (e) { 43 | logger.debug("[hosting] unable to store hash cache [" + name + "]", e.stack); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/deploy/hosting/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | prepare: require("./prepare"), 5 | deploy: require("./deploy"), 6 | release: require("./release"), 7 | }; 8 | -------------------------------------------------------------------------------- /src/deploy/hosting/release.js: -------------------------------------------------------------------------------- 1 | const api = require("../../api"); 2 | const utils = require("../../utils"); 3 | const logger = require("../../logger"); 4 | 5 | module.exports = function(context, options) { 6 | if (!context.hosting || !context.hosting.deploys) { 7 | return Promise.resolve(); 8 | } 9 | 10 | logger.debug(JSON.stringify(context.hosting.deploys, null, 2)); 11 | return Promise.all( 12 | context.hosting.deploys.map(function(deploy) { 13 | utils.logLabeledBullet("hosting[" + deploy.site + "]", "finalizing version..."); 14 | return api 15 | .request("PATCH", "/v1beta1/" + deploy.version + "?updateMask=status", { 16 | origin: api.hostingApiOrigin, 17 | auth: true, 18 | data: { status: "FINALIZED" }, 19 | }) 20 | .then(function(result) { 21 | logger.debug("[hosting] finalized version for " + deploy.site + ":", result.body); 22 | utils.logLabeledSuccess("hosting[" + deploy.site + "]", "version finalized"); 23 | utils.logLabeledBullet("hosting[" + deploy.site + "]", "releasing new version..."); 24 | return api.request( 25 | "POST", 26 | "/v1beta1/sites/" + deploy.site + "/releases?version_name=" + deploy.version, 27 | { 28 | auth: true, 29 | origin: api.hostingApiOrigin, 30 | data: { message: options.message || null }, 31 | } 32 | ); 33 | }) 34 | .then(function(result) { 35 | logger.debug("[hosting] release:", result.body); 36 | utils.logLabeledSuccess("hosting[" + deploy.site + "]", "release complete"); 37 | }); 38 | }) 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/deploy/storage/deploy.ts: -------------------------------------------------------------------------------- 1 | import { get } from "lodash"; 2 | 3 | import { RulesDeploy, RulesetServiceType } from "../../rulesDeploy"; 4 | 5 | /** 6 | * Deploys Firebase Storage rulesets. 7 | * @param context The deploy context. 8 | */ 9 | export default async function(context: any): Promise { 10 | const rulesDeploy: RulesDeploy = get(context, "storage.rulesDeploy"); 11 | if (!rulesDeploy) { 12 | return; 13 | } 14 | await rulesDeploy.createRulesets(RulesetServiceType.FIREBASE_STORAGE); 15 | } 16 | -------------------------------------------------------------------------------- /src/deploy/storage/index.ts: -------------------------------------------------------------------------------- 1 | import prepare from "./prepare"; 2 | import deploy from "./deploy"; 3 | import release from "./release"; 4 | 5 | export { prepare, deploy, release }; 6 | -------------------------------------------------------------------------------- /src/deploy/storage/prepare.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | 3 | import gcp = require("../../gcp"); 4 | import { RulesDeploy, RulesetServiceType } from "../../rulesDeploy"; 5 | 6 | /** 7 | * Prepares for a Firebase Storage deployment. 8 | * @param context The deploy context. 9 | * @param options The CLI options object. 10 | */ 11 | export default async function(context: any, options: any): Promise { 12 | let rulesConfig = options.config.get("storage"); 13 | if (!rulesConfig) { 14 | return; 15 | } 16 | 17 | _.set(context, "storage.rules", rulesConfig); 18 | 19 | const rulesDeploy = new RulesDeploy(options, RulesetServiceType.FIREBASE_STORAGE); 20 | _.set(context, "storage.rulesDeploy", rulesDeploy); 21 | 22 | if (_.isPlainObject(rulesConfig)) { 23 | const defaultBucket = await gcp.storage.getDefaultBucket(options.project); 24 | rulesConfig = [_.assign(rulesConfig, { bucket: defaultBucket })]; 25 | _.set(context, "storage.rules", rulesConfig); 26 | } 27 | 28 | rulesConfig.forEach((ruleConfig: any) => { 29 | if (ruleConfig.target) { 30 | options.rc.requireTarget(context.projectId, "storage", ruleConfig.target); 31 | } 32 | rulesDeploy.addFile(ruleConfig.rules); 33 | }); 34 | 35 | await rulesDeploy.compile(); 36 | } 37 | -------------------------------------------------------------------------------- /src/deploy/storage/release.ts: -------------------------------------------------------------------------------- 1 | import { get } from "lodash"; 2 | 3 | import { RulesDeploy, RulesetServiceType } from "../../rulesDeploy"; 4 | 5 | /** 6 | * Releases Firebase Storage rules. 7 | * @param context The deploy context. 8 | * @param options The CLI options object. 9 | */ 10 | export default async function(context: any, options: any): Promise { 11 | const rules = get(context, "storage.rules", []); 12 | const rulesDeploy: RulesDeploy = get(context, "storage.rulesDeploy"); 13 | if (!rules.length || !rulesDeploy) { 14 | return; 15 | } 16 | 17 | const toRelease: Array<{ bucket: string; rules: any }> = []; 18 | for (const ruleConfig of rules) { 19 | if (ruleConfig.target) { 20 | options.rc 21 | .target(options.project, "storage", ruleConfig.target) 22 | .forEach(function(bucket: string) { 23 | toRelease.push({ bucket: bucket, rules: ruleConfig.rules }); 24 | }); 25 | } else { 26 | toRelease.push({ bucket: ruleConfig.bucket, rules: ruleConfig.rules }); 27 | } 28 | } 29 | 30 | await Promise.all( 31 | toRelease.map((release) => { 32 | return rulesDeploy.release( 33 | release.rules, 34 | RulesetServiceType.FIREBASE_STORAGE, 35 | release.bucket 36 | ); 37 | }) 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/deploymentTool.js: -------------------------------------------------------------------------------- 1 | const BASE = "cli-firebase"; 2 | 3 | function _value() { 4 | if (!process.env.FIREBASE_DEPLOY_AGENT) { 5 | return BASE; 6 | } 7 | 8 | return [BASE, process.env.FIREBASE_DEPLOY_AGENT].join("--"); 9 | } 10 | 11 | module.exports = { 12 | base: BASE, 13 | get value() { 14 | return _value(); 15 | }, 16 | get labels() { 17 | return { 18 | "deployment-tool": _value(), 19 | }; 20 | }, 21 | check: function(labels) { 22 | return labels && labels["deployment-tool"] && labels["deployment-tool"].indexOf(BASE) === 0; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/detectProjectRoot.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fsutils = require("./fsutils"); 4 | var path = require("path"); 5 | 6 | module.exports = function(cwd) { 7 | var projectRootDir = cwd || process.cwd(); 8 | while (!fsutils.fileExistsSync(path.resolve(projectRootDir, "./firebase.json"))) { 9 | var parentDir = path.dirname(projectRootDir); 10 | if (parentDir === projectRootDir) { 11 | return null; 12 | } 13 | projectRootDir = parentDir; 14 | } 15 | return projectRootDir; 16 | }; 17 | -------------------------------------------------------------------------------- /src/emulator/emulatorServer.ts: -------------------------------------------------------------------------------- 1 | import { EmulatorInstance } from "./types"; 2 | import { EmulatorRegistry } from "./registry"; 3 | import * as controller from "./controller"; 4 | import { FirebaseError } from "../error"; 5 | 6 | /** 7 | * Wrapper object to expose an EmulatorInstance for "firebase serve" that 8 | * also registers the emulator with the registry. 9 | */ 10 | export class EmulatorServer { 11 | constructor(public instance: EmulatorInstance) {} 12 | 13 | async start(): Promise { 14 | const { port, host } = this.instance.getInfo(); 15 | const portOpen = await controller.checkPortOpen(port, host); 16 | 17 | if (!portOpen) { 18 | throw new FirebaseError( 19 | `Port ${port} is not open on ${host}, could not start ${this.instance.getName()} emulator.` 20 | ); 21 | } 22 | 23 | await EmulatorRegistry.start(this.instance); 24 | } 25 | 26 | async connect(): Promise { 27 | await this.instance.connect(); 28 | } 29 | 30 | async stop(): Promise { 31 | await EmulatorRegistry.stop(this.instance.getName()); 32 | } 33 | 34 | get(): EmulatorInstance { 35 | return this.instance; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/emulator/events/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The types in this file are stolen from the firebase-functions SDK. They are not subject 3 | * to change because they are TS interpretations of protobuf wire formats already used in 4 | * productiton services. 5 | * 6 | * We can't import some of them because they are marked "internal". 7 | */ 8 | 9 | import * as _ from "lodash"; 10 | 11 | import { Resource } from "firebase-functions"; 12 | 13 | /** 14 | * Wire formal for v1beta1 EventFlow. 15 | * Used by Firestore and some other services. 16 | */ 17 | export interface LegacyEvent { 18 | data: any; 19 | eventType?: string; 20 | resource?: string; 21 | eventId?: string; 22 | timestamp?: string; 23 | params?: { [option: string]: any }; 24 | auth?: AuthMode; 25 | } 26 | 27 | /** 28 | * Wire format for v1beta2 Eventflow (and likely v1). 29 | * Used by PubSub, RTDB, and some other services. 30 | */ 31 | export interface Event { 32 | context: { 33 | eventId: string; 34 | timestamp: string; 35 | eventType: string; 36 | resource: Resource; 37 | }; 38 | data: any; 39 | } 40 | 41 | /** 42 | * Legacy AuthMode format. 43 | */ 44 | export interface AuthMode { 45 | admin: boolean; 46 | variable?: any; 47 | } 48 | 49 | /** 50 | * Utilities for determining event types. 51 | */ 52 | export class EventUtils { 53 | static isEvent(proto: any): proto is Event { 54 | return _.has(proto, "context") && _.has(proto, "data"); 55 | } 56 | 57 | static isLegacyEvent(proto: any): proto is LegacyEvent { 58 | return _.has(proto, "data") && _.has(proto, "resource"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/emulator/hostingEmulator.ts: -------------------------------------------------------------------------------- 1 | import serveHosting = require("../serve/hosting"); 2 | import { EmulatorInfo, EmulatorInstance, Emulators } from "../emulator/types"; 3 | import { Constants } from "./constants"; 4 | 5 | interface HostingEmulatorArgs { 6 | options: any; 7 | port?: number; 8 | host?: string; 9 | } 10 | 11 | export class HostingEmulator implements EmulatorInstance { 12 | constructor(private args: HostingEmulatorArgs) {} 13 | 14 | async start(): Promise { 15 | this.args.options.host = this.args.host; 16 | this.args.options.port = this.args.port; 17 | 18 | return serveHosting.start(this.args.options); 19 | } 20 | 21 | async connect(): Promise { 22 | return; 23 | } 24 | 25 | async stop(): Promise { 26 | return serveHosting.stop(); 27 | } 28 | 29 | getInfo(): EmulatorInfo { 30 | const host = this.args.host || Constants.getDefaultHost(Emulators.HOSTING); 31 | const port = this.args.port || Constants.getDefaultPort(Emulators.HOSTING); 32 | 33 | return { 34 | host, 35 | port, 36 | }; 37 | } 38 | 39 | getName(): Emulators { 40 | return Emulators.HOSTING; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ensureCloudResourceLocation.ts: -------------------------------------------------------------------------------- 1 | import { FirebaseError } from "./error"; 2 | 3 | /** 4 | * Simple helper function that returns an error with a helpful 5 | * message on event of cloud resource location that is not set. 6 | * This was made into its own file because this error gets thrown 7 | * in several places: in init for Firestore, Storage, and for scheduled 8 | * function deployments. 9 | * @param location cloud resource location, like "us-central1" 10 | * @throws { FirebaseError } if location is not set 11 | */ 12 | export function ensureLocationSet(location: string, feature: string): void { 13 | if (!location) { 14 | throw new FirebaseError( 15 | `Cloud resource location is not set for this project but the operation ` + 16 | `you are attempting to perform in ${feature} requires it. ` + 17 | `Please see this documentation for more details: https://firebase.google.com/docs/projects/locations` 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ensureDefaultCredentials.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs-extra"); 4 | var path = require("path"); 5 | 6 | var api = require("./api"); 7 | var configstore = require("./configstore"); 8 | var logger = require("./logger"); 9 | 10 | var configDir = function() { 11 | // Windows has a dedicated low-rights location for apps at ~/Application Data 12 | if (process.platform === "win32") { 13 | return process.env.APPDATA; 14 | } 15 | return process.env.HOME && path.resolve(process.env.HOME, ".config"); 16 | }; 17 | 18 | /* 19 | Ensures that default credentials are available on the local machine, as specified by: 20 | https://developers.google.com/identity/protocols/application-default-credentials 21 | */ 22 | module.exports = function() { 23 | if (!configDir()) { 24 | logger.debug("Cannot ensure default credentials, no home directory found."); 25 | return; 26 | } 27 | 28 | var GCLOUD_CREDENTIAL_DIR = path.resolve(configDir(), "gcloud"); 29 | var GCLOUD_CREDENTIAL_PATH = path.join( 30 | GCLOUD_CREDENTIAL_DIR, 31 | "application_default_credentials.json" 32 | ); 33 | 34 | var tokens = configstore.get("tokens") || {}; 35 | 36 | var credentials = { 37 | client_id: api.clientId, 38 | client_secret: api.clientSecret, 39 | type: "authorized_user", 40 | refresh_token: tokens.refresh_token || process.env.FIREBASE_TOKEN, 41 | }; 42 | // Mimic the effects of running "gcloud auth application-default login" 43 | fs.ensureDirSync(GCLOUD_CREDENTIAL_DIR); 44 | fs.writeFileSync(GCLOUD_CREDENTIAL_PATH, JSON.stringify(credentials, null, 2)); 45 | return; 46 | }; 47 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | import { defaultTo } from "lodash"; 2 | 3 | interface FirebaseErrorOptions { 4 | children?: unknown[]; 5 | context?: unknown; 6 | exit?: number; 7 | original?: Error; 8 | status?: number; 9 | } 10 | 11 | const DEFAULT_CHILDREN: NonNullable = []; 12 | const DEFAULT_EXIT: NonNullable = 1; 13 | const DEFAULT_STATUS: NonNullable = 500; 14 | 15 | export class FirebaseError extends Error { 16 | readonly children: unknown[]; 17 | readonly context: unknown | undefined; 18 | readonly exit: number; 19 | readonly message: string; 20 | readonly name = "FirebaseError"; 21 | readonly original: Error | undefined; 22 | readonly status: number; 23 | 24 | constructor(message: string, options: FirebaseErrorOptions = {}) { 25 | super(); 26 | 27 | this.children = defaultTo(options.children, DEFAULT_CHILDREN); 28 | this.context = options.context; 29 | this.exit = defaultTo(options.exit, DEFAULT_EXIT); 30 | this.message = message; 31 | this.original = options.original; 32 | this.status = defaultTo(options.status, DEFAULT_STATUS); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/errorOut.ts: -------------------------------------------------------------------------------- 1 | import logError = require("./logError"); 2 | import { FirebaseError } from "./error"; 3 | 4 | /** 5 | * Errors out by calling `process.exit` with an exit code of 2. 6 | * @param error an Error to be logged. 7 | */ 8 | export function errorOut(error: Error): void { 9 | let fbError: FirebaseError; 10 | if (error instanceof FirebaseError) { 11 | fbError = error; 12 | } else { 13 | fbError = new FirebaseError("An unexpected error has occurred.", { 14 | original: error, 15 | exit: 2, 16 | }); 17 | } 18 | 19 | logError(fbError); 20 | process.exitCode = fbError.exit || 2; 21 | setTimeout(function() { 22 | process.exit(); 23 | }, 250); 24 | } 25 | -------------------------------------------------------------------------------- /src/extensions/generateInstanceId.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as extensionsApi from "./extensionsApi"; 3 | import { FirebaseError } from "../error"; 4 | 5 | const SUFFIX_CHAR_SET = "abcdefghijklmnopqrstuvwxyz0123456789"; 6 | 7 | export async function generateInstanceId( 8 | projectId: string, 9 | extensionName: string 10 | ): Promise { 11 | const instanceRes = await extensionsApi.getInstance(projectId, extensionName, { 12 | resolveOnHTTPError: true, 13 | }); 14 | if (instanceRes.error) { 15 | if (_.get(instanceRes, "error.code") === 404) { 16 | return extensionName; 17 | } 18 | const msg = 19 | "Unexpected error when generating instance ID: " + _.get(instanceRes, "error.message"); 20 | throw new FirebaseError(msg, { 21 | original: instanceRes.error, 22 | }); 23 | } 24 | // If there is already an instance named extensionName 25 | return `${extensionName}-${getRandomString(4)}`; 26 | } 27 | 28 | export function getRandomString(length: number): string { 29 | let result = ""; 30 | for (let i = 0; i < length; i++) { 31 | result += SUFFIX_CHAR_SET.charAt(Math.floor(Math.random() * SUFFIX_CHAR_SET.length)); 32 | } 33 | return result; 34 | } 35 | -------------------------------------------------------------------------------- /src/extensions/listExtensions.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as clc from "cli-color"; 3 | import Table = require("cli-table"); 4 | 5 | import { ExtensionInstance, listInstances } from "./extensionsApi"; 6 | import { logPrefix } from "./extensionsHelper"; 7 | import * as utils from "../utils"; 8 | import * as logger from "../logger"; 9 | 10 | export async function listExtensions( 11 | projectId: string 12 | ): Promise<{ instances: ExtensionInstance[] }> { 13 | const instances = await listInstances(projectId); 14 | if (instances.length < 1) { 15 | utils.logLabeledBullet( 16 | logPrefix, 17 | `there are no extensions installed on project ${clc.bold(projectId)}.` 18 | ); 19 | return { instances: [] }; 20 | } 21 | 22 | const table = new Table({ 23 | head: ["Extension Instance ID", "State", "Extension Version", "Create Time", "Update Time"], 24 | style: { head: ["yellow"] }, 25 | }); 26 | 27 | // Order instances newest to oldest. 28 | const sorted = _.sortBy(instances, "createTime", "asc").reverse(); 29 | sorted.forEach((instance) => { 30 | table.push([ 31 | _.last(instance.name.split("/")), 32 | instance.state, 33 | _.get(instance, "config.source.spec.version", ""), 34 | instance.createTime, 35 | instance.updateTime, 36 | ]); 37 | }); 38 | 39 | utils.logLabeledBullet(logPrefix, `list of extensions installed in ${clc.bold(projectId)}:`); 40 | logger.info(table.toString()); 41 | return { instances: sorted }; 42 | } 43 | -------------------------------------------------------------------------------- /src/extensions/populatePostinstall.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | 3 | /** 4 | * Substitutes environment variables into usage instructions, 5 | * and returns the substituted instructions. 6 | * @param params The params to substitute into instructions 7 | * @param instructions The pre- or post- install instructions from a extension 8 | * @returns Message to print out the the user 9 | */ 10 | export function populatePostinstall( 11 | instructions: string, 12 | params: { [key: string]: string } 13 | ): string { 14 | return _.reduce( 15 | params, 16 | (content, value, key) => { 17 | const regex = new RegExp("\\$\\{param:" + key + "\\}", "g"); 18 | return _.replace(content, regex, value); 19 | }, 20 | instructions 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/extensions/utils.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { promptOnce } from "../prompt"; 3 | import { ParamOption } from "./extensionsApi"; 4 | import { RegistryEntry } from "./resolveSource"; 5 | 6 | // Modified version of the once function from prompt, to return as a joined string. 7 | export async function onceWithJoin(question: any): Promise { 8 | const response = await promptOnce(question); 9 | if (Array.isArray(response)) { 10 | return response.join(","); 11 | } 12 | return response; 13 | } 14 | 15 | interface ListItem { 16 | name?: string; // User friendly display name for the option 17 | value: string; // Value of the option 18 | checked: boolean; // Whether the option should be checked by default 19 | } 20 | 21 | // Convert extension option to Inquirer-friendly list for the prompt, with all items unchecked. 22 | export function convertExtensionOptionToLabeledList(options: ParamOption[]): ListItem[] { 23 | return options.map( 24 | (option: ParamOption): ListItem => { 25 | return { 26 | checked: false, 27 | name: option.label, 28 | value: option.value, 29 | }; 30 | } 31 | ); 32 | } 33 | 34 | // Convert map of RegistryEntry into Inquirer-friendly list for prompt, with all items unchecked. 35 | export function convertOfficialExtensionsToList(officialExts: { 36 | [key: string]: RegistryEntry; 37 | }): ListItem[] { 38 | return _.map(officialExts, (entry: RegistryEntry, key: string) => { 39 | return { 40 | checked: false, 41 | value: key, 42 | }; 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/extractTriggers.js: -------------------------------------------------------------------------------- 1 | // NOTE: DO NOT ADD NPM DEPENDENCIES TO THIS FILE. It is executed 2 | // as part of the trigger parser which should be a vanilla Node.js 3 | // script. 4 | "use strict"; 5 | 6 | var extractTriggers = function(mod, triggers, prefix) { 7 | prefix = prefix || ""; 8 | for (var funcName of Object.keys(mod)) { 9 | var child = mod[funcName]; 10 | if (typeof child === "function" && child.__trigger && typeof child.__trigger === "object") { 11 | if (funcName.indexOf("-") >= 0) { 12 | throw new Error( 13 | 'Function name "' + funcName + '" is invalid. Function names cannot contain dashes.' 14 | ); 15 | } 16 | 17 | var trigger = {}; 18 | for (var key of Object.keys(child.__trigger)) { 19 | trigger[key] = child.__trigger[key]; 20 | } 21 | trigger.name = prefix + funcName; 22 | trigger.entryPoint = trigger.name.replace(/-/g, "."); 23 | triggers.push(trigger); 24 | } else if (typeof child === "object") { 25 | extractTriggers(child, triggers, prefix + funcName + "-"); 26 | } 27 | } 28 | }; 29 | 30 | module.exports = extractTriggers; 31 | -------------------------------------------------------------------------------- /src/fetchMOTD.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var logger = require("./logger"); 3 | var request = require("request"); 4 | var configstore = require("./configstore"); 5 | var _ = require("lodash"); 6 | var pkg = require("../package.json"); 7 | var semver = require("semver"); 8 | var clc = require("cli-color"); 9 | var utils = require("./utils"); 10 | var api = require("./api"); 11 | 12 | var ONE_DAY_MS = 1000 * 60 * 60 * 24; 13 | 14 | module.exports = function() { 15 | var motd = configstore.get("motd"); 16 | var motdFetched = configstore.get("motd.fetched") || 0; 17 | 18 | if (motd && motdFetched > Date.now() - ONE_DAY_MS) { 19 | if (motd.minVersion && semver.gt(motd.minVersion, pkg.version)) { 20 | logger.error( 21 | clc.red("Error:"), 22 | "CLI is out of date (on", 23 | clc.bold(pkg.version), 24 | ", need at least", 25 | clc.bold(motd.minVersion) + ")\n\nRun", 26 | clc.bold("npm install -g firebase-tools"), 27 | "to upgrade." 28 | ); 29 | process.exit(1); 30 | } 31 | 32 | if (motd.message && process.stdout.isTTY) { 33 | var lastMessage = configstore.get("motd.lastMessage"); 34 | if (lastMessage !== motd.message) { 35 | logger.info(); 36 | logger.info(motd.message); 37 | logger.info(); 38 | configstore.set("motd.lastMessage", motd.message); 39 | } 40 | } 41 | } else { 42 | request( 43 | { 44 | url: utils.addSubdomain(api.realtimeOrigin, "firebase-public") + "/cli.json", 45 | json: true, 46 | }, 47 | function(err, res, body) { 48 | if (err) { 49 | return; 50 | } 51 | motd = _.assign({}, body); 52 | configstore.set("motd", motd); 53 | configstore.set("motd.fetched", Date.now()); 54 | } 55 | ); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/fetchWebSetup.ts: -------------------------------------------------------------------------------- 1 | import * as api from "./api"; 2 | import * as getProjectId from "./getProjectId"; 3 | import * as configstore from "./configstore"; 4 | 5 | export interface WebConfig { 6 | projectId: string; 7 | appId?: string; 8 | databaseURL?: string; 9 | storageBucket?: string; 10 | locationId?: string; 11 | apiKey?: string; 12 | authDomain?: string; 13 | messagingSenderId?: string; 14 | } 15 | 16 | const CONFIGSTORE_KEY = "webconfig"; 17 | 18 | function setCachedWebSetup(projectId: string, config: WebConfig) { 19 | const allConfigs = configstore.get(CONFIGSTORE_KEY) || {}; 20 | allConfigs[projectId] = config; 21 | configstore.set(CONFIGSTORE_KEY, allConfigs); 22 | } 23 | 24 | /** 25 | * Get the last known WebConfig from the cache. 26 | */ 27 | export function getCachedWebSetup(options: any): WebConfig | undefined { 28 | const projectId = getProjectId(options, false); 29 | const allConfigs = configstore.get(CONFIGSTORE_KEY) || {}; 30 | return allConfigs[projectId]; 31 | } 32 | 33 | /** 34 | * TODO: deprecate this function in favor of `getAppConfig()` in `/src/management/apps.ts` 35 | */ 36 | export async function fetchWebSetup(options: any): Promise { 37 | const projectId = getProjectId(options, false); 38 | const response = await api.request("GET", `/v1beta1/projects/${projectId}/webApps/-/config`, { 39 | auth: true, 40 | origin: api.firebaseApiOrigin, 41 | }); 42 | const config = response.body; 43 | setCachedWebSetup(config.projectId, config); 44 | return config; 45 | } 46 | -------------------------------------------------------------------------------- /src/filterTargets.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var { FirebaseError } = require("./error"); 5 | 6 | module.exports = function(options, validTargets) { 7 | var targets = validTargets.filter(function(t) { 8 | return options.config.has(t); 9 | }); 10 | if (options.only) { 11 | targets = _.intersection( 12 | targets, 13 | options.only.split(",").map(function(opt) { 14 | return opt.split(":")[0]; 15 | }) 16 | ); 17 | } else if (options.except) { 18 | targets = _.difference(targets, options.except.split(",")); 19 | } 20 | 21 | if (targets.length === 0) { 22 | throw new FirebaseError( 23 | "Cannot understand what targets to deploy. Check that you specified valid targets" + 24 | " if you used the --only or --except flag. Otherwise, check your firebase.json to" + 25 | " ensure that your project is initialized for the desired features.", 26 | { exit: 1 } 27 | ); 28 | } 29 | return targets; 30 | }; 31 | -------------------------------------------------------------------------------- /src/firestore/encodeFirestoreValue.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | 3 | import { FirebaseError } from "../error"; 4 | 5 | function isPlainObject(input: any): boolean { 6 | return ( 7 | typeof input === "object" && 8 | input !== null && 9 | _.isEqual(Object.getPrototypeOf(input), Object.prototype) 10 | ); 11 | } 12 | 13 | function encodeHelper(val: any): any { 14 | if (_.isString(val)) { 15 | return { stringValue: val }; 16 | } 17 | if (_.isBoolean(val)) { 18 | return { booleanValue: val }; 19 | } 20 | if (_.isInteger(val)) { 21 | return { integerValue: val }; 22 | } 23 | // Integers are handled above, the remaining numbers are treated as doubles 24 | if (_.isNumber(val)) { 25 | return { doubleValue: val }; 26 | } 27 | if (_.isDate(val)) { 28 | return { timestampValue: val.toISOString() }; 29 | } 30 | if (_.isArray(val)) { 31 | const encodedElements = []; 32 | for (const v of val) { 33 | const enc = encodeHelper(v); 34 | if (enc) { 35 | encodedElements.push(enc); 36 | } 37 | } 38 | return { 39 | arrayValue: { values: encodedElements }, 40 | }; 41 | } 42 | if (_.isNull(val)) { 43 | return { nullValue: "NULL_VALUE" }; 44 | } 45 | if (val instanceof Buffer || val instanceof Uint8Array) { 46 | return { bytesValue: val }; 47 | } 48 | if (isPlainObject(val)) { 49 | return { 50 | mapValue: { fields: encodeFirestoreValue(val) }, 51 | }; 52 | } 53 | throw new FirebaseError( 54 | `Cannot encode ${val} to a Firestore Value. ` + 55 | "The emulator does not yet support Firestore document reference values or geo points." 56 | ); 57 | } 58 | 59 | export function encodeFirestoreValue(data: any): { [key: string]: any } { 60 | return _.mapValues(data, encodeHelper); 61 | } 62 | -------------------------------------------------------------------------------- /src/firestore/indexes-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The v1beta1 indexes API used a 'mode' field to represent the indexing mode. 3 | * This information has now been split into the fields 'arrayConfig' and 'order'. 4 | * We allow use of 'mode' (for now) so that the move to v1beta2/v1 is not 5 | * breaking when we can understand the developer's intent. 6 | */ 7 | export enum Mode { 8 | ASCENDING = "ASCENDING", 9 | DESCENDING = "DESCENDING", 10 | ARRAY_CONTAINS = "ARRAY_CONTAINS", 11 | } 12 | 13 | export enum QueryScope { 14 | COLLECTION = "COLLECTION", 15 | COLLECTION_GROUP = "COLLECTION_GROUP", 16 | } 17 | 18 | export enum Order { 19 | ASCENDING = "ASCENDING", 20 | DESCENDING = "DESCENDING", 21 | } 22 | 23 | export enum ArrayConfig { 24 | CONTAINS = "CONTAINS", 25 | } 26 | 27 | export enum State { 28 | CREATING = "CREATING", 29 | READY = "READY", 30 | NEEDS_REPAIR = "NEEDS_REPAIR", 31 | } 32 | 33 | /** 34 | * An Index as it is represented in the Firestore v1beta2 indexes API. 35 | */ 36 | export interface Index { 37 | name?: string; 38 | queryScope: QueryScope; 39 | fields: IndexField[]; 40 | state?: State; 41 | } 42 | 43 | /** 44 | * A field in an index. 45 | */ 46 | export interface IndexField { 47 | fieldPath: string; 48 | order?: Order; 49 | arrayConfig?: ArrayConfig; 50 | } 51 | 52 | /** 53 | * Represents a single field in the database. 54 | * 55 | * If a field has an empty indexConfig, that means all 56 | * default indexes are exempted. 57 | */ 58 | export interface Field { 59 | name: string; 60 | indexConfig: IndexConfig; 61 | } 62 | 63 | /** 64 | * Index configuration overrides for a field. 65 | */ 66 | export interface IndexConfig { 67 | ancestorField?: string; 68 | indexes?: Index[]; 69 | } 70 | -------------------------------------------------------------------------------- /src/firestore/indexes-spec.ts: -------------------------------------------------------------------------------- 1 | import * as API from "./indexes-api"; 2 | 3 | /** 4 | * An entry specifying a compound or other non-default index. 5 | */ 6 | export interface Index { 7 | collectionGroup: string; 8 | queryScope: API.QueryScope; 9 | fields: API.IndexField[]; 10 | } 11 | 12 | /** 13 | * An entry specifying field index configuration override. 14 | */ 15 | export interface FieldOverride { 16 | collectionGroup: string; 17 | fieldPath: string; 18 | indexes: FieldIndex[]; 19 | } 20 | 21 | /** 22 | * Entry specifying a single-field index. 23 | */ 24 | export interface FieldIndex { 25 | queryScope: API.QueryScope; 26 | order?: API.Order; 27 | arrayConfig?: API.ArrayConfig; 28 | } 29 | 30 | /** 31 | * Specification for the JSON file that is used for index deployment, 32 | */ 33 | export interface IndexFile { 34 | indexes: Index[]; 35 | fieldOverrides?: FieldOverride[]; 36 | } 37 | -------------------------------------------------------------------------------- /src/firestore/util.ts: -------------------------------------------------------------------------------- 1 | import { FirebaseError } from "../error"; 2 | 3 | interface IndexName { 4 | projectId: string; 5 | collectionGroupId: string; 6 | indexId: string; 7 | } 8 | 9 | interface FieldName { 10 | projectId: string; 11 | collectionGroupId: string; 12 | fieldPath: string; 13 | } 14 | 15 | // projects/$PROJECT_ID/databases/(default)/collectionGroups/$COLLECTION_GROUP_ID/indexes/$INDEX_ID 16 | const INDEX_NAME_REGEX = /projects\/([^\/]+?)\/databases\/\(default\)\/collectionGroups\/([^\/]+?)\/indexes\/([^\/]*)/; 17 | 18 | // projects/$PROJECT_ID/databases/(default)/collectionGroups/$COLLECTION_GROUP_ID/fields/$FIELD_ID 19 | const FIELD_NAME_REGEX = /projects\/([^\/]+?)\/databases\/\(default\)\/collectionGroups\/([^\/]+?)\/fields\/([^\/]*)/; 20 | 21 | /** 22 | * Parse an Index name into useful pieces. 23 | */ 24 | export function parseIndexName(name?: string): IndexName { 25 | if (!name) { 26 | throw new FirebaseError(`Cannot parse undefined index name.`); 27 | } 28 | 29 | const m = name.match(INDEX_NAME_REGEX); 30 | if (!m || m.length < 4) { 31 | throw new FirebaseError(`Error parsing index name: ${name}`); 32 | } 33 | 34 | return { 35 | projectId: m[1], 36 | collectionGroupId: m[2], 37 | indexId: m[3], 38 | }; 39 | } 40 | 41 | /** 42 | * Parse an Field name into useful pieces. 43 | */ 44 | export function parseFieldName(name: string): FieldName { 45 | const m = name.match(FIELD_NAME_REGEX); 46 | if (!m || m.length < 4) { 47 | throw new FirebaseError(`Error parsing field name: ${name}`); 48 | } 49 | 50 | return { 51 | projectId: m[1], 52 | collectionGroupId: m[2], 53 | fieldPath: m[3], 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/firestore/validator.ts: -------------------------------------------------------------------------------- 1 | import * as clc from "cli-color"; 2 | import { FirebaseError } from "../error"; 3 | 4 | /** 5 | * Throw an error if 'obj' does not have a value for the property 'prop'. 6 | */ 7 | export function assertHas(obj: any, prop: string): void { 8 | const objString = clc.cyan(JSON.stringify(obj)); 9 | if (!obj[prop]) { 10 | throw new FirebaseError(`Must contain "${prop}": ${objString}`); 11 | } 12 | } 13 | 14 | /** 15 | * throw an error if 'obj' does not have a value for exactly one of the 16 | * properties in 'props'. 17 | */ 18 | export function assertHasOneOf(obj: any, props: string[]): void { 19 | const objString = clc.cyan(JSON.stringify(obj)); 20 | let count = 0; 21 | props.forEach((prop) => { 22 | if (obj[prop]) { 23 | count++; 24 | } 25 | }); 26 | 27 | if (count !== 1) { 28 | throw new FirebaseError(`Must contain exactly one of "${props.join(",")}": ${objString}`); 29 | } 30 | } 31 | 32 | /** 33 | * Throw an error if the value of the property 'prop' on 'obj' is not one of 34 | * the values in the the array 'valid'. 35 | */ 36 | export function assertEnum(obj: any, prop: string, valid: any[]): void { 37 | const objString = clc.cyan(JSON.stringify(obj)); 38 | if (valid.indexOf(obj[prop]) < 0) { 39 | throw new FirebaseError(`Field "${prop}" must be one of ${valid.join(", ")}: ${objString}`); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/fsutils.ts: -------------------------------------------------------------------------------- 1 | import { statSync } from "fs"; 2 | 3 | export function fileExistsSync(path: string): boolean { 4 | try { 5 | return statSync(path).isFile(); 6 | } catch (e) { 7 | return false; 8 | } 9 | } 10 | 11 | export function dirExistsSync(path: string): boolean { 12 | try { 13 | return statSync(path).isDirectory(); 14 | } catch (e) { 15 | return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/gcp/cloudbilling.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var api = require("../api"); 4 | var utils = require("../utils"); 5 | 6 | var API_VERSION = "v1"; 7 | 8 | /** 9 | * Returns whether or not project has billing enabled. 10 | * @param {string} projectId 11 | * @return {!Promise} 12 | */ 13 | function _checkBillingEnabled(projectId) { 14 | return api 15 | .request("GET", utils.endpoint([API_VERSION, "projects", projectId, "billingInfo"]), { 16 | auth: true, 17 | origin: api.cloudbillingOrigin, 18 | retryCodes: [500, 503], 19 | }) 20 | .then(function(response) { 21 | return response.body.billingEnabled; 22 | }); 23 | } 24 | 25 | /** 26 | * Sets billing account for project and returns whether or not action was successful. 27 | * @param {string} projectId 28 | * @return {!Promise} 29 | */ 30 | function _setBillingAccount(projectId, billingAccount) { 31 | return api 32 | .request("PUT", utils.endpoint([API_VERSION, "projects", projectId, "billingInfo"]), { 33 | auth: true, 34 | origin: api.cloudbillingOrigin, 35 | retryCodes: [500, 503], 36 | data: { 37 | billingAccountName: billingAccount, 38 | }, 39 | }) 40 | .then(function(response) { 41 | return response.body.billingEnabled; 42 | }); 43 | } 44 | 45 | /** 46 | * Lists the billing accounts that the current authenticated user has permission to view. 47 | * @return {!Promise} 48 | */ 49 | function _listBillingAccounts() { 50 | return api 51 | .request("GET", utils.endpoint([API_VERSION, "billingAccounts"]), { 52 | auth: true, 53 | origin: api.cloudbillingOrigin, 54 | retryCodes: [500, 503], 55 | }) 56 | .then(function(response) { 57 | return response.body.billingAccounts || []; 58 | }); 59 | } 60 | 61 | module.exports = { 62 | checkBillingEnabled: _checkBillingEnabled, 63 | listBillingAccounts: _listBillingAccounts, 64 | setBillingAccount: _setBillingAccount, 65 | }; 66 | -------------------------------------------------------------------------------- /src/gcp/cloudlogging.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var api = require("../api"); 4 | 5 | var version = "v2beta1"; 6 | 7 | var _listEntries = function(projectId, filter, pageSize, order) { 8 | return api 9 | .request("POST", "/" + version + "/entries:list", { 10 | auth: true, 11 | data: { 12 | projectIds: [projectId], 13 | filter: filter, 14 | orderBy: "timestamp " + order, 15 | pageSize: pageSize, 16 | }, 17 | origin: api.cloudloggingOrigin, 18 | }) 19 | .then(function(result) { 20 | return Promise.resolve(result.body.entries); 21 | }); 22 | }; 23 | 24 | module.exports = { 25 | listEntries: _listEntries, 26 | }; 27 | -------------------------------------------------------------------------------- /src/gcp/firedata.ts: -------------------------------------------------------------------------------- 1 | import * as api from "../api"; 2 | import * as logger from "../logger"; 3 | import * as utils from "../utils"; 4 | 5 | export interface DatabaseInstance { 6 | // The globally unique name of the Database instance. 7 | // Required to be URL safe. ex: 'red-ant' 8 | instance: string; 9 | } 10 | 11 | function _handleErrorResponse(response: any): any { 12 | if (response.body && response.body.error) { 13 | return utils.reject(response.body.error, { code: 2 }); 14 | } 15 | 16 | logger.debug("[firedata] error:", response.status, response.body); 17 | return utils.reject("Unexpected error encountered with FireData.", { 18 | code: 2, 19 | }); 20 | } 21 | 22 | /** 23 | * Create a new Realtime Database instance 24 | * @param projectId Project from which you want to get the ruleset. 25 | * @param instanceName The name for the new Realtime Database instance. 26 | */ 27 | export async function createDatabaseInstance( 28 | projectNumber: number, 29 | instanceName: string 30 | ): Promise { 31 | const response = await api.request("POST", `/v1/projects/${projectNumber}/databases`, { 32 | auth: true, 33 | origin: api.firedataOrigin, 34 | json: { 35 | instance: instanceName, 36 | }, 37 | }); 38 | if (response.status === 200) { 39 | return response.body.instance; 40 | } 41 | return _handleErrorResponse(response); 42 | } 43 | 44 | /** 45 | * Create a new Realtime Database instance 46 | * @param projectId Project from which you want to get the ruleset. 47 | * @param instanceName The name for the new Realtime Database instance. 48 | */ 49 | export async function listDatabaseInstances(projectNumber: number): Promise { 50 | const response = await api.request("GET", `/v1/projects/${projectNumber}/databases`, { 51 | auth: true, 52 | origin: api.firedataOrigin, 53 | }); 54 | if (response.status === 200) { 55 | return response.body.instance; 56 | } 57 | return _handleErrorResponse(response); 58 | } 59 | -------------------------------------------------------------------------------- /src/gcp/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | cloudfunctions: require("./cloudfunctions"), 5 | cloudscheduler: require("./cloudscheduler"), 6 | cloudlogging: require("./cloudlogging"), 7 | iam: require("./iam"), 8 | pubsub: require("./pubsub"), 9 | storage: require("./storage"), 10 | rules: require("./rules"), 11 | }; 12 | -------------------------------------------------------------------------------- /src/gcp/pubsub.ts: -------------------------------------------------------------------------------- 1 | import * as api from "../api"; 2 | import { logLabeledBullet, logLabeledSuccess } from "../utils"; 3 | 4 | const VERSION = "v1"; 5 | 6 | export function createTopic(name: string): Promise { 7 | return api.request("PUT", `/${VERSION}/${name}`, { 8 | auth: true, 9 | origin: api.pubsubOrigin, 10 | data: { labels: { deployment: "firebase-schedule" } }, 11 | }); 12 | } 13 | 14 | export function deleteTopic(name: string): Promise { 15 | return api.request("DELETE", `/${VERSION}/${name}`, { 16 | auth: true, 17 | origin: api.pubsubOrigin, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/gcp/storage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var api = require("../api"); 4 | var logger = require("../logger"); 5 | var { FirebaseError } = require("../error"); 6 | 7 | function _getDefaultBucket(projectId) { 8 | return api 9 | .request("GET", "/v1/apps/" + projectId, { 10 | auth: true, 11 | origin: api.appengineOrigin, 12 | }) 13 | .then( 14 | function(resp) { 15 | if (resp.body.defaultBucket === "undefined") { 16 | logger.debug("Default storage bucket is undefined."); 17 | return Promise.reject( 18 | new FirebaseError( 19 | "Your project is being set up. Please wait a minute before deploying again." 20 | ) 21 | ); 22 | } 23 | return Promise.resolve(resp.body.defaultBucket); 24 | }, 25 | function(err) { 26 | logger.info( 27 | "\n\nThere was an issue deploying your functions. Verify that your project has a Google App Engine instance setup at https://console.cloud.google.com/appengine and try again. If this issue persists, please contact support." 28 | ); 29 | return Promise.reject(err); 30 | } 31 | ); 32 | } 33 | 34 | function _uploadSource(source, uploadUrl) { 35 | return api.request("PUT", uploadUrl, { 36 | data: source.stream, 37 | headers: { 38 | "Content-Type": "application/zip", 39 | "x-goog-content-length-range": "0,104857600", 40 | }, 41 | json: false, 42 | origin: api.storageOrigin, 43 | logOptions: { skipRequestBody: true }, 44 | }); 45 | } 46 | 47 | module.exports = { 48 | getDefaultBucket: _getDefaultBucket, 49 | upload: _uploadSource, 50 | }; 51 | -------------------------------------------------------------------------------- /src/getInstanceId.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var { getFirebaseProject } = require("./management/projects"); 5 | var logger = require("./logger"); 6 | 7 | /** 8 | * Tries to determine the instance ID for the provided 9 | * project. 10 | * @param {Object} options The command-line options object 11 | * @returns {Promise} The instance ID 12 | */ 13 | module.exports = function(options) { 14 | return getFirebaseProject(options.project).then(function(project) { 15 | if (!_.has(project, "resources.realtimeDatabaseInstance")) { 16 | logger.debug( 17 | "[WARNING] Unable to fetch default resources. Falling back to project id (" + 18 | options.project + 19 | ")" 20 | ); 21 | return options.project; 22 | } 23 | 24 | return _.get(project, "resources.realtimeDatabaseInstance"); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/getProjectId.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var clc = require("cli-color"); 5 | 6 | var { FirebaseError } = require("./error"); 7 | 8 | /** 9 | * Tries to determine the correct app name for commands that 10 | * only require an app name. Uses passed in firebase option 11 | * first, then falls back to firebase.json. 12 | * @param {Object} options The command-line options object 13 | * @param {boolean} allowNull Whether or not the firebase flag 14 | * is required 15 | * @returns {String} The firebase name 16 | */ 17 | module.exports = function(options, allowNull = false) { 18 | if (!options.project && !allowNull) { 19 | var aliases = _.get(options, "rc.projects", {}); 20 | var aliasCount = _.size(aliases); 21 | 22 | if (aliasCount === 0) { 23 | throw new FirebaseError( 24 | "No project active. Run with " + 25 | clc.bold("--project ") + 26 | " or define an alias by\nrunning " + 27 | clc.bold("firebase use --add"), 28 | { 29 | exit: 1, 30 | } 31 | ); 32 | } else { 33 | var aliasList = _.map(aliases, function(projectId, aname) { 34 | return " " + aname + " (" + projectId + ")"; 35 | }).join("\n"); 36 | 37 | throw new FirebaseError( 38 | "No project active, but project aliases are available.\n\nRun " + 39 | clc.bold("firebase use ") + 40 | " with one of these options:\n\n" + 41 | aliasList 42 | ); 43 | } 44 | } 45 | return options.project; 46 | }; 47 | -------------------------------------------------------------------------------- /src/getProjectNumber.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var getProjectId = require("./getProjectId"); 4 | var api = require("./api"); 5 | 6 | module.exports = function(options) { 7 | if (options.projectNumber) { 8 | return Promise.resolve(options.projectNumber); 9 | } 10 | var projectId = getProjectId(options); 11 | return api 12 | .request("GET", "/v1beta1/projects/" + projectId, { 13 | auth: true, 14 | origin: api.firebaseApiOrigin, 15 | }) 16 | .then(function(response) { 17 | options.projectNumber = response.body.projectNumber; 18 | return options.projectNumber; 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /src/handlePreviewToggles.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var clc = require("cli-color"); 5 | 6 | var auth = require("./auth"); 7 | var configstore = require("./configstore"); 8 | var previews = require("./previews"); 9 | 10 | var _errorOut = function(name) { 11 | console.log(clc.bold.red("Error:"), "Did not recognize preview feature", clc.bold(name)); 12 | process.exit(1); 13 | }; 14 | 15 | module.exports = function(args) { 16 | var isValidPreview = _.has(previews, args[1]); 17 | if (args[0] === "--open-sesame") { 18 | if (isValidPreview) { 19 | console.log("Enabling preview feature", clc.bold(args[1]) + "..."); 20 | previews[args[1]] = true; 21 | configstore.set("previews", previews); 22 | var tokens = configstore.get("tokens"); 23 | 24 | var next; 25 | if (tokens && tokens.refresh_token) { 26 | next = auth.logout(tokens.refresh_token); 27 | } else { 28 | next = Promise.resolve(); 29 | } 30 | return next.then(function() { 31 | console.log("Preview feature enabled!"); 32 | console.log(); 33 | console.log("Please run", clc.bold("firebase login"), "to re-authorize the CLI."); 34 | return process.exit(0); 35 | }); 36 | } 37 | 38 | _errorOut(); 39 | } else if (args[0] === "--close-sesame") { 40 | if (isValidPreview) { 41 | console.log("Disabling preview feature", clc.bold(args[1])); 42 | _.unset(previews, args[1]); 43 | configstore.set("previews", previews); 44 | return process.exit(0); 45 | } 46 | 47 | _errorOut(); 48 | } 49 | 50 | return undefined; 51 | }; 52 | -------------------------------------------------------------------------------- /src/hosting/functionsProxy.ts: -------------------------------------------------------------------------------- 1 | import { includes } from "lodash"; 2 | import { RequestHandler } from "express"; 3 | 4 | import { proxyRequestHandler } from "./proxy"; 5 | import * as getProjectId from "../getProjectId"; 6 | import { EmulatorRegistry } from "../emulator/registry"; 7 | import { Emulators } from "../emulator/types"; 8 | import { FunctionsEmulator } from "../emulator/functionsEmulator"; 9 | 10 | export interface FunctionsProxyOptions { 11 | port: number; 12 | project?: string; 13 | targets: string[]; 14 | } 15 | 16 | export interface FunctionProxyRewrite { 17 | function: string; 18 | } 19 | 20 | /** 21 | * Returns a function which, given a FunctionProxyRewrite, returns a Promise 22 | * that resolves with a middleware-like function that proxies the request to a 23 | * hosted or live function. 24 | */ 25 | export default function( 26 | options: FunctionsProxyOptions 27 | ): (r: FunctionProxyRewrite) => Promise { 28 | return async (rewrite: FunctionProxyRewrite) => { 29 | // TODO(samstern): This proxy assumes all functions are in the default region, but this is 30 | // not a safe assumption. 31 | const projectId = getProjectId(options, false); 32 | let url = `https://us-central1-${projectId}.cloudfunctions.net/${rewrite.function}`; 33 | let destLabel = "live"; 34 | 35 | if (includes(options.targets, "functions")) { 36 | destLabel = "local"; 37 | 38 | // If the functions emulator is running we know the port, otherwise 39 | // things still point to production. 40 | const functionsEmu = EmulatorRegistry.get(Emulators.FUNCTIONS); 41 | if (functionsEmu) { 42 | url = FunctionsEmulator.getHttpFunctionUrl( 43 | functionsEmu.getInfo().host, 44 | functionsEmu.getInfo().port, 45 | projectId, 46 | rewrite.function, 47 | "us-central1" 48 | ); 49 | } 50 | } 51 | 52 | return await proxyRequestHandler(url, `${destLabel} Function ${rewrite.function}`); 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/hosting/implicitInit.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var clc = require("cli-color"); 5 | var fs = require("fs"); 6 | var { fetchWebSetup, getCachedWebSetup } = require("../fetchWebSetup"); 7 | var utils = require("../utils"); 8 | var logger = require("../logger"); 9 | 10 | var INIT_TEMPLATE = fs.readFileSync(__dirname + "/../../templates/hosting/init.js", "utf8"); 11 | 12 | module.exports = async function(options) { 13 | let config; 14 | try { 15 | config = await fetchWebSetup(options); 16 | } catch (e) { 17 | logger.debug("fetchWebSetup error: " + e); 18 | const statusCode = _.get(e, "context.response.statusCode"); 19 | if (statusCode === 403) { 20 | utils.logLabeledWarning( 21 | "hosting", 22 | `Authentication error when trying to fetch your current web app configuration, have you run ${clc.bold( 23 | "firebase login" 24 | )}?` 25 | ); 26 | } 27 | } 28 | 29 | if (!config) { 30 | config = getCachedWebSetup(options); 31 | if (config) { 32 | utils.logLabeledWarning("hosting", "Using web app configuration from cache."); 33 | } 34 | } 35 | 36 | if (!config) { 37 | config = undefined; 38 | utils.logLabeledWarning( 39 | "hosting", 40 | "Could not fetch web app configuration and there is no cached configuration on this machine. " + 41 | "Check your internet connection and make sure you are authenticated. " + 42 | "To continue, you must call firebase.initializeApp({...}) in your code before using Firebase." 43 | ); 44 | } 45 | 46 | const configJson = JSON.stringify(config, null, 2); 47 | return { 48 | js: INIT_TEMPLATE.replace("/*--CONFIG--*/", `var firebaseConfig = ${configJson};`), 49 | json: configJson, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/hosting/initMiddleware.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var request = require("request"); 4 | var logger = require("../logger"); 5 | var utils = require("../utils"); 6 | 7 | var SDK_PATH_REGEXP = /^\/__\/firebase\/([^/]+)\/([^/]+)$/; 8 | 9 | module.exports = function(init) { 10 | return function(req, res, next) { 11 | var match = req.url.match(SDK_PATH_REGEXP); 12 | if (match) { 13 | var version = match[1]; 14 | var sdkName = match[2]; 15 | var url = "https://www.gstatic.com/firebasejs/" + version + "/" + sdkName; 16 | var preq = request(url) 17 | .on("response", function(pres) { 18 | if (pres.statusCode === 404) { 19 | return next(); 20 | } 21 | return preq.pipe(res); 22 | }) 23 | .on("error", function(e) { 24 | utils.logLabeledWarning( 25 | "hosting", 26 | `Could not load Firebase SDK ${sdkName} v${version}, check your internet connection.` 27 | ); 28 | logger.debug(e); 29 | }); 30 | } else if (req.url === "/__/firebase/init.js") { 31 | res.setHeader("Content-Type", "application/javascript"); 32 | res.end(init.js); 33 | } else if (req.url === "/__/firebase/init.json") { 34 | res.setHeader("Content-Type", "application/json"); 35 | res.end(init.json); 36 | } else { 37 | next(); 38 | } 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/hosting/normalizedHostingConfigs.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | 3 | function _filterOnly(configs, onlyString) { 4 | if (!onlyString) { 5 | return configs; 6 | } 7 | 8 | var onlyTargets = onlyString.split(","); 9 | // If an unqualified "hosting" is in the --only, 10 | // all hosting sites should be deployed. 11 | if (_.includes(onlyTargets, "hosting")) { 12 | return configs; 13 | } 14 | 15 | onlyTargets = onlyTargets 16 | .filter(function(anOnly) { 17 | return anOnly.indexOf("hosting:") === 0; 18 | }) 19 | .map(function(anOnly) { 20 | return anOnly.replace("hosting:", ""); 21 | }); 22 | 23 | return configs.filter(function(config) { 24 | return _.includes(onlyTargets, config.target || config.site); 25 | }); 26 | } 27 | 28 | module.exports = function(options) { 29 | let configs = options.config.get("hosting"); 30 | if (!configs) { 31 | return []; 32 | } else if (!_.isArray(configs)) { 33 | if (!configs.target && !configs.site) { 34 | // The default Hosting site is the same as the default RTDB instance, 35 | // since for projects created since mid-2016 they are both the same 36 | // as the project id, and for projects created before the Hosting 37 | // site was created along with the RTDB instance. 38 | configs.site = options.instance; 39 | } 40 | configs = [configs]; 41 | } 42 | 43 | return _filterOnly(configs, options.only); 44 | }; 45 | -------------------------------------------------------------------------------- /src/identifierToProjectId.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | 5 | var api = require("./api"); 6 | 7 | /** 8 | * This function is deprecated due to its functionality is no longer relevant for the commands. 9 | * TODO: remove this function when tools:migrate is removed. 10 | */ 11 | module.exports = function(id) { 12 | return api.getProjects().then(function(projects) { 13 | // if exact match for a project id, return it 14 | if (_.includes(_.keys(projects), id)) { 15 | return id; 16 | } 17 | 18 | for (var projectId in projects) { 19 | if (projects.hasOwnProperty(projectId)) { 20 | var instance = _.get(projects, [projectId, "instances", "database", "0"]); 21 | if (id === instance) { 22 | return projectId; 23 | } 24 | } 25 | } 26 | 27 | return null; 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/init/features/firestore/index.ts: -------------------------------------------------------------------------------- 1 | import { ensureLocationSet } from "../../../ensureCloudResourceLocation"; 2 | import { requirePermissions } from "../../../requirePermissions"; 3 | import * as rules from "./rules"; 4 | import * as indexes from "./indexes"; 5 | 6 | export async function doSetup(setup: any, config: any): Promise { 7 | setup.config.firestore = {}; 8 | 9 | ensureLocationSet(setup.projectLocation, "Cloud Firestore"); 10 | await requirePermissions({ project: setup.projectId }); 11 | await rules.initRules(setup, config); 12 | await indexes.initIndexes(setup, config); 13 | } 14 | -------------------------------------------------------------------------------- /src/init/features/functions/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var clc = require("cli-color"); 4 | 5 | var _ = require("lodash"); 6 | 7 | var logger = require("../../../logger"); 8 | var { prompt } = require("../../../prompt"); 9 | var enableApi = require("../../../ensureApiEnabled").enable; 10 | var { requirePermissions } = require("../../../requirePermissions"); 11 | 12 | module.exports = function(setup, config) { 13 | logger.info(); 14 | logger.info( 15 | "A " + clc.bold("functions") + " directory will be created in your project with a Node.js" 16 | ); 17 | logger.info( 18 | "package pre-configured. Functions can be deployed with " + clc.bold("firebase deploy") + "." 19 | ); 20 | logger.info(); 21 | 22 | setup.functions = {}; 23 | var projectId = _.get(setup, "rcfile.projects.default"); 24 | var enableApis = Promise.resolve(); 25 | if (projectId) { 26 | enableApis = requirePermissions({ project: projectId }).then(() => { 27 | return Promise.all([ 28 | enableApi(projectId, "cloudfunctions.googleapis.com"), 29 | enableApi(projectId, "runtimeconfig.googleapis.com"), 30 | ]); 31 | }); 32 | } 33 | return enableApis.then(function() { 34 | return prompt(setup.functions, [ 35 | { 36 | type: "list", 37 | name: "language", 38 | message: "What language would you like to use to write Cloud Functions?", 39 | default: "javascript", 40 | choices: [ 41 | { 42 | name: "JavaScript", 43 | value: "javascript", 44 | }, 45 | { 46 | name: "TypeScript", 47 | value: "typescript", 48 | }, 49 | ], 50 | }, 51 | ]).then(function() { 52 | return require("./" + setup.functions.language)(setup, config); 53 | }); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /src/init/features/functions/npm-dependencies.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var spawn = require("cross-spawn"); 4 | 5 | var logger = require("../../../logger"); 6 | var { prompt } = require("../../../prompt"); 7 | 8 | exports.askInstallDependencies = function(setup, config) { 9 | return prompt(setup.functions, [ 10 | { 11 | name: "npm", 12 | type: "confirm", 13 | message: "Do you want to install dependencies with npm now?", 14 | default: true, 15 | }, 16 | ]).then(function() { 17 | if (setup.functions.npm) { 18 | return new Promise(function(resolve) { 19 | var installer = spawn("npm", ["install"], { 20 | cwd: config.projectDir + "/functions", 21 | stdio: "inherit", 22 | }); 23 | 24 | installer.on("error", function(err) { 25 | logger.debug(err.stack); 26 | }); 27 | 28 | installer.on("close", function(code) { 29 | if (code === 0) { 30 | return resolve(); 31 | } 32 | logger.info(); 33 | logger.error("NPM install failed, continuing with Firebase initialization..."); 34 | return resolve(); 35 | }); 36 | }); 37 | } 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/init/features/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | database: require("./database"), 5 | firestore: require("./firestore").doSetup, 6 | functions: require("./functions"), 7 | hosting: require("./hosting"), 8 | storage: require("./storage").doSetup, 9 | emulators: require("./emulators").doSetup, 10 | // always runs, sets up .firebaserc 11 | project: require("./project").doSetup, 12 | }; 13 | -------------------------------------------------------------------------------- /src/init/features/storage.ts: -------------------------------------------------------------------------------- 1 | import * as clc from "cli-color"; 2 | import * as fs from "fs"; 3 | 4 | import * as logger from "../../logger"; 5 | import { promptOnce } from "../../prompt"; 6 | import { ensureLocationSet } from "../../ensureCloudResourceLocation"; 7 | 8 | const RULES_TEMPLATE = fs.readFileSync( 9 | __dirname + "/../../../templates/init/storage/storage.rules", 10 | "utf8" 11 | ); 12 | 13 | export async function doSetup(setup: any, config: any): Promise { 14 | setup.config.storage = {}; 15 | ensureLocationSet(setup.projectLocation, "Cloud Storage"); 16 | 17 | logger.info(); 18 | logger.info("Firebase Storage Security Rules allow you to define how and when to allow"); 19 | logger.info("uploads and downloads. You can keep these rules in your project directory"); 20 | logger.info("and publish them with " + clc.bold("firebase deploy") + "."); 21 | logger.info(); 22 | 23 | const storageRulesFile = await promptOnce({ 24 | type: "input", 25 | name: "rules", 26 | message: "What file should be used for Storage Rules?", 27 | default: "storage.rules", 28 | }); 29 | setup.config.storage.rules = storageRulesFile; 30 | config.writeProjectFile(setup.config.storage.rules, RULES_TEMPLATE); 31 | } 32 | -------------------------------------------------------------------------------- /src/init/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var clc = require("cli-color"); 5 | 6 | var logger = require("../logger"); 7 | var features = require("./features"); 8 | var utils = require("../utils"); 9 | 10 | var init = function(setup, config, options) { 11 | var nextFeature = setup.features.shift(); 12 | if (nextFeature) { 13 | if (!features[nextFeature]) { 14 | return utils.reject( 15 | clc.bold(nextFeature) + 16 | " is not a valid feature. Must be one of " + 17 | _.without(_.keys(features), "project").join(", ") 18 | ); 19 | } 20 | 21 | logger.info(clc.bold("\n" + clc.white("=== ") + _.capitalize(nextFeature) + " Setup")); 22 | return Promise.resolve(features[nextFeature](setup, config, options)).then(function() { 23 | return init(setup, config, options); 24 | }); 25 | } 26 | return Promise.resolve(); 27 | }; 28 | 29 | module.exports = init; 30 | -------------------------------------------------------------------------------- /src/listFiles.ts: -------------------------------------------------------------------------------- 1 | import { sync } from "glob"; 2 | 3 | export function listFiles(cwd: string, ignore: string[] = []): string[] { 4 | return sync("**/*", { 5 | cwd, 6 | dot: true, 7 | follow: true, 8 | ignore: ["**/firebase-debug.log", ".firebase/*"].concat(ignore), 9 | nodir: true, 10 | nosort: true, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/loadCJSON.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var { FirebaseError } = require("./error"); 4 | var cjson = require("cjson"); 5 | 6 | module.exports = function(path) { 7 | try { 8 | return cjson.load(path); 9 | } catch (e) { 10 | if (e.code === "ENOENT") { 11 | throw new FirebaseError("File " + path + " does not exist", { exit: 1 }); 12 | } 13 | throw new FirebaseError("Parse Error in " + path + ":\n\n" + e.message); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/logError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var logger = require("./logger"); 4 | var clc = require("cli-color"); 5 | 6 | /* istanbul ignore next */ 7 | module.exports = function(error) { 8 | if (error.children && error.children.length) { 9 | logger.error(clc.bold.red("Error:"), clc.underline(error.message) + ":"); 10 | error.children.forEach(function(child) { 11 | var out = "- "; 12 | if (child.name) { 13 | out += clc.bold(child.name) + " "; 14 | } 15 | out += child.message; 16 | 17 | logger.error(out); 18 | }); 19 | } else { 20 | if (error.original) { 21 | logger.debug(error.original.stack); 22 | } 23 | logger.error(); 24 | logger.error(clc.bold.red("Error:"), error.message); 25 | } 26 | if (error.context) { 27 | logger.debug("Error Context:", JSON.stringify(error.context, undefined, 2)); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const winston = require("winston"); 4 | 5 | function expandErrors(logger) { 6 | const oldLogFunc = logger.log; 7 | logger.log = function(...logArgs) { 8 | const args = logArgs.slice(0); 9 | if (args.length >= 2 && args[1] instanceof Error) { 10 | args[1] = args[1].stack; 11 | } 12 | return oldLogFunc.apply(this, args); 13 | }; 14 | return logger; 15 | } 16 | 17 | const logger = expandErrors(new winston.Logger()); 18 | 19 | const debug = logger.debug; 20 | logger.debug = function(...args) { 21 | args[0] = "[" + new Date().toISOString() + "] " + (args[0] || ""); 22 | debug(...args); 23 | }; 24 | 25 | logger.exitOnError = false; 26 | 27 | module.exports = logger; 28 | -------------------------------------------------------------------------------- /src/parseBoltRules.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | var spawn = require("cross-spawn"); 5 | var { FirebaseError } = require("./error"); 6 | var clc = require("cli-color"); 7 | 8 | module.exports = function(filename) { 9 | var ruleSrc = fs.readFileSync(filename); 10 | 11 | var result = spawn.sync("firebase-bolt", { 12 | input: ruleSrc, 13 | timeout: 10000, 14 | encoding: "utf-8", 15 | }); 16 | 17 | if (result.error && result.error.code === "ENOENT") { 18 | throw new FirebaseError("Bolt not installed, run " + clc.bold("npm install -g firebase-bolt"), { 19 | exit: 1, 20 | }); 21 | } else if (result.error) { 22 | throw new FirebaseError("Unexpected error parsing Bolt rules file", { 23 | exit: 2, 24 | }); 25 | } else if (result.status > 0) { 26 | throw new FirebaseError(result.stderr, { exit: 1 }); 27 | } 28 | 29 | return result.stdout; 30 | }; 31 | -------------------------------------------------------------------------------- /src/pollOperations.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | 5 | var MAX_POLL_RETRIES = 2; 6 | 7 | function pollOperation(op, pollFunction, interval, pollFailCount) { 8 | pollFailCount = pollFailCount || 0; 9 | return new Promise(function(resolve, reject) { 10 | function poll() { 11 | pollFunction(op) 12 | .then(function(result) { 13 | if (result.done) { 14 | resolve(result); 15 | } else { 16 | setTimeout(poll, interval); 17 | } 18 | }) 19 | .catch(function() { 20 | if (pollFailCount < MAX_POLL_RETRIES) { 21 | pollFailCount += 1; 22 | setTimeout(poll, interval * 2); 23 | } else { 24 | reject("Failed to get status of operation."); 25 | } 26 | }); 27 | } 28 | poll(); 29 | }); 30 | } 31 | 32 | function pollAndRetryOperations( 33 | operations, 34 | pollFunction, 35 | interval, 36 | printSuccess, 37 | printFail, 38 | retryCondition 39 | ) { 40 | // This function assumes that a Google.LongRunning operation is being polled 41 | return Promise.all( 42 | _.map(operations, function(op) { 43 | return pollOperation(op, pollFunction, interval).then(function(result) { 44 | if (!result.error) { 45 | return printSuccess(op); 46 | } 47 | if (!retryCondition(result)) { 48 | return printFail(op); 49 | } 50 | 51 | return op 52 | .retryFunction() 53 | .then(function(retriedOperation) { 54 | return pollOperation(retriedOperation, pollFunction, interval); 55 | }) 56 | .then(function(retriedResult) { 57 | if (retriedResult.error) { 58 | return printFail(op); 59 | } 60 | return printSuccess(op); 61 | }); 62 | }); 63 | }) 64 | ); 65 | } 66 | 67 | module.exports = { 68 | pollAndRetry: pollAndRetryOperations, 69 | poll: pollOperation, 70 | }; 71 | -------------------------------------------------------------------------------- /src/prepareUpload.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | var path = require("path"); 5 | 6 | var tar = require("tar"); 7 | var tmp = require("tmp"); 8 | 9 | var { listFiles } = require("./listFiles"); 10 | var { FirebaseError } = require("./error"); 11 | var fsutils = require("./fsutils"); 12 | 13 | module.exports = function(options) { 14 | var hostingConfig = options.config.get("hosting"); 15 | var publicDir = options.config.path(hostingConfig.public); 16 | var indexPath = path.join(publicDir, "index.html"); 17 | 18 | var tmpFile = tmp.fileSync({ 19 | prefix: "firebase-upload-", 20 | postfix: ".tar.gz", 21 | }); 22 | var manifest = listFiles(publicDir, hostingConfig.ignore); 23 | 24 | return tar 25 | .c( 26 | { 27 | gzip: true, 28 | file: tmpFile.name, 29 | cwd: publicDir, 30 | prefix: "public", 31 | follow: true, 32 | noDirRecurse: true, 33 | portable: true, 34 | }, 35 | manifest.slice(0) 36 | ) 37 | .then(function() { 38 | var stats = fs.statSync(tmpFile.name); 39 | return { 40 | file: tmpFile.name, 41 | stream: fs.createReadStream(tmpFile.name), 42 | manifest: manifest, 43 | foundIndex: fsutils.fileExistsSync(indexPath), 44 | size: stats.size, 45 | }; 46 | }) 47 | .catch(function(err) { 48 | return Promise.reject( 49 | new FirebaseError("There was an issue preparing Hosting files for upload.", { 50 | original: err, 51 | exit: 2, 52 | }) 53 | ); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /src/previews.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var configstore = require("./configstore"); 5 | 6 | var previews = _.assign( 7 | { 8 | // insert previews here... 9 | rtdbrules: false, 10 | }, 11 | configstore.get("previews") 12 | ); 13 | 14 | if (process.env.FIREBASE_CLI_PREVIEWS) { 15 | process.env.FIREBASE_CLI_PREVIEWS.split(",").forEach(function(feature) { 16 | if (_.has(previews, feature)) { 17 | _.set(previews, feature, true); 18 | } 19 | }); 20 | } 21 | 22 | module.exports = previews; 23 | -------------------------------------------------------------------------------- /src/projectPath.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as detectProjectRoot from "./detectProjectRoot"; 3 | 4 | /** 5 | * Returns a fully qualified path to the wanted file/directory inside the project. 6 | * @param cwd current working directory. 7 | * @param filePath the target file or directory in the project. 8 | */ 9 | export function resolveProjectPath(cwd: string, filePath: string): string { 10 | return path.resolve(detectProjectRoot(cwd), filePath); 11 | } 12 | -------------------------------------------------------------------------------- /src/requireConfig.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var { FirebaseError } = require("./error"); 4 | 5 | module.exports = function(options) { 6 | if (options.config) { 7 | return Promise.resolve(); 8 | } 9 | return Promise.reject( 10 | options.configError || 11 | new FirebaseError("Not in a Firebase project directory (could not locate firebase.json)", { 12 | exit: 1, 13 | }) 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/requireInstance.js: -------------------------------------------------------------------------------- 1 | const getInstanceId = require("./getInstanceId"); 2 | 3 | module.exports = function(options) { 4 | if (options.instance) { 5 | return Promise.resolve(); 6 | } 7 | 8 | return getInstanceId(options).then((instance) => { 9 | options.instance = instance; 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/requirePermissions.ts: -------------------------------------------------------------------------------- 1 | import { difference } from "lodash"; 2 | import { bold } from "cli-color"; 3 | 4 | import { request, resourceManagerOrigin } from "./api"; 5 | import getProjectId = require("./getProjectId"); 6 | import requireAuth = require("./requireAuth"); 7 | import { debug } from "./logger"; 8 | import { FirebaseError } from "./error"; 9 | 10 | // Permissions required for all commands. 11 | const BASE_PERMISSIONS = ["firebase.projects.get"]; 12 | 13 | export async function requirePermissions(options: any, permissions: string[] = []): Promise { 14 | const projectId = getProjectId(options); 15 | const requiredPermissions = BASE_PERMISSIONS.concat(permissions).sort(); 16 | 17 | await requireAuth(options); 18 | debug( 19 | `[iam] checking project ${projectId} for permissions ${JSON.stringify(requiredPermissions)}` 20 | ); 21 | 22 | let response: any; 23 | try { 24 | response = await request("POST", `/v1/projects/${projectId}:testIamPermissions`, { 25 | auth: true, 26 | data: { permissions: requiredPermissions }, 27 | origin: resourceManagerOrigin, 28 | }); 29 | } catch (err) { 30 | debug(`[iam] error while checking permissions, command may fail: ${err}`); 31 | return; 32 | } 33 | 34 | const allowedPermissions = (response.body.permissions || []).sort(); 35 | const missingPermissions = difference(requiredPermissions, allowedPermissions); 36 | if (missingPermissions.length) { 37 | throw new FirebaseError( 38 | `Authorization failed. This account is missing the following required permissions on project ${bold( 39 | projectId 40 | )}:\n\n ${missingPermissions.join("\n ")}` 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/responseToError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const { FirebaseError } = require("./error"); 5 | 6 | module.exports = function(response, body) { 7 | if (typeof body === "string" && response.statusCode === 404) { 8 | body = { 9 | error: { 10 | message: "Not Found", 11 | }, 12 | }; 13 | } 14 | 15 | if (response.statusCode < 400) { 16 | return null; 17 | } 18 | 19 | if (typeof body !== "object") { 20 | try { 21 | body = JSON.parse(body); 22 | } catch (e) { 23 | body = {}; 24 | } 25 | } 26 | 27 | if (!body.error) { 28 | const errMessage = response.statusCode === 404 ? "Not Found" : "Unknown Error"; 29 | body.error = { 30 | message: errMessage, 31 | }; 32 | } 33 | 34 | const message = "HTTP Error: " + response.statusCode + ", " + (body.error.message || body.error); 35 | 36 | let exitCode; 37 | if (response.statusCode >= 500) { 38 | // 5xx errors are unexpected 39 | exitCode = 2; 40 | } else { 41 | // 4xx errors happen sometimes 42 | exitCode = 1; 43 | } 44 | 45 | _.unset(response, "request.headers"); 46 | return new FirebaseError(message, { 47 | context: { 48 | body: body, 49 | response: response, 50 | }, 51 | exit: exitCode, 52 | status: response.statusCode, 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /src/rtdb.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var api = require("./api"); 4 | var { FirebaseError } = require("./error"); 5 | var utils = require("./utils"); 6 | 7 | exports.updateRules = function(instance, src, options) { 8 | options = options || {}; 9 | var url = "/.settings/rules.json"; 10 | if (options.dryRun) { 11 | url += "?dryRun=true"; 12 | } 13 | 14 | return api 15 | .request("PUT", url, { 16 | origin: utils.addSubdomain(api.realtimeOrigin, instance), 17 | auth: true, 18 | data: src, 19 | json: false, 20 | resolveOnHTTPError: true, 21 | }) 22 | .then(function(response) { 23 | if (response.status === 400) { 24 | throw new FirebaseError( 25 | "Syntax error in database rules:\n\n" + JSON.parse(response.body).error 26 | ); 27 | } else if (response.status > 400) { 28 | throw new FirebaseError("Unexpected error while deploying database rules.", { exit: 2 }); 29 | } 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/scopes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | // default scopes 5 | OPENID: "openid", 6 | EMAIL: "email", 7 | CLOUD_PROJECTS_READONLY: "https://www.googleapis.com/auth/cloudplatformprojects.readonly", 8 | FIREBASE_PLATFORM: "https://www.googleapis.com/auth/firebase", 9 | 10 | // incremental scopes 11 | CLOUD_PLATFORM: "https://www.googleapis.com/auth/cloud-platform", 12 | CLOUD_STORAGE: "https://www.googleapis.com/auth/devstorage.read_write", 13 | CLOUD_PUBSUB: "https://www.googleapis.com/auth/pubsub", 14 | }; 15 | -------------------------------------------------------------------------------- /src/serve/database.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseEmulator } from "../emulator/databaseEmulator"; 2 | import { EmulatorServer } from "../emulator/emulatorServer"; 3 | 4 | module.exports = new EmulatorServer(new DatabaseEmulator({})); 5 | -------------------------------------------------------------------------------- /src/serve/firestore.ts: -------------------------------------------------------------------------------- 1 | import { FirestoreEmulator } from "../emulator/firestoreEmulator"; 2 | import { EmulatorServer } from "../emulator/emulatorServer"; 3 | 4 | module.exports = new EmulatorServer(new FirestoreEmulator({})); 5 | -------------------------------------------------------------------------------- /src/serve/index.ts: -------------------------------------------------------------------------------- 1 | import { EmulatorServer } from "../emulator/emulatorServer"; 2 | import * as _ from "lodash"; 3 | import * as logger from "../logger"; 4 | 5 | const TARGETS: { 6 | [key: string]: 7 | | EmulatorServer 8 | | { start: (o: any) => void; stop: (o: any) => void; connect: () => void }; 9 | } = { 10 | hosting: require("./hosting"), 11 | functions: require("./functions"), 12 | database: require("./database"), 13 | firestore: require("./firestore"), 14 | }; 15 | 16 | /** 17 | * Serve runs the emulators for a set of targets provided in options. 18 | * @param options Firebase CLI options. 19 | */ 20 | export async function serve(options: any): Promise { 21 | const targetNames = options.targets; 22 | options.port = parseInt(options.port, 10); 23 | await Promise.all( 24 | _.map(targetNames, (targetName: string) => { 25 | return TARGETS[targetName].start(options); 26 | }) 27 | ); 28 | await Promise.all( 29 | _.map(targetNames, (targetName: string) => { 30 | return TARGETS[targetName].connect(); 31 | }) 32 | ); 33 | await new Promise((resolve) => { 34 | process.on("SIGINT", () => { 35 | logger.info("Shutting down..."); 36 | return Promise.all( 37 | _.map(targetNames, (targetName: string) => { 38 | return TARGETS[targetName].stop(options); 39 | }) 40 | ) 41 | .then(resolve) 42 | .catch(resolve); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/test/archiveDirectory.spec.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import { expect } from "chai"; 3 | import { FirebaseError } from "../error"; 4 | 5 | import { archiveDirectory } from "../archiveDirectory"; 6 | 7 | const SOME_FIXTURE_DIRECTORY = resolve(__dirname, "./fixtures/config-imports"); 8 | 9 | describe("archiveDirectory", () => { 10 | it("should archive happy little directories", async () => { 11 | const result = await archiveDirectory(SOME_FIXTURE_DIRECTORY, {}); 12 | expect(result.source).to.equal(SOME_FIXTURE_DIRECTORY); 13 | expect(result.size).to.be.greaterThan(0); 14 | }); 15 | 16 | it("should throw a happy little error if the directory doesn't exist", async () => { 17 | await expect(archiveDirectory(resolve(__dirname, "foo"), {})).to.be.rejectedWith(FirebaseError); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/test/database/listRemote.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as nock from "nock"; 3 | 4 | import * as utils from "../../utils"; 5 | import * as api from "../../api"; 6 | import * as helpers from "../helpers"; 7 | import { RTDBListRemote } from "../../database/listRemote"; 8 | 9 | describe("ListRemote", () => { 10 | const instance = "fake-db"; 11 | const remote = new RTDBListRemote(instance); 12 | const serverUrl = utils.addSubdomain(api.realtimeOrigin, instance); 13 | 14 | afterEach(() => { 15 | nock.cleanAll(); 16 | }); 17 | 18 | it("should return subpaths from shallow get request", async () => { 19 | nock(serverUrl) 20 | .get("/.json") 21 | .query({ shallow: true, limitToFirst: "1234" }) 22 | .reply(200, { 23 | a: true, 24 | x: true, 25 | f: true, 26 | }); 27 | await expect(remote.listPath("/", 1234)).to.eventually.eql(["a", "x", "f"]); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/test/emulators/controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Emulators } from "../../emulator/types"; 2 | import { startEmulator } from "../../emulator/controller"; 3 | import { EmulatorRegistry } from "../../emulator/registry"; 4 | import { expect } from "chai"; 5 | import { FakeEmulator } from "./fakeEmulator"; 6 | 7 | describe("EmulatorController", () => { 8 | afterEach(async () => { 9 | await EmulatorRegistry.stopAll(); 10 | }); 11 | 12 | it("should start and stop an emulator", async () => { 13 | const name = Emulators.FUNCTIONS; 14 | 15 | expect(EmulatorRegistry.isRunning(name)).to.be.false; 16 | 17 | await startEmulator(new FakeEmulator(name, "localhost", 7777)); 18 | 19 | expect(EmulatorRegistry.isRunning(name)).to.be.true; 20 | expect(EmulatorRegistry.getPort(name)).to.eql(7777); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/test/emulators/emulatorServer.spec.ts: -------------------------------------------------------------------------------- 1 | import { Emulators } from "../../emulator/types"; 2 | import { EmulatorRegistry } from "../../emulator/registry"; 3 | import { expect } from "chai"; 4 | import { FakeEmulator } from "./fakeEmulator"; 5 | import { EmulatorServer } from "../../emulator/emulatorServer"; 6 | 7 | describe("EmulatorServer", () => { 8 | it("should correctly start and stop an emulator", async () => { 9 | const name = Emulators.FUNCTIONS; 10 | const emulator = new FakeEmulator(name, "localhost", 5000); 11 | const server = new EmulatorServer(emulator); 12 | 13 | await server.start(); 14 | 15 | expect(EmulatorRegistry.isRunning(name)).to.be.true; 16 | expect(EmulatorRegistry.get(name)).to.eql(emulator); 17 | 18 | await server.stop(); 19 | 20 | expect(EmulatorRegistry.isRunning(name)).to.be.false; 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/test/emulators/fakeEmulator.ts: -------------------------------------------------------------------------------- 1 | import { EmulatorInfo, EmulatorInstance, Emulators } from "../../emulator/types"; 2 | import * as express from "express"; 3 | import * as http from "http"; 4 | 5 | /** 6 | * A thing that acts like an emulator by just occupying a port. 7 | */ 8 | export class FakeEmulator implements EmulatorInstance { 9 | private exp: express.Express; 10 | private server?: http.Server; 11 | 12 | constructor(public name: Emulators, public host: string, public port: number) { 13 | this.exp = express(); 14 | } 15 | 16 | start(): Promise { 17 | this.server = this.exp.listen(this.port); 18 | return Promise.resolve(); 19 | } 20 | connect(): Promise { 21 | return Promise.resolve(); 22 | } 23 | stop(): Promise { 24 | if (this.server) { 25 | this.server.close(); 26 | this.server = undefined; 27 | } 28 | return Promise.resolve(); 29 | } 30 | getInfo(): EmulatorInfo { 31 | return { 32 | host: this.host, 33 | port: this.port, 34 | }; 35 | } 36 | getName(): Emulators { 37 | return this.name; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/emulators/registry.spec.ts: -------------------------------------------------------------------------------- 1 | import { ALL_EMULATORS, Emulators } from "../../emulator/types"; 2 | import { EmulatorRegistry } from "../../emulator/registry"; 3 | import { expect } from "chai"; 4 | import { FakeEmulator } from "./fakeEmulator"; 5 | 6 | describe("EmulatorRegistry", () => { 7 | afterEach(async () => { 8 | await EmulatorRegistry.stopAll(); 9 | }); 10 | 11 | it("should not report any running emulators when empty", async () => { 12 | for (const name of ALL_EMULATORS) { 13 | expect(EmulatorRegistry.isRunning(name)).to.be.false; 14 | } 15 | 16 | expect(EmulatorRegistry.listRunning()).to.be.empty; 17 | }); 18 | 19 | it("should correctly return information about a running emulator", async () => { 20 | const name = Emulators.FUNCTIONS; 21 | const emu = new FakeEmulator(name, "localhost", 5000); 22 | 23 | expect(EmulatorRegistry.isRunning(name)).to.be.false; 24 | 25 | await EmulatorRegistry.start(emu); 26 | 27 | expect(EmulatorRegistry.isRunning(name)).to.be.true; 28 | expect(EmulatorRegistry.listRunning()).to.eql([name]); 29 | expect(EmulatorRegistry.get(name)).to.eql(emu); 30 | expect(EmulatorRegistry.getPort(name)).to.eql(5000); 31 | }); 32 | 33 | it("once stopped, an emulator is no longer running", async () => { 34 | const name = Emulators.FUNCTIONS; 35 | const emu = new FakeEmulator(name, "localhost", 5000); 36 | 37 | expect(EmulatorRegistry.isRunning(name)).to.be.false; 38 | await EmulatorRegistry.start(emu); 39 | expect(EmulatorRegistry.isRunning(name)).to.be.true; 40 | await EmulatorRegistry.stop(name); 41 | expect(EmulatorRegistry.isRunning(name)).to.be.false; 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/test/error.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { FirebaseError } from "../error"; 3 | 4 | describe("error", () => { 5 | describe("FirebaseError", () => { 6 | it("should be an instance of Error", () => { 7 | const error = new FirebaseError("test-message"); 8 | 9 | expect(error).to.be.instanceOf(Error); 10 | }); 11 | 12 | it("should apply default options", () => { 13 | const error = new FirebaseError("test-message"); 14 | 15 | expect(error).to.deep.include({ children: [], exit: 1, name: "FirebaseError", status: 500 }); 16 | }); 17 | 18 | it("should persist all options", () => { 19 | /** 20 | * All possible options that might be provided to `FirebaseError`. 21 | */ 22 | type FirebaseErrorOptions = ConstructorParameters[1]; 23 | 24 | /* 25 | * The following `Required` ensures all options are defined, so the test 26 | * covers all properties. 27 | */ 28 | const allOptions: Required = { 29 | children: ["test-child-1", "test-child-2"], 30 | context: "test-context", 31 | exit: 123, 32 | original: new Error("test-original-error-message"), 33 | status: 456, 34 | }; 35 | 36 | const error = new FirebaseError("test-message", allOptions); 37 | 38 | expect(error).to.deep.include(allOptions); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/test/extensions/askUserForConsent.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const clc = require("cli-color"); 5 | 6 | const chai = require("chai"); 7 | chai.use(require("chai-as-promised")); 8 | const sinon = require("sinon"); 9 | 10 | const askUserForConsent = require("../../extensions/askUserForConsent"); 11 | 12 | const expect = chai.expect; 13 | 14 | describe("askUserForConsent", function() { 15 | describe("_formatDescription", function() { 16 | beforeEach(function() { 17 | sinon.stub(askUserForConsent, "_retrieveRoleInfo"); 18 | askUserForConsent._retrieveRoleInfo.rejects("UNDEFINED TEST BEHAVIOR"); 19 | }); 20 | 21 | afterEach(function() { 22 | askUserForConsent._retrieveRoleInfo.restore(); 23 | }); 24 | 25 | const partialSpec = { roles: [{ role: "storage.objectAdmin" }, { role: "datastore.viewer" }] }; 26 | 27 | it("format description correctly", function() { 28 | const extensionName = "extension-for-test"; 29 | const projectId = "project-for-test"; 30 | const question = `${clc.bold( 31 | extensionName 32 | )} will be granted the following access to project ${clc.bold(projectId)}`; 33 | const storageDescription = "- Storage Object Admin (Full control of GCS objects.)"; 34 | const datastoreDescription = 35 | "- Cloud Datastore Viewer (Read access to all Cloud Datastore resources.)"; 36 | const expected = _.join([question, storageDescription, datastoreDescription], "\n"); 37 | 38 | askUserForConsent._retrieveRoleInfo.onFirstCall().resolves(storageDescription); 39 | askUserForConsent._retrieveRoleInfo.onSecondCall().resolves(datastoreDescription); 40 | 41 | const actual = askUserForConsent._formatDescription( 42 | extensionName, 43 | projectId, 44 | partialSpec.roles 45 | ); 46 | 47 | return expect(actual).to.eventually.deep.equal(expected); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/test/extensions/generateInstanceId.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as sinon from "sinon"; 3 | 4 | import { FirebaseError } from "../../error"; 5 | import { generateInstanceId } from "../../extensions/generateInstanceId"; 6 | import * as extensionsApi from "../../extensions/extensionsApi"; 7 | 8 | const TEST_NAME = "image-resizer"; 9 | 10 | describe("generateInstanceId", () => { 11 | let getInstanceStub: sinon.SinonStub; 12 | 13 | beforeEach(() => { 14 | getInstanceStub = sinon.stub(extensionsApi, "getInstance"); 15 | }); 16 | 17 | afterEach(() => { 18 | getInstanceStub.restore(); 19 | }); 20 | 21 | it("should return extensionSpec.name if no extension with that name exists yet", async () => { 22 | getInstanceStub.resolves({ error: { code: 404 } }); 23 | 24 | const instanceId = await generateInstanceId("proj", TEST_NAME); 25 | expect(instanceId).to.equal(TEST_NAME); 26 | }); 27 | 28 | it("should return extensionSpec.name plus a random string if a extension with that name exists", async () => { 29 | getInstanceStub.resolves({ name: TEST_NAME }); 30 | 31 | const instanceId = await generateInstanceId("proj", TEST_NAME); 32 | expect(instanceId).to.include(TEST_NAME); 33 | expect(instanceId.length).to.equal(TEST_NAME.length + 5); 34 | }); 35 | 36 | it("should throw if it gets an unexpected error response from getInstance", async () => { 37 | getInstanceStub.resolves({ error: { code: 500 } }); 38 | 39 | await expect(generateInstanceId("proj", TEST_NAME)).to.be.rejectedWith( 40 | FirebaseError, 41 | "Unexpected error when generating instance ID:" 42 | ); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/test/extensions/populatePostinstall.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { populatePostinstall } from "../../extensions/populatePostinstall"; 4 | 5 | describe("populatePostinstallInstructions", () => { 6 | const instructions = "These are instructions, param foo is ${param:FOO}"; 7 | const params = { FOO: "bar" }; 8 | 9 | it("should substitute user-provided params into instructions", async () => { 10 | const result = await populatePostinstall(instructions, params); 11 | const expected = "These are instructions, param foo is bar"; 12 | expect(result).to.include(expected); 13 | }); 14 | 15 | it("should ignore substitutions that don't have user-provided params", async () => { 16 | const result = await populatePostinstall(instructions, {}); 17 | const expected = "These are instructions, param foo is ${param:FOO}"; 18 | expect(result).to.include(expected); 19 | }); 20 | 21 | it("should substitute all occurrences of substitution markers", async () => { 22 | const result = await populatePostinstall(instructions + " " + instructions, params); 23 | const expected = 24 | "These are instructions, param foo is bar These are instructions, param foo is bar"; 25 | expect(result).to.include(expected); 26 | }); 27 | 28 | it("should ignore user provided-params the don't appear in the instructions", async () => { 29 | const moreParams = { FOO: "bar", BAR: "foo" }; 30 | const result = await populatePostinstall(instructions, params); 31 | const expected = "These are instructions, param foo is bar"; 32 | expect(result).to.include(expected); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/test/extractTriggers.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var chai = require("chai"); 4 | var expect = chai.expect; 5 | 6 | var extractTriggers = require("../extractTriggers"); 7 | 8 | describe("extractTriggers", function() { 9 | var fnWithTrigger = function() {}; 10 | fnWithTrigger.__trigger = { service: "function.with.trigger" }; 11 | var fnWithoutTrigger = function() {}; 12 | var triggers; 13 | 14 | beforeEach(function() { 15 | triggers = []; 16 | }); 17 | 18 | it("should find exported functions with __trigger", function() { 19 | extractTriggers( 20 | { 21 | foo: fnWithTrigger, 22 | bar: fnWithoutTrigger, 23 | baz: fnWithTrigger, 24 | }, 25 | triggers 26 | ); 27 | 28 | expect(triggers.length).to.eq(2); 29 | }); 30 | 31 | it("should attach name and entryPoint to exported triggers", function() { 32 | extractTriggers( 33 | { 34 | foo: fnWithTrigger, 35 | }, 36 | triggers 37 | ); 38 | expect(triggers[0].name).to.eq("foo"); 39 | expect(triggers[0].entryPoint).to.eq("foo"); 40 | }); 41 | 42 | it("should find nested functions and set name and entryPoint", function() { 43 | extractTriggers( 44 | { 45 | foo: { 46 | bar: fnWithTrigger, 47 | baz: { 48 | qux: fnWithTrigger, 49 | not: fnWithoutTrigger, 50 | }, 51 | }, 52 | baz: fnWithTrigger, 53 | }, 54 | triggers 55 | ); 56 | 57 | expect(triggers[0].name).to.eq("foo-bar"); 58 | expect(triggers[0].entryPoint).to.eq("foo.bar"); 59 | expect(triggers.length).to.eq(3); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/test/firestore/encodeFirestoreValue.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import { FirebaseError } from "../../error"; 4 | import { encodeFirestoreValue } from "../../firestore/encodeFirestoreValue"; 5 | 6 | describe("encodeFirestoreValue", () => { 7 | it("should encode known types", () => { 8 | const d = new Date(); 9 | const tests = [ 10 | { in: { foo: "str" }, res: { foo: { stringValue: "str" } } }, 11 | { in: { foo: true }, res: { foo: { booleanValue: true } } }, 12 | { in: { foo: 1 }, res: { foo: { integerValue: 1 } } }, 13 | { in: { foo: 3.14 }, res: { foo: { doubleValue: 3.14 } } }, 14 | { in: { foo: d }, res: { foo: { timestampValue: d.toISOString() } } }, 15 | { 16 | in: { foo: ["str", true] }, 17 | res: { foo: { arrayValue: { values: [{ stringValue: "str" }, { booleanValue: true }] } } }, 18 | }, 19 | { in: { foo: null }, res: { foo: { nullValue: "NULL_VALUE" } } }, 20 | { in: { foo: Buffer.from("buffer") }, res: { foo: { bytesValue: Buffer.from("buffer") } } }, 21 | { 22 | in: { foo: { nested: true } }, 23 | res: { foo: { mapValue: { fields: { nested: { booleanValue: true } } } } }, 24 | }, 25 | ]; 26 | 27 | for (const test of tests) { 28 | expect(encodeFirestoreValue(test.in)).to.deep.equal(test.res); 29 | } 30 | }); 31 | 32 | it("should throw an error with unknown types", () => { 33 | const tests = [ 34 | { in: { foo: undefined } }, 35 | { in: { foo: process.stdout } }, 36 | { in: { foo: /regex/ } }, 37 | ]; 38 | 39 | for (const test of tests) { 40 | expect(() => encodeFirestoreValue(test.in)).to.throw(FirebaseError); 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/test/fixtures/config-imports/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": "hosting.json", 3 | "rules": "rules.json" 4 | } 5 | -------------------------------------------------------------------------------- /src/test/fixtures/config-imports/hosting.json: -------------------------------------------------------------------------------- 1 | { 2 | // this is a comment, deal with it 3 | "public": ".", 4 | "ignore": ["**/.*"], 5 | "extra": true 6 | } 7 | -------------------------------------------------------------------------------- /src/test/fixtures/config-imports/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | ".read": false, 3 | ".write": false 4 | } 5 | -------------------------------------------------------------------------------- /src/test/fixtures/config-imports/unsupported.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google-oss-bot/firebase-tools/5e501afd85a4d65cea68b330ea9c7a1d0eca8353/src/test/fixtures/config-imports/unsupported.txt -------------------------------------------------------------------------------- /src/test/fixtures/dup-top-level/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firebase":"random", 3 | "rules":"rules.json" 4 | } 5 | -------------------------------------------------------------------------------- /src/test/fixtures/dup-top-level/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/test/fixtures/fbrc/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "top": true, 3 | "projects": { 4 | "default": "top", 5 | "other": "top" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/fixtures/fbrc/conflict/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "conflict", 4 | "unconflicted": "conflict" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/test/fixtures/fbrc/firebase.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/test/fixtures/fbrc/invalid/.firebaserc: -------------------------------------------------------------------------------- 1 | {{INVALID JSON}} 2 | -------------------------------------------------------------------------------- /src/test/fixtures/fbrc/invalid/firebase.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/test/fixtures/ignores/.hiddenfile: -------------------------------------------------------------------------------- 1 | THIS SHOULD BE IGNORED 2 | -------------------------------------------------------------------------------- /src/test/fixtures/ignores/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": ".", 4 | "ignore": [ 5 | "ignored.txt", 6 | "ignored/**/*.txt" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/test/fixtures/ignores/ignored.txt: -------------------------------------------------------------------------------- 1 | THIS SHOULD BE IGNORED 2 | -------------------------------------------------------------------------------- /src/test/fixtures/ignores/ignored/deeper/index.txt: -------------------------------------------------------------------------------- 1 | THIS SHOULD BE IGNORED 2 | -------------------------------------------------------------------------------- /src/test/fixtures/ignores/ignored/ignore.txt: -------------------------------------------------------------------------------- 1 | THIS SHOULD BE IGNORED 2 | -------------------------------------------------------------------------------- /src/test/fixtures/ignores/ignored/index.html: -------------------------------------------------------------------------------- 1 | THIS SHOULD NOT BE IGNORED 2 | -------------------------------------------------------------------------------- /src/test/fixtures/ignores/index.html: -------------------------------------------------------------------------------- 1 | THIS SHOULD NOT BE IGNORED 2 | -------------------------------------------------------------------------------- /src/test/fixtures/ignores/present/index.html: -------------------------------------------------------------------------------- 1 | THIS SHOULD NOT BE IGNORED 2 | -------------------------------------------------------------------------------- /src/test/fixtures/invalid-config/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firebase": "myfirebase", 3 | "ignore": [ 4 | "firebase.json", 5 | "**/.*", 6 | "**/node_modules/**" 7 | ], 8 | "rules": "config/security-rules.json", 9 | "redirects": [ { 10 | "source" : "/foo", 11 | "destination" : "/bar", 12 | "type" : 301 13 | }, { 14 | "source" : "/firebase/*", 15 | "destination" : "https://firebase.google.com", 16 | "type" : 302 17 | } ], 18 | "rewrites": [ { 19 | "source": "**", 20 | "destination": "/index.html" 21 | } ], 22 | "headers": [ { 23 | "source" : "**/*.@(eot|otf|ttf|ttc|woff|font.css)", 24 | "headers" : [ { 25 | "key" : "Access-Control-Allow-Origin", 26 | "value" : "*" 27 | } ] 28 | }, { 29 | "source" : "**/*.@(jpg|jpeg|gif|png)", 30 | "headers" : [ { 31 | "key" : "Cache-Control", 32 | "value" : "max-age=7200" 33 | } ] 34 | }, { 35 | "source" : "404.html", 36 | "headers" : [ { 37 | "key" : "Cache-Control", 38 | "value" : "max-age=300" 39 | } ] 40 | } ] 41 | } 42 | -------------------------------------------------------------------------------- /src/test/fixtures/rulesDeploy/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "storage": { 3 | "rules": "storage.rules" 4 | }, 5 | "firestore": { 6 | "rules": "firestore.rules", 7 | "indexes": "firestore.indexes.json" 8 | } 9 | } -------------------------------------------------------------------------------- /src/test/fixtures/rulesDeploy/firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore {{ 2 | match /databases/{database}/documents { 3 | match /{document=**} { 4 | allow read, write: if false; 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/test/fixtures/rulesDeploy/storage.rules: -------------------------------------------------------------------------------- 1 | service firebase.storage { 2 | match /b/{bucket}/o { 3 | match /{allPaths=**} { 4 | allow read, write: if request.auth != null; 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/test/fixtures/valid-config/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firebase": "myfirebase", 3 | "public": "app", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rules": "config/security-rules.json", 10 | "redirects": [ { 11 | "source" : "/foo", 12 | "destination" : "/bar", 13 | "type" : 301 14 | }, { 15 | "source" : "/firebase/*", 16 | "destination" : "https://firebase.google.com", 17 | "type" : 302 18 | } ], 19 | "rewrites": [ { 20 | "source": "**", 21 | "destination": "/index.html" 22 | } ], 23 | "headers": [ { 24 | "source" : "**/*.@(eot|otf|ttf|ttc|woff|font.css)", 25 | "headers" : [ { 26 | "key" : "Access-Control-Allow-Origin", 27 | "value" : "*" 28 | } ] 29 | }, { 30 | "source" : "**/*.@(jpg|jpeg|gif|png)", 31 | "headers" : [ { 32 | "key" : "Cache-Control", 33 | "value" : "max-age=7200" 34 | } ] 35 | }, { 36 | "source" : "404.html", 37 | "headers" : [ { 38 | "key" : "Cache-Control", 39 | "value" : "max-age=300" 40 | } ] 41 | } ] 42 | } 43 | -------------------------------------------------------------------------------- /src/test/fsutils.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import * as fsutils from "../fsutils"; 4 | 5 | describe("fsutils", () => { 6 | describe("fileExistsSync", () => { 7 | it("should return true if the file exists", () => { 8 | expect(fsutils.fileExistsSync(__filename)).to.be.true; 9 | }); 10 | 11 | it("should return false if the file does not exist", () => { 12 | expect(fsutils.fileExistsSync(`${__filename}/nope.never`)).to.be.false; 13 | }); 14 | 15 | it("should return false if the path is a directory", () => { 16 | expect(fsutils.fileExistsSync(__dirname)).to.be.false; 17 | }); 18 | }); 19 | 20 | describe("dirExistsSync", () => { 21 | it("should return true if the directory exists", () => { 22 | expect(fsutils.dirExistsSync(__dirname)).to.be.true; 23 | }); 24 | 25 | it("should return false if the directory does not exist", () => { 26 | expect(fsutils.dirExistsSync(`${__dirname}/nope/never`)).to.be.false; 27 | }); 28 | 29 | it("should return false if the path is a file", () => { 30 | expect(fsutils.dirExistsSync(__filename)).to.be.false; 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/test/functionsConfig.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var chai = require("chai"); 4 | var expect = chai.expect; 5 | 6 | var functionsConfig = require("../functionsConfig"); 7 | 8 | describe("config.parseSetArgs", function() { 9 | it("should throw if a reserved namespace is used", function() { 10 | expect(function() { 11 | functionsConfig.parseSetArgs(["firebase.something=else"]); 12 | }).to.throw("reserved namespace"); 13 | }); 14 | 15 | it("should throw if a malformed arg is used", function() { 16 | expect(function() { 17 | functionsConfig.parseSetArgs(["foo.bar=baz", "qux"]); 18 | }).to.throw("must be in key=val format"); 19 | }); 20 | 21 | it("should parse args into correct config and variable IDs", function() { 22 | expect(functionsConfig.parseSetArgs(["foo.bar.faz=val"])).to.deep.eq([ 23 | { 24 | configId: "foo", 25 | varId: "bar/faz", 26 | val: "val", 27 | }, 28 | ]); 29 | }); 30 | }); 31 | 32 | describe("config.parseUnsetArgs", function() { 33 | it("should throw if a reserved namespace is used", function() { 34 | expect(function() { 35 | functionsConfig.parseUnsetArgs(["firebase.something"]); 36 | }).to.throw("reserved namespace"); 37 | }); 38 | 39 | it("should parse args into correct config and variable IDs", function() { 40 | expect(functionsConfig.parseUnsetArgs(["foo.bar.faz"])).to.deep.eq([ 41 | { 42 | configId: "foo", 43 | varId: "bar/faz", 44 | }, 45 | ]); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/test/helpers/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var auth = require("../../auth"); 4 | 5 | exports.mockAuth = function(sandbox) { 6 | var authMock = sandbox.mock(auth); 7 | authMock 8 | .expects("getAccessToken") 9 | .atLeast(1) 10 | .resolves({ access_token: "an_access_token" }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/test/helpers/mocha-bootstrap.js: -------------------------------------------------------------------------------- 1 | const chai = require("chai"); 2 | const chaiAsPromised = require("chai-as-promised"); 3 | const sinonChai = require("sinon-chai"); 4 | 5 | chai.use(chaiAsPromised); 6 | chai.use(sinonChai); 7 | 8 | process.on("unhandledRejection", (error) => { 9 | throw error; 10 | }); 11 | -------------------------------------------------------------------------------- /src/test/identifierToProjectId.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var chai = require("chai"); 4 | var expect = chai.expect; 5 | var sinon = require("sinon"); 6 | 7 | var helpers = require("./helpers"); 8 | var identifierToProjectId = require("../identifierToProjectId"); 9 | var api = require("../api"); 10 | 11 | describe("identifierToProjectId", function() { 12 | var sandbox; 13 | var mockApi; 14 | 15 | beforeEach(function() { 16 | sandbox = sinon.createSandbox(); 17 | helpers.mockAuth(sandbox); 18 | mockApi = sandbox.mock(api); 19 | }); 20 | 21 | afterEach(function() { 22 | sandbox.restore(); 23 | }); 24 | 25 | it("should return a project id if there is an exact match", function() { 26 | mockApi.expects("getProjects").resolves({ foobar: {} }); 27 | return expect(identifierToProjectId("foobar")).to.eventually.equal("foobar"); 28 | }); 29 | 30 | it("should return an instance if one is a match", function() { 31 | mockApi.expects("getProjects").resolves({ 32 | foo: { instances: { database: ["bar"] } }, 33 | }); 34 | return expect(identifierToProjectId("bar")).to.eventually.equal("foo"); 35 | }); 36 | 37 | it("should return null if no match is found", function() { 38 | mockApi.expects("getProjects").resolves({ 39 | foo: { instances: { database: ["bar"] } }, 40 | }); 41 | return expect(identifierToProjectId("nope")).to.eventually.be.null; 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/test/init/features/firestore.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as _ from "lodash"; 3 | import * as sinon from "sinon"; 4 | 5 | import { FirebaseError } from "../../../error"; 6 | import * as firestore from "../../../init/features/firestore"; 7 | import * as indexes from "../../../init/features/firestore/indexes"; 8 | import * as rules from "../../../init/features/firestore/rules"; 9 | import * as requirePermissions from "../../../requirePermissions"; 10 | 11 | describe("firestore", () => { 12 | const sandbox: sinon.SinonSandbox = sinon.createSandbox(); 13 | 14 | afterEach(() => { 15 | sandbox.restore(); 16 | }); 17 | 18 | describe("doSetup", () => { 19 | it("should require access, set up rules and indices, ensure cloud resource location set", async () => { 20 | const requirePermissionsStub = sandbox 21 | .stub(requirePermissions, "requirePermissions") 22 | .resolves(); 23 | const initIndexesStub = sandbox.stub(indexes, "initIndexes").resolves(); 24 | const initRulesStub = sandbox.stub(rules, "initRules").resolves(); 25 | const setup = { config: {}, projectId: "my-project-123", projectLocation: "us-central1" }; 26 | 27 | await firestore.doSetup(setup, {}); 28 | 29 | expect(requirePermissionsStub).to.have.been.calledOnce; 30 | expect(initRulesStub).to.have.been.calledOnce; 31 | expect(initIndexesStub).to.have.been.calledOnce; 32 | expect(_.get(setup, "config.firestore")).to.deep.equal({}); 33 | }); 34 | 35 | it("should error when cloud resource location is not set", async () => { 36 | const setup = { config: {}, projectId: "my-project-123" }; 37 | 38 | expect(firestore.doSetup(setup, {})).to.eventually.be.rejectedWith( 39 | FirebaseError, 40 | "Cloud resource location is not set" 41 | ); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/test/init/features/storage.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as _ from "lodash"; 3 | import * as sinon from "sinon"; 4 | 5 | import { FirebaseError } from "../../../error"; 6 | import * as Config from "../../../config"; 7 | import { doSetup } from "../../../init/features/storage"; 8 | import * as prompt from "../../../prompt"; 9 | 10 | describe("storage", () => { 11 | const sandbox: sinon.SinonSandbox = sinon.createSandbox(); 12 | let writeProjectFileStub: sinon.SinonStub; 13 | let promptStub: sinon.SinonStub; 14 | 15 | beforeEach(() => { 16 | writeProjectFileStub = sandbox.stub(Config.prototype, "writeProjectFile"); 17 | promptStub = sandbox.stub(prompt, "promptOnce"); 18 | }); 19 | 20 | afterEach(() => { 21 | sandbox.restore(); 22 | }); 23 | 24 | describe("doSetup", () => { 25 | it("should set up the correct properties in the project", async () => { 26 | const setup = { 27 | config: {}, 28 | rcfile: {}, 29 | projectId: "my-project-123", 30 | projectLocation: "us-central", 31 | }; 32 | promptStub.returns("storage.rules"); 33 | writeProjectFileStub.resolves(); 34 | 35 | await doSetup(setup, new Config("/path/to/src", {})); 36 | 37 | expect(_.get(setup, "config.storage.rules")).to.deep.equal("storage.rules"); 38 | }); 39 | 40 | it("should error when cloud resource location is not set", async () => { 41 | const setup = { 42 | config: {}, 43 | rcfile: {}, 44 | projectId: "my-project-123", 45 | }; 46 | 47 | expect(doSetup(setup, new Config("/path/to/src", {}))).to.eventually.be.rejectedWith( 48 | FirebaseError, 49 | "Cloud resource location is not set" 50 | ); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/test/listFiles.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { resolve } from "path"; 3 | 4 | import { listFiles } from "../listFiles"; 5 | 6 | describe("listFiles", () => { 7 | // for details, see the file structure and firebase.json in test/fixtures/ignores 8 | it("should ignore firebase-debug.log, specified ignores, and nothing else", () => { 9 | const fileNames = listFiles(resolve(__dirname, "./fixtures/ignores"), [ 10 | "**/.*", 11 | "firebase.json", 12 | "ignored.txt", 13 | "ignored/**/*.txt", 14 | ]); 15 | expect(fileNames).to.deep.equal(["index.html", "ignored/index.html", "present/index.html"]); 16 | }); 17 | 18 | it("should allow us to not specify additional ignores", () => { 19 | const fileNames = listFiles(resolve(__dirname, "./fixtures/ignores")); 20 | expect(fileNames.sort()).to.have.members([ 21 | ".hiddenfile", 22 | "firebase.json", 23 | "ignored.txt", 24 | "ignored/deeper/index.txt", 25 | "ignored/ignore.txt", 26 | "ignored/index.html", 27 | "index.html", 28 | "present/index.html", 29 | ]); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/test/throttler/queue.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import Queue from "../../throttler/queue"; 4 | import { createHandler, createTask, Task } from "./throttler.spec"; 5 | 6 | describe("Queue", () => { 7 | it("should have default name of queue", () => { 8 | const queue = new Queue({}); 9 | expect(queue.name).to.equal("queue"); 10 | }); 11 | 12 | it("should be first-in-first-out", async () => { 13 | const order: string[] = []; 14 | const queue = new Queue({ 15 | handler: createHandler(order), 16 | concurrency: 1, 17 | }); 18 | 19 | const blocker = await createTask("blocker", false); 20 | queue.add(blocker); 21 | queue.add(await createTask("1", true)); 22 | queue.add(await createTask("2", true)); 23 | 24 | blocker.resolve(); 25 | 26 | queue.close(); 27 | await queue.wait(); 28 | expect(order).to.deep.equal(["blocker", "1", "2"]); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/test/throttler/stack.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import Stack from "../../throttler/stack"; 4 | import { createHandler, createTask, Task } from "./throttler.spec"; 5 | 6 | describe("Stack", () => { 7 | it("should have default name of stack", () => { 8 | const stack = new Stack({}); 9 | expect(stack.name).to.equal("stack"); 10 | }); 11 | 12 | it("should be first-in-last-out", async () => { 13 | const order: string[] = []; 14 | const queue = new Stack({ 15 | handler: createHandler(order), 16 | concurrency: 1, 17 | }); 18 | 19 | const blocker = await createTask("blocker", false); 20 | queue.add(blocker); 21 | queue.add(await createTask("1", true)); 22 | queue.add(await createTask("2", true)); 23 | 24 | blocker.resolve(); 25 | 26 | queue.close(); 27 | await queue.wait(); 28 | expect(order).to.deep.equal(["blocker", "2", "1"]); 29 | }); 30 | 31 | it("should not repeat completed tasks", async () => { 32 | const order: string[] = []; 33 | const queue = new Stack({ 34 | handler: createHandler(order), 35 | concurrency: 1, 36 | }); 37 | 38 | const t1 = await createTask("t1", false); 39 | queue.add(t1); 40 | const t2 = await createTask("t2", false); 41 | queue.add(t2); 42 | 43 | queue.add(await createTask("added before t1 finished a", true)); 44 | queue.add(await createTask("added before t1 finished b", true)); 45 | t1.resolve(); 46 | 47 | await t2.startExecutePromise; // wait until t2 starts to execute 48 | 49 | queue.add(await createTask("added before t2 finished a", true)); 50 | queue.add(await createTask("added before t2 finished b", true)); 51 | t2.resolve(); 52 | 53 | queue.close(); 54 | await queue.wait(); 55 | expect(order).to.deep.equal([ 56 | "t1", 57 | "added before t1 finished b", 58 | "added before t1 finished a", 59 | "t2", 60 | "added before t2 finished b", 61 | "added before t2 finished a", 62 | ]); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/throttler/errors/retries-exhausted-error.ts: -------------------------------------------------------------------------------- 1 | import TaskError from "./task-error"; 2 | 3 | export default class RetriesExhaustedError extends TaskError { 4 | constructor(taskName: string, totalRetries: number, lastTrialError: Error) { 5 | super(taskName, `retries exhausted after ${totalRetries + 1} attempts`, { 6 | original: lastTrialError, 7 | }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/throttler/errors/task-error.ts: -------------------------------------------------------------------------------- 1 | import { FirebaseError } from "../../error"; 2 | 3 | export default abstract class TaskError extends FirebaseError { 4 | constructor(taskName: string, message: string, options: object = {}) { 5 | super(`Task ${taskName} failed: ${message}`, options); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/throttler/errors/timeout-error.ts: -------------------------------------------------------------------------------- 1 | import TaskError from "./task-error"; 2 | 3 | export default class TimeoutError extends TaskError { 4 | constructor(taskName: string, timeout: number) { 5 | super(taskName, `timed out after ${timeout}ms.`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/throttler/queue.ts: -------------------------------------------------------------------------------- 1 | import { Throttler, ThrottlerOptions } from "./throttler"; 2 | 3 | export class Queue extends Throttler { 4 | cursor: number = 0; 5 | 6 | constructor(options: ThrottlerOptions) { 7 | super(options); 8 | this.name = this.name || "queue"; 9 | } 10 | 11 | hasWaitingTask(): boolean { 12 | return this.cursor !== this.total; 13 | } 14 | 15 | nextWaitingTaskIndex(): number { 16 | if (this.cursor >= this.total) { 17 | throw new Error("There is no more task in queue"); 18 | } 19 | this.cursor++; 20 | return this.cursor - 1; 21 | } 22 | } 23 | 24 | export default Queue; 25 | -------------------------------------------------------------------------------- /src/throttler/stack.ts: -------------------------------------------------------------------------------- 1 | import { Throttler, ThrottlerOptions } from "./throttler"; 2 | 3 | export class Stack extends Throttler { 4 | lastTotal: number = 0; 5 | stack: number[] = []; 6 | 7 | constructor(options: ThrottlerOptions) { 8 | super(options); 9 | this.name = this.name || "stack"; 10 | } 11 | 12 | hasWaitingTask(): boolean { 13 | return this.lastTotal !== this.total || this.stack.length > 0; 14 | } 15 | 16 | nextWaitingTaskIndex(): number { 17 | while (this.lastTotal < this.total) { 18 | this.stack.push(this.lastTotal); 19 | this.lastTotal++; 20 | } 21 | const next = this.stack.pop(); 22 | if (next === undefined) { 23 | throw new Error("There is no more task in stack"); 24 | } 25 | return next; 26 | } 27 | } 28 | 29 | export default Stack; 30 | -------------------------------------------------------------------------------- /src/track.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var ua = require("universal-analytics"); 4 | 5 | var _ = require("lodash"); 6 | var configstore = require("./configstore"); 7 | var pkg = require("../package.json"); 8 | var uuid = require("uuid"); 9 | var logger = require("./logger"); 10 | 11 | var anonId = configstore.get("analytics-uuid"); 12 | if (!anonId) { 13 | anonId = uuid.v4(); 14 | configstore.set("analytics-uuid", anonId); 15 | } 16 | 17 | var visitor = ua(process.env.FIREBASE_ANALYTICS_UA || "UA-29174744-3", anonId, { 18 | strictCidFormat: false, 19 | https: true, 20 | }); 21 | 22 | visitor.set("cd1", process.platform); // Platform 23 | visitor.set("cd2", process.version); // NodeVersion 24 | visitor.set("cd3", process.env.FIREPIT_VERSION || "none"); // FirepitVersion 25 | 26 | module.exports = function(action, label, duration) { 27 | return new Promise(function(resolve) { 28 | if (!_.isString(action) || !_.isString(label)) { 29 | logger.debug("track received non-string arguments:", action, label); 30 | resolve(); 31 | } 32 | duration = duration || 0; 33 | 34 | if (configstore.get("tokens") && configstore.get("usage")) { 35 | visitor.event("Firebase CLI " + pkg.version, action, label, duration).send(function() { 36 | // we could handle errors here, but we won't 37 | resolve(); 38 | }); 39 | } else { 40 | resolve(); 41 | } 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/types/marked-terminal/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "marked-terminal" { 2 | import * as marked from "marked"; 3 | 4 | class TerminalRenderer extends marked.Renderer { 5 | constructor(options?: marked.MarkedOptions); 6 | } 7 | 8 | export = TerminalRenderer; 9 | } 10 | -------------------------------------------------------------------------------- /src/validateJsonRules.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var cjson = require("cjson"); 4 | var _ = require("lodash"); 5 | 6 | module.exports = function(rules) { 7 | var parsed = cjson.parse(rules); 8 | return _.has(parsed, "rules"); 9 | }; 10 | -------------------------------------------------------------------------------- /src/validator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var JSONSchema = require("jsonschema"); 4 | var jsonschema = new JSONSchema.Validator(); 5 | var request = require("request"); 6 | 7 | var { FirebaseError } = require("./error"); 8 | 9 | var NAMED_SCHEMAS = { 10 | firebase: 11 | "https://gist.githubusercontent.com/mbleigh/6040df46f12f349889b2/raw/1c11a6e00a7295c84508dca80f2c92b00ba44006/firebase-schema.json", 12 | }; 13 | 14 | var Validator = function(url) { 15 | this._validateQueue = []; 16 | 17 | var self = this; 18 | request.get(url, function(err, response, body) { 19 | if (!err && response.statusCode === 200) { 20 | self.schema = JSON.parse(body); 21 | self._process(); 22 | } 23 | }); 24 | }; 25 | 26 | Validator.prototype.validate = function(data) { 27 | var self = this; 28 | return new Promise(function(resolve, reject) { 29 | self._validateQueue.push({ 30 | data: data, 31 | resolve: resolve, 32 | reject: reject, 33 | }); 34 | self._process(); 35 | }); 36 | }; 37 | 38 | Validator.prototype._process = function() { 39 | if (!this.schema) { 40 | return; 41 | } 42 | while (this._validateQueue.length) { 43 | var item = this._validateQueue.shift(); 44 | var result = jsonschema.validate(item.data, this.schema); 45 | 46 | var err = new FirebaseError("Your document has validation errors", { 47 | children: this._decorateErrors(result.errors), 48 | exit: 2, 49 | }); 50 | 51 | if (result.valid) { 52 | item.resolve(); 53 | } else { 54 | item.reject(err); 55 | } 56 | } 57 | }; 58 | 59 | Validator.prototype._decorateErrors = function(errors) { 60 | errors.forEach(function(error) { 61 | error.name = error.property.replace(/^instance/, "root"); 62 | }); 63 | return errors; 64 | }; 65 | 66 | for (var name in NAMED_SCHEMAS) { 67 | if ({}.hasOwnProperty.call(NAMED_SCHEMAS, name)) { 68 | Validator[name] = new Validator(NAMED_SCHEMAS[name]); 69 | } 70 | } 71 | 72 | module.exports = Validator; 73 | -------------------------------------------------------------------------------- /standalone/check.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file is used in firepit.js#VerifyNodePath() to test if the binary file is acting as a Node.js 3 | runtime or not. If it is acting as a runtime, then this script will return a checkmark, otherwise 4 | Firepit will respond with a non-checkmark log. 5 | */ 6 | console.log("✓"); 7 | -------------------------------------------------------------------------------- /standalone/config.template.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /* 3 | Headless mode forces the firepit builds to exactly imitate firebase-tools, 4 | so the resulting binary "firebase" is a drop in replacement for the script 5 | installed via npm. This is the behavior for CI / Cloud Shell / Docker etc. 6 | 7 | When headless mode is disabled, the "double click" experience is enabled 8 | which allows the binary to spawn a terminal on Windows and Mac. The is the 9 | behavior for desktop users. 10 | */ 11 | headless: false, 12 | 13 | /* 14 | This is generally set to "firebase-tools@latest" however a custom value 15 | can be supplied for EAPs which would like to have builds pointed at 16 | specific tgz bundles. 17 | */ 18 | firebase_tools_package: "" 19 | }; 20 | -------------------------------------------------------------------------------- /standalone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firepit", 3 | "version": "1.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "fmt": "prettier --write *.js", 8 | "pkg": "pkg -c package.json firepit.js --out-path dist/ && shx chmod +x dist/firepit-*", 9 | "ship": "gsutil -m cp dist/* gs://fir-tools-builds/firepit/ && gsutil iam ch allUsers:objectViewer gs://fir-tools-builds" 10 | }, 11 | "author": "", 12 | "license": "MIT", 13 | "dependencies": { 14 | "chalk": "^2.4.2", 15 | "npm": "^6.10.2", 16 | "shelljs": "^0.8.3", 17 | "shx": "^0.3.2", 18 | "user-home": "^2.0.0" 19 | }, 20 | "pkg": { 21 | "scripts": [ 22 | "node_modules/npm/src/**/*.js" 23 | ], 24 | "assets": [ 25 | "node_modules/.bin/**", 26 | "node_modules/npm/bin/**/*", 27 | "node_modules/npm/node_modules/node-gyp/**/*", 28 | "vendor/**" 29 | ] 30 | }, 31 | "devDependencies": { 32 | "pkg": "^4.4.2", 33 | "prettier": "^1.15.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /templates/_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 | -------------------------------------------------------------------------------- /templates/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | ######## #### ######## ######## ######## ### ###### ######## 3 | ## ## ## ## ## ## ## ## ## ## ## 4 | ###### ## ######## ###### ######## ######### ###### ###### 5 | ## ## ## ## ## ## ## ## ## ## ## 6 | ## #### ## ## ######## ######## ## ## ###### ######## 7 | -------------------------------------------------------------------------------- /templates/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firebase": null, 3 | "public": null, 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /templates/hosting/init.js: -------------------------------------------------------------------------------- 1 | if (typeof firebase === 'undefined') throw new Error('hosting/init-error: Firebase SDK not detected. You must include it before /__/firebase/init.js'); 2 | /*--CONFIG--*/ 3 | if (firebaseConfig) { 4 | firebase.initializeApp(firebaseConfig); 5 | } 6 | -------------------------------------------------------------------------------- /templates/init/firestore/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | // Example: 3 | // 4 | // "indexes": [ 5 | // { 6 | // "collectionGroup": "widgets", 7 | // "queryScope": "COLLECTION", 8 | // "fields": [ 9 | // { "fieldPath": "foo", "arrayConfig": "CONTAINS" }, 10 | // { "fieldPath": "bar", "mode": "DESCENDING" } 11 | // ] 12 | // }, 13 | // 14 | // "fieldOverrides": [ 15 | // { 16 | // "collectionGroup": "widgets", 17 | // "fieldPath": "baz", 18 | // "indexes": [ 19 | // { "order": "ASCENDING", "queryScope": "COLLECTION" } 20 | // ] 21 | // }, 22 | // ] 23 | // ] 24 | "indexes": [], 25 | "fieldOverrides": [] 26 | } -------------------------------------------------------------------------------- /templates/init/firestore/firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | match /{document=**} { 4 | allow read, write; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /templates/init/functions/javascript/_gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /templates/init/functions/javascript/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | 3 | // // Create and Deploy Your First Cloud Functions 4 | // // https://firebase.google.com/docs/functions/write-firebase-functions 5 | // 6 | // exports.helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | -------------------------------------------------------------------------------- /templates/init/functions/javascript/package.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "serve": "firebase serve --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "8" 14 | }, 15 | "dependencies": { 16 | "firebase-admin": "^8.6.0", 17 | "firebase-functions": "^3.3.0" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^5.12.0", 21 | "eslint-plugin-promise": "^4.0.1", 22 | "firebase-functions-test": "^0.1.6" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /templates/init/functions/javascript/package.nolint.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 | "firebase-admin": "^8.6.0", 16 | "firebase-functions": "^3.3.0" 17 | }, 18 | "devDependencies": { 19 | "firebase-functions-test": "^0.1.6" 20 | }, 21 | "private": true 22 | } 23 | -------------------------------------------------------------------------------- /templates/init/functions/typescript/_gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Typescript v1 declaration files 6 | typings/ 7 | 8 | node_modules/ -------------------------------------------------------------------------------- /templates/init/functions/typescript/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | 3 | // // Start writing Firebase Functions 4 | // // https://firebase.google.com/docs/functions/typescript 5 | // 6 | // export const helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | -------------------------------------------------------------------------------- /templates/init/functions/typescript/package.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase serve --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "8" 14 | }, 15 | "main": "lib/index.js", 16 | "dependencies": { 17 | "firebase-admin": "^8.6.0", 18 | "firebase-functions": "^3.3.0" 19 | }, 20 | "devDependencies": { 21 | "tslint": "^5.12.0", 22 | "typescript": "^3.2.2", 23 | "firebase-functions-test": "^0.1.6" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /templates/init/functions/typescript/package.nolint.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 | "firebase-admin": "^8.6.0", 17 | "firebase-functions": "^3.3.0" 18 | }, 19 | "devDependencies": { 20 | "typescript": "^3.2.2", 21 | "firebase-functions-test": "^0.1.6" 22 | }, 23 | "private": true 24 | } 25 | -------------------------------------------------------------------------------- /templates/init/functions/typescript/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 | -------------------------------------------------------------------------------- /templates/init/hosting/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /templates/init/storage/storage.rules: -------------------------------------------------------------------------------- 1 | service firebase.storage { 2 | match /b/{bucket}/o { 3 | match /{allPaths=**} { 4 | allow read, write: if request.auth!=null; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /templates/loginFailure.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Firebase CLI 7 | 8 | 23 | 24 | 25 |
26 |

Oops!

27 |

Firebase CLI Login Failed

28 |

The Firebase CLI login request was rejected or an error occurred. Please 29 | run firebase login again or contact support if you continue 30 | to have difficulty logging in.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /templates/loginSuccess.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Firebase CLI 7 | 8 | 24 | 25 | 26 |
27 |

Woohoo!

28 |

Firebase CLI Login Successful

29 |

You are logged in to the Firebase Command-Line interface. You can immediately close this window and continue using the CLI.

30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /templates/setup/web.js: -------------------------------------------------------------------------------- 1 | // Copy and paste this into your JavaScript code to initialize the Firebase SDK. 2 | // You will also need to load the Firebase SDK. 3 | // See https://firebase.google.com/docs/web/setup for more details. 4 | 5 | firebase.initializeApp({/*--CONFIG--*/}); 6 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": false, 5 | "module": "commonjs", 6 | "strict": true, 7 | "outDir": "dev", 8 | "removeComments": false, 9 | "target": "es2015", 10 | "sourceMap": true, 11 | "typeRoots": [ 12 | "node_modules/@types", 13 | "src/types" 14 | ] 15 | }, 16 | "include": ["scripts/**/*"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": false, 5 | "module": "commonjs", 6 | "strict": true, 7 | "outDir": "lib", 8 | "removeComments": true, 9 | "target": "es2015", 10 | "sourceMap": true, 11 | "typeRoots": [ 12 | "node_modules/@types", 13 | "src/types" 14 | ] 15 | }, 16 | "include": ["src/**/*"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": false, 5 | "module": "commonjs", 6 | "strict": true, 7 | "outDir": "lib", 8 | "removeComments": true, 9 | "target": "es2015", 10 | "sourceMap": false, 11 | "typeRoots": [ 12 | "node_modules/@types", 13 | "src/types" 14 | ] 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["src/test/**/*"] 18 | } 19 | --------------------------------------------------------------------------------