diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index aada22910c..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,649 +0,0 @@ -# Unreleased - -- - -# v8.6.1 - -- - -# v8.6.0 - -- - -# v8.4.0 - -- [added] Added support for specifying the analytics label for - notifications - -# v8.3.0 - -- [added] `admin.database().getRules()` method to retrieve the currently - applied RTDB rules text. -- [added] `admin.database().getRulesJSON()` method to retrieve the currently - applied RTDB rules as a parsed JSON object. -- [added] `admin.database().setRules()` method to update the RTDB rules. - -# v8.2.0 - -- [fixed] Gracefully handling array-like objects in `messaging.sendAll()` and - `messaging.sendMulticast()` APIs. -- [fixed] Updated the metadata server URL (used by the application default credentials) - to the `v1` endpoint. - -# v8.1.0 - -- [added] `admin.projectManagement().listAppMetadata()` method to list the app summary of up to 100 - apps in a Firebase project -- [added] `admin.projectManagement().setDisplayName()` method to update the display name of a - Firebase project -- [fixed] The SDK now automatically retries HTTP calls failing due to 503 errors. - -# v8.0.0 - -- [changed] Dropped support for Node 6. Developers must use Node 8.13.0 or - higher. -- [changed] Upgraded Cloud Firestore client to v2.0.0. - -# v7.4.0 - -- [changed] Upgraded Cloud Firestore client to v1.3.0. - -# v7.3.0 - -- [feature] Added the provider config management APIs for managing OIDC and SAML - provider configurations (CRUD) via - `auth.listProviderConfigs()`, `auth.getProviderConfig()`, - `auth.deleteProviderConfig()`, `auth.updateProviderConfig()` and - `auth.createProviderConfig()`. - -# v7.2.0 - -- [changed] Updated the Google Cloud Firestore client to v1.2.0. This update - exposes the `v1beta` and `v1` clients and provides direct access to the - underlying Firestore and Firestore Admin RPCs. Please note that you will have - to provide your Firebase credentials directly to these clients. - -# v7.1.1 - -- [fixed] Fixed a bug in the FCM batch APIs that prevented them from correctly - handling some message parameters like `AndroidConfig.ttl`. - -# v7.1.0 - -- [added] A new `messaging.sendAll()` API for sending multiple messages as a - single batch. -- [added] A new `messaging.sendMulticast()` API for sending a message to - multiple device registration tokens. -- [fixed] Upgraded Cloud Firestore client version to 1.1.0. -- [fixed] Improved typings of `UpdateRequest` interface to support deletion of - properties. - -# v7.0.0 - -- [changed] Updated the Google Cloud Firestore client to v1.0.1. This contains - breaking changes. Refer to Cloud Firestore - [release notes](https://github.com/googleapis/nodejs-firestore/releases/tag/v0.20.0) - for more details and migration instructions. -- [changed] Updated the Google Cloud Storage client to v2.3.0. This contains - breaking changes. Refer to Cloud Storage - [release notes](https://github.com/googleapis/nodejs-storage/releases/tag/v2.0.0) - for more details and migration instructions. -- [changed] `verifyIdToken()` and `verifySessionCookie()` methods now return - `auth/id-token-expired` and `auth/session-cookie-expired` error codes for - expired JWTs. -- [fixed] Including additional helpful details in the errors thrown due to - credentials-related problems. - -# v6.5.1 - -- [fixed] Implemented a Node.js environment check that will be executed at - package import time. -- [fixed] Setting the `GOOGLE_APPLICATION_CREDENTIALS` environment variable - to a refresh token instead of a service account is now supported. - -# v6.5.0 - -- [fixed] Correctly parses error codes sent by Firebase Auth backend servers. -- [fixed] Correctly marked the optional fields in `UserRecord` types. -- [added] `admin.projectManagement().shaCertificate()` method to create an - instance of admin.projectManagement.ShaCertificate. - -# v6.4.0 - -- [added] `messaging.Aps` type now supports configuring a critical sound. - A new `messaging.CriticalSound` type has been introduced for this purpose. -- [added] `messaging.AndroidNotification` type now supports `channel_id`. -- [added] `AppOptions` now accepts an optional `http.Agent` object. The - `http.Agent` specified via this API is used when the SDK makes backend - HTTP calls. This can be used when it is required to deploy the Admin SDK - behind a proxy. -- [added] `admin.credential.cert()`, `admin.credential.applicationDefault()`, - and `admin.credential.refreshToken()` methods now accept an `http.Agent` - as an optional argument. If specified, the `http.Agent` will be used - when calling Google backend servers to fetch OAuth2 access tokens. - -# v6.3.0 - -- [added] A new `ProjectManagement` service, which includes the ability to - create, list, and get details about Android and iOS apps associated with your - Firebase Project. -- [added] `messaging.ApsAlert` type now supports subtitle in its payload. - -# v6.2.0 - -- [added] Added the email action link generation APIs for creating links for - password reset, email verification and email link sign-in via - `auth.generatePasswordResetLink()`, `auth.generateEmailVerificationLink()` - and `auth.generateSignInWithEmailLink()`. -- [changed] Upgraded Cloud Firestore client to v0.19.0. -- [added] Exposed the `Transaction` type from the `admin.firestore` namespace. -- [fixed] Fixing error handling in FCM. The SDK now checks the key - `type.googleapis.com/google.firebase.fcm.v1.FcmError` to set error code. - -# v6.1.0 - -- [changed] Upgraded Cloud Firestore client to v0.18.0. -- [added] Exposed the `CollectionReference`, `WriteBatch`, `WriteResult` and - `QueryDocumentSnapshot` types from the `admin.firestore` namespace. - -# v6.0.0 - -- [changed] Upgraded Cloud Firestore client to v0.16.0. -- [changed] Firestore and Storage client libraries are now defined as optional - dependencies. -- [changed] Dropped support for Node.js 4. - -# v5.13.1 - -- [changed] Upgraded Cloud Firestore client to v0.15.4. -- [changed] Exposed the Firestore `Timestamp` type from the `admin.firestore` - namespace. - -# v5.13.0 - -- [changed] Admin SDK can now create custom tokens without being initialized - with service account credentials. When a service account private key is not - available, the SDK uses the remote IAM service to sign JWTs in the cloud. -- [changed] Updated the typings of the `admin.database.Query.once()` - method to return a more specific type. -- [changed] Admin SDK can now read the Firebase/GCP project ID from both - `GCLOUD_PROJECT` and `GOOGLE_CLOUD_PROJECT` environment variables. -- [changed] Updated the `WebpushNotification` typings to match - [the current API](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#webpushconfig). -- [changed] Upgraded Cloud Firestore client to v0.15.2. - -# v5.12.1 - -- [changed] Admin SDK now lazy loads all child namespaces and certain heavy - dependencies for faster load times. This change also ensures that only - the sources for namespaces that are actually used get loaded into the - Node.js process. -- [changed] Upgraded Cloud Firestore client to v0.14.0. - -# v5.12.0 - -- [feature] Added the session cookie management APIs for creating and verifying - session cookies, via `auth.createSessionCookie()` and - `auth.verifySessionCookie()`. -- [added] Added the `mutableContent` optional field to the `Aps` type of - the FCM API. -- [added] Added the support for specifying arbitrary custom key-value - fields in the `Aps` type. - -# v5.11.0 - -- [changed] Added the `auth.importUsers()` method for importing users to - Firebase Auth in bulk. - -# v5.10.0 - -- [changed] Upgraded Realtime Database client to v0.2.0. With this upgrade - developers can call the `admin.database().ref()` method with another - `Reference` instance as the argument. -- [changed] Upgraded Cloud Firestore client to v0.13.0. - -# v5.9.1 - -- [changed] The `admin.initializeApp()` method can now be invoked without an - explicit `credential` option. In that case the SDK will get initialized with - Google application default credentials. -- [changed] Upgraded Realtime Database client to v0.1.11. -- [changed] Modified the Realtime Database client integration to report the - correct user agent header. -- [changed] Upgraded Cloud Firestire client to v0.12.0. -- [changed] Improved error handling in FCM by mapping more server-side errors - to client-side error codes. - -# v5.9.0 - -- [added] Added the `messaging.send()` method and the new `Message` type for - sending Cloud Messaging notifications via the - [new FCM REST endpoint](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages). - -# v5.8.2 - -- [changed] Exposed `admin.firestore.DocumentReference` and - `admin.firestore.DocumentSnapshot` types from the Admin SDK typings. -- [changed] Upgraded Firestore dependency version to - [0.11.2](https://github.com/googleapis/nodejs-firestore/releases/tag/v0.11.2). - -# v5.8.1 - -- [changed] Upgraded Firestore dependency version from 0.10.0 to 0.11.1. - This includes several bug fixes in Cloud Firestore. - -# v5.8.0 - -### Initialization - -- [added] The [`admin.initializeApp()`](https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp) - method can now be invoked without any arguments. This initializes an app - using Google Application Default Credentials, and other - [`AppOptions`](https://firebase.google.com/docs/reference/admin/node/admin.app.AppOptions) loaded from - the `FIREBASE_CONFIG` environment variable. - -### Authentication - -- [changed] Upgraded the `jsonwebtoken` library to 8.1.0. - -# v5.7.0 - -### Authentication - -- [added] A new [`revokeRefreshTokens()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#revokeRefreshTokens) - method for revoking refresh tokens issued to a user. -- [added] The [`verifyIdToken()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#verifyIdToken) - method now accepts an optional `checkRevoked` argument, which can be used to - check if a given ID token has been revoked. - -# v5.6.0 - -- [added] A new [`admin.instanceId()`](https://firebase.google.com/docs/reference/admin/node/admin.instanceId) - API that facilitates deleting instance IDs and associated user data from - Firebase projects. -- [changed] Updated the TypeScript typings for `admin.AppOptions` to reflect the - introduction of the `projectId` option. -- [changed] Removed some unused third party dependencies. - -# v5.5.1 - -### Cloud Firestore - -- [changed] Upgraded the Cloud Firestore client to the latest available - version, which adds input validation to several operations, and retry logic - to handle network errors. - -### Realtime Database - -- [changed] Fixed an issue in the TypeScript typings of the Realtime Database API. - -# v5.5.0 - -### Realtime Database - -- [added] [`app.database()`](https://firebase.google.com/docs/reference/admin/node/admin.app.App#database) - method now optionally accepts a database URL. This feature can be used to - access multiple Realtime Database instances from the same app. -- [changed] Upgraded the Realtime Database client to the latest available - version. - -### Cloud Firestore - -- [changed] Upgraded the Cloud Firestore client to the latest available - version. - -# v5.4.3 - -- [changed] Fixed a regression in module loading that prevented using - the Admin SDK in environments like AWS Lambda. This regression was - introduced in the 5.4.0 release, which added a new dependency to Firestore - and gRPC. This fix lazily loads Firestore and gRPC, thus enabling - Admin SDK usage in the affected environments as long as no explicit - attempts are made to use the Firestore API. - - -# v5.4.2 - -- [changed] Upgraded the Cloud Firestore client dependency to 0.8.2, which - resolves an issue with saving objects with nested document references. - -# v5.4.1 - -- [changed] Upgraded the Firestore client dependency to 0.8.1, which resolves - the installation issues reported in the Yarn environment. - -# v5.4.0 - -- [added] A new [`admin.firestore()`](https://firebase.google.com/docs/reference/admin/node/admin.firestore) - API that facilitates accessing [Google Cloud Firestore](https://firebase.google.com/docs/firestore) - databases using the - [`@google-cloud/firestore`](https://cloud.google.com/nodejshttps://firebase.google.com/docs/reference/firestore/latest/) - library. See [Set Up Your Node.js App for Cloud Firestore](https://firebase.google.com/docs/firestore/server/setup-node) - to get started. - -# v5.3.0 - -- [changed] SDK now retries outbound HTTP calls on all low-level I/O errors. - -### Authentication - -- [added] A new [`setCustomUserClaims()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#setCustomUserClaims) - method for setting custom claims on user accounts. Custom claims set via this - method become available on the ID tokens of the corresponding users when they - sign in. To learn how to use this API for controlling access to Firebase - resources, see - [Control Access with Custom Claims and Security Rules](https://firebase.google.com/docs/auth/admin/custom-claims). -- [added] A new [`listUsers()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listUsers) - method for listing all the users in a Firebase project in batches. - -### Storage - -- [changed] Declared a more concrete TypeScript return type (`Bucket`) for the - [`bucket()`](https://firebase.google.com/docs/reference/admin/node/admin.storage.Storage#bucket) method - in the Storage API. - -# v5.2.1 - -- [changed] A bug in the TypeScript type declarations that come bundled with the - SDK (`index.d.ts`) has been fixed. - -# v5.2.0 -- [added] A new [Cloud Storage API](https://firebase.google.com/docs/reference/admin/node/admin.storage) - that facilitates accessing Google Cloud Storage buckets using the - [`@google-cloud/storage`](https://googlecloudplatform.github.io/google-cloud-node/#https://firebase.google.com/docs/storage/latest/storage) - library. - -### Authentication - -- [changed] New type definitions for the arguments of `createUser()` and - `updateUser()` methods. - -### Cloud Messaging - -- [changed] Redefined the arguments of `sendToDevice()` using intersection - instead of overloading. - -# v5.1.0 - -### Authentication - -- [added] Added the method - [`getUserByPhoneNumber()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#getUserByPhoneNumber) - to the [`admin.auth`](https://firebase.google.com/docs/reference/admin/node/admin.auth) interface. This method - enables retrieving user profile information by a phone number. -- [added] [`createUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createUser) - and [`updateUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#updateUser) methods - now accept a `phoneNumber` property, which can be used to create users with a phone - number field and/or update the phone number associated with a user. -- [added] Added the `phoneNumber` field to - [`admin.auth.UserRecord`](https://firebase.google.com/docs/reference/admin/node/admin.auth.UserRecord), - which exposes the phone number associated with a user account. -- [added] Added the `phoneNumber` field to - [`admin.auth.UserInfo`](https://firebase.google.com/docs/reference/admin/node/admin.auth.UserInfo), - which exposes the phone number associated with a user account by a linked - identity provider. - -# v5.0.1 - -- [changed] Improved the error messages thrown in the case of network and RPC - errors. These errors now include outgoing HTTP request details that make - it easier to localize and debug issues. - -### Authentication - -- [changed] Implemented support in the user management API for handling photo - URLs with special characters. - -# v5.0.0 - -### Initialization - -- [changed] The deprecated `serviceAccount` property in the - [`admin.App.Options`](https://firebase.google.com/docs/reference/admin/node/admin.app.AppOptions) - type has been removed in favor of the `credential` property. -- [changed] Initializing the SDK without setting a credential - results in an exception. -- [changed] Initializing the SDK with a malformed private key string - results in an exception. - -### Authentication - -- [changed] `createdAt` and `lastSignedInAt` properties in - [`admin.auth.UserMetadata`](https://firebase.google.com/docs/reference/admin/node/admin.auth.UserMetadata) - have been renamed to `creationTime` and `lastSignInTime`. Also these - properties now provide UTC formatted strings instead of `Date` values. - -# v4.2.1 - -- [changed] Updated the SDK to periodically refresh the OAuth access token - internally used by `FirebaseApp`. This reduces the number of authentication - failures encountered at runtime by SDK components like Realtime Database. - -# v4.2.0 - -### Cloud Messaging - -- [added] Added the methods - [`subscribeToTopic()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#subscribeToTopic) - and - [`unsubscribeFromTopic()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#unsubscribeFromTopic) - to the [`admin.messaging()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging) - service. The new methods allow subscribing to and unsubscribing from {{messaging}} - topics via registration tokens. - -# v4.1.4 - -### Authentication - -- [changed] Cleaned up a number of types to improve the log output, thereby - making debugging easier. - -### Realtime Database - -- [changed] Fixed an issue which could cause infinite loops when using `push()` - with no arguments. - -# v4.1.3 - -- [changed] Fixed incorrect usage of `undefined` - as opposed to `void` - in - several places in the TypeScript typings. -- [changed] Added missing properties to the TypeScript typings for - [`DecodedIdToken`](https://firebase.google.com/docs/reference/admin/node/admin.auth.DecodedIdToken). -- [changed] Fixed issues when using some types with the TypeScript - `strictNullChecks` option enabled. -- [changed] Removed incorrect `admin.Promise` type from the TypeScript typings - in favor of the Node.js built-in `Promise` type, which the SDK actually uses. -- [changed] Added error codes to all app-level errors. All errors in the SDK - now properly implement the - [`FirebaseError`](https://firebase.google.com/docs/reference/admin/node/admin.FirebaseError) interface. -- [changed] Improved error handling when initializing the SDK with a credential - that cannot generate valid access tokens. -- [added] Added new `admin.database.EventType` to the TypeScript typings. - -### Realtime Database - -- [changed] Improved how the Realtime Database reports errors when provided with - various types of invalid credentials. - -# v4.1.2 - -### Authentication - -- [changed] Improved input validation and error messages for all user - management methods. -- [changed] - [`verifyIdToken()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#verifyIdToken) - now works with non-cert credentials, assuming the `GCLOUD_PROJECT` environment - variable is set to your project ID, which is the case when running on Google - infrastructure such as Google App Engine and Google Compute Engine. - -### Realtime Database - -- [changed] Added `toJSON()` methods to the `DataSnapshot` and `Query` objects - to make them properly JSON-serializable. - -### Cloud Messaging - -- [changed] Improved response parsing when - [`sendToDevice()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDevice) - and - [`sendToDeviceGroup()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDeviceGroup) - are provided with unexpected inputs. - - -# v4.1.1 - -- [changed] Added in missing TypeScript typings for the `FirebaseError.toJSON()` - method. - -### Authentication - -- [changed] Fixed issue with - [`createUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createUser) - which sometimes caused multiple users to share the same email. - - -# v4.1.0 - -- [changed] Added in missing TypeScript typings for the `toJSON()` method off - of several objects. - -### Cloud Messaging - -- [added] A new - [`admin.messaging()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging) service - allows you to send messages through - [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/admin/). The new service - includes the - [`sendToDevice()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDevice), - [`sendToDeviceGroup()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDeviceGroup), - [`sendToTopic()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToTopic), - and - [`sendToCondition()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToCondition) - methods. - - -# v4.0.6 - -### Initialization - -- [changed] Fixed an issue which caused importing the library via the ES2015 - import syntax (`import * as admin from "firebase-admin"`) to not work - properly. - - -# v4.0.5 - -- [changed] TypeScript support has been greatly improved. Typings for the - Realtime Database are now available and all other known issues with incorrect or - incomplete type information have been resolved. - -### Initialization - -- [changed] Fixed an issue which caused the SDK to appear to hang when provided - with a credential that generated invalid access tokens. The most common cause - of this was using a credential whose access had been revoked. Now, an error - will be logged to the console in this scenario. - -### Authentication - -- [added] The error message for an `auth/internal-error` error now includes - the raw server response to more easily debug and track down unhandled errors. -- [changed] Fixed an issue that caused an `auth/internal-error` error to be - thrown when calling - [`getUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#getUser) or - [`getUserByEmail()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#getUserByEmail) - for a user without a creation date. -- [changed] Fixed an issue which caused an `auth/internal-error` error to be - thrown when calling - [`createUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createUser) with - an email that corresponds to an existing user. -- [changed] Fixed an issue which caused an `auth/internal-error` error to be - thrown when calling Authentication methods with a credential with insufficient - permission. Now, an `auth/insufficient-permission` error will be thrown - instead. - - -# v4.0.4 - -### Authentication - -- [changed] Fixed an issue that caused several Authentication methods to throw - an error when provided with inputs containing Unicode characters. - - -# v4.0.3 - -### Initialization - -- [changed] Fixed an issue that caused a `null` value for the - `databaseAuthVariableOverride` property to be ignored when passed as part - of the first argument to - [`initializeApp()`](https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp), which - caused the app to still have full admin access. Now, passing this value has - the expected behavior: the app has unauthenticated access to the - Realtime Database, and behaves as if no user is logged into the app. - -### Authentication - -- [changed] Fixed an issue that caused an `auth/invalid-uid` error to - be thrown for valid `uid` values passed to several Authentication methods. - - -# v4.0.2 - -- [added] Improved error messages throughout the Admin Node.js SDK. -- [changed] Upgraded dependencies so that the Admin Node.js SDK no longer - throws warnings for using deprecated `Buffer` APIs in Node.js `7.x.x`. - - -# v4.0.1 - -- [changed] Fixed issue which caused the 4.0.0 release to not - include the `README.md` and `npm-shrinkwrap.json` files. - - -# v4.0.0 - -- [added] The Admin Node.js SDK (available on npm as `firebase-admin`) is a - new SDK which replaces and expands the admin capabilities of the standard - `firebase` npm module. See - [Add the Firebase Admin SDK to your Server](https://firebase.google.com/docs/admin/setup/) to get - started. -- [issue] This version does not include the `README.md` and - `npm-shrinkwrap.json` files. This was fixed in version 4.0.1. - -### Initialization - -- [deprecated] The `serviceAccount` property of the options passed as the - first argument to - [`initializeApp()`](https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp) has been - deprecated in favor of a new `credential` property. See - [Initialize the SDK](https://firebase.google.com/docs/admin/setup/#initialize_the_sdk) for more details. -- [added] The new - [`admin.credential.cert()`](https://firebase.google.com/docs/reference/admin/node/admin.credential#.cert) - method allows you to authenticate the SDK with a service account key file. -- [added] The new - [`admin.credential.refreshToken()`](https://firebase.google.com/docs/reference/admin/node/admin.credential#.refreshToken) - method allows you to authenticate the SDK with a Google OAuth2 refresh token. -- [added] The new - [`admin.credential.applicationDefault()`](https://firebase.google.com/docs/reference/admin/node/admin.credential#.applicationDefault) - method allows you to authenticate the SDK with Google Application Default - Credentials. - -### Authentication - -- [added] A new Admin API for managing your Firebase Authentication users is now - available. This API lets you manage your users without using their existing - credentials, and without worrying about client-side rate limiting. The new - methods included in this API are - [`getUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#getUser), - [`getUserByEmail()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#getUserByEmail), - [`createUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createUser), - [`updateUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#updateUser), and - [`deleteUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#deleteUser). See - [Manage Users](https://firebase.google.com/docs/auth/admin/manage-users) for more details. -- [changed] The - [`createCustomToken()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createCustomToken) - method is now asynchronous, returning a `Promise` instead of a - `string`. diff --git a/createReleaseTarball.sh b/createReleaseTarball.sh index ca41edc2d3..8ea6edf7bd 100755 --- a/createReleaseTarball.sh +++ b/createReleaseTarball.sh @@ -88,13 +88,6 @@ echo "[INFO] Updating version number in package.json to ${VERSION_WITHOUT_RC}... sed -i '' -e s/"\"version\": \".*\""/"\"version\": \"${VERSION_WITHOUT_RC}\""/ package.json echo -######################### -# UPDATE CHANGELOG.md # -######################### -echo "[INFO] Updating version number in CHANGELOG.md to ${VERSION_WITHOUT_RC}..." -sed -i '' -e "/^# Unreleased$/d" CHANGELOG.md -echo -e "# Unreleased\n\n-\n\n# v${VERSION_WITHOUT_RC}" | cat - CHANGELOG.md > TEMP_CHANGELOG.md -mv TEMP_CHANGELOG.md CHANGELOG.md ############################ # REINSTALL DEPENDENCIES # diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index c286a464eb..78941f6b6a 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -127,6 +127,8 @@ toc: path: /docs/reference/admin/node/admin.messaging.AndroidNotification - title: "FcmOptions" path: /docs/reference/admin/node/admin.messaging.FcmOptions + - title: "LightSettings" + path: /docs/reference/admin/node/admin.messaging.LightSettings - title: "Messaging" path: /docs/reference/admin/node/admin.messaging.Messaging - title: "MessagingConditionResponse" diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js index 86c4e3f34f..b0abb98c9d 100644 --- a/docgen/generate-docs.js +++ b/docgen/generate-docs.js @@ -40,7 +40,7 @@ const contentPath = path.resolve(`${__dirname}/content-sources/node`); const tempHomePath = path.resolve(`${contentPath}/HOME_TEMP.md`); const devsitePath = `/docs/reference/admin/node/`; -const firestoreExcludes = ['v1', 'v1beta1', 'setLogFunction']; +const firestoreExcludes = ['v1', 'v1beta1', 'setLogFunction','DocumentData']; const firestoreHtmlPath = `${docPath}/admin.firestore.html`; const firestoreHeader = `

Type aliases

diff --git a/gulpfile.js b/gulpfile.js index eb5a870f30..82c10c774c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -37,7 +37,12 @@ var replace = require('gulp-replace'); /****************/ var paths = { src: [ - 'src/**/*.ts' + 'src/**/*.ts', + ], + + test: [ + 'test/**/*.ts', + '!test/integration/typescript/src/example*.ts', ], databaseSrc: [ @@ -52,6 +57,8 @@ var paths = { // rather than including both src and test in the lib dir. var buildProject = ts.createProject('tsconfig.json', {rootDir: 'src'}); +var buildTest = ts.createProject('tsconfig.json'); + var banner = `/*! firebase-admin v${pkg.version} */\n`; /***********/ @@ -79,6 +86,16 @@ gulp.task('compile', function() { .pipe(gulp.dest(paths.build)) }); +/** + * Task only used to capture typescript compilation errors in the test suite. + * Output is discarded. + */ +gulp.task('compile_test', function() { + return gulp.src(paths.test) + // Compile Typescript into .js and .d.ts files + .pipe(buildTest()) +}); + gulp.task('copyDatabase', function() { return gulp.src(paths.databaseSrc) // Add headers @@ -98,9 +115,11 @@ gulp.task('copyTypings', function() { .pipe(gulp.dest(paths.build)) }); +gulp.task('compile_all', gulp.series('compile', 'copyDatabase', 'copyTypings', 'compile_test')); + // Regenerates js every time a source file changes gulp.task('watch', function() { - gulp.watch(paths.src, ['compile']); + gulp.watch(paths.src.concat(paths.test), {ignoreInitial: false}, gulp.series('compile_all')); }); // Build task diff --git a/package-lock.json b/package-lock.json index 87f74326c6..4265ec2be2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "8.6.1", + "version": "8.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -160,68 +160,68 @@ } }, "@firebase/app": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.4.10.tgz", - "integrity": "sha512-w1Dc3zNNluDq4IYzTSKoO2kgNgVMiaBB3ki2OfHwhnfPh0H5WbIGOF5dLepk56iBsZjiRvpmHgDQbjLyI9foEQ==", + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.4.23.tgz", + "integrity": "sha512-0CSfdo0o4NGvdownwcOIpMWpnxyx8M4Ucp0vovBLnJkK3qoLo1AXTvt5Q/C3Rla1kLG3nygE0vF6jue18qDJsA==", "dev": true, "requires": { - "@firebase/app-types": "0.4.0", - "@firebase/logger": "0.1.18", - "@firebase/util": "0.2.21", + "@firebase/app-types": "0.4.7", + "@firebase/logger": "0.1.29", + "@firebase/util": "0.2.32", "dom-storage": "2.1.0", - "tslib": "1.9.3", + "tslib": "1.10.0", "xmlhttprequest": "1.8.0" }, "dependencies": { "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true } } }, "@firebase/app-types": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.4.0.tgz", - "integrity": "sha512-8erNMHc0V26gA6Nj4W9laVrQrXHsj9K2TEM7eL2IQogGSHLL4vet3UNekYfcGQ2cjfvwUjMzd+BNS/8S7GnfiA==" + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.4.7.tgz", + "integrity": "sha512-4LnhDYsUhgxMBnCfQtWvrmMy9XxeZo059HiRbpt3ufdpUcZZOBDOouQdjkODwHLhcnNrB7LeyiqYpS2jrLT8Mw==" }, "@firebase/auth": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.11.3.tgz", - "integrity": "sha512-MFjnQGzZM89pqQItHNf8QPbCj0PjaFomd3JGUpnyxVwMyuovsRxVmBofi8mq/eiwzy7qwvRHFB8ngevWkkdAMA==", + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.12.4.tgz", + "integrity": "sha512-nGzXJDB6NlGnd4JH16Myl2n+vQKRlJ5Wmjk10CB5ZTJu5NGs65uRf4wLBB6P2VyK0cGD/WcE+mfE34RxY/26hA==", "dev": true, "requires": { - "@firebase/auth-types": "0.7.0" + "@firebase/auth-types": "0.8.2" } }, "@firebase/auth-types": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.7.0.tgz", - "integrity": "sha512-QEG9azYwssGWcb4NaKFHe3Piez0SG46nRlu76HM4/ob0sjjNpNTY1Z5C3IoeJYknp2kMzuQi0TTW8tjEgkUAUA==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.8.2.tgz", + "integrity": "sha512-qcP7wZ76CIb7IN+K544GomA42cCS36KZmQ3n9Ou1JsYplEaMo52x4UuQTZFqlRoMaUWi61oQ9jiuE5tOAMJwDA==", "dev": true }, "@firebase/database": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.5.1.tgz", - "integrity": "sha512-foXZVl32fUcekk+G8I0eWn2jJqWnJMKIsENKwtlBRbBai8ud8oqHkz704D35zff0MndsNlVxuWAEl4gaPLjRDQ==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.5.11.tgz", + "integrity": "sha512-YEakG5uILYkZ3qEDU4F9pe1HyvPlPG2Zk1FJ5RN2Yt564lTNJTrnltRELoutWoSCAtgEUXEfiTDV+864qFSG9g==", "requires": { - "@firebase/database-types": "0.4.3", - "@firebase/logger": "0.1.23", - "@firebase/util": "0.2.26", + "@firebase/database-types": "0.4.7", + "@firebase/logger": "0.1.29", + "@firebase/util": "0.2.32", "faye-websocket": "0.11.3", "tslib": "1.10.0" }, "dependencies": { "@firebase/logger": { - "version": "0.1.23", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.1.23.tgz", - "integrity": "sha512-/j4B4w/10gy5pG1SCudnjpc5jjqTkIQ+MfSXf7nnED0uTHmdODIWy59YK3cAH3tV7L/OSYPLwcRen7XURXRijQ==" + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.1.29.tgz", + "integrity": "sha512-0GDGHT0eCskNMnDwB1Bx85lHzux9zrf7OJmG/0+kdVkQYFmqJpKwEJnb0mAxLVIVdhYmcYZXPBxUGnN/cQzHNQ==" }, "@firebase/util": { - "version": "0.2.26", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.26.tgz", - "integrity": "sha512-GcKcDAlJ85i1MsURKr8v2k5fkE0FkuM0ap/rYuWs44vxd2U5x6fUdoUQrKnZlclTH/xj0z+qHVQB9Vrwvp7alw==", + "version": "0.2.32", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.32.tgz", + "integrity": "sha512-n5l1RDxzhQeLOFWRPdatyGt3ig1NLEmtO1wnG4x3Z5rOZAb09aBp+kYBu5HExJ4o6e+36lJ6l3nwdRnsJWaUlQ==", "requires": { "tslib": "1.10.0" } @@ -234,52 +234,51 @@ } }, "@firebase/database-types": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.4.3.tgz", - "integrity": "sha512-21yCiJA2Tyt6dJYwWeB69MwoawBu5UWNtP6MAY0ugyRBHVdjAMHMYalPxCjZ46LAmhfim0+i8NXRadOFVS3hUA==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.4.7.tgz", + "integrity": "sha512-7UHZ0n6aj3sR5W4HsU18dysHMSIS6348xWTMypoA0G4mORaQSuleCSL6zJLaCosarDEojnncy06yW69fyFxZtA==", "requires": { - "@firebase/app-types": "0.x" + "@firebase/app-types": "0.4.7" } }, "@firebase/logger": { - "version": "0.1.18", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.1.18.tgz", - "integrity": "sha512-/2l28mC9xPXi3Kqe/xUJ/vQ8h4NalwAiYkNihE/JogkzluhqON17ton8OezcZ+gjq12mF9Oq2Xd9WxplMXK6vA==", + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.1.29.tgz", + "integrity": "sha512-0GDGHT0eCskNMnDwB1Bx85lHzux9zrf7OJmG/0+kdVkQYFmqJpKwEJnb0mAxLVIVdhYmcYZXPBxUGnN/cQzHNQ==", "dev": true }, "@firebase/util": { - "version": "0.2.21", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.21.tgz", - "integrity": "sha512-80ZblYuorX4Udr4wPzht1upQzk99xS2SIRfl4gvTNiu5WXKTSKKk5WbpiR8IL2bYVSo/dd634B2L7BOTEjHcqA==", + "version": "0.2.32", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.32.tgz", + "integrity": "sha512-n5l1RDxzhQeLOFWRPdatyGt3ig1NLEmtO1wnG4x3Z5rOZAb09aBp+kYBu5HExJ4o6e+36lJ6l3nwdRnsJWaUlQ==", "dev": true, "requires": { - "tslib": "1.9.3" + "tslib": "1.10.0" }, "dependencies": { "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true } } }, "@google-cloud/common": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.0.3.tgz", - "integrity": "sha512-1FPOfQ+ZVSRge+wqaWr/6qCa9DWizxJcoZUWegWFTNp9yy3k8WOQsM+C55Ssjivs1TOD5ekEaE4MY9EW5r5vnA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.2.3.tgz", + "integrity": "sha512-lvw54mGKn8VqVIy2NzAk0l5fntBFX4UwQhHk6HaqkyCQ7WBl5oz4XhzKMtMilozF/3ObPcDogqwuyEWyZ6rnQQ==", "optional": true, "requires": { "@google-cloud/projectify": "^1.0.0", "@google-cloud/promisify": "^1.0.0", - "@types/request": "^2.48.1", "arrify": "^2.0.0", "duplexify": "^3.6.0", "ent": "^2.2.0", "extend": "^3.0.2", - "google-auth-library": "^4.0.0", + "google-auth-library": "^5.5.0", "retry-request": "^4.0.0", - "teeny-request": "^4.0.0" + "teeny-request": "^5.2.1" }, "dependencies": { "arrify": { @@ -291,29 +290,26 @@ } }, "@google-cloud/firestore": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-2.0.0.tgz", - "integrity": "sha512-KZy9VXXP5zGCnp5y79SMDORGpFJj72V/MhFw7L8ZK1QS4ajEbbuxqTTv6abIca162FDoob8WZVRMvEVSdjoZkw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-2.6.0.tgz", + "integrity": "sha512-5bpC7KZA+dCc+4Byp9yA7uvmM1kmVaXm6QiSQbf2Zz/rWftTr0N23f+5BKe9OXyY/nT44l2ygZjmP4Aw3ngLFg==", "optional": true, "requires": { "bun": "^0.0.12", "deep-equal": "^1.0.1", "functional-red-black-tree": "^1.0.1", - "google-gax": "^1.0.0", - "lodash.merge": "^4.6.1", + "google-gax": "^1.7.5", "through2": "^3.0.0" } }, "@google-cloud/paginator": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-1.0.2.tgz", - "integrity": "sha512-mUqsRAJ/OT/Zo/Qh2v+kEeWsEgKZtK4vs2skSiVeudPLwjLSVng+fYZYtLK4kx05OSnm16MqurcPqW14g1/TgQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.1.tgz", + "integrity": "sha512-HZ6UTGY/gHGNriD7OCikYWL/Eu0sTEur2qqse2w6OVsz+57se3nTkqH14JIPxtf0vlEJ8IJN5w3BdZ22pjCB8g==", "optional": true, "requires": { "arrify": "^2.0.0", - "extend": "^3.0.1", - "split-array-stream": "^2.0.0", - "stream-events": "^1.0.4" + "extend": "^3.0.2" }, "dependencies": { "arrify": { @@ -333,32 +329,32 @@ "@google-cloud/promisify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.2.tgz", - "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g==", - "optional": true + "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g==" }, "@google-cloud/storage": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-3.0.2.tgz", - "integrity": "sha512-vIKaTSEpZJkWXUWhAN4wrEisL0JJ6SYjuwWMZKGSit/nRbhAxC8IA82Yrhbm/jI6R9VdBpB+oyHbhQLcMiNJvQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.1.2.tgz", + "integrity": "sha512-kYP7h2SMx5KmbIbeQ4qHoBm9uYFRZOR96BCYpzGWYO8ii157sA1nmmULai0lcrVwOhfVXoReZUpflTc5lFN80g==", "optional": true, "requires": { - "@google-cloud/common": "^2.0.0", - "@google-cloud/paginator": "^1.0.0", + "@google-cloud/common": "^2.1.1", + "@google-cloud/paginator": "^2.0.0", "@google-cloud/promisify": "^1.0.0", "arrify": "^2.0.0", "compressible": "^2.0.12", "concat-stream": "^2.0.0", - "date-and-time": "^0.7.0", + "date-and-time": "^0.10.0", "duplexify": "^3.5.0", - "extend": "^3.0.0", + "extend": "^3.0.2", "gaxios": "^2.0.1", - "gcs-resumable-upload": "^2.0.0", - "hash-stream-validation": "^0.2.1", + "gcs-resumable-upload": "^2.2.4", + "hash-stream-validation": "^0.2.2", "mime": "^2.2.0", "mime-types": "^2.0.8", "onetime": "^5.1.0", "p-limit": "^2.2.0", - "pumpify": "^1.5.1", + "pumpify": "^2.0.0", + "readable-stream": "^3.4.0", "snakeize": "^0.1.0", "stream-events": "^1.0.1", "through2": "^3.0.0", @@ -383,49 +379,87 @@ "typedarray": "^0.0.6" } }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "optional": true, + "requires": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + } + } + }, "readable-stream": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", - "optional": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } } } }, "@grpc/grpc-js": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.4.0.tgz", - "integrity": "sha512-UbGDPnstJamJrSiHzCSwSavIX260IfLOZLRJYDqRKJA/jmVZa3hPMWDjhFrcCKDq2MLc/O/nauFED3r4khcZrA==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.6.9.tgz", + "integrity": "sha512-r1nDOEEiYmAsVYBaS4DPPqdwPOXPw7YhVOnnpPdWhlNtKbYzPash6DqWTTza9gBiYMA5d2Wiq6HzrPqsRaP4yA==", "optional": true, "requires": { - "semver": "^6.0.0" + "semver": "^6.2.0" }, "dependencies": { "semver": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", - "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "optional": true } } }, "@grpc/proto-loader": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.1.tgz", - "integrity": "sha512-3y0FhacYAwWvyXshH18eDkUI40wT/uGio7MAegzY8lO5+wVsc19+1A7T0pPptae4kl7bdITL+0cHpnAPmryBjQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.3.tgz", + "integrity": "sha512-8qvUtGg77G2ZT2HqdqYoM/OY97gQd/0crSG34xNmZ4ZOsv3aQT/FQV9QfZPazTGna6MIoyUd+u6AxsoZjJ/VMQ==", "optional": true, "requires": { "lodash.camelcase": "^4.3.0", @@ -435,32 +469,27 @@ "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", - "optional": true + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" }, "@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "optional": true + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" }, "@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "optional": true + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" }, "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", - "optional": true + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -469,32 +498,27 @@ "@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", - "optional": true + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", - "optional": true + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", - "optional": true + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", - "optional": true + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", - "optional": true + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "@sinonjs/commons": { "version": "1.4.0", @@ -554,7 +578,8 @@ "@types/caseless": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", - "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==" + "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==", + "dev": true }, "@types/chai": { "version": "3.5.2", @@ -582,6 +607,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "dev": true, "requires": { "@types/node": "*" } @@ -604,8 +630,7 @@ "@types/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", - "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==", - "optional": true + "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" }, "@types/minimatch": { "version": "3.0.3", @@ -635,9 +660,9 @@ } }, "@types/node": { - "version": "8.10.38", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.38.tgz", - "integrity": "sha512-EibsnbJerd0hBFaDjJStFrVbVBAtOy4dgL8zZFw0uOvPqzBAX59Ci8cgjg3+RgJIWhsB5A4c+pi+D4P9tQQh/A==" + "version": "8.10.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", + "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==" }, "@types/promises-a-plus": { "version": "0.0.27", @@ -649,6 +674,7 @@ "version": "2.48.1", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", + "dev": true, "requires": { "@types/caseless": "*", "@types/form-data": "*", @@ -694,7 +720,8 @@ "@types/tough-cookie": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-Set5ZdrAaKI/qHdFlVMgm/GsAv/wkXhSTuZFkJ+JI7HK+wIkIlOaUXSXieIvJ0+OvGIqtREFoE+NHJtEq0gtEw==" + "integrity": "sha512-Set5ZdrAaKI/qHdFlVMgm/GsAv/wkXhSTuZFkJ+JI7HK+wIkIlOaUXSXieIvJ0+OvGIqtREFoE+NHJtEq0gtEw==", + "dev": true }, "abab": { "version": "2.0.0", @@ -706,7 +733,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "optional": true, "requires": { "event-target-shim": "^5.0.0" } @@ -734,10 +760,9 @@ "dev": true }, "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "optional": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", "requires": { "es6-promisify": "^5.0.0" } @@ -1767,8 +1792,7 @@ "bignumber.js": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", - "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==", - "optional": true + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" }, "binary-extensions": { "version": "1.13.1", @@ -2234,9 +2258,9 @@ }, "dependencies": { "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", + "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==", "optional": true } } @@ -2323,9 +2347,9 @@ }, "dependencies": { "write-file-atomic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.0.tgz", - "integrity": "sha512-EIgkf60l2oWsffja2Sf2AL384dx328c0B+cIYPTQq5q2rOYuDV00/iPFBOUiDKKwKMOhkymH8AidPaRvzfxY+Q==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", + "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", "optional": true, "requires": { "imurmurhash": "^0.1.4", @@ -2462,9 +2486,9 @@ } }, "date-and-time": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.7.0.tgz", - "integrity": "sha512-qPHBPG0AQqbjP7wVf7vLv25/0bZRjYPiJiJtE0t6RqTswJR/6ExCXQLDnL5w4986j7i6470TMtalJxC8/UHrww==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.10.0.tgz", + "integrity": "sha512-IbIzxtvK80JZOVsWF6+NOjunTaoFVYxkAQoyzmflJyuRCJAJebehy48mPiCAedcGp4P7/UO3QYRWa0fe6INftg==", "optional": true }, "dateformat": { @@ -2676,9 +2700,9 @@ } }, "dot-prop": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.1.0.tgz", - "integrity": "sha512-n1oC6NBF+KM9oVXtjmen4Yo7HyAVWV2UUl50dCYJdw2924K6dX9bf9TTTWaKtYlRn0FEtxG27KS80ayVLixxJA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", "optional": true, "requires": { "is-obj": "^2.0.0" @@ -2857,16 +2881,14 @@ } }, "es6-promise": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", - "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==", - "optional": true + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, "es6-promisify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "optional": true, "requires": { "es6-promise": "^4.0.3" } @@ -2948,8 +2970,7 @@ "event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "optional": true + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, "execa": { "version": "1.0.0", @@ -3148,8 +3169,7 @@ "fast-text-encoding": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", - "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==", - "optional": true + "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==" }, "faye-websocket": { "version": "0.11.3", @@ -3476,8 +3496,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -3498,14 +3517,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3520,20 +3537,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3650,8 +3664,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -3663,7 +3676,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3678,7 +3690,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3686,14 +3697,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3712,7 +3721,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3800,8 +3808,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3813,7 +3820,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3899,8 +3905,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -3936,7 +3941,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3956,7 +3960,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4000,14 +4003,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -4024,39 +4025,106 @@ "optional": true }, "gaxios": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.0.1.tgz", - "integrity": "sha512-c1NXovTxkgRJTIgB2FrFmOFg4YIV6N/bAa4f/FZ4jIw13Ql9ya/82x69CswvotJhbV3DiGnlTZwoq2NVXk2Irg==", - "optional": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.1.0.tgz", + "integrity": "sha512-Gtpb5sdQmb82sgVkT2GnS2n+Kx4dlFwbeMYcDlD395aEvsLCSQXJJcHt7oJ2LrGxDEAeiOkK79Zv2A8Pzt6CFg==", "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", - "https-proxy-agent": "^2.2.1", + "https-proxy-agent": "^3.0.0", + "is-stream": "^2.0.0", "node-fetch": "^2.3.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + } } }, "gcp-metadata": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-2.0.0.tgz", - "integrity": "sha512-BN6KUUWo6WLkDRst+Y7bqpXq1PYMrKUecNLRdZESp7oYtMjWcZdAM0UYvcip8wb0GXNO/j8Z8HTccK4iYtMvyQ==", - "optional": true, + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.2.1.tgz", + "integrity": "sha512-JjDedBWnbXVXWwTpjBdpb9RpVLiowXG4/50rra4hPH8REXAi2si6Xbb48B2SwkQBLz9Wu6+o32GDTvVy2kkLoQ==", "requires": { - "gaxios": "^2.0.0", + "gaxios": "^2.1.0", "json-bigint": "^0.3.0" } }, "gcs-resumable-upload": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.1.1.tgz", - "integrity": "sha512-E3fb3yYHbhq0h5lE7oF0AWaYF6oAEszVb05bMAumPCZmd8Ik/ecvDFR0J1nR3EDqDgJ55rw2mIzM2h832XOwFg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.0.tgz", + "integrity": "sha512-PclXJiEngrVx0c4K0LfE1XOxhmOkBEy39Rrhspdn6jAbbwe4OQMZfjo7Z1LHBrh57+bNZeIN4M+BooYppCoHSg==", "optional": true, "requires": { "abort-controller": "^3.0.0", "configstore": "^5.0.0", "gaxios": "^2.0.0", - "google-auth-library": "^4.0.0", - "pumpify": "^1.5.1", + "google-auth-library": "^5.0.0", + "pumpify": "^2.0.0", "stream-events": "^1.0.4" + }, + "dependencies": { + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "optional": true, + "requires": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "optional": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } } }, "get-caller-file": { @@ -4274,49 +4342,43 @@ } }, "google-auth-library": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-4.0.0.tgz", - "integrity": "sha512-yyxl74G16GjKLevccXK3/DYEXphtI9Q2Qw3Eh7y8scjBKNL0IbAZF1mi999gC0tkfG6J23sCbd9tMEbNYeWfJQ==", - "optional": true, + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.1.tgz", + "integrity": "sha512-zCtjQccWS/EHYyFdXRbfeSGM/gW+d7uMAcVnvXRnjBXON5ijo6s0nsObP0ifqileIDSbZjTlLtgo+UoN8IFJcg==", "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "fast-text-encoding": "^1.0.0", - "gaxios": "^2.0.0", - "gcp-metadata": "^2.0.0", - "gtoken": "^3.0.0", + "gaxios": "^2.1.0", + "gcp-metadata": "^3.2.0", + "gtoken": "^4.1.0", "jws": "^3.1.5", - "lru-cache": "^5.0.0", - "semver": "^6.0.0" + "lru-cache": "^5.0.0" }, "dependencies": { "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "optional": true - }, - "semver": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", - "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==", - "optional": true + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" } } }, "google-gax": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.1.1.tgz", - "integrity": "sha512-30CLetXzyd9B1Ilqvt4q9ETaeSUgJ54ygwtLRDyPrvl6Wb+s2U7WdwCpfkrbWWmEUxh+FTQq5PMcyW8HQ+BiGA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.10.0.tgz", + "integrity": "sha512-x2+Ra6W3tCNUqceGwLJoBQVcBraVfDv2FBsQGMVvgJNhX4X0uGoH8zc4Lzy63jCGxhDdvrQknEIrXR4RKunPog==", "optional": true, "requires": { - "@grpc/grpc-js": "^0.4.0", + "@grpc/grpc-js": "0.6.9", "@grpc/proto-loader": "^0.5.1", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", "duplexify": "^3.6.0", - "google-auth-library": "^4.0.0", + "google-auth-library": "^5.0.0", "is-stream-ended": "^0.1.4", "lodash.at": "^4.6.0", "lodash.has": "^4.5.2", + "node-fetch": "^2.6.0", "protobufjs": "^6.8.8", "retry-request": "^4.0.0", "semver": "^6.0.0", @@ -4324,27 +4386,25 @@ }, "dependencies": { "semver": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", - "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "optional": true } } }, "google-p12-pem": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.0.tgz", - "integrity": "sha512-n8eGSKzWOb9/EmSBIh81sPvsQM939QlpHMXahTZDzuRIpCu09x3Oaqz+mXGjL4TeCvSbcnOC0YZRvjkJ9s9lnA==", - "optional": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.2.tgz", + "integrity": "sha512-UfnEARfJKI6pbmC1hfFFm+UAcZxeIwTiEcHfqKe/drMsXD/ilnVjF7zgOGpHXyhuvX6jNJK3S8A0hOQjwtFxEw==", "requires": { - "node-forge": "^0.8.0" + "node-forge": "^0.9.0" }, "dependencies": { "node-forge": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.3.tgz", - "integrity": "sha512-5lv9UKmvTBog+m4AWL8XpZnr3WbNKxYL2M77i903ylY/huJIooSTDHyUWQ/OppFuKQpAGMk6qNtDymSJNRIEIg==", - "optional": true + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", + "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" } } }, @@ -4360,24 +4420,14 @@ "dev": true }, "gtoken": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-3.0.0.tgz", - "integrity": "sha512-IY9HVi78D4ykVHn+ThI7rlcpdFtKyo9e9YLim9S9T3rp6fEnfeTexcrqzSpExVshPofsdauLKIa8dEnzX7ZLfQ==", - "optional": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.1.tgz", + "integrity": "sha512-2FEmEDGi4NdM6u+mtaLjSDDtHiw5wT+nBsI+yrSeFO6fVqPEytYVF6uiIpRaOaZhRP+ozjYWuwwtMlrjAyTcYA==", "requires": { - "gaxios": "^2.0.0", + "gaxios": "^2.1.0", "google-p12-pem": "^2.0.0", "jws": "^3.1.5", - "mime": "^2.2.0", - "pify": "^4.0.0" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "optional": true - } + "mime": "^2.2.0" } }, "gulp": { @@ -4813,9 +4863,9 @@ } }, "hash-stream-validation": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.1.tgz", - "integrity": "sha1-7Mm5l7IYvluzEphii7gHhptz3NE=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.2.tgz", + "integrity": "sha512-cMlva5CxWZOrlS/cY0C+9qAzesn5srhFA8IT1VPiHc9bWWBLkJfEUIZr7MWoi89oOOGmpg8ymchaOjiArsGu5A==", "optional": true, "requires": { "through2": "^2.0.0" @@ -4926,6 +4976,16 @@ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "optional": true, + "requires": { + "agent-base": "4", + "debug": "3.1.0" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -4938,12 +4998,11 @@ } }, "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", - "optional": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", "requires": { - "agent-base": "^4.1.0", + "agent-base": "^4.3.0", "debug": "^3.1.0" } }, @@ -5547,7 +5606,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", - "optional": true, "requires": { "bignumber.js": "^7.0.0" } @@ -5948,12 +6006,6 @@ "lodash.isarray": "^3.0.0" } }, - "lodash.merge": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", - "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==", - "optional": true - }, "lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -6007,14 +6059,12 @@ "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "optional": true + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "optional": true, "requires": { "yallist": "^3.0.2" } @@ -6035,9 +6085,9 @@ }, "dependencies": { "semver": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", - "integrity": "sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "optional": true } } @@ -6178,10 +6228,9 @@ } }, "mime": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.3.tgz", - "integrity": "sha512-QgrPRJfE+riq5TPZMcHZOtm8c6K/yYrMbKIoRfapfiGLxS8OTeIfRhUGW5LU7MlRa52KOAGCfUNruqLrIBvWZw==", - "optional": true + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" }, "mime-db": { "version": "1.37.0", @@ -6454,8 +6503,7 @@ "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "optional": true + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" }, "node-forge": { "version": "0.7.4", @@ -7155,7 +7203,6 @@ "version": "6.8.8", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", - "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -7173,10 +7220,9 @@ }, "dependencies": { "@types/node": { - "version": "10.14.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.7.tgz", - "integrity": "sha512-on4MmIDgHXiuJDELPk1NFaKVUxxCFr37tm8E9yN6rAiF5Pzp/9bBfBHkoexqRiY+hk/Z04EJU9kKEb59YqJ82A==", - "optional": true + "version": "10.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.5.tgz", + "integrity": "sha512-RElZIr/7JreF1eY6oD5RF3kpmdcreuQPjg5ri4oQ5g9sq7YWU8HkfB3eH8GwAwxf5OaCh0VPi7r4N/yoTGelrA==" } } }, @@ -7196,6 +7242,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7205,6 +7252,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, "requires": { "duplexify": "^3.6.0", "inherits": "^2.0.3", @@ -7650,52 +7698,63 @@ "dev": true }, "retry-request": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.0.0.tgz", - "integrity": "sha512-S4HNLaWcMP6r8E4TMH52Y7/pM8uNayOcTDDQNBwsCccL1uI+Ol2TljxRDPzaNfbhOB30+XWP5NnZkB3LiJxi1w==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz", + "integrity": "sha512-BINDzVtLI2BDukjWmjAIRZ0oglnCAkpP2vQjM3jdLhmT62h0xnQgciPwBRDAvHqpkPT2Wo1XuUyLyn6nbGrZQQ==", "optional": true, "requires": { - "through2": "^2.0.0" + "debug": "^4.1.1", + "through2": "^3.0.1" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "optional": true }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", "optional": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "optional": true + }, "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "optional": true, "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", "optional": true, "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "readable-stream": "2 || 3" } } } @@ -8096,15 +8155,6 @@ "integrity": "sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==", "dev": true }, - "split-array-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-2.0.0.tgz", - "integrity": "sha512-hmMswlVY91WvGMxs0k8MRgq8zb2mSen4FmDNc5AFiTWtrBpdZN6nwD6kROVe4vNL+ywrvbCKsWVCnEd4riELIg==", - "optional": true, - "requires": { - "is-stream-ended": "^0.1.4" - } - }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -8180,7 +8230,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "optional": true, "requires": { "stubs": "^3.0.0" } @@ -8261,8 +8310,7 @@ "stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", - "optional": true + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" }, "supports-color": { "version": "2.0.0", @@ -8287,13 +8335,15 @@ "dev": true }, "teeny-request": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-4.0.0.tgz", - "integrity": "sha512-Kk87eePsBQZsn5rOIwupObYV7doBMedW3fUOmu3LFVRGEJQ7oeClwWkGFS3nkFs9TFL36qf08vGJd34swMorHQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-5.3.1.tgz", + "integrity": "sha512-hnUeun3xryzv92FbrnprltcdeDfSVaGFBlFPRvKJ2fO/ioQx9N0aSUbbXSfTO+ArRXine1gSWdWFWcgfrggWXw==", "optional": true, "requires": { - "https-proxy-agent": "^2.2.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", "node-fetch": "^2.2.0", + "stream-events": "^1.0.5", "uuid": "^3.3.2" } }, @@ -8822,9 +8872,9 @@ } }, "typescript": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz", - "integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz", + "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==", "dev": true }, "uglify-js": { @@ -9225,9 +9275,9 @@ } }, "walkdir": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.0.tgz", - "integrity": "sha512-Ps0LSr9doEPbF4kEQi6sk5RgzIGLz9+OroGj1y2osIVnufjNQWSLEGIbZwW5V+j/jK8lCj/+8HSWs+6Q/rnViA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", + "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", "optional": true }, "webidl-conversions": { @@ -9336,8 +9386,7 @@ "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "optional": true + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" }, "xml-name-validator": { "version": "3.0.0", @@ -9369,10 +9418,9 @@ "dev": true }, "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "optional": true + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "yargs": { "version": "13.2.4", diff --git a/package.json b/package.json index 9a9b61107e..b2ab115f3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "8.6.1", + "version": "8.8.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -54,19 +54,20 @@ ], "types": "./lib/index.d.ts", "dependencies": { - "@firebase/database": "^0.5.1", - "@types/node": "^8.0.53", + "@firebase/database": "^0.5.11", + "@types/node": "^8.10.59", "dicer": "^0.3.0", "jsonwebtoken": "8.1.0", "node-forge": "0.7.4" }, "optionalDependencies": { - "@google-cloud/firestore": "^2.0.0", - "@google-cloud/storage": "^3.0.2" + "@google-cloud/firestore": "^2.6.0", + "@google-cloud/storage": "^4.1.2" }, "devDependencies": { - "@firebase/app": "^0.4.10", - "@firebase/auth": "^0.11.3", + "@firebase/app": "^0.4.23", + "@firebase/auth": "^0.12.4", + "@firebase/auth-types": "^0.8.2", "@types/bcrypt": "^2.0.0", "@types/chai": "^3.4.34", "@types/chai-as-promised": "0.0.29", @@ -110,7 +111,7 @@ "ts-node": "^3.3.0", "tslint": "^5.17.0", "typedoc": "^0.15.0", - "typescript": "^3.1.0", + "typescript": "^3.7.3", "yargs": "^13.2.2" } } diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 7a8227b0e7..7152174879 100755 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -98,7 +98,9 @@ enum WriteOperationType { /** Defines a base utility to help with resource URL construction. */ class AuthResourceUrlBuilder { + protected urlFormat: string; + private projectId: string; /** * The resource URL builder constructor. @@ -107,7 +109,7 @@ class AuthResourceUrlBuilder { * @param {string} version The endpoint API version. * @constructor */ - constructor(protected projectId: string, protected version: string = 'v1') { + constructor(protected app: FirebaseApp, protected version: string = 'v1') { this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; } @@ -117,17 +119,41 @@ class AuthResourceUrlBuilder { * @param {string=} api The backend API name. * @param {object=} params The optional additional parameters to substitute in the * URL path. - * @return {string} The corresponding resource URL. + * @return {Promise} The corresponding resource URL. */ - public getUrl(api?: string, params?: object): string { - const baseParams = { - version: this.version, - projectId: this.projectId, - api: api || '', - }; - const baseUrl = utils.formatString(this.urlFormat, baseParams); - // Substitute additional api related parameters. - return utils.formatString(baseUrl, params || {}); + public getUrl(api?: string, params?: object): Promise { + return this.getProjectId() + .then((projectId) => { + const baseParams = { + version: this.version, + projectId, + api: api || '', + }; + const baseUrl = utils.formatString(this.urlFormat, baseParams); + // Substitute additional api related parameters. + return utils.formatString(baseUrl, params || {}); + }); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CREDENTIAL, + 'Failed to determine project ID for Auth. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', + ); + } + + this.projectId = projectId; + return projectId; + }); } } @@ -142,8 +168,8 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { * @param {string} tenantId The tenant ID. * @constructor */ - constructor(protected projectId: string, protected version: string, protected tenantId: string) { - super(projectId, version); + constructor(protected app: FirebaseApp, protected version: string, protected tenantId: string) { + super(app, version); this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; } @@ -153,10 +179,13 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { * @param {string=} api The backend API name. * @param {object=} params The optional additional parameters to substitute in the * URL path. - * @return {string} The corresponding resource URL. + * @return {Promise} The corresponding resource URL. */ - public getUrl(api?: string, params?: object) { - return utils.formatString(super.getUrl(api, params), {tenantId: this.tenantId}); + public getUrl(api?: string, params?: object): Promise { + return super.getUrl(api, params) + .then((url) => { + return utils.formatString(url, {tenantId: this.tenantId}); + }); } } @@ -464,7 +493,7 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat // mfaInfo is used for importUsers. // mfa.enrollments is used for setAccountInfo. // enrollments has to be an array of valid AuthFactorInfo requests. - let enrollments: AuthFactorInfo[]; + let enrollments: AuthFactorInfo[] | null = null; if (request.mfaInfo) { enrollments = request.mfaInfo; } else if (request.mfa && request.mfa.enrollments) { @@ -780,7 +809,7 @@ const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET') * Class that provides the mechanism to send requests to the Firebase Auth backend endpoints. */ export abstract class AbstractAuthRequestHandler { - protected readonly projectId: string; + protected readonly httpClient: AuthorizedHttpClient; private authUrlBuilder: AuthResourceUrlBuilder; private projectConfigUrlBuilder: AuthResourceUrlBuilder; @@ -790,15 +819,21 @@ export abstract class AbstractAuthRequestHandler { * @return {string|null} The error code if present; null otherwise. */ private static getErrorCode(response: any): string | null { - return (validator.isNonNullObject(response) && response.error && (response.error as any).message) || null; + return (validator.isNonNullObject(response) && response.error && response.error.message) || null; } /** * @param {FirebaseApp} app The app used to fetch access tokens to sign API requests. * @constructor */ - constructor(app: FirebaseApp) { - this.projectId = utils.getProjectId(app); + constructor(protected readonly app: FirebaseApp) { + if (typeof app !== 'object' || app === null || !('options' in app)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'First argument passed to admin.auth() must be a valid Firebase app instance.', + ); + } + this.httpClient = new AuthorizedHttpClient(app); } @@ -941,7 +976,7 @@ export abstract class AbstractAuthRequestHandler { } // If no remaining user in request after client side processing, there is no need // to send the request to the server. - if (request.users.length === 0) { + if (!request.users || request.users.length === 0) { return Promise.resolve(userImportBuilder.buildResponse([])); } return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_UPLOAD_ACCOUNT, request) @@ -978,7 +1013,7 @@ export abstract class AbstractAuthRequestHandler { * @return {Promise} A promise that resolves when the operation completes * with the user id that was edited. */ - public setCustomUserClaims(uid: string, customUserClaims: object): Promise { + public setCustomUserClaims(uid: string, customUserClaims: object | null): Promise { // Validate user UID. if (!validator.isUid(uid)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)); @@ -1204,7 +1239,7 @@ export abstract class AbstractAuthRequestHandler { * @param {string} email The email of the user the link is being sent to. * @param {ActionCodeSettings=} actionCodeSettings The optional action code setings which defines whether * the link is to be handled by a mobile app and the additional state information to be passed in the - * deep link, etc. + * deep link, etc. Required when requestType == 'EMAIL_SIGNIN' * @return {Promise} A promise that resolves with the email action link. */ public getEmailActionLink( @@ -1213,9 +1248,17 @@ export abstract class AbstractAuthRequestHandler { let request = {requestType, email, returnOobLink: true}; // ActionCodeSettings required for email link sign-in to determine the url where the sign-in will // be completed. + if (typeof actionCodeSettings === 'undefined' && requestType === 'EMAIL_SIGNIN') { + return Promise.reject( + new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "`actionCodeSettings` is required when `requestType` === 'EMAIL_SIGNIN'", + ), + ); + } if (typeof actionCodeSettings !== 'undefined' || requestType === 'EMAIL_SIGNIN') { try { - const builder = new ActionCodeSettingsBuilder(actionCodeSettings); + const builder = new ActionCodeSettingsBuilder(actionCodeSettings!); request = deepExtend(request, builder.buildRequest()); } catch (e) { return Promise.reject(e); @@ -1301,7 +1344,7 @@ export abstract class AbstractAuthRequestHandler { // Construct backend request. let request; try { - request = OIDCConfig.buildServerRequest(options); + request = OIDCConfig.buildServerRequest(options) || {}; } catch (e) { return Promise.reject(e); } @@ -1423,7 +1466,7 @@ export abstract class AbstractAuthRequestHandler { // Construct backend request. let request; try { - request = SAMLConfig.buildServerRequest(options); + request = SAMLConfig.buildServerRequest(options) || {}; } catch (e) { return Promise.reject(e); } @@ -1456,7 +1499,7 @@ export abstract class AbstractAuthRequestHandler { // Construct backend request. let request: SAMLConfigServerRequest; try { - request = SAMLConfig.buildServerRequest(options, true); + request = SAMLConfig.buildServerRequest(options, true) || {}; } catch (e) { return Promise.reject(e); } @@ -1485,15 +1528,15 @@ export abstract class AbstractAuthRequestHandler { protected invokeRequestHandler( urlBuilder: AuthResourceUrlBuilder, apiSettings: ApiSettings, requestData: object, additionalResourceParams?: object): Promise { - return Promise.resolve() - .then(() => { + return urlBuilder.getUrl(apiSettings.getEndpoint(), additionalResourceParams) + .then((url) => { // Validate request. const requestValidator = apiSettings.getRequestValidator(); requestValidator(requestData); // Process request. const req: HttpRequestConfig = { method: apiSettings.getHttpMethod(), - url: urlBuilder.getUrl(apiSettings.getEndpoint(), additionalResourceParams), + url, headers: FIREBASE_AUTH_HEADER, data: requestData, timeout: FIREBASE_AUTH_TIMEOUT, @@ -1511,6 +1554,14 @@ export abstract class AbstractAuthRequestHandler { if (err instanceof HttpError) { const error = err.response.data; const errorCode = AbstractAuthRequestHandler.getErrorCode(error); + if (!errorCode) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Error returned from server: ' + error + '. Additionally, an ' + + 'internal error occurred while attempting to extract the ' + + 'errorcode from the error.', + ); + } throw FirebaseAuthError.fromServerError(errorCode, /* message */ undefined, error); } throw err; @@ -1632,21 +1683,21 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ constructor(app: FirebaseApp) { super(app); - this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(utils.getProjectId(app), 'v2beta1'); + this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(app, 'v2'); } /** * @return {AuthResourceUrlBuilder} A new Auth user management resource URL builder instance. */ protected newAuthUrlBuilder(): AuthResourceUrlBuilder { - return new AuthResourceUrlBuilder(this.projectId, 'v1'); + return new AuthResourceUrlBuilder(this.app, 'v1'); } /** * @return {AuthResourceUrlBuilder} A new project config resource URL builder instance. */ protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder { - return new AuthResourceUrlBuilder(this.projectId, 'v2beta1'); + return new AuthResourceUrlBuilder(this.app, 'v2'); } /** @@ -1782,14 +1833,14 @@ export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler { * @return {AuthResourceUrlBuilder} A new Auth user management resource URL builder instance. */ protected newAuthUrlBuilder(): AuthResourceUrlBuilder { - return new TenantAwareAuthResourceUrlBuilder(this.projectId, 'v1', this.tenantId); + return new TenantAwareAuthResourceUrlBuilder(this.app, 'v1', this.tenantId); } /** * @return {AuthResourceUrlBuilder} A new project config resource URL builder instance. */ protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder { - return new TenantAwareAuthResourceUrlBuilder(this.projectId, 'v2beta1', this.tenantId); + return new TenantAwareAuthResourceUrlBuilder(this.app, 'v2', this.tenantId); } /** diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 96144dc244..0249769128 100755 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -193,7 +193,7 @@ export class EmailSignInConfig implements EmailSignInProviderConfig { * * @param {any} options The options object to validate. */ - private static validate(options: {[key: string]: any}) { + private static validate(options: EmailSignInProviderConfig) { // TODO: Validate the request. const validKeys = { enabled: true, @@ -306,7 +306,7 @@ export class SAMLConfig implements SAMLAuthProviderConfig { }; if (options.x509Certificates) { for (const cert of (options.x509Certificates || [])) { - request.idpConfig.idpCertificates.push({x509Certificate: cert}); + request.idpConfig!.idpCertificates!.push({x509Certificate: cert}); } } } @@ -467,7 +467,10 @@ export class SAMLConfig implements SAMLAuthProviderConfig { constructor(response: SAMLConfigServerResponse) { if (!response || !response.idpConfig || + !response.idpConfig.idpEntityId || + !response.idpConfig.ssoUrl || !response.spConfig || + !response.spConfig.spEntityId || !response.name || !(validator.isString(response.name) && SAMLConfig.getProviderIdFromResourceName(response.name))) { @@ -475,7 +478,15 @@ export class SAMLConfig implements SAMLAuthProviderConfig { AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid SAML configuration response'); } - this.providerId = SAMLConfig.getProviderIdFromResourceName(response.name); + + const providerId = SAMLConfig.getProviderIdFromResourceName(response.name); + if (!providerId) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid SAML configuration response'); + } + this.providerId = providerId; + // RP config. this.rpEntityId = response.spConfig.spEntityId; this.callbackURL = response.spConfig.callbackUri; @@ -663,7 +674,15 @@ export class OIDCConfig implements OIDCAuthProviderConfig { AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid OIDC configuration response'); } - this.providerId = OIDCConfig.getProviderIdFromResourceName(response.name); + + const providerId = OIDCConfig.getProviderIdFromResourceName(response.name); + if (!providerId) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid SAML configuration response'); + } + this.providerId = providerId; + this.clientId = response.clientId; this.issuer = response.issuer; // When enabled is undefined, it takes its default value of false. diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 30cd2fa74f..032f955fdd 100755 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -16,7 +16,7 @@ import {UserRecord, CreateRequest, UpdateRequest} from './user-record'; import {FirebaseApp} from '../firebase-app'; -import {FirebaseTokenGenerator, CryptoSigner, cryptoSignerFromApp} from './token-generator'; +import {FirebaseTokenGenerator, cryptoSignerFromApp} from './token-generator'; import { AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, } from './auth-api-request'; @@ -90,6 +90,7 @@ export interface SessionCookieOptions { * Base Auth class. Mainly used for user management APIs. */ export class BaseAuth { + protected readonly tokenGenerator: FirebaseTokenGenerator; protected readonly idTokenVerifier: FirebaseTokenVerifier; protected readonly sessionCookieVerifier: FirebaseTokenVerifier; @@ -97,19 +98,15 @@ export class BaseAuth { /** * The BaseAuth class constructor. * - * @param {string} projectId The corresponding project ID. * @param {T} authRequestHandler The RPC request handler * for this instance. - * @param {CryptoSigner} cryptoSigner The instance crypto signer used for custom token - * minting. * @constructor */ - constructor(protected readonly projectId: string, - protected readonly authRequestHandler: T, - cryptoSigner: CryptoSigner) { + constructor(app: FirebaseApp, protected readonly authRequestHandler: T) { + const cryptoSigner = cryptoSignerFromApp(app); this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner); - this.sessionCookieVerifier = createSessionCookieVerifier(projectId); - this.idTokenVerifier = createIdTokenVerifier(projectId); + this.sessionCookieVerifier = createSessionCookieVerifier(app); + this.idTokenVerifier = createIdTokenVerifier(app); } /** @@ -569,7 +566,6 @@ export class BaseAuth { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - /** * Verifies the decoded Firebase issued JWT is not revoked. Returns a promise that resolves * with the decoded claims on success. Rejects the promise with revocation error if revoked. @@ -617,10 +613,7 @@ export class TenantAwareAuth extends BaseAuth { * @constructor */ constructor(app: FirebaseApp, tenantId: string) { - super( - utils.getProjectId(app), - new TenantAwareAuthRequestHandler(app, tenantId), - cryptoSignerFromApp(app)); + super(app, new TenantAwareAuthRequestHandler(app, tenantId)); utils.addReadonlyGetter(this, 'tenantId', tenantId); } @@ -721,35 +714,17 @@ export class TenantAwareAuth extends BaseAuth { * An Auth instance can have multiple tenants. */ export class Auth extends BaseAuth implements FirebaseServiceInterface { + public INTERNAL: AuthInternals = new AuthInternals(); private readonly tenantManager_: TenantManager; private readonly app_: FirebaseApp; - /** - * Returns the FirebaseApp's project ID. - * - * @param {FirebaseApp} app The project ID for an app. - * @return {string} The FirebaseApp's project ID. - */ - private static getProjectId(app: FirebaseApp): string { - if (typeof app !== 'object' || app === null || !('options' in app)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'First argument passed to admin.auth() must be a valid Firebase app instance.', - ); - } - return utils.getProjectId(app); - } - /** * @param {object} app The app for this Auth service. * @constructor */ constructor(app: FirebaseApp) { - super( - Auth.getProjectId(app), - new AuthRequestHandler(app), - cryptoSignerFromApp(app)); + super(app, new AuthRequestHandler(app)); this.app_ = app; this.tenantManager_ = new TenantManager(app); } diff --git a/src/auth/credential.ts b/src/auth/credential.ts index 6428b2651a..521f71bedd 100644 --- a/src/auth/credential.ts +++ b/src/auth/credential.ts @@ -19,7 +19,7 @@ import fs = require('fs'); import os = require('os'); import path = require('path'); -import {AppErrorCodes, FirebaseAppError} from '../utils/error'; +import {AppErrorCodes, FirebaseAppError, FirebaseAuthError, AuthClientErrorCode} from '../utils/error'; import {HttpClient, HttpRequestConfig, HttpError, HttpResponse} from '../utils/api-request'; import {Agent} from 'http'; @@ -69,7 +69,7 @@ export class RefreshToken { * Tries to load a RefreshToken from a path. If the path is not present, returns null. * Throws if data at the path is invalid. */ - public static fromPath(filePath: string): RefreshToken { + public static fromPath(filePath: string): RefreshToken | null { let jsonString: string; try { @@ -224,7 +224,7 @@ function getDetailFromResponse(response: HttpResponse): string { } return detail; } - return response.text; + return response.text || 'Missing error payload'; } /** @@ -234,7 +234,7 @@ export class CertCredential implements FirebaseCredential { private readonly certificate: Certificate; private readonly httpClient: HttpClient; - private readonly httpAgent: Agent; + private readonly httpAgent?: Agent; constructor(serviceAccountPathOrObject: string | object, httpAgent?: Agent) { this.certificate = (typeof serviceAccountPathOrObject === 'string') ? @@ -306,8 +306,8 @@ export interface FirebaseCredential extends Credential { * @param {Credential} credential A Credential instance. * @return {Certificate} A Certificate instance or null. */ -export function tryGetCertificate(credential: Credential): Certificate | null { - if (isFirebaseCredential(credential)) { +export function tryGetCertificate(credential: Credential | null | undefined): Certificate | null { + if (credential && isFirebaseCredential(credential)) { return credential.getCertificate(); } @@ -325,11 +325,22 @@ export class RefreshTokenCredential implements Credential { private readonly refreshToken: RefreshToken; private readonly httpClient: HttpClient; - private readonly httpAgent: Agent; + private readonly httpAgent?: Agent; constructor(refreshTokenPathOrObject: string | object, httpAgent?: Agent) { - this.refreshToken = (typeof refreshTokenPathOrObject === 'string') ? - RefreshToken.fromPath(refreshTokenPathOrObject) : new RefreshToken(refreshTokenPathOrObject); + if (typeof refreshTokenPathOrObject === 'string') { + const refreshToken = RefreshToken.fromPath(refreshTokenPathOrObject); + if (!refreshToken) { + throw new FirebaseAuthError( + AuthClientErrorCode.NOT_FOUND, + 'The file refered to by the refreshTokenPathOrObject parameter (' + + refreshTokenPathOrObject + ') was not found.', + ); + } + this.refreshToken = refreshToken; + } else { + this.refreshToken = new RefreshToken(refreshTokenPathOrObject); + } this.httpClient = new HttpClient(); this.httpAgent = httpAgent; } @@ -362,7 +373,7 @@ export class RefreshTokenCredential implements Credential { export class MetadataServiceCredential implements Credential { private readonly httpClient = new HttpClient(); - private readonly httpAgent: Agent; + private readonly httpAgent?: Agent; constructor(httpAgent?: Agent) { this.httpAgent = httpAgent; @@ -396,10 +407,12 @@ export class ApplicationDefaultCredential implements FirebaseCredential { } // It is OK to not have this file. If it is present, it must be valid. - const refreshToken = RefreshToken.fromPath(GCLOUD_CREDENTIAL_PATH); - if (refreshToken) { - this.credential_ = new RefreshTokenCredential(refreshToken, httpAgent); - return; + if (GCLOUD_CREDENTIAL_PATH) { + const refreshToken = RefreshToken.fromPath(GCLOUD_CREDENTIAL_PATH); + if (refreshToken) { + this.credential_ = new RefreshTokenCredential(refreshToken, httpAgent); + return; + } } this.credential_ = new MetadataServiceCredential(httpAgent); @@ -409,7 +422,7 @@ export class ApplicationDefaultCredential implements FirebaseCredential { return this.credential_.getAccessToken(); } - public getCertificate(): Certificate { + public getCertificate(): Certificate | null { return tryGetCertificate(this.credential_); } diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index ea626e8c39..b63e5c5186 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -27,7 +27,7 @@ const ALGORITHM_RS256 = 'RS256'; const ONE_HOUR_IN_SECONDS = 60 * 60; // List of blacklisted claims which cannot be provided when creating a custom token -const BLACKLISTED_CLAIMS = [ +export const BLACKLISTED_CLAIMS = [ 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat', 'iss', 'jti', 'nbf', 'nonce', ]; @@ -133,7 +133,7 @@ export class ServiceAccountSigner implements CryptoSigner { */ export class IAMSigner implements CryptoSigner { private readonly httpClient: AuthorizedHttpClient; - private serviceAccountId: string; + private serviceAccountId?: string; constructor(httpClient: AuthorizedHttpClient, serviceAccountId?: string) { if (!httpClient) { @@ -169,15 +169,20 @@ export class IAMSigner implements CryptoSigner { }).catch((err) => { if (err instanceof HttpError) { const error = err.response.data; - let errorCode: string; - let errorMsg: string; if (validator.isNonNullObject(error) && error.error) { - errorCode = error.error.status || null; + const errorCode = error.error.status; const description = 'Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens ' + 'for more details on how to use and troubleshoot this feature.'; - errorMsg = `${error.error.message}; ${description}` || null; + const errorMsg = `${error.error.message}; ${description}`; + + throw FirebaseAuthError.fromServerError(errorCode, errorMsg, error); } - throw FirebaseAuthError.fromServerError(errorCode, errorMsg, error); + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Error returned from server: ' + error + '. Additionally, an ' + + 'internal error occurred while attempting to extract the ' + + 'errorcode from the error.', + ); } throw err; }); @@ -199,8 +204,14 @@ export class IAMSigner implements CryptoSigner { }; const client = new HttpClient(); return client.send(request).then((response) => { + if (!response.text) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'HTTP Response missing payload', + ); + } this.serviceAccountId = response.text; - return this.serviceAccountId; + return response.text; }).catch((err) => { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CREDENTIAL, @@ -220,9 +231,11 @@ export class IAMSigner implements CryptoSigner { * @return {CryptoSigner} A CryptoSigner instance. */ export function cryptoSignerFromApp(app: FirebaseApp): CryptoSigner { - const cert = tryGetCertificate(app.options.credential); - if (cert != null && validator.isNonEmptyString(cert.privateKey) && validator.isNonEmptyString(cert.clientEmail)) { - return new ServiceAccountSigner(cert); + if (app.options.credential) { + const cert = tryGetCertificate(app.options.credential); + if (cert != null && validator.isNonEmptyString(cert.privateKey) && validator.isNonEmptyString(cert.clientEmail)) { + return new ServiceAccountSigner(cert); + } } return new IAMSigner(new AuthorizedHttpClient(app), app.options.serviceAccountId); } @@ -254,7 +267,7 @@ export class FirebaseTokenGenerator { * service account key and containing the provided payload. */ public createCustomToken(uid: string, developerClaims?: {[key: string]: any}): Promise { - let errorMessage: string; + let errorMessage: string | undefined; if (typeof uid !== 'string' || uid === '') { errorMessage = 'First argument to createCustomToken() must be a non-empty string uid.'; } else if (uid.length > 128) { @@ -263,7 +276,7 @@ export class FirebaseTokenGenerator { errorMessage = 'Second argument to createCustomToken() must be an object containing the developer claims.'; } - if (typeof errorMessage !== 'undefined') { + if (errorMessage) { throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); } diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index aac7b31b54..6a72f1e8a5 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -16,9 +16,12 @@ import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo} from '../utils/error'; +import * as util from '../utils/index'; import * as validator from '../utils/validator'; import * as jwt from 'jsonwebtoken'; import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; +import { DecodedIdToken } from './auth'; +import { FirebaseApp } from '../firebase-app'; // Audience to use for Firebase Auth Custom tokens const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; @@ -73,8 +76,8 @@ export class FirebaseTokenVerifier { private readonly shortNameArticle: string; constructor(private clientCertUrl: string, private algorithm: string, - private issuer: string, private projectId: string, - private tokenInfo: FirebaseTokenInfo) { + private issuer: string, private tokenInfo: FirebaseTokenInfo, + private readonly app: FirebaseApp) { if (!validator.isURL(clientCertUrl)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -130,10 +133,10 @@ export class FirebaseTokenVerifier { * Verifies the format and signature of a Firebase Auth JWT token. * * @param {string} jwtToken The Firebase Auth JWT token to verify. - * @return {Promise} A promise fulfilled with the decoded claims of the Firebase Auth ID + * @return {Promise} A promise fulfilled with the decoded claims of the Firebase Auth ID * token. */ - public verifyJWT(jwtToken: string): Promise { + public verifyJWT(jwtToken: string): Promise { if (!validator.isString(jwtToken)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -141,7 +144,14 @@ export class FirebaseTokenVerifier { ); } - if (!validator.isNonEmptyString(this.projectId)) { + return util.findProjectId(this.app) + .then((projectId) => { + return this.verifyJWTWithProjectId(jwtToken, projectId); + }); + } + + private verifyJWTWithProjectId(jwtToken: string, projectId: string | null): Promise { + if (!validator.isNonEmptyString(projectId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CREDENTIAL, `Must initialize app with a cert credential or set your Firebase project ID as the ` + @@ -161,7 +171,7 @@ export class FirebaseTokenVerifier { const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; - let errorMessage: string; + let errorMessage: string | undefined; if (!fullDecodedToken) { errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed the entire string JWT ` + `which represents ${this.shortNameArticle} ${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; @@ -183,13 +193,13 @@ export class FirebaseTokenVerifier { } else if (header.alg !== this.algorithm) { errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + `" but got ` + `"` + header.alg + `".` + verifyJwtTokenDocsMessage; - } else if (payload.aud !== this.projectId) { + } else if (payload.aud !== projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + - this.projectId + `" but got "` + payload.aud + `".` + projectIdMatchMessage + + projectId + `" but got "` + payload.aud + `".` + projectIdMatchMessage + verifyJwtTokenDocsMessage; - } else if (payload.iss !== this.issuer + this.projectId) { + } else if (payload.iss !== this.issuer + projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` + - `"${this.issuer}"` + this.projectId + `" but got "` + + `"${this.issuer}"` + projectId + `" but got "` + payload.iss + `".` + projectIdMatchMessage + verifyJwtTokenDocsMessage; } else if (typeof payload.sub !== 'string') { errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage; @@ -199,7 +209,7 @@ export class FirebaseTokenVerifier { errorMessage = `${this.tokenInfo.jwtName} has "sub" (subject) claim longer than 128 characters.` + verifyJwtTokenDocsMessage; } - if (typeof errorMessage !== 'undefined') { + if (errorMessage) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); } @@ -224,16 +234,16 @@ export class FirebaseTokenVerifier { * Verifies the JWT signature using the provided public key. * @param {string} jwtToken The JWT token to verify. * @param {string} publicKey The public key certificate. - * @return {Promise} A promise that resolves with the decoded JWT claims on successful + * @return {Promise} A promise that resolves with the decoded JWT claims on successful * verification. */ - private verifyJwtSignatureWithKey(jwtToken: string, publicKey: string): Promise { + private verifyJwtSignatureWithKey(jwtToken: string, publicKey: string): Promise { const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; return new Promise((resolve, reject) => { jwt.verify(jwtToken, publicKey, { algorithms: [this.algorithm], - }, (error: jwt.VerifyErrors, decodedToken: any) => { + }, (error: jwt.VerifyErrors, decodedToken: string | object) => { if (error) { if (error.name === 'TokenExpiredError') { const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + @@ -246,8 +256,19 @@ export class FirebaseTokenVerifier { } return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message)); } else { - decodedToken.uid = decodedToken.sub; - resolve(decodedToken); + // TODO(rsgowman): I think the typing on jwt.verify is wrong. It claims that this can be either a string or an + // object, but the code always seems to call it as an object. Investigate and upstream typing changes if this + // is actually correct. + if (typeof decodedToken === 'string') { + return reject(new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + "Unexpected decodedToken. Expected an object but got a string: '" + decodedToken + "'", + )); + } else { + const decodedIdToken = (decodedToken as DecodedIdToken); + decodedIdToken.uid = decodedIdToken.sub; + resolve(decodedIdToken); + } } }); }); @@ -270,6 +291,7 @@ export class FirebaseTokenVerifier { const request: HttpRequestConfig = { method: 'GET', url: this.clientCertUrl, + httpAgent: this.app.options.httpAgent, }; return client.send(request).then((resp) => { if (!resp.isJson() || resp.data.error) { @@ -312,31 +334,31 @@ export class FirebaseTokenVerifier { /** * Creates a new FirebaseTokenVerifier to verify Firebase ID tokens. * - * @param {string} projectId Project ID string. + * @param {FirebaseApp} app Firebase app instance. * @return {FirebaseTokenVerifier} */ -export function createIdTokenVerifier(projectId: string): FirebaseTokenVerifier { +export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier { return new FirebaseTokenVerifier( - CLIENT_CERT_URL, - ALGORITHM_RS256, - 'https://securetoken.google.com/', - projectId, - ID_TOKEN_INFO, + CLIENT_CERT_URL, + ALGORITHM_RS256, + 'https://securetoken.google.com/', + ID_TOKEN_INFO, + app, ); } /** * Creates a new FirebaseTokenVerifier to verify Firebase session cookies. * - * @param {string} projectId Project ID string. + * @param {FirebaseApp} app Firebase app instance. * @return {FirebaseTokenVerifier} */ -export function createSessionCookieVerifier(projectId: string): FirebaseTokenVerifier { +export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier { return new FirebaseTokenVerifier( SESSION_COOKIE_CERT_URL, ALGORITHM_RS256, 'https://session.firebase.google.com/', - projectId, SESSION_COOKIE_INFO, + app, ); } diff --git a/src/auth/user-import-builder.ts b/src/auth/user-import-builder.ts index ecb5f3eb00..145c373304 100755 --- a/src/auth/user-import-builder.ts +++ b/src/auth/user-import-builder.ts @@ -192,13 +192,13 @@ export function convertMultiFactorInfoToServerFormat(multiFactorInfo: SecondFact /** * @param {any} obj The object to check for number field within. * @param {string} key The entry key. - * @return {number|undefined} The corresponding number if available. + * @return {number} The corresponding number if available. Otherwise, NaN. */ -function getNumberField(obj: any, key: string): number | undefined { +function getNumberField(obj: any, key: string): number { if (typeof obj[key] !== 'undefined' && obj[key] !== null) { return parseInt(obj[key].toString(), 10); } - return undefined; + return NaN; } @@ -250,7 +250,7 @@ function populateUploadAccountUser( } if (validator.isArray(user.providerData)) { user.providerData.forEach((providerData) => { - result.providerUserInfo.push({ + result.providerUserInfo!.push({ providerId: providerData.providerId, rawId: providerData.uid, email: providerData.email, @@ -264,7 +264,7 @@ function populateUploadAccountUser( if (validator.isNonNullObject(user.multiFactor) && validator.isNonEmptyArray(user.multiFactor.enrolledFactors)) { user.multiFactor.enrolledFactors.forEach((multiFactorInfo) => { - result.mfaInfo.push(convertMultiFactorInfoToServerFormat(multiFactorInfo)); + result.mfaInfo!.push(convertMultiFactorInfoToServerFormat(multiFactorInfo)); }); } @@ -275,10 +275,10 @@ function populateUploadAccountUser( delete result[key]; } } - if (result.providerUserInfo.length === 0) { + if (result.providerUserInfo!.length === 0) { delete result.providerUserInfo; } - if (result.mfaInfo.length === 0) { + if (result.mfaInfo!.length === 0) { delete result.mfaInfo; } // Validate the constructured user individual request. This will throw if an error @@ -309,16 +309,16 @@ export class UserImportBuilder { * @constructor */ constructor( - private users: UserImportRecord[], - private options?: UserImportOptions, - private userRequestValidator?: ValidatorFunction) { + users: UserImportRecord[], + options?: UserImportOptions, + userRequestValidator?: ValidatorFunction) { this.requiresHashOptions = false; this.validatedUsers = []; this.userImportResultErrors = []; this.indexMap = {}; - this.validatedUsers = this.populateUsers(this.users, this.userRequestValidator); - this.validatedOptions = this.populateOptions(this.options, this.requiresHashOptions); + this.validatedUsers = this.populateUsers(users, userRequestValidator); + this.validatedOptions = this.populateOptions(options, this.requiresHashOptions); } /** @@ -342,7 +342,7 @@ export class UserImportBuilder { failedUploads: Array<{index: number, message: string}>): UserImportResult { // Initialize user import result. const importResult: UserImportResult = { - successCount: this.users.length - this.userImportResultErrors.length, + successCount: this.validatedUsers.length, failureCount: this.userImportResultErrors.length, errors: deepCopy(this.userImportResultErrors), }; @@ -374,7 +374,7 @@ export class UserImportBuilder { * @return {UploadAccountOptions} The populated UploadAccount options. */ private populateOptions( - options: UserImportOptions, requiresHashOptions: boolean): UploadAccountOptions { + options: UserImportOptions | undefined, requiresHashOptions: boolean): UploadAccountOptions { let populatedOptions: UploadAccountOptions; if (!requiresHashOptions) { return {}; @@ -422,6 +422,22 @@ export class UserImportBuilder { case 'SHA1': case 'SHA256': case 'SHA512': + // MD5 is [0,8192] but SHA1, SHA256, and SHA512 are [1,8192] + rounds = getNumberField(options.hash, 'rounds'); + const minRounds = options.hash.algorithm === 'MD5' ? 0 : 1; + if (isNaN(rounds) || rounds < minRounds || rounds > 8192) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_ROUNDS, + `A valid "hash.rounds" number between ${minRounds} and 8192 must be provided for ` + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + populatedOptions = { + hashAlgorithm: options.hash.algorithm, + rounds, + }; + break; + case 'PBKDF_SHA1': case 'PBKDF2_SHA256': rounds = getNumberField(options.hash, 'rounds'); diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 468951310c..386f2d3c83 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -27,10 +27,10 @@ const B64_REDACTED = Buffer.from('REDACTED').toString('base64'); /** * Parses a time stamp string or number and returns the corresponding date if valid. * - * @param time The unix timestamp string or number in milliseconds. - * @return The corresponding date as a UTC string, if valid. + * @param {any} time The unix timestamp string or number in milliseconds. + * @return {string} The corresponding date as a UTC string, if valid. Otherwise, null. */ -function parseDate(time: any): string { +function parseDate(time: any): string | null { try { const date = new Date(parseInt(time, 10)); if (!isNaN(date.getTime())) { @@ -405,14 +405,12 @@ export class UserRecord { } utils.addReadonlyGetter(this, 'passwordSalt', response.salt); - try { + if (response.customAttributes) { utils.addReadonlyGetter( - this, 'customClaims', JSON.parse(response.customAttributes)); - } catch (e) { - // Ignore error. - utils.addReadonlyGetter(this, 'customClaims', undefined); + this, 'customClaims', JSON.parse(response.customAttributes)); } - let validAfterTime: string = null; + + let validAfterTime: string | null = null; // Convert validSince first to UTC milliseconds and then to UTC date string. if (typeof response.validSince !== 'undefined') { validAfterTime = parseDate(parseInt(response.validSince, 10) * 1000); diff --git a/src/database/database.ts b/src/database/database.ts index 184c423c5c..666447f4ae 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -2,7 +2,7 @@ import {URL} from 'url'; import * as path from 'path'; import {FirebaseApp} from '../firebase-app'; -import {FirebaseDatabaseError, AppErrorCodes} from '../utils/error'; +import {FirebaseDatabaseError, AppErrorCodes, FirebaseAppError} from '../utils/error'; import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; import {Database} from '@firebase/database'; @@ -140,6 +140,9 @@ class DatabaseRulesClient { }; return this.httpClient.send(req) .then((resp) => { + if (!resp.text) { + throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.'); + } return resp.text; }) .catch((err) => { diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 1b21aefa8b..8ac3a6ba88 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -67,7 +67,7 @@ export interface FirebaseAccessToken { export class FirebaseAppInternals { private isDeleted_ = false; private cachedToken_: FirebaseAccessToken; - private cachedTokenPromise_: Promise; + private cachedTokenPromise_: Promise | null; private tokenListeners_: Array<(token: string) => void>; private tokenRefreshTimeout_: NodeJS.Timer; @@ -282,7 +282,7 @@ export class FirebaseApp { (this as {[key: string]: any})[serviceName] = this.getService_.bind(this, serviceName); }); - this.INTERNAL = new FirebaseAppInternals(this.options_.credential); + this.INTERNAL = new FirebaseAppInternals(credential); } /** diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts index c284ee67e7..c7afa87b7c 100644 --- a/src/firestore/firestore.ts +++ b/src/firestore/firestore.ts @@ -71,8 +71,8 @@ export function getFirestoreOptions(app: FirebaseApp): Settings { }); } - const projectId: string = utils.getProjectId(app); - const cert: Certificate = tryGetCertificate(app.options.credential); + const projectId: string | null = utils.getProjectId(app); + const cert: Certificate | null = tryGetCertificate(app.options.credential); const { version: firebaseVersion } = require('../../package.json'); if (cert != null) { // cert is available when the SDK has been initialized with a service account JSON file, diff --git a/src/index.d.ts b/src/index.d.ts index caba04096c..151f787466 100755 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -140,7 +140,7 @@ declare namespace admin { * [Authenticate with limited privileges](/docs/database/admin/start#authenticate-with-limited-privileges) * for detailed documentation and code samples. */ - databaseAuthVariableOverride?: Object; + databaseAuthVariableOverride?: Object | null; /** * The URL of the Realtime Database from which to read and write data. @@ -891,7 +891,7 @@ declare namespace admin.auth { * The salt separator in buffer bytes which is appended to salt when * verifying a password. This is only used by the `SCRYPT` algorithm. */ - saltSeparator?: string; + saltSeparator?: Buffer; /** * The number of rounds for hashing calculation. @@ -4038,6 +4038,120 @@ declare namespace admin.messaging { * ID specified in the app manifest. */ channelId?: string; + + /** + * Sets the "ticker" text, which is sent to accessibility services. Prior to + * API level 21 (Lollipop), sets the text that is displayed in the status bar + * when the notification first arrives. + */ + ticker?: string; + + /** + * When set to `false` or unset, the notification is automatically dismissed when + * the user clicks it in the panel. When set to `true`, the notification persists + * even when the user clicks it. + */ + sticky?: boolean; + + /** + * For notifications that inform users about events with an absolute time reference, sets + * the time that the event in the notification occurred. Notifications + * in the panel are sorted by this time. + */ + eventTimestamp?: Date; + + /** + * Sets whether or not this notification is relevant only to the current device. + * Some notifications can be bridged to other devices for remote display, such as + * a Wear OS watch. This hint can be set to recommend this notification not be bridged. + * See [Wear OS guides](https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging) + */ + localOnly?: boolean; + + /** + * Sets the relative priority for this notification. Low-priority notifications + * may be hidden from the user in certain situations. Note this priority differs + * from `AndroidMessagePriority`. This priority is processed by the client after + * the message has been delivered. Whereas `AndroidMessagePriority` is an FCM concept + * that controls when the message is delivered. + */ + priority?: ('min' | 'low' | 'default' | 'high' | 'max'); + + /** + * Sets the vibration pattern to use. Pass in an array of milliseconds to + * turn the vibrator on or off. The first value indicates the duration to wait before + * turning the vibrator on. The next value indicates the duration to keep the + * vibrator on. Subsequent values alternate between duration to turn the vibrator + * off and to turn the vibrator on. If `vibrate_timings` is set and `default_vibrate_timings` + * is set to `true`, the default value is used instead of the user-specified `vibrate_timings`. + */ + vibrateTimingsMillis?: number[]; + + /** + * If set to `true`, use the Android framework's default vibrate pattern for the + * notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * If `default_vibrate_timings` is set to `true` and `vibrate_timings` is also set, + * the default value is used instead of the user-specified `vibrate_timings`. + */ + defaultVibrateTimings?: boolean; + + /** + * If set to `true`, use the Android framework's default sound for the notification. + * Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + */ + defaultSound?: boolean; + + /** + * Settings to control the notification's LED blinking rate and color if LED is + * available on the device. The total blinking time is controlled by the OS. + */ + lightSettings?: LightSettings; + + /** + * If set to `true`, use the Android framework's default LED light settings + * for the notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * If `default_light_settings` is set to `true` and `light_settings` is also set, + * the user-specified `light_settings` is used instead of the default value. + */ + defaultLightSettings?: boolean; + + /** + * Sets the visibility of the notification. Must be either `private`, `public`, + * or `secret`. If unspecified, defaults to `private`. + */ + visibility?: ('private' | 'public' | 'secret'); + + /** + * Sets the number of items this notification represents. May be displayed as a + * badge count for Launchers that support badging. See [`NotificationBadge`(https://developer.android.com/training/notify-user/badges). + * For example, this might be useful if you're using just one notification to + * represent multiple new messages but you want the count here to represent + * the number of total new messages. If zero or unspecified, systems + * that support badging use the default, which is to increment a number + * displayed on the long-press menu each time a new notification arrives. + */ + notificationCount?: number; + } + + /** + * Represents settings to control notification LED that can be included in + * {@link admin.messaging.AndroidNotification}. + */ + interface LightSettings { + /** + * Required. Sets color of the LED in `#rrggbb` or `#rrggbbaa` format. + */ + color: string; + + /** + * Required. Along with `light_off_duration`, defines the blink rate of LED flashes. + */ + lightOnDurationMillis: number; + + /** + * Required. Along with `light_on_duration`, defines the blink rate of LED flashes. + */ + lightOffDurationMillis: number; } /** @@ -4924,7 +5038,7 @@ declare namespace admin.messaging { * return value. * * @param messages A non-empty array - * containing up to 100 messages. + * containing up to 500 messages. * @param dryRun Whether to send the messages in the dry-run * (validation only) mode. * @return A Promise fulfilled with an object representing the result of the @@ -4947,7 +5061,7 @@ declare namespace admin.messaging { * a `BatchResponse` return value. * * @param message A multicast message - * containing up to 100 tokens. + * containing up to 500 tokens. * @param dryRun Whether to send the message in the dry-run * (validation only) mode. * @return A Promise fulfilled with an object representing the result of the diff --git a/src/instance-id/instance-id-request.ts b/src/instance-id/instance-id-request.ts index 6a31025f03..37b5bf9c85 100644 --- a/src/instance-id/instance-id-request.ts +++ b/src/instance-id/instance-id-request.ts @@ -62,7 +62,7 @@ export class FirebaseInstanceIdRequestHandler { this.path = FIREBASE_IID_PATH + `project/${projectId}/instanceId/`; } - public deleteInstanceId(instanceId: string): Promise { + public deleteInstanceId(instanceId: string): Promise { if (!validator.isNonEmptyString(instanceId)) { return Promise.reject(new FirebaseInstanceIdError( InstanceIdClientErrorCode.INVALID_INSTANCE_ID, @@ -76,9 +76,9 @@ export class FirebaseInstanceIdRequestHandler { * Invokes the request handler based on the API settings object passed. * * @param {ApiSettings} apiSettings The API endpoint settings to apply to request and response. - * @return {Promise} A promise that resolves with the response. + * @return {Promise} A promise that resolves when the request is complete. */ - private invokeRequestHandler(apiSettings: ApiSettings): Promise { + private invokeRequestHandler(apiSettings: ApiSettings): Promise { const path: string = this.path + apiSettings.getEndpoint(); return Promise.resolve() .then(() => { @@ -90,7 +90,7 @@ export class FirebaseInstanceIdRequestHandler { return this.httpClient.send(req); }) .then((response) => { - return response.data; + // return nothing on success }) .catch((err) => { if (err instanceof HttpError) { diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index ee544fa4e0..60bc52565b 100644 --- a/src/instance-id/instance-id.ts +++ b/src/instance-id/instance-id.ts @@ -55,7 +55,7 @@ export class InstanceId implements FirebaseServiceInterface { ); } - const projectId: string = utils.getProjectId(app); + const projectId: string | null = utils.getProjectId(app); if (!validator.isNonEmptyString(projectId)) { // Assert for an explicit projct ID (either via AppOptions or the cert itself). throw new FirebaseInstanceIdError( diff --git a/src/messaging/batch-request.ts b/src/messaging/batch-request.ts index 76cdb0f8b1..b0b2937a32 100644 --- a/src/messaging/batch-request.ts +++ b/src/messaging/batch-request.ts @@ -17,6 +17,7 @@ import { HttpClient, HttpRequestConfig, HttpResponse, parseHttpResponse, } from '../utils/api-request'; +import { FirebaseAppError, AppErrorCodes } from '../utils/error'; const PART_BOUNDARY: string = '__END_OF_PART__'; const TEN_SECONDS_IN_MILLIS = 10000; @@ -74,6 +75,9 @@ export class BatchRequestClient { timeout: TEN_SECONDS_IN_MILLIS, }; return this.httpClient.send(request).then((response) => { + if (!response.multipart) { + throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'Expected a multipart response.'); + } return response.multipart.map((buff) => { return parseHttpResponse(buff, request); }); @@ -128,7 +132,7 @@ function serializeSubRequest(request: SubRequest): string { messagePayload += 'Content-Type: application/json; charset=UTF-8\r\n'; if (request.headers) { Object.keys(request.headers).forEach((key) => { - messagePayload += `${key}: ${request.headers[key]}\r\n`; + messagePayload += `${key}: ${request.headers![key]}\r\n`; }); } messagePayload += '\r\n'; diff --git a/src/messaging/messaging-errors.ts b/src/messaging/messaging-errors.ts index 90bca0f2b1..1fd1809aec 100644 --- a/src/messaging/messaging-errors.ts +++ b/src/messaging/messaging-errors.ts @@ -67,21 +67,22 @@ export function createFirebaseError(err: HttpError): FirebaseMessagingError { */ export function getErrorCode(response: any): string | null { if (validator.isNonNullObject(response) && 'error' in response) { - if (validator.isString(response.error)) { - return response.error; + const error = response.error; + if (validator.isString(error)) { + return error; } - if (validator.isArray(response.error.details)) { + if (validator.isArray(error.details)) { const fcmErrorType = 'type.googleapis.com/google.firebase.fcm.v1.FcmError'; - for (const element of response.error.details) { + for (const element of error.details) { if (element['@type'] === fcmErrorType) { return element.errorCode; } } } - if ('status' in response.error) { - return response.error.status; + if ('status' in error) { + return error.status; } else { - return response.error.message; + return error.message; } } @@ -97,8 +98,8 @@ export function getErrorCode(response: any): string | null { function getErrorMessage(response: any): string | null { if (validator.isNonNullObject(response) && 'error' in response && - validator.isNonEmptyString(response.error.message)) { - return response.error.message; + validator.isNonEmptyString((response as any).error.message)) { + return (response as any).error.message; } return null; } diff --git a/src/messaging/messaging-types.ts b/src/messaging/messaging-types.ts index aaea9e01c6..50bb3a4b37 100644 --- a/src/messaging/messaging-types.ts +++ b/src/messaging/messaging-types.ts @@ -171,6 +171,24 @@ export interface AndroidNotification { titleLocKey?: string; titleLocArgs?: string[]; channelId?: string; + ticker?: string; + sticky?: boolean; + eventTimestamp?: Date; + localOnly?: boolean; + priority?: ('min' | 'low' | 'default' | 'high' | 'max'); + vibrateTimingsMillis?: number[]; + defaultVibrateTimings?: boolean; + defaultSound?: boolean; + lightSettings?: LightSettings; + defaultLightSettings?: boolean; + visibility?: ('private' | 'public' | 'secret'); + notificationCount?: number; +} + +export interface LightSettings { + color: string; + lightOnDurationMillis: number; + lightOffDurationMillis: number; } export interface AndroidFcmOptions { @@ -196,7 +214,7 @@ export interface NotificationMessagePayload { clickAction?: string; titleLocKey?: string; titleLocArgs?: string; - [other: string]: string; + [other: string]: string | undefined; } /* Composite messaging payload (data and notification payloads are both optional) */ @@ -319,7 +337,7 @@ export function validateMessage(message: Message) { * @param {object} map An object to be validated. * @param {string} label A label to be included in the errors thrown. */ -function validateStringMap(map: {[key: string]: any}, label: string) { +function validateStringMap(map: {[key: string]: any} | undefined, label: string) { if (typeof map === 'undefined') { return; } else if (!validator.isNonNullObject(map)) { @@ -339,7 +357,7 @@ function validateStringMap(map: {[key: string]: any}, label: string) { * * @param {WebpushConfig} config An object to be validated. */ -function validateWebpushConfig(config: WebpushConfig) { +function validateWebpushConfig(config: WebpushConfig | undefined) { if (typeof config === 'undefined') { return; } else if (!validator.isNonNullObject(config)) { @@ -356,7 +374,7 @@ function validateWebpushConfig(config: WebpushConfig) { * * @param {ApnsConfig} config An object to be validated. */ -function validateApnsConfig(config: ApnsConfig) { +function validateApnsConfig(config: ApnsConfig | undefined) { if (typeof config === 'undefined') { return; } else if (!validator.isNonNullObject(config)) { @@ -373,7 +391,7 @@ function validateApnsConfig(config: ApnsConfig) { * * @param {ApnsFcmOptions} fcmOptions An object to be validated. */ -function validateApnsFcmOptions(fcmOptions: ApnsFcmOptions) { +function validateApnsFcmOptions(fcmOptions: ApnsFcmOptions | undefined) { if (typeof fcmOptions === 'undefined') { return; } else if (!validator.isNonNullObject(fcmOptions)) { @@ -411,7 +429,7 @@ function validateApnsFcmOptions(fcmOptions: ApnsFcmOptions) { * * @param {FcmOptions} fcmOptions An object to be validated. */ -function validateFcmOptions(fcmOptions: FcmOptions) { +function validateFcmOptions(fcmOptions: FcmOptions | undefined) { if (typeof fcmOptions === 'undefined') { return; } else if (!validator.isNonNullObject(fcmOptions)) { @@ -430,7 +448,7 @@ function validateFcmOptions(fcmOptions: FcmOptions) { * * @param {Notification} notification An object to be validated. */ -function validateNotification(notification: Notification) { +function validateNotification(notification: Notification | undefined) { if (typeof notification === 'undefined') { return; } else if (!validator.isNonNullObject(notification)) { @@ -461,7 +479,7 @@ function validateNotification(notification: Notification) { * * @param {ApnsPayload} payload An object to be validated. */ -function validateApnsPayload(payload: ApnsPayload) { +function validateApnsPayload(payload: ApnsPayload | undefined) { if (typeof payload === 'undefined') { return; } else if (!validator.isNonNullObject(payload)) { @@ -519,7 +537,7 @@ function validateAps(aps: Aps) { } } -function validateApsSound(sound: string | CriticalSound) { +function validateApsSound(sound: string | CriticalSound | undefined) { if (typeof sound === 'undefined' || validator.isNonEmptyString(sound)) { return; } else if (!validator.isNonNullObject(sound)) { @@ -565,7 +583,7 @@ function validateApsSound(sound: string | CriticalSound) { * * @param {string | ApsAlert} alert An alert string or an object to be validated. */ -function validateApsAlert(alert: string | ApsAlert) { +function validateApsAlert(alert: string | ApsAlert | undefined) { if (typeof alert === 'undefined' || validator.isString(alert)) { return; } else if (!validator.isNonNullObject(alert)) { @@ -614,7 +632,7 @@ function validateApsAlert(alert: string | ApsAlert) { * * @param {AndroidConfig} config An object to be validated. */ -function validateAndroidConfig(config: AndroidConfig) { +function validateAndroidConfig(config: AndroidConfig | undefined) { if (typeof config === 'undefined') { return; } else if (!validator.isNonNullObject(config)) { @@ -628,18 +646,7 @@ function validateAndroidConfig(config: AndroidConfig) { MessagingClientErrorCode.INVALID_PAYLOAD, 'TTL must be a non-negative duration in milliseconds'); } - const seconds = Math.floor(config.ttl / 1000); - const nanos = (config.ttl - seconds * 1000) * 1000000; - let duration: string; - if (nanos > 0) { - let nanoString = nanos.toString(); - while (nanoString.length < 9) { - nanoString = '0' + nanoString; - } - duration = `${seconds}.${nanoString}s`; - } else { - duration = `${seconds}s`; - } + const duration: string = transformMillisecondsToSecondsString(config.ttl); (config as any).ttl = duration; } validateStringMap(config.data, 'android.data'); @@ -660,7 +667,7 @@ function validateAndroidConfig(config: AndroidConfig) { * * @param {AndroidNotification} notification An object to be validated. */ -function validateAndroidNotification(notification: AndroidNotification) { +function validateAndroidNotification(notification: AndroidNotification | undefined) { if (typeof notification === 'undefined') { return; } else if (!validator.isNonNullObject(notification)) { @@ -691,6 +698,47 @@ function validateAndroidNotification(notification: AndroidNotification) { 'android.notification.imageUrl must be a valid URL string'); } + if (typeof notification.eventTimestamp !== 'undefined') { + if (!(notification.eventTimestamp instanceof Date)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.eventTimestamp must be a valid `Date` object'); + } + // Convert timestamp to RFC3339 UTC "Zulu" format, example "2014-10-02T15:01:23.045123456Z" + const zuluTimestamp = notification.eventTimestamp.toISOString(); + (notification as any).eventTimestamp = zuluTimestamp; + } + + if (typeof notification.vibrateTimingsMillis !== 'undefined') { + if (!validator.isNonEmptyArray(notification.vibrateTimingsMillis)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.vibrateTimingsMillis must be a non-empty array of numbers'); + } + const vibrateTimings: string[] = []; + notification.vibrateTimingsMillis.forEach((value) => { + if (!validator.isNumber(value) || value < 0) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.vibrateTimingsMillis must be non-negative durations in milliseconds'); + } + const duration = transformMillisecondsToSecondsString(value); + vibrateTimings.push(duration); + }); + (notification as any).vibrateTimingsMillis = vibrateTimings; + } + + if (typeof notification.priority !== 'undefined') { + const priority = 'PRIORITY_' + notification.priority.toUpperCase(); + (notification as any).priority = priority; + } + + if (typeof notification.visibility !== 'undefined') { + const visibility = notification.visibility.toUpperCase(); + (notification as any).visibility = visibility; + } + + validateLightSettings(notification.lightSettings); + const propertyMappings = { clickAction: 'click_action', bodyLocKey: 'body_loc_key', @@ -699,16 +747,84 @@ function validateAndroidNotification(notification: AndroidNotification) { titleLocArgs: 'title_loc_args', channelId: 'channel_id', imageUrl: 'image', + eventTimestamp: 'event_time', + localOnly: 'local_only', + priority: 'notification_priority', + vibrateTimingsMillis: 'vibrate_timings', + defaultVibrateTimings: 'default_vibrate_timings', + defaultSound: 'default_sound', + lightSettings: 'light_settings', + defaultLightSettings: 'default_light_settings', + notificationCount: 'notification_count', }; renameProperties(notification, propertyMappings); } +/** + * Checks if the given LightSettings object is valid. The object must have valid color and + * light on/off duration parameters. If successful, transforms the input object by renaming + * keys to valid Android keys. + * + * @param {LightSettings} lightSettings An object to be validated. + */ +function validateLightSettings(lightSettings?: LightSettings) { + if (typeof lightSettings === 'undefined') { + return; + } else if (!validator.isNonNullObject(lightSettings)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings must be a non-null object'); + } + + if (!validator.isNumber(lightSettings.lightOnDurationMillis) || lightSettings.lightOnDurationMillis < 0) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.lightSettings.lightOnDurationMillis must be a non-negative duration in milliseconds'); + } + const durationOn = transformMillisecondsToSecondsString(lightSettings.lightOnDurationMillis); + (lightSettings as any).lightOnDurationMillis = durationOn; + + if (!validator.isNumber(lightSettings.lightOffDurationMillis) || lightSettings.lightOffDurationMillis < 0) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.lightSettings.lightOffDurationMillis must be a non-negative duration in milliseconds'); + } + const durationOff = transformMillisecondsToSecondsString(lightSettings.lightOffDurationMillis); + (lightSettings as any).lightOffDurationMillis = durationOff; + + if (!validator.isString(lightSettings.color) || + (!/^#[0-9a-fA-F]{6}$/.test(lightSettings.color) && !/^#[0-9a-fA-F]{8}$/.test(lightSettings.color))) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.lightSettings.color must be in the form #RRGGBB or #RRGGBBAA format'); + } + const colorString = lightSettings.color.length === 7 ? lightSettings.color + 'FF' : lightSettings.color; + const rgb = /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/i.exec(colorString); + if (!rgb || rgb.length < 4) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INTERNAL_ERROR, + 'regex to extract rgba values from ' + colorString + ' failed.'); + } + const color = { + red: parseInt(rgb[1], 16) / 255.0, + green: parseInt(rgb[2], 16) / 255.0, + blue: parseInt(rgb[3], 16) / 255.0, + alpha: parseInt(rgb[4], 16) / 255.0, + }; + (lightSettings as any).color = color; + + const propertyMappings = { + lightOnDurationMillis: 'light_on_duration', + lightOffDurationMillis: 'light_off_duration', + }; + renameProperties(lightSettings, propertyMappings); +} + /** * Checks if the given AndroidFcmOptions object is valid. * * @param {AndroidFcmOptions} fcmOptions An object to be validated. */ -function validateAndroidFcmOptions(fcmOptions: AndroidFcmOptions) { +function validateAndroidFcmOptions(fcmOptions: AndroidFcmOptions | undefined) { if (typeof fcmOptions === 'undefined') { return; } else if (!validator.isNonNullObject(fcmOptions)) { @@ -721,3 +837,28 @@ function validateAndroidFcmOptions(fcmOptions: AndroidFcmOptions) { MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } } + +/** + * Transforms milliseconds to the format expected by FCM service. + * Returns the duration in seconds with up to nine fractional + * digits, terminated by 's'. Example: "3.5s". + * + * @param {number} milliseconds The duration in milliseconds. + * @return {string} The resulting formatted string in seconds with up to nine fractional + * digits, terminated by 's'. + */ +function transformMillisecondsToSecondsString(milliseconds: number): string { + let duration: string; + const seconds = Math.floor(milliseconds / 1000); + const nanos = (milliseconds - seconds * 1000) * 1000000; + if (nanos > 0) { + let nanoString = nanos.toString(); + while (nanoString.length < 9) { + nanoString = '0' + nanoString; + } + duration = `${seconds}.${nanoString}s`; + } else { + duration = `${seconds}s`; + } + return duration; +} diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index 6444bdf557..b37f8c987d 100644 --- a/src/messaging/messaging.ts +++ b/src/messaging/messaging.ts @@ -21,7 +21,7 @@ import { Message, validateMessage, MessagingDevicesResponse, MessagingDeviceGroupResponse, MessagingTopicManagementResponse, MessagingPayload, MessagingOptions, MessagingTopicResponse, - MessagingConditionResponse, BatchResponse, MulticastMessage, + MessagingConditionResponse, BatchResponse, MulticastMessage, DataMessagePayload, NotificationMessagePayload, } from './messaging-types'; import {FirebaseMessagingRequestHandler} from './messaging-api-request'; import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; @@ -40,7 +40,7 @@ const FCM_TOPIC_MANAGEMENT_ADD_PATH = '/iid/v1:batchAdd'; const FCM_TOPIC_MANAGEMENT_REMOVE_PATH = '/iid/v1:batchRemove'; // Maximum messages that can be included in a batch request. -const FCM_MAX_BATCH_SIZE = 100; +const FCM_MAX_BATCH_SIZE = 500; // Key renames for the messaging notification payload object. const CAMELCASED_NOTIFICATION_PAYLOAD_KEYS_MAP = { @@ -206,8 +206,8 @@ export class Messaging implements FirebaseServiceInterface { public INTERNAL: MessagingInternals = new MessagingInternals(); private urlPath: string; - private appInternal: FirebaseApp; - private messagingRequestHandler: FirebaseMessagingRequestHandler; + private readonly appInternal: FirebaseApp; + private readonly messagingRequestHandler: FirebaseMessagingRequestHandler; /** * @param {FirebaseApp} app The app for this Messaging service. @@ -221,18 +221,6 @@ export class Messaging implements FirebaseServiceInterface { ); } - const projectId: string = utils.getProjectId(app); - if (!validator.isNonEmptyString(projectId)) { - // Assert for an explicit project ID (either via AppOptions or the cert itself). - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, - 'Failed to determine project ID for Messaging. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', - ); - } - - this.urlPath = `/v1/projects/${projectId}/messages:send`; this.appInternal = app; this.messagingRequestHandler = new FirebaseMessagingRequestHandler(app); } @@ -261,13 +249,13 @@ export class Messaging implements FirebaseServiceInterface { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); } - return Promise.resolve() - .then(() => { + return this.getUrlPath() + .then((urlPath) => { const request: {message: Message, validate_only?: boolean} = {message: copy}; if (dryRun) { request.validate_only = true; } - return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, this.urlPath, request); + return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, urlPath, request); }) .then((response) => { return (response as any).name; @@ -283,7 +271,7 @@ export class Messaging implements FirebaseServiceInterface { * An error from this method indicates a total failure -- i.e. none of the messages in the * list could be sent. Partial failures are indicated by a BatchResponse return value. * - * @param {Message[]} messages A non-empty array containing up to 100 messages. + * @param {Message[]} messages A non-empty array containing up to 500 messages. * @param {boolean=} dryRun Whether to send the message in the dry-run (validation only) mode. * * @return {Promise} A Promise fulfilled with an object representing the result @@ -312,18 +300,21 @@ export class Messaging implements FirebaseServiceInterface { MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); } - const requests: SubRequest[] = copy.map((message) => { - validateMessage(message); - const request: {message: Message, validate_only?: boolean} = {message}; - if (dryRun) { - request.validate_only = true; - } - return { - url: `https://${FCM_SEND_HOST}${this.urlPath}`, - body: request, - }; - }); - return this.messagingRequestHandler.sendBatchRequest(requests); + return this.getUrlPath() + .then((urlPath) => { + const requests: SubRequest[] = copy.map((message) => { + validateMessage(message); + const request: {message: Message, validate_only?: boolean} = {message}; + if (dryRun) { + request.validate_only = true; + } + return { + url: `https://${FCM_SEND_HOST}${urlPath}`, + body: request, + }; + }); + return this.messagingRequestHandler.sendBatchRequest(requests); + }); } /** @@ -335,7 +326,7 @@ export class Messaging implements FirebaseServiceInterface { * indicates a total failure -- i.e. none of the tokens in the list could be sent to. Partial * failures are indicated by a BatchResponse return value. * - * @param {MulticastMessage} message A multicast message containing up to 100 tokens. + * @param {MulticastMessage} message A multicast message containing up to 500 tokens. * @param {boolean=} dryRun Whether to send the message in the dry-run (validation only) mode. * * @return {Promise} A Promise fulfilled with an object representing the result @@ -365,6 +356,7 @@ export class Messaging implements FirebaseServiceInterface { data: copy.data, notification: copy.notification, webpush: copy.webpush, + fcmOptions: copy.fcmOptions, }; }); return this.sendAll(messages, dryRun); @@ -644,6 +636,28 @@ export class Messaging implements FirebaseServiceInterface { ); } + private getUrlPath(): Promise { + if (this.urlPath) { + return Promise.resolve(this.urlPath); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + // Assert for an explicit project ID (either via AppOptions or the cert itself). + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, + 'Failed to determine project ID for Messaging. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', + ); + } + + this.urlPath = `/v1/projects/${projectId}/messages:send`; + return this.urlPath; + }); + } + /** * Helper method which sends and handles topic subscription management requests. * @@ -758,9 +772,7 @@ export class Messaging implements FirebaseServiceInterface { ); } - payloadKeys.forEach((payloadKey: keyof MessagingPayload) => { - const value = payloadCopy[payloadKey]; - + const validatePayload = (payloadKey: string, value: DataMessagePayload | NotificationMessagePayload) => { // Validate each top-level key in the payload is an object if (!validator.isNonNullObject(value)) { throw new FirebaseMessagingError( @@ -786,12 +798,19 @@ export class Messaging implements FirebaseServiceInterface { ); } }); - }); + }; + + if (payloadCopy.data !== undefined) { + validatePayload('data', payloadCopy.data); + } + if (payloadCopy.notification !== undefined) { + validatePayload('notification', payloadCopy.notification); + } // Validate the data payload object does not contain blacklisted properties if ('data' in payloadCopy) { BLACKLISTED_DATA_PAYLOAD_KEYS.forEach((blacklistedKey) => { - if (blacklistedKey in payloadCopy.data) { + if (blacklistedKey in payloadCopy.data!) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, `Messaging payload contains the blacklisted "data.${ blacklistedKey }" property.`, @@ -801,7 +820,7 @@ export class Messaging implements FirebaseServiceInterface { } // Convert whitelisted camelCase keys to underscore_case - if ('notification' in payloadCopy) { + if (payloadCopy.notification) { utils.renameProperties(payloadCopy.notification, CAMELCASED_NOTIFICATION_PAYLOAD_KEYS_MAP); } diff --git a/src/project-management/project-management-api-request.ts b/src/project-management/project-management-api-request.ts index abbdc9d656..14d533ddb0 100644 --- a/src/project-management/project-management-api-request.ts +++ b/src/project-management/project-management-api-request.ts @@ -64,7 +64,7 @@ export class ProjectManagementRequestHandler { `https://${PROJECT_MANAGEMENT_HOST_AND_PORT}${PROJECT_MANAGEMENT_BETA_PATH}`; private readonly httpClient: AuthorizedHttpClient; - private static wrapAndRethrowHttpError(errStatusCode: number, errText: string) { + private static wrapAndRethrowHttpError(errStatusCode: number, errText?: string) { let errorCode: ProjectManagementErrorCode; let errorMessage: string; @@ -102,6 +102,9 @@ export class ProjectManagementRequestHandler { errorMessage = 'An unknown server error was returned.'; } + if (!errText) { + errText = ''; + } throw new FirebaseProjectManagementError( errorCode, `${ errorMessage } Status code: ${ errStatusCode }. Raw server response: "${ errText }".`); @@ -216,7 +219,7 @@ export class ProjectManagementRequestHandler { return this .invokeRequestHandler( 'PATCH', `${resourceName}?update_mask=display_name`, requestData, 'v1beta1') - .then(() => null); + .then(() => undefined); } /** @@ -240,7 +243,7 @@ export class ProjectManagementRequestHandler { }; return this .invokeRequestHandler('POST', `${parentResourceName}/sha`, requestData, 'v1beta1') - .then(() => null); + .then(() => undefined); } /** @@ -267,7 +270,7 @@ export class ProjectManagementRequestHandler { public deleteResource(resourceName: string): Promise { return this .invokeRequestHandler('DELETE', resourceName, /* requestData */ null, 'v1beta1') - .then(() => null); + .then(() => undefined); } private pollRemoteOperationWithExponentialBackoff( @@ -301,7 +304,7 @@ export class ProjectManagementRequestHandler { private invokeRequestHandler( method: HttpMethod, path: string, - requestData: object, + requestData: object | null, apiVersion: ('v1' | 'v1beta1') = 'v1'): Promise { const baseUrlToUse = (apiVersion === 'v1') ? this.baseUrl : this.baseBetaUrl; const request: HttpRequestConfig = { diff --git a/src/project-management/project-management.ts b/src/project-management/project-management.ts index 434bfbc412..2c9c39794c 100644 --- a/src/project-management/project-management.ts +++ b/src/project-management/project-management.ts @@ -62,14 +62,15 @@ export class ProjectManagement implements FirebaseServiceInterface { } // Assert that a specific project ID was provided within the app. - this.projectId = utils.getProjectId(app); - if (!validator.isNonEmptyString(this.projectId)) { + const projectId = utils.getProjectId(app); + if (!validator.isNonEmptyString(projectId)) { throw new FirebaseProjectManagementError( 'invalid-project-id', 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' + 'environment variable.'); } + this.projectId = projectId; this.resourceName = `projects/${this.projectId}`; this.requestHandler = new ProjectManagementRequestHandler(app); diff --git a/src/security-rules/security-rules-api-client.ts b/src/security-rules/security-rules-api-client.ts index 9d957f5e6e..4e8102ec42 100644 --- a/src/security-rules/security-rules-api-client.ts +++ b/src/security-rules/security-rules-api-client.ts @@ -57,7 +57,7 @@ export class SecurityRulesApiClient { private readonly projectIdPrefix: string; private readonly url: string; - constructor(private readonly httpClient: HttpClient, projectId: string) { + constructor(private readonly httpClient: HttpClient, projectId: string | null) { if (!validator.isNonNullObject(httpClient)) { throw new FirebaseSecurityRulesError( 'invalid-argument', 'HttpClient must be a non-null object.'); @@ -235,7 +235,10 @@ export class SecurityRulesApiClient { } const error: Error = (response.data as ErrorResponse).error || {}; - const code = ERROR_CODE_MAPPING[error.status] || 'unknown-error'; + let code: SecurityRulesErrorCode = 'unknown-error'; + if (error.status && error.status in ERROR_CODE_MAPPING) { + code = ERROR_CODE_MAPPING[error.status]; + } const message = error.message || `Unknown server error: ${response.text}`; return new FirebaseSecurityRulesError(code, message); } diff --git a/src/security-rules/security-rules.ts b/src/security-rules/security-rules.ts index 8e5258c03a..9205d3ace9 100644 --- a/src/security-rules/security-rules.ts +++ b/src/security-rules/security-rules.ts @@ -370,5 +370,5 @@ class SecurityRulesInternals implements FirebaseServiceInternalsInterface { } function stripProjectIdPrefix(name: string): string { - return name.split('/').pop(); + return name.split('/').pop()!; } diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 5ec64fce20..aa4b3992f9 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -70,7 +70,7 @@ export class Storage implements FirebaseServiceInterface { }); } - const cert: Certificate = tryGetCertificate(app.options.credential); + const cert: Certificate | null = tryGetCertificate(app.options.credential); if (cert != null) { // cert is available when the SDK has been initialized with a service account JSON file, // or by setting the GOOGLE_APPLICATION_CREDENTIALS envrionment variable. diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index fa5b669e91..0ec85bfe8f 100644 --- a/src/utils/api-request.ts +++ b/src/utils/api-request.ts @@ -38,7 +38,7 @@ export interface HttpRequestConfig { /** Target URL of the request. Should be a well-formed URL including protocol, hostname, port and path. */ url: string; headers?: {[key: string]: string}; - data?: string | object | Buffer; + data?: string | object | Buffer | null; /** Connect and read timeout (in milliseconds) for the outgoing request. */ timeout?: number; httpAgent?: http.Agent; @@ -51,9 +51,9 @@ export interface HttpResponse { readonly status: number; readonly headers: any; /** Response data as a raw string. */ - readonly text: string; + readonly text?: string; /** Response data as a parsed JSON object. */ - readonly data: any; + readonly data?: any; /** For multipart responses, the payloads of individual parts. */ readonly multipart?: Buffer[]; /** @@ -66,8 +66,8 @@ export interface HttpResponse { interface LowLevelResponse { status: number; headers: http.IncomingHttpHeaders; - request: http.ClientRequest; - data: string; + request: http.ClientRequest | null; + data?: string; multipart?: Buffer[]; config: HttpRequestConfig; } @@ -83,7 +83,7 @@ class DefaultHttpResponse implements HttpResponse { public readonly status: number; public readonly headers: any; - public readonly text: string; + public readonly text?: string; private readonly parsedData: any; private readonly parseError: Error; @@ -97,6 +97,9 @@ class DefaultHttpResponse implements HttpResponse { this.headers = resp.headers; this.text = resp.data; try { + if (!resp.data) { + throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.'); + } this.parsedData = JSON.parse(resp.data); } catch (err) { this.parsedData = undefined; @@ -130,7 +133,7 @@ class MultipartHttpResponse implements HttpResponse { public readonly status: number; public readonly headers: any; - public readonly multipart: Buffer[]; + public readonly multipart?: Buffer[]; constructor(resp: LowLevelResponse) { this.status = resp.status; @@ -239,7 +242,7 @@ function validateRetryConfig(retry: RetryConfig) { export class HttpClient { - constructor(private readonly retry: RetryConfig = defaultRetryConfig()) { + constructor(private readonly retry: RetryConfig | null = defaultRetryConfig()) { if (this.retry) { validateRetryConfig(this.retry); } @@ -278,7 +281,7 @@ export class HttpClient { }) .catch((err: LowLevelError) => { const [delayMillis, canRetry] = this.getRetryDelayMillis(retryAttempts, err); - if (canRetry && delayMillis <= this.retry.maxDelayInMillis) { + if (canRetry && this.retry && delayMillis <= this.retry.maxDelayInMillis) { return this.waitForRetry(delayMillis).then(() => { return this.sendWithRetry(config, retryAttempts + 1); }); @@ -354,8 +357,12 @@ export class HttpClient { return statusCodes.indexOf(err.response.status) !== -1; } - const retryCodes = this.retry.ioErrorCodes || []; - return retryCodes.indexOf(err.code) !== -1; + if (err.code) { + const retryCodes = this.retry.ioErrorCodes || []; + return retryCodes.indexOf(err.code) !== -1; + } + + return false; } /** @@ -380,6 +387,10 @@ export class HttpClient { return 0; } + if (!this.retry) { + throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'Expected this.retry to exist.'); + } + const backOffFactor = this.retry.backOffFactor || 0; const delayInSeconds = (2 ** retryAttempts) * backOffFactor; return Math.min(delayInSeconds * 1000, this.retry.maxDelayInMillis); @@ -442,7 +453,7 @@ class AsyncHttpCall { private readonly config: HttpRequestConfigImpl; private readonly options: https.RequestOptions; - private readonly entity: Buffer; + private readonly entity: Buffer | undefined; private readonly promise: Promise; private resolve: (_: any) => void; @@ -459,7 +470,7 @@ class AsyncHttpCall { try { this.config = new HttpRequestConfigImpl(config); this.options = this.config.buildRequestOptions(); - this.entity = this.config.buildEntity(this.options.headers); + this.entity = this.config.buildEntity(this.options.headers!); this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; @@ -484,10 +495,10 @@ class AsyncHttpCall { this.enhanceAndReject(err, null, req); }); - const timeout: number = this.config.timeout; + const timeout: number | undefined = this.config.timeout; if (timeout) { // Listen to timeouts and throw an error. - req.setTimeout(this.config.timeout, () => { + req.setTimeout(timeout, () => { req.abort(); this.rejectWithError(`timeout of ${timeout}ms exceeded`, 'ETIMEDOUT', req); }); @@ -502,6 +513,12 @@ class AsyncHttpCall { return; } + if (!res.statusCode) { + throw new FirebaseAppError( + AppErrorCodes.INTERNAL_ERROR, + 'Expected a statusCode on the response from a ClientRequest'); + } + const response: LowLevelResponse = { status: res.statusCode, headers: res.headers, @@ -522,10 +539,13 @@ class AsyncHttpCall { /** * Extracts multipart boundary from the HTTP header. The content-type header of a multipart * response has the form 'multipart/subtype; boundary=string'. + * + * If the content-type header does not exist, or does not start with + * 'multipart/', then null will be returned. */ - private getMultipartBoundary(headers: http.IncomingHttpHeaders): string { + private getMultipartBoundary(headers: http.IncomingHttpHeaders): string | null { const contentType = headers['content-type']; - if (!contentType.startsWith('multipart/')) { + if (!contentType || !contentType.startsWith('multipart/')) { return null; } @@ -550,7 +570,7 @@ class AsyncHttpCall { // Uncompress the response body transparently if required. let respStream: Readable = res; const encodings = ['gzip', 'compress', 'deflate']; - if (encodings.indexOf(res.headers['content-encoding']) !== -1) { + if (res.headers['content-encoding'] && encodings.indexOf(res.headers['content-encoding']) !== -1) { // Add the unzipper to the body stream processing pipeline. const zlib: typeof zlibmod = require('zlib'); respStream = respStream.pipe(zlib.createUnzip()); @@ -579,7 +599,7 @@ class AsyncHttpCall { }); multipartParser.on('finish', () => { - response.data = null; + response.data = undefined; response.multipart = responseBuffer; this.finalizeResponse(response); }); @@ -594,8 +614,8 @@ class AsyncHttpCall { }); respStream.on('error', (err) => { - const req: http.ClientRequest = response.request; - if (req.aborted) { + const req: http.ClientRequest | null = response.request; + if (req && req.aborted) { return; } this.enhanceAndReject(err, null, req); @@ -630,8 +650,8 @@ class AsyncHttpCall { */ private rejectWithError( message: string, - code?: string, - request?: http.ClientRequest, + code?: string | null, + request?: http.ClientRequest | null, response?: LowLevelResponse) { const error = new Error(message); @@ -640,8 +660,8 @@ class AsyncHttpCall { private enhanceAndReject( error: any, - code: string, - request?: http.ClientRequest, + code?: string | null, + request?: http.ClientRequest | null, response?: LowLevelResponse) { this.reject(this.enhanceError(error, code, request, response)); @@ -653,8 +673,8 @@ class AsyncHttpCall { */ private enhanceError( error: any, - code: string, - request?: http.ClientRequest, + code?: string | null, + request?: http.ClientRequest | null, response?: LowLevelResponse): LowLevelError { error.config = this.config; @@ -688,7 +708,7 @@ class HttpRequestConfigImpl implements HttpRequestConfig { return this.config.headers; } - get data(): string | object | Buffer | undefined { + get data(): string | object | Buffer | undefined | null { return this.config.data; } @@ -703,7 +723,7 @@ class HttpRequestConfigImpl implements HttpRequestConfig { public buildRequestOptions(): https.RequestOptions { const parsed = this.buildUrl(); const protocol = parsed.protocol; - let port: string = parsed.port; + let port: string | undefined = parsed.port; if (!port) { const isHttps = protocol === 'https:'; port = isHttps ? '443' : '80'; @@ -720,8 +740,8 @@ class HttpRequestConfigImpl implements HttpRequestConfig { }; } - public buildEntity(headers: http.OutgoingHttpHeaders): Buffer { - let data: Buffer; + public buildEntity(headers: http.OutgoingHttpHeaders): Buffer | undefined { + let data: Buffer | undefined; if (!this.hasEntity() || !this.isEntityEnclosingRequest()) { return data; } @@ -834,7 +854,7 @@ export class ApiSettings { * @param {ApiCallbackFunction} requestValidator The request validator. * @return {ApiSettings} The current API settings instance. */ - public setRequestValidator(requestValidator: ApiCallbackFunction): ApiSettings { + public setRequestValidator(requestValidator: ApiCallbackFunction | null): ApiSettings { const nullFunction: (_: object) => void = (_: object) => undefined; this.requestValidator = requestValidator || nullFunction; return this; @@ -849,7 +869,7 @@ export class ApiSettings { * @param {ApiCallbackFunction} responseValidator The response validator. * @return {ApiSettings} The current API settings instance. */ - public setResponseValidator(responseValidator: ApiCallbackFunction): ApiSettings { + public setResponseValidator(responseValidator: ApiCallbackFunction | null): ApiSettings { const nullFunction: (_: object) => void = (_: object) => undefined; this.responseValidator = responseValidator || nullFunction; return this; @@ -892,7 +912,7 @@ export class ExponentialBackoffPoller extends EventEmitter { private masterTimer: NodeJS.Timer; private repollTimer: NodeJS.Timer; - private pollCallback: () => Promise; + private pollCallback?: () => Promise; private resolve: (result: object) => void; private reject: (err: object) => void; @@ -937,7 +957,7 @@ export class ExponentialBackoffPoller extends EventEmitter { } private repoll(): void { - this.pollCallback() + this.pollCallback!() .then((result) => { if (this.completed) { return; diff --git a/src/utils/error.ts b/src/utils/error.ts index 6baff7a80c..088c7ab33a 100755 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -249,12 +249,15 @@ export class FirebaseMessagingError extends PrefixedFirebaseError { * @return {FirebaseMessagingError} The corresponding developer-facing error. */ public static fromServerError( - serverErrorCode: string, - message?: string, + serverErrorCode: string | null, + message?: string | null, rawServerResponse?: object, ): FirebaseMessagingError { // If not found, default to unknown error. - const clientCodeKey = MESSAGING_SERVER_TO_CLIENT_CODE[serverErrorCode] || 'UNKNOWN_ERROR'; + let clientCodeKey = 'UNKNOWN_ERROR'; + if (serverErrorCode && serverErrorCode in MESSAGING_SERVER_TO_CLIENT_CODE) { + clientCodeKey = MESSAGING_SERVER_TO_CLIENT_CODE[serverErrorCode]; + } const error: ErrorInfo = deepCopy((MessagingClientErrorCode as any)[clientCodeKey]); error.message = message || error.message; @@ -650,6 +653,10 @@ export class AuthClientErrorCode { code: 'user-not-found', message: 'There is no user record corresponding to the provided identifier.', }; + public static NOT_FOUND = { + code: 'not-found', + message: 'The requested resource was not found.', + }; } /** @@ -718,8 +725,8 @@ export class MessagingClientErrorCode { code: 'message-rate-exceeded', message: 'Sending limit exceeded for the message target.', }; - public static INVALID_APNS_CREDENTIALS = { - code: 'invalid-apns-credentials', + public static THIRD_PARTY_AUTH_ERROR = { + code: 'third-party-auth-error', message: 'A message targeted to an iOS device could not be sent because the required APNs ' + 'SSL certificate was not uploaded or has expired. Check the validity of your development ' + 'and production certificates.', @@ -912,20 +919,21 @@ const MESSAGING_SERVER_TO_CLIENT_CODE: ServerToClientCode = { // Topics message rate exceeded. TopicsMessageRateExceeded: 'TOPICS_MESSAGE_RATE_EXCEEDED', // Invalid APNs credentials. - InvalidApnsCredential: 'INVALID_APNS_CREDENTIALS', + InvalidApnsCredential: 'THIRD_PARTY_AUTH_ERROR', /* FCM v1 canonical error codes */ NOT_FOUND: 'REGISTRATION_TOKEN_NOT_REGISTERED', PERMISSION_DENIED: 'MISMATCHED_CREDENTIAL', RESOURCE_EXHAUSTED: 'MESSAGE_RATE_EXCEEDED', - UNAUTHENTICATED: 'INVALID_APNS_CREDENTIALS', + UNAUTHENTICATED: 'THIRD_PARTY_AUTH_ERROR', /* FCM v1 new error codes */ - APNS_AUTH_ERROR: 'INVALID_APNS_CREDENTIALS', + APNS_AUTH_ERROR: 'THIRD_PARTY_AUTH_ERROR', INTERNAL: 'INTERNAL_ERROR', INVALID_ARGUMENT: 'INVALID_ARGUMENT', QUOTA_EXCEEDED: 'MESSAGE_RATE_EXCEEDED', SENDER_ID_MISMATCH: 'MISMATCHED_CREDENTIAL', + THIRD_PARTY_AUTH_ERROR: 'THIRD_PARTY_AUTH_ERROR', UNAVAILABLE: 'SERVER_UNAVAILABLE', UNREGISTERED: 'REGISTRATION_TOKEN_NOT_REGISTERED', UNSPECIFIED_ERROR: 'UNKNOWN_ERROR', diff --git a/src/utils/index.ts b/src/utils/index.ts index ea91418730..214b2b7f10 100755 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -63,13 +63,13 @@ export function addReadonlyGetter(obj: object, prop: string, value: any): void { * * @return {string} A project ID string or null. */ -export function getProjectId(app: FirebaseApp): string { +export function getProjectId(app: FirebaseApp): string | null { const options: FirebaseAppOptions = app.options; if (validator.isNonEmptyString(options.projectId)) { return options.projectId; } - const cert: Certificate = tryGetCertificate(options.credential); + const cert: Certificate | null = tryGetCertificate(options.credential); if (cert != null && validator.isNonEmptyString(cert.projectId)) { return cert.projectId; } @@ -81,6 +81,20 @@ export function getProjectId(app: FirebaseApp): string { return null; } +/** + * Determines the Google Cloud project ID associated with a Firebase app by examining + * the Firebase app options, credentials and the local environment in that order. This + * is an async wrapper of the getProjectId method. This enables us to migrate the rest + * of the SDK into asynchronously determining the current project ID. See b/143090254. + * + * @param {FirebaseApp} app A Firebase app to get the project ID from. + * + * @return {Promise} A project ID string or null. + */ +export function findProjectId(app: FirebaseApp): Promise { + return Promise.resolve(getProjectId(app)); +} + /** * Encodes data using web-safe-base64. * diff --git a/src/utils/validator.ts b/src/utils/validator.ts index b53ba9f8cc..1d36ff16ee 100755 --- a/src/utils/validator.ts +++ b/src/utils/validator.ts @@ -22,7 +22,7 @@ import url = require('url'); * @param {any} value The value to validate. * @return {boolean} Whether the value is byte buffer or not. */ -export function isBuffer(value: any): boolean { +export function isBuffer(value: any): value is Buffer { return value instanceof Buffer; } @@ -32,7 +32,7 @@ export function isBuffer(value: any): boolean { * @param {any} value The value to validate. * @return {boolean} Whether the value is an array or not. */ -export function isArray(value: any): boolean { +export function isArray(value: any): value is T[] { return Array.isArray(value); } @@ -122,7 +122,7 @@ export function isObject(value: any): boolean { * @param {any} value The value to validate. * @return {boolean} Whether the value is a non-null object or not. */ -export function isNonNullObject(value: any): boolean { +export function isNonNullObject(value: T | null | undefined): value is T { return isObject(value) && value !== null; } @@ -244,7 +244,7 @@ export function isURL(urlStr: any): boolean { } // Validate hostname: Can contain letters, numbers, underscore and dashes separated by a dot. // Each zone must not start with a hyphen or underscore. - if (!/^[a-zA-Z0-9]+[\w\-]*([\.]?[a-zA-Z0-9]+[\w\-]*)*$/.test(hostname)) { + if (!hostname || !/^[a-zA-Z0-9]+[\w\-]*([\.]?[a-zA-Z0-9]+[\w\-]*)*$/.test(hostname)) { return false; } // Allow for pathnames: (/chars+)*/? diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 62b45867f9..187471f64f 100755 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -30,6 +30,7 @@ import url = require('url'); import * as mocks from '../resources/mocks'; import { AuthProviderConfig } from '../../src/auth/auth-config'; import { deepExtend, deepCopy } from '../../src/utils/deep-copy'; +import { User } from '@firebase/auth-types'; /* tslint:disable:no-var-requires */ const chalk = require('chalk'); @@ -239,7 +240,7 @@ describe('admin.auth', () => { + 'add the "Firebase Authentication Admin" permission. See ' + 'instructions in CONTRIBUTING.md', ).to.be.ok; - expect(listUsersResult.users[0].passwordHash.length).greaterThan(0); + expect(listUsersResult.users[0].passwordHash!.length).greaterThan(0); expect( listUsersResult.users[0].passwordSalt, @@ -247,23 +248,24 @@ describe('admin.auth', () => { + 'add the "Firebase Authentication Admin" permission. See ' + 'instructions in CONTRIBUTING.md', ).to.be.ok; - expect(listUsersResult.users[0].passwordSalt.length).greaterThan(0); + expect(listUsersResult.users[0].passwordSalt!.length).greaterThan(0); expect(listUsersResult.users[1].uid).to.equal(uids[2]); - expect(listUsersResult.users[1].passwordHash.length).greaterThan(0); - expect(listUsersResult.users[1].passwordSalt.length).greaterThan(0); + expect(listUsersResult.users[1].passwordHash!.length).greaterThan(0); + expect(listUsersResult.users[1].passwordSalt!.length).greaterThan(0); }); }); it('revokeRefreshTokens() invalidates existing sessions and ID tokens', () => { - let currentIdToken: string = null; - let currentUser: any = null; + let currentIdToken: string; + let currentUser: User; // Sign in with an email and password account. - return firebase.auth().signInWithEmailAndPassword(mockUserData.email, mockUserData.password) + return firebase.auth!().signInWithEmailAndPassword(mockUserData.email, mockUserData.password) .then(({user}) => { - currentUser = user; + expect(user).to.exist; + currentUser = user!; // Get user's ID token. - return user.getIdToken(); + return user!.getIdToken(); }) .then((idToken) => { currentIdToken = idToken; @@ -293,12 +295,13 @@ describe('admin.auth', () => { }) .then(() => { // New sign-in should succeed. - return firebase.auth().signInWithEmailAndPassword( + return firebase.auth!().signInWithEmailAndPassword( mockUserData.email, mockUserData.password); }) .then(({user}) => { // Get new session's ID token. - return user.getIdToken(); + expect(user).to.exist; + return user!.getIdToken(); }) .then((idToken) => { // ID token for new session should be valid even with revocation check. @@ -316,12 +319,14 @@ describe('admin.auth', () => { .then((userRecord) => { // Confirm custom claims set on the UserRecord. expect(userRecord.customClaims).to.deep.equal(customClaims); - return firebase.auth().signInWithEmailAndPassword( - userRecord.email, mockUserData.password); + expect(userRecord.email).to.exist; + return firebase.auth!().signInWithEmailAndPassword( + userRecord.email!, mockUserData.password); }) .then(({user}) => { - // Get the user's ID token. - return user.getIdToken(); + // Get the user's ID token. + expect(user).to.exist; + return user!.getIdToken(); }) .then((idToken) => { // Verify ID token contents. @@ -344,7 +349,8 @@ describe('admin.auth', () => { // Custom claims should be cleared. expect(userRecord.customClaims).to.deep.equal({}); // Force token refresh. All claims should be cleared. - return firebase.auth().currentUser.getIdToken(true); + expect(firebase.auth!().currentUser).to.exist; + return firebase.auth!().currentUser!.getIdToken(true); }) .then((idToken) => { // Verify ID token contents. @@ -454,10 +460,11 @@ describe('admin.auth', () => { isAdmin: true, }) .then((customToken) => { - return firebase.auth().signInWithCustomToken(customToken); + return firebase.auth!().signInWithCustomToken(customToken); }) .then(({user}) => { - return user.getIdToken(); + expect(user).to.exist; + return user!.getIdToken(); }) .then((idToken) => { return admin.auth().verifyIdToken(idToken); @@ -473,10 +480,11 @@ describe('admin.auth', () => { isAdmin: true, }) .then((customToken) => { - return firebase.auth().signInWithCustomToken(customToken); + return firebase.auth!().signInWithCustomToken(customToken); }) .then(({user}) => { - return user.getIdToken(); + expect(user).to.exist; + return user!.getIdToken(); }) .then((idToken) => { return admin.auth(noServiceAccountApp).verifyIdToken(idToken); @@ -510,7 +518,7 @@ describe('admin.auth', () => { // Sign out after each test. afterEach(() => { - return firebase.auth().signOut(); + return firebase.auth!().signOut(); }); // Delete test user at the end of test suite. @@ -527,15 +535,16 @@ describe('admin.auth', () => { .then((link) => { const code = getActionCode(link); expect(getContinueUrl(link)).equal(actionCodeSettings.url); - return firebase.auth().confirmPasswordReset(code, newPassword); + return firebase.auth!().confirmPasswordReset(code, newPassword); }) .then(() => { - return firebase.auth().signInWithEmailAndPassword(email, newPassword); + return firebase.auth!().signInWithEmailAndPassword(email, newPassword); }) .then((result) => { - expect(result.user.email).to.equal(email); + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); // Password reset also verifies the user's email. - expect(result.user.emailVerified).to.be.true; + expect(result.user!.emailVerified).to.be.true; }); }); @@ -549,14 +558,15 @@ describe('admin.auth', () => { .then((link) => { const code = getActionCode(link); expect(getContinueUrl(link)).equal(actionCodeSettings.url); - return firebase.auth().applyActionCode(code); + return firebase.auth!().applyActionCode(code); }) .then(() => { - return firebase.auth().signInWithEmailAndPassword(email, userData.password); + return firebase.auth!().signInWithEmailAndPassword(email, userData.password); }) .then((result) => { - expect(result.user.email).to.equal(email); - expect(result.user.emailVerified).to.be.true; + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; }); }); @@ -564,11 +574,12 @@ describe('admin.auth', () => { return admin.auth().generateSignInWithEmailLink(email, actionCodeSettings) .then((link) => { expect(getContinueUrl(link)).equal(actionCodeSettings.url); - return firebase.auth().signInWithEmailLink(email, link); + return firebase.auth!().signInWithEmailLink(email, link); }) .then((result) => { - expect(result.user.email).to.equal(email); - expect(result.user.emailVerified).to.be.true; + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; }); }); }); @@ -755,7 +766,8 @@ describe('admin.auth', () => { return tenantAwareAuth.getUser(createdUserUid); }) .then((userRecord) => { - expect(new Date(userRecord.tokensValidAfterTime).getTime()) + expect(userRecord.tokensValidAfterTime).to.exist; + expect(new Date(userRecord.tokensValidAfterTime!).getTime()) .to.be.greaterThan(lastValidSinceTime); }); }); @@ -1284,8 +1296,11 @@ describe('admin.auth', () => { it('creates a valid Firebase session cookie', () => { return admin.auth().createCustomToken(uid, {admin: true, groupId: '1234'}) - .then((customToken) => firebase.auth().signInWithCustomToken(customToken)) - .then(({user}) => user.getIdToken()) + .then((customToken) => firebase.auth!().signInWithCustomToken(customToken)) + .then(({user}) => { + expect(user).to.exist; + return user!.getIdToken(); + }) .then((idToken) => { currentIdToken = idToken; return admin.auth().verifyIdToken(idToken); @@ -1317,8 +1332,11 @@ describe('admin.auth', () => { it('creates a revocable session cookie', () => { let currentSessionCookie: string; return admin.auth().createCustomToken(uid2) - .then((customToken) => firebase.auth().signInWithCustomToken(customToken)) - .then(({user}) => user.getIdToken()) + .then((customToken) => firebase.auth!().signInWithCustomToken(customToken)) + .then(({user}) => { + expect(user).to.exist; + return user!.getIdToken(); + }) .then((idToken) => { // One day long session cookie. return admin.auth().createSessionCookie(idToken, {expiresIn}); @@ -1341,8 +1359,11 @@ describe('admin.auth', () => { it('fails when called with a revoked ID token', () => { return admin.auth().createCustomToken(uid3, {admin: true, groupId: '1234'}) - .then((customToken) => firebase.auth().signInWithCustomToken(customToken)) - .then(({user}) => user.getIdToken()) + .then((customToken) => firebase.auth!().signInWithCustomToken(customToken)) + .then(({user}) => { + expect(user).to.exist; + return user!.getIdToken(); + }) .then((idToken) => { currentIdToken = idToken; return new Promise((resolve) => setTimeout(() => resolve( @@ -1366,8 +1387,11 @@ describe('admin.auth', () => { it('fails when called with a Firebase ID token', () => { return admin.auth().createCustomToken(uid) - .then((customToken) => firebase.auth().signInWithCustomToken(customToken)) - .then(({user}) => user.getIdToken()) + .then((customToken) => firebase.auth!().signInWithCustomToken(customToken)) + .then(({user}) => { + expect(user).to.exist; + return user!.getIdToken(); + }) .then((idToken) => { return admin.auth().verifySessionCookie(idToken) .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); @@ -1410,7 +1434,8 @@ describe('admin.auth', () => { }, } as any, computePasswordHash: (userImportTest: UserImportTest): Buffer => { - const currentHashKey = userImportTest.importOptions.hash.key.toString('utf8'); + expect(userImportTest.importOptions.hash.key).to.exist; + const currentHashKey = userImportTest.importOptions.hash.key!.toString('utf8'); const currentRawPassword = userImportTest.rawPassword; const currentRawSalt = userImportTest.rawSalt; return crypto.createHmac('sha256', currentHashKey) @@ -1424,7 +1449,7 @@ describe('admin.auth', () => { importOptions: { hash: { algorithm: 'SHA256', - rounds: 0, + rounds: 1, }, } as any, computePasswordHash: (userImportTest: UserImportTest): Buffer => { @@ -1477,11 +1502,22 @@ describe('admin.auth', () => { } as any, computePasswordHash: (userImportTest: UserImportTest): Buffer => { const currentRawPassword = userImportTest.rawPassword; - const currentRawSalt = userImportTest.rawSalt; - const N = userImportTest.importOptions.hash.memoryCost; - const r = userImportTest.importOptions.hash.blockSize; - const p = userImportTest.importOptions.hash.parallelization; - const dkLen = userImportTest.importOptions.hash.derivedKeyLength; + + expect(userImportTest.rawSalt).to.exist; + const currentRawSalt = userImportTest.rawSalt!; + + expect(userImportTest.importOptions.hash.memoryCost).to.exist; + const N = userImportTest.importOptions.hash.memoryCost!; + + expect(userImportTest.importOptions.hash.blockSize).to.exist; + const r = userImportTest.importOptions.hash.blockSize!; + + expect(userImportTest.importOptions.hash.parallelization).to.exist; + const p = userImportTest.importOptions.hash.parallelization!; + + expect(userImportTest.importOptions.hash.derivedKeyLength).to.exist; + const dkLen = userImportTest.importOptions.hash.derivedKeyLength!; + return Buffer.from(scrypt.hashSync( currentRawPassword, {N, r, p}, dkLen, Buffer.from(currentRawSalt))); }, @@ -1498,8 +1534,10 @@ describe('admin.auth', () => { } as any, computePasswordHash: (userImportTest: UserImportTest): Buffer => { const currentRawPassword = userImportTest.rawPassword; - const currentRawSalt = userImportTest.rawSalt; - const currentRounds = userImportTest.importOptions.hash.rounds; + expect(userImportTest.rawSalt).to.exist; + const currentRawSalt = userImportTest.rawSalt!; + expect(userImportTest.importOptions.hash.rounds).to.exist; + const currentRounds = userImportTest.importOptions.hash.rounds!; return crypto.pbkdf2Sync( currentRawPassword, currentRawSalt, currentRounds, 64, 'sha256'); }, @@ -1693,12 +1731,14 @@ function testImportAndSignInUser( expect(result.successCount).to.equal(1); expect(result.errors.length).to.equal(0); // Sign in with an email and password to the imported account. - return firebase.auth().signInWithEmailAndPassword(users[0].email, rawPassword); + return firebase.auth!().signInWithEmailAndPassword(users[0].email, rawPassword); }) .then(({user}) => { // Confirm successful sign-in. - expect(user.email).to.equal(users[0].email); - expect(user.providerData[0].providerId).to.equal('password'); + expect(user).to.exist; + expect(user!.email).to.equal(users[0].email); + expect(user!.providerData[0]).to.exist; + expect(user!.providerData[0]!.providerId).to.equal('password'); }); } @@ -1754,7 +1794,9 @@ function cleanup() { */ function getActionCode(link: string): string { const parsedUrl = new url.URL(link); - return parsedUrl.searchParams.get('oobCode'); + const oobCode = parsedUrl.searchParams.get('oobCode'); + expect(oobCode).to.exist; + return oobCode!; } /** @@ -1765,7 +1807,9 @@ function getActionCode(link: string): string { */ function getContinueUrl(link: string): string { const parsedUrl = new url.URL(link); - return parsedUrl.searchParams.get('continueUrl'); + const continueUrl = parsedUrl.searchParams.get('continueUrl'); + expect(continueUrl).to.exist; + return continueUrl!; } /** @@ -1776,7 +1820,9 @@ function getContinueUrl(link: string): string { */ function getTenantId(link: string): string { const parsedUrl = new url.URL(link); - return parsedUrl.searchParams.get('tenantId'); + const tenantId = parsedUrl.searchParams.get('tenantId'); + expect(tenantId).to.exist; + return tenantId!; } /** diff --git a/test/integration/database.spec.ts b/test/integration/database.spec.ts index e98395a220..3f9818a405 100644 --- a/test/integration/database.spec.ts +++ b/test/integration/database.spec.ts @@ -178,7 +178,7 @@ describe('admin.database', () => { // @ts-ignore: purposely unused method. function addValueEventListener( db: admin.database.Database, - callback: (s: admin.database.DataSnapshot) => any) { + callback: (s: admin.database.DataSnapshot | null) => any) { const eventType: admin.database.EventType = 'value'; db.ref().on(eventType, callback); } diff --git a/test/integration/firestore.spec.ts b/test/integration/firestore.spec.ts index 62a62dd4cd..085212b9c2 100644 --- a/test/integration/firestore.spec.ts +++ b/test/integration/firestore.spec.ts @@ -75,8 +75,9 @@ describe('admin.firestore', () => { }) .then((snapshot) => { const data = snapshot.data(); - expect(data.timestamp).is.not.null; - expect(data.timestamp).to.be.instanceOf(admin.firestore.Timestamp); + expect(data).to.exist; + expect(data!.timestamp).is.not.null; + expect(data!.timestamp).to.be.instanceOf(admin.firestore.Timestamp); return reference.delete(); }) .should.eventually.be.fulfilled; @@ -124,7 +125,8 @@ describe('admin.firestore', () => { }) .then((snapshot) => { const data = snapshot.data(); - expect(data.sisterCity.path).to.deep.equal(source.path); + expect(data).to.exist; + expect(data!.sisterCity.path).to.deep.equal(source.path); const promises = []; promises.push(source.delete()); promises.push(target.delete()); diff --git a/test/integration/messaging.spec.ts b/test/integration/messaging.spec.ts index c282a3511c..7f0d7e24f9 100644 --- a/test/integration/messaging.spec.ts +++ b/test/integration/messaging.spec.ts @@ -49,6 +49,25 @@ const message: admin.messaging.Message = { }, android: { restrictedPackageName: 'com.google.firebase.testing', + notification: { + title: 'test.title', + ticker: 'test.ticker', + sticky: true, + visibility: 'private', + eventTimestamp: new Date(), + localOnly: true, + priority: 'high', + vibrateTimingsMillis: [100, 50, 250], + defaultVibrateTimings: false, + defaultSound: true, + lightSettings: { + color: '#AABBCC55', + lightOnDurationMillis: 200, + lightOffDurationMillis: 300, + }, + defaultLightSettings: false, + notificationCount: 1, + }, }, apns: { payload: { @@ -103,9 +122,9 @@ describe('admin.messaging', () => { }); }); - it('sendAll(100)', () => { + it('sendAll(500)', () => { const messages: admin.messaging.Message[] = []; - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 500; i++) { messages.push({topic: `foo-bar-${i % 10}`}); } return admin.messaging().sendAll(messages, true) diff --git a/test/integration/project-management.spec.ts b/test/integration/project-management.spec.ts index 12de46db80..4cf269c363 100644 --- a/test/integration/project-management.spec.ts +++ b/test/integration/project-management.spec.ts @@ -62,7 +62,7 @@ describe('admin.projectManagement', () => { const metadataOwnedByTest = metadatas.find((metadata) => isIntegrationTestApp(metadata.packageName)); expect(metadataOwnedByTest).to.exist; - expect(metadataOwnedByTest.appId).to.equal(androidApp.appId); + expect(metadataOwnedByTest!.appId).to.equal(androidApp.appId); }); }); }); @@ -76,7 +76,7 @@ describe('admin.projectManagement', () => { const metadataOwnedByTest = metadatas.find((metadata) => isIntegrationTestApp(metadata.bundleId)); expect(metadataOwnedByTest).to.exist; - expect(metadataOwnedByTest.appId).to.equal(iosApp.appId); + expect(metadataOwnedByTest!.appId).to.equal(iosApp.appId); }); }); }); @@ -258,7 +258,7 @@ function deleteAllShaCertificates(androidApp: admin.projectManagement.AndroidApp .then((shaCertificates: admin.projectManagement.ShaCertificate[]) => { return Promise.all(shaCertificates.map((cert) => androidApp.deleteShaCertificate(cert))); }) - .then(() => null); + .then(() => undefined); } /** @@ -286,14 +286,14 @@ function generateUniqueProjectDisplayName() { * @return {boolean} True if the specified appNamespace belongs to these integration tests. */ function isIntegrationTestApp(appNamespace: string): boolean { - return appNamespace && appNamespace.startsWith(APP_NAMESPACE_PREFIX); + return appNamespace ? appNamespace.startsWith(APP_NAMESPACE_PREFIX) : false; } /** * @return {boolean} True if the specified appDisplayName belongs to these integration tests. */ -function isIntegrationTestAppDisplayName(appDisplayName: string): boolean { - return appDisplayName && appDisplayName.startsWith(APP_DISPLAY_NAME_PREFIX); +function isIntegrationTestAppDisplayName(appDisplayName: string | undefined): boolean { + return appDisplayName ? appDisplayName.startsWith(APP_DISPLAY_NAME_PREFIX) : false; } /** diff --git a/test/integration/security-rules.spec.ts b/test/integration/security-rules.spec.ts index 83d00d4b18..ce86ed932e 100644 --- a/test/integration/security-rules.spec.ts +++ b/test/integration/security-rules.spec.ts @@ -45,7 +45,6 @@ const RULESET_NAME_PATTERN = /[0-9a-zA-Z-]+/; describe('admin.securityRules', () => { - let testRuleset: admin.securityRules.Ruleset = null; const rulesetsToDelete: string[] = []; function scheduleForDelete(ruleset: admin.securityRules.Ruleset) { @@ -65,7 +64,17 @@ describe('admin.securityRules', () => { return Promise.all(promises); } - after(() => { + function createTemporaryRuleset(): Promise { + const name = 'firestore.rules'; + const rulesFile = admin.securityRules().createRulesFileFromSource(name, SAMPLE_FIRESTORE_RULES); + return admin.securityRules().createRuleset(rulesFile) + .then((ruleset) => { + scheduleForDelete(ruleset); + return ruleset; + }); + } + + afterEach(() => { return deleteTempRulesets(); }); @@ -91,7 +100,6 @@ describe('admin.securityRules', () => { RULES_FILE_NAME, SAMPLE_FIRESTORE_RULES); return admin.securityRules().createRuleset(rulesFile) .then((ruleset) => { - testRuleset = ruleset; scheduleForDelete(ruleset); verifyFirestoreRuleset(ruleset); }); @@ -107,38 +115,41 @@ describe('admin.securityRules', () => { describe('getRuleset()', () => { it('rejects with not-found when the Ruleset does not exist', () => { - const name = 'e1212' + testRuleset.name.substring(5); - return admin.securityRules().getRuleset(name) + const nonExistingName = '00000000-1111-2222-3333-444444444444'; + return admin.securityRules().getRuleset(nonExistingName) .should.eventually.be.rejected.and.have.property('code', 'security-rules/not-found'); }); it('rejects with invalid-argument when the Ruleset name is invalid', () => { - return admin.securityRules().getRuleset('invalid') + return admin.securityRules().getRuleset('invalid uuid') .should.eventually.be.rejected.and.have.property('code', 'security-rules/invalid-argument'); }); it('resolves with existing Ruleset', () => { - return admin.securityRules().getRuleset(testRuleset.name) - .then((ruleset) => { - verifyFirestoreRuleset(ruleset); - }); + return createTemporaryRuleset() + .then((expectedRuleset) => + admin.securityRules().getRuleset(expectedRuleset.name) + .then((actualRuleset) => { + expect(actualRuleset).to.deep.equal(expectedRuleset); + }), + ); }); }); describe('Cloud Firestore', () => { - let oldRuleset: admin.securityRules.Ruleset = null; - let newRuleset: admin.securityRules.Ruleset = null; + let oldRuleset: admin.securityRules.Ruleset | null = null; + let newRuleset: admin.securityRules.Ruleset | null = null; - function revertFirestoreRuleset(): Promise { - if (!newRuleset) { + function revertFirestoreRulesetIfModified(): Promise { + if (!newRuleset || !oldRuleset) { return Promise.resolve(); } return admin.securityRules().releaseFirestoreRuleset(oldRuleset); } - after(() => { - return revertFirestoreRuleset(); + afterEach(() => { + return revertFirestoreRulesetIfModified(); }); it('getFirestoreRuleset() returns the Ruleset currently in effect', () => { @@ -162,31 +173,31 @@ describe('admin.securityRules', () => { scheduleForDelete(ruleset); newRuleset = ruleset; - expect(ruleset.name).to.not.equal(oldRuleset.name); + expect(ruleset.name).to.not.equal(oldRuleset!.name); verifyFirestoreRuleset(ruleset); return admin.securityRules().getFirestoreRuleset(); }) .then((ruleset) => { - expect(ruleset.name).to.equal(newRuleset.name); + expect(ruleset.name).to.equal(newRuleset!.name); verifyFirestoreRuleset(ruleset); }); }); }); describe('Cloud Storage', () => { - let oldRuleset: admin.securityRules.Ruleset = null; - let newRuleset: admin.securityRules.Ruleset = null; + let oldRuleset: admin.securityRules.Ruleset | null = null; + let newRuleset: admin.securityRules.Ruleset | null = null; - function revertStorageRuleset(): Promise { - if (!newRuleset) { + function revertStorageRulesetIfModified(): Promise { + if (!newRuleset || !oldRuleset) { return Promise.resolve(); } return admin.securityRules().releaseStorageRuleset(oldRuleset); } - after(() => { - return revertStorageRuleset(); + afterEach(() => { + return revertStorageRulesetIfModified(); }); it('getStorageRuleset() returns the currently applied Storage rules', () => { @@ -210,14 +221,14 @@ describe('admin.securityRules', () => { scheduleForDelete(ruleset); newRuleset = ruleset; - expect(ruleset.name).to.not.equal(oldRuleset.name); + expect(ruleset.name).to.not.equal(oldRuleset!.name); expect(ruleset.name).to.match(RULESET_NAME_PATTERN); const createTime = new Date(ruleset.createTime); expect(ruleset.createTime).equals(createTime.toUTCString()); return admin.securityRules().getStorageRuleset(); }) .then((ruleset) => { - expect(ruleset.name).to.equal(newRuleset.name); + expect(ruleset.name).to.equal(newRuleset!.name); }); }); }); @@ -240,9 +251,13 @@ describe('admin.securityRules', () => { }); } - return listAllRulesets() - .then((rulesets) => { - expect(rulesets.some((rs) => rs.name === testRuleset.name)).to.be.true; + return Promise.all([createTemporaryRuleset(), createTemporaryRuleset()]) + .then((expectedRulesets) => { + return listAllRulesets().then((actualRulesets) => { + expectedRulesets.forEach((expectedRuleset) => { + expect(actualRulesets.map((r) => r.name)).to.deep.include(expectedRuleset.name); + }); + }); }); }); @@ -257,26 +272,27 @@ describe('admin.securityRules', () => { describe('deleteRuleset()', () => { it('rejects with not-found when the Ruleset does not exist', () => { - const name = 'e1212' + testRuleset.name.substring(5); - return admin.securityRules().deleteRuleset(name) + const nonExistingName = '00000000-1111-2222-3333-444444444444'; + return admin.securityRules().deleteRuleset(nonExistingName) .should.eventually.be.rejected.and.have.property('code', 'security-rules/not-found'); }); it('rejects with invalid-argument when the Ruleset name is invalid', () => { - return admin.securityRules().deleteRuleset('invalid') + return admin.securityRules().deleteRuleset('invalid uuid') .should.eventually.be.rejected.and.have.property('code', 'security-rules/invalid-argument'); }); it('deletes existing Ruleset', () => { - return admin.securityRules().deleteRuleset(testRuleset.name) - .then(() => { - return admin.securityRules().getRuleset(testRuleset.name) - .should.eventually.be.rejected.and.have.property('code', 'security-rules/not-found'); - }) - .then(() => { - unscheduleForDelete(testRuleset); // Already deleted. - testRuleset = null; - }); + return createTemporaryRuleset().then((ruleset) => { + return admin.securityRules().deleteRuleset(ruleset.name) + .then(() => { + return admin.securityRules().getRuleset(ruleset.name) + .should.eventually.be.rejected.and.have.property('code', 'security-rules/not-found'); + }) + .then(() => { + unscheduleForDelete(ruleset); // Already deleted. + }); + }); }); }); diff --git a/test/integration/setup.ts b/test/integration/setup.ts index 414a7366d4..332ad5d83a 100644 --- a/test/integration/setup.ts +++ b/test/integration/setup.ts @@ -118,7 +118,7 @@ class CertificatelessCredential implements Credential { return this.delegate.getAccessToken(); } - public getCertificate(): Certificate { + public getCertificate(): Certificate | null { return null; } } diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index 822cd68ff0..3928141a44 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -86,7 +86,7 @@ export class MockCredential implements Credential { }); } - public getCertificate(): Certificate { + public getCertificate(): Certificate | null { return null; } } @@ -111,7 +111,7 @@ export function appWithOptions(options: FirebaseAppOptions): FirebaseApp { } export function appReturningNullAccessToken(): FirebaseApp { - const nullFn: () => Promise = () => null; + const nullFn: () => Promise|null = () => null; return new FirebaseApp({ credential: { getAccessToken: nullFn, @@ -226,7 +226,7 @@ export function generateSessionCookie(overrides?: object, expiresIn?: number): s export function firebaseServiceFactory( firebaseApp: FirebaseApp, - extendApp: (props: object) => void, + extendApp?: (props: object) => void, ): FirebaseServiceInterface { const result = { app: firebaseApp, diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 465daaf236..4e492fa4db 100755 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -1731,7 +1731,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const path = handler.path('v1', '/accounts:update', 'project_id'); const method = 'POST'; const uid = '12345678'; - const validData = { + const validData: any = { displayName: 'John Doe', email: 'user@example.com', emailVerified: true, @@ -1757,6 +1757,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { ], }, }; + (validData as any).ignoredProperty = 'value'; const expectedValidData = { localId: uid, displayName: 'John Doe', @@ -2439,7 +2440,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); // Send create new account request with no enrolled factors. - return requestHandler.createNewAccount({uid, multiFactor: {enrolledFactors: null}}) + const request: any = {uid, multiFactor: {enrolledFactors: null}}; + return requestHandler.createNewAccount(request) .then((returnedUid: string) => { // uid should be returned. expect(returnedUid).to.be.equal(uid); @@ -2833,19 +2835,10 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given requestType:EMAIL_SIGNIN and no ActionCodeSettings', () => { const invalidRequestType = 'EMAIL_SIGNIN'; - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `"ActionCodeSettings" must be a non-null object.`, - ); - const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink(invalidRequestType, email) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); - }); + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); it('should be rejected given an invalid email', () => { @@ -2949,7 +2942,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { describe('getOAuthIdpConfig()', () => { const providerId = 'oidc.provider'; - const path = handler.path('v2beta1', `/oauthIdpConfigs/${providerId}`, 'project_id'); + const path = handler.path('v2', `/oauthIdpConfigs/${providerId}`, 'project_id'); const expectedHttpMethod = 'GET'; const expectedResult = utils.responseFrom({ name: `projects/project1/oauthIdpConfigs/${providerId}`, @@ -3007,7 +3000,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); describe('listOAuthIdpConfigs()', () => { - const path = handler.path('v2beta1', '/oauthIdpConfigs', 'project_id'); + const path = handler.path('v2', '/oauthIdpConfigs', 'project_id'); const expectedHttpMethod = 'GET'; const nextPageToken = 'PAGE_TOKEN'; const maxResults = 50; @@ -3128,7 +3121,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { describe('deleteOAuthIdpConfig()', () => { const providerId = 'oidc.provider'; - const path = handler.path('v2beta1', `/oauthIdpConfigs/${providerId}`, 'project_id'); + const path = handler.path('v2', `/oauthIdpConfigs/${providerId}`, 'project_id'); const expectedHttpMethod = 'DELETE'; const expectedResult = utils.responseFrom({}); @@ -3185,7 +3178,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { describe('createOAuthIdpConfig', () => { const providerId = 'oidc.provider'; - const path = handler.path('v2beta1', `/oauthIdpConfigs?oauthIdpConfigId=${providerId}`, 'project_id'); + const path = handler.path('v2', `/oauthIdpConfigs?oauthIdpConfigId=${providerId}`, 'project_id'); const expectedHttpMethod = 'POST'; const configOptions = { providerId, @@ -3277,7 +3270,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { describe('updateOAuthIdpConfig()', () => { const providerId = 'oidc.provider'; - const path = handler.path('v2beta1', `/oauthIdpConfigs/${providerId}`, 'project_id'); + const path = handler.path('v2', `/oauthIdpConfigs/${providerId}`, 'project_id'); const expectedHttpMethod = 'PATCH'; const configOptions = { displayName: 'OIDC_DISPLAY_NAME', @@ -3442,7 +3435,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { describe('getInboundSamlConfig()', () => { const providerId = 'saml.provider'; - const path = handler.path('v2beta1', `/inboundSamlConfigs/${providerId}`, 'project_id'); + const path = handler.path('v2', `/inboundSamlConfigs/${providerId}`, 'project_id'); const expectedHttpMethod = 'GET'; const expectedResult = utils.responseFrom({ @@ -3499,7 +3492,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); describe('listInboundSamlConfigs()', () => { - const path = handler.path('v2beta1', '/inboundSamlConfigs', 'project_id'); + const path = handler.path('v2', '/inboundSamlConfigs', 'project_id'); const expectedHttpMethod = 'GET'; const nextPageToken = 'PAGE_TOKEN'; const maxResults = 50; @@ -3616,7 +3609,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { describe('deleteInboundSamlConfig()', () => { const providerId = 'saml.provider'; - const path = handler.path('v2beta1', `/inboundSamlConfigs/${providerId}`, 'project_id'); + const path = handler.path('v2', `/inboundSamlConfigs/${providerId}`, 'project_id'); const expectedHttpMethod = 'DELETE'; const expectedResult = utils.responseFrom({}); @@ -3671,7 +3664,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { describe('createInboundSamlConfig', () => { const providerId = 'saml.provider'; - const path = handler.path('v2beta1', `/inboundSamlConfigs?inboundSamlConfigId=${providerId}`, 'project_id'); + const path = handler.path('v2', `/inboundSamlConfigs?inboundSamlConfigId=${providerId}`, 'project_id'); const expectedHttpMethod = 'POST'; const configOptions = { providerId, @@ -3778,7 +3771,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { describe('updateInboundSamlConfig()', () => { const providerId = 'saml.provider'; - const path = handler.path('v2beta1', `/inboundSamlConfigs/${providerId}`, 'project_id'); + const path = handler.path('v2', `/inboundSamlConfigs/${providerId}`, 'project_id'); const expectedHttpMethod = 'PATCH'; const configOptions = { @@ -3986,7 +3979,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { if (handler.supportsTenantManagement) { describe('getTenant', () => { - const path = '/v2beta1/projects/project_id/tenants/tenant-id'; + const path = '/v2/projects/project_id/tenants/tenant-id'; const method = 'GET'; const tenantId = 'tenant-id'; const expectedResult = utils.responseFrom({ @@ -4042,7 +4035,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); describe('listTenants', () => { - const path = '/v2beta1/projects/project_id/tenants'; + const path = '/v2/projects/project_id/tenants'; const method = 'GET'; const nextPageToken = 'PAGE_TOKEN'; const maxResults = 500; @@ -4158,7 +4151,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); describe('deleteTenant', () => { - const path = '/v2beta1/projects/project_id/tenants/tenant-id'; + const path = '/v2/projects/project_id/tenants/tenant-id'; const method = 'DELETE'; const tenantId = 'tenant-id'; const expectedResult = utils.responseFrom({}); @@ -4212,7 +4205,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); describe('createTenant', () => { - const path = '/v2beta1/projects/project_id/tenants'; + const path = '/v2/projects/project_id/tenants'; const postMethod = 'POST'; const tenantOptions: TenantOptions = { displayName: 'TENANT-DISPLAY-NAME', @@ -4323,7 +4316,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); describe('updateTenant', () => { - const path = '/v2beta1/projects/project_id/tenants/tenant-id'; + const path = '/v2/projects/project_id/tenants/tenant-id'; const patchMethod = 'PATCH'; const tenantId = 'tenant-id'; const tenantOptions = { diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 70ec695393..163d472823 100755 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -26,7 +26,7 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import {Auth, TenantAwareAuth, BaseAuth, DecodedIdToken} from '../../../src/auth/auth'; -import {UserRecord} from '../../../src/auth/user-record'; +import {UserRecord, UpdateRequest} from '../../../src/auth/user-record'; import {FirebaseApp} from '../../../src/firebase-app'; import {FirebaseTokenGenerator} from '../../../src/auth/token-generator'; import { @@ -304,6 +304,15 @@ AUTH_CONFIGS.forEach((testConfig) => { }).to.throw('First argument passed to admin.auth() must be a valid Firebase app instance.'); }); + it('should reject given no project ID', () => { + const authWithoutProjectId = new Auth(mocks.mockCredentialApp()); + authWithoutProjectId.getUser('uid') + .should.eventually.be.rejectedWith( + 'Failed to determine project ID for Auth. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'); + }); + it('should not throw given a valid app', () => { expect(() => { return new Auth(mockApp); @@ -399,22 +408,20 @@ AUTH_CONFIGS.forEach((testConfig) => { } }); - it('verifyIdToken() should throw when project ID is not specified', () => { + it('verifyIdToken() should reject when project ID is not specified', () => { const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); const expected = 'Must initialize app with a cert credential or set your Firebase project ID ' + 'as the GOOGLE_CLOUD_PROJECT environment variable to call verifyIdToken().'; - expect(() => { - mockCredentialAuth.verifyIdToken(mocks.generateIdToken()); - }).to.throw(expected); + return mockCredentialAuth.verifyIdToken(mocks.generateIdToken()) + .should.eventually.be.rejectedWith(expected); }); - it('verifySessionCookie() should throw when project ID is not specified', () => { + it('verifySessionCookie() should reject when project ID is not specified', () => { const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); const expected = 'Must initialize app with a cert credential or set your Firebase project ID ' + 'as the GOOGLE_CLOUD_PROJECT environment variable to call verifySessionCookie().'; - expect(() => { - mockCredentialAuth.verifySessionCookie(mocks.generateSessionCookie()); - }).to.throw(expected); + return mockCredentialAuth.verifySessionCookie(mocks.generateSessionCookie()) + .should.eventually.be.rejectedWith(expected); }); describe('verifyIdToken()', () => { @@ -423,7 +430,11 @@ AUTH_CONFIGS.forEach((testConfig) => { const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); // Set auth_time of token to expected user's tokensValidAfterTime. - const validSince = new Date(expectedUserRecord.tokensValidAfterTime); + expect( + expectedUserRecord.tokensValidAfterTime, + "getValidUserRecord didn't properly set tokensValueAfterTime", + ).to.exist; + const validSince = new Date(expectedUserRecord.tokensValidAfterTime!); // Set expected uid to expected user's. const uid = expectedUserRecord.uid; // Set expected decoded ID token with expected UID and auth time. @@ -664,6 +675,9 @@ AUTH_CONFIGS.forEach((testConfig) => { const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); // Set auth_time of token to expected user's tokensValidAfterTime. + if (!expectedUserRecord.tokensValidAfterTime) { + throw new Error("getValidUserRecord didn't properly set tokensValidAfterTime."); + } const validSince = new Date(expectedUserRecord.tokensValidAfterTime); // Set expected uid to expected user's. const uid = expectedUserRecord.uid; @@ -1248,7 +1262,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be rejected given invalid properties', () => { - return auth.createUser(null) + return auth.createUser(null as any) .then(() => { throw new Error('Unexpected success'); }) @@ -1402,7 +1416,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be rejected given invalid properties', () => { - return auth.updateUser(uid, null) + return auth.updateUser(uid, null as unknown as UpdateRequest) .then(() => { throw new Error('Unexpected success'); }) @@ -1633,8 +1647,6 @@ AUTH_CONFIGS.forEach((testConfig) => { }) .catch((error) => { expect(error).to.have.property('code', 'auth/invalid-page-token'); - expect(validator.isNonEmptyString) - .to.have.been.calledOnce.and.calledWith(invalidToken); }); }); @@ -1939,6 +1951,9 @@ AUTH_CONFIGS.forEach((testConfig) => { const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); // Set auth_time of token to expected user's tokensValidAfterTime. + if (!expectedUserRecord.tokensValidAfterTime) { + throw new Error("getValidUserRecord didn't properly set tokensValidAfterTime."); + } const validSince = new Date(expectedUserRecord.tokensValidAfterTime); // Set expected uid to expected user's. const uid = expectedUserRecord.uid; @@ -1972,7 +1987,6 @@ AUTH_CONFIGS.forEach((testConfig) => { }) .catch((error) => { expect(error).to.have.property('code', 'auth/invalid-id-token'); - expect(validator.isNonEmptyString).to.have.been.calledOnce.and.calledWith(invalidIdToken); }); }); diff --git a/test/unit/auth/credential.spec.ts b/test/unit/auth/credential.spec.ts index 76c624fb2b..792db85c20 100644 --- a/test/unit/auth/credential.spec.ts +++ b/test/unit/auth/credential.spec.ts @@ -44,7 +44,10 @@ chai.use(chaiAsPromised); const expect = chai.expect; const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json'; -const GCLOUD_CREDENTIAL_PATH = path.resolve(process.env.HOME, '.config', GCLOUD_CREDENTIAL_SUFFIX); +if (!process.env.HOME) { + throw new Error('$HOME environment variable must be set to run the tests.'); +} +const GCLOUD_CREDENTIAL_PATH = path.resolve(process.env.HOME!, '.config', GCLOUD_CREDENTIAL_SUFFIX); const MOCK_REFRESH_TOKEN_CONFIG = { client_id: 'test_client_id', client_secret: 'test_client_secret', diff --git a/test/unit/auth/tenant-manager.spec.ts b/test/unit/auth/tenant-manager.spec.ts index 0b86792144..bf1c937e63 100644 --- a/test/unit/auth/tenant-manager.spec.ts +++ b/test/unit/auth/tenant-manager.spec.ts @@ -401,7 +401,7 @@ describe('TenantManager', () => { }); it('should be rejected given invalid TenantOptions', () => { - return tenantManager.createTenant(null) + return tenantManager.createTenant(null as any) .then(() => { throw new Error('Unexpected success'); }) @@ -509,7 +509,7 @@ describe('TenantManager', () => { }); it('should be rejected given invalid TenantOptions', () => { - return tenantManager.updateTenant(tenantId, null) + return tenantManager.updateTenant(tenantId, null as unknown as TenantOptions) .then(() => { throw new Error('Unexpected success'); }) diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index c4f63a6de0..1fb1e24dfa 100755 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -20,7 +20,7 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import {deepCopy} from '../../../src/utils/deep-copy'; -import {EmailSignInConfig} from '../../../src/auth/auth-config'; +import {EmailSignInConfig, EmailSignInProviderConfig} from '../../../src/auth/auth-config'; import { Tenant, TenantOptions, TenantServerResponse, } from '../../../src/auth/tenant'; @@ -33,14 +33,14 @@ chai.use(chaiAsPromised); const expect = chai.expect; describe('Tenant', () => { - const serverRequest = { + const serverRequest: TenantServerResponse = { name: 'projects/project1/tenants/TENANT-ID', displayName: 'TENANT-DISPLAY-NAME', allowPasswordSignup: true, enableEmailLinkSignin: true, }; - const clientRequest = { + const clientRequest: TenantOptions = { displayName: 'TENANT-DISPLAY-NAME', emailSignInConfig: { enabled: true, @@ -62,7 +62,7 @@ describe('Tenant', () => { it('should throw on invalid EmailSignInConfig object', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); - tenantOptionsClientRequest.emailSignInConfig = null; + tenantOptionsClientRequest.emailSignInConfig = null as unknown as EmailSignInProviderConfig; expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) .to.throw('"EmailSignInConfig" must be a non-null object.'); }); @@ -123,7 +123,7 @@ describe('Tenant', () => { it('should throw on invalid EmailSignInConfig', () => { const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); - tenantOptionsClientRequest.emailSignInConfig = null; + tenantOptionsClientRequest.emailSignInConfig = null as unknown as EmailSignInProviderConfig; expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest)) .to.throw('"EmailSignInConfig" must be a non-null object.'); @@ -216,8 +216,9 @@ describe('Tenant', () => { expect(tenantWithoutAllowPasswordSignup.displayName).to.equal(serverResponse.displayName); expect(tenantWithoutAllowPasswordSignup.tenantId).to.equal('TENANT-ID'); - expect(tenantWithoutAllowPasswordSignup.emailSignInConfig.enabled).to.be.false; - expect(tenantWithoutAllowPasswordSignup.emailSignInConfig.passwordRequired).to.be.true; + expect(tenantWithoutAllowPasswordSignup.emailSignInConfig).to.exist; + expect(tenantWithoutAllowPasswordSignup.emailSignInConfig!.enabled).to.be.false; + expect(tenantWithoutAllowPasswordSignup.emailSignInConfig!.passwordRequired).to.be.true; }).not.to.throw(); }); }); diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index 072d6c2509..d36d73d3b6 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -24,7 +24,9 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../../resources/mocks'; -import {FirebaseTokenGenerator, ServiceAccountSigner, IAMSigner} from '../../../src/auth/token-generator'; +import { + BLACKLISTED_CLAIMS, FirebaseTokenGenerator, ServiceAccountSigner, IAMSigner, +} from '../../../src/auth/token-generator'; import {Certificate} from '../../../src/auth/credential'; import { AuthorizedHttpClient, HttpClient } from '../../../src/utils/api-request'; @@ -40,10 +42,6 @@ const expect = chai.expect; const ALGORITHM = 'RS256'; const ONE_HOUR_IN_SECONDS = 60 * 60; const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; -const BLACKLISTED_CLAIMS = [ - 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat', 'iss', 'jti', - 'nbf', 'nonce', -]; /** * Verifies a token is signed with the private key corresponding to the provided public key. @@ -257,7 +255,7 @@ describe('CryptoSigner', () => { describe('FirebaseTokenGenerator', () => { let tokenGenerator: FirebaseTokenGenerator; - let clock: sinon.SinonFakeTimers; + let clock: sinon.SinonFakeTimers | undefined; beforeEach(() => { const cert = new Certificate(mocks.certificateObject); tokenGenerator = new FirebaseTokenGenerator(new ServiceAccountSigner(cert)); @@ -441,13 +439,13 @@ describe('FirebaseTokenGenerator', () => { .then((result) => { token = result; - clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + clock!.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); // Token should still be valid return verifyToken(token, mocks.keyPairs[0].public); }) .then(() => { - clock.tick(1); + clock!.tick(1); // Token should now be invalid return verifyToken(token, mocks.keyPairs[0].public) diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index a7f09634f4..2fe2183eb5 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -33,6 +33,7 @@ import * as verifier from '../../../src/auth/token-verifier'; import {Certificate} from '../../../src/auth/credential'; import { AuthClientErrorCode } from '../../../src/utils/error'; +import { FirebaseApp } from '../../../src/firebase-app'; chai.should(); chai.use(sinonChai); @@ -105,20 +106,22 @@ function mockFailedFetchPublicKeys(): nock.Scope { describe('FirebaseTokenVerifier', () => { + let app: FirebaseApp; let tokenVerifier: verifier.FirebaseTokenVerifier; let tokenGenerator: FirebaseTokenGenerator; - let clock: sinon.SinonFakeTimers; + let clock: sinon.SinonFakeTimers | undefined; let httpsSpy: sinon.SinonSpy; beforeEach(() => { // Needed to generate custom token for testing. + app = mocks.app(); const cert: Certificate = new Certificate(mocks.certificateObject); tokenGenerator = new FirebaseTokenGenerator(new ServiceAccountSigner(cert)); tokenVerifier = new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - 'project_id', verifier.ID_TOKEN_INFO, + app, ); httpsSpy = sinon.spy(https, 'request'); }); @@ -142,7 +145,6 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', 'RS256', 'https://www.example.com/issuer/', - 'project_id', { url: 'https://docs.example.com/verify-tokens', verifyApiName: 'verifyToken()', @@ -150,6 +152,7 @@ describe('FirebaseTokenVerifier', () => { shortName: 'token', expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, }, + app, ); }).not.to.throw(); }); @@ -162,8 +165,9 @@ describe('FirebaseTokenVerifier', () => { invalidCertUrl as any, 'RS256', 'https://www.example.com/issuer/', - 'project_id', - verifier.ID_TOKEN_INFO); + verifier.ID_TOKEN_INFO, + app, + ); }).to.throw('The provided public client certificate URL is an invalid URL.'); }); }); @@ -176,8 +180,8 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', invalidAlgorithm as any, 'https://www.example.com/issuer/', - 'project_id', - verifier.ID_TOKEN_INFO); + verifier.ID_TOKEN_INFO, + app); }).to.throw('The provided JWT algorithm is an empty string.'); }); }); @@ -190,8 +194,9 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', 'RS256', invalidIssuer as any, - 'project_id', - verifier.ID_TOKEN_INFO); + verifier.ID_TOKEN_INFO, + app, + ); }).to.throw('The provided JWT issuer is an invalid URL.'); }); }); @@ -204,14 +209,15 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', 'RS256', 'https://www.example.com/issuer/', - 'project_id', { url: 'https://docs.example.com/verify-tokens', verifyApiName: invalidVerifyApiName as any, jwtName: 'Important Token', shortName: 'token', expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - }); + }, + app, + ); }).to.throw('The JWT verify API name must be a non-empty string.'); }); }); @@ -224,14 +230,15 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', 'RS256', 'https://www.example.com/issuer/', - 'project_id', { url: 'https://docs.example.com/verify-tokens', verifyApiName: 'verifyToken()', jwtName: invalidJwtName as any, shortName: 'token', expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - }); + }, + app, + ); }).to.throw('The JWT public full name must be a non-empty string.'); }); }); @@ -244,14 +251,15 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', 'RS256', 'https://www.example.com/issuer/', - 'project_id', { url: 'https://docs.example.com/verify-tokens', verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: invalidShortName as any, expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, - }); + }, + app, + ); }).to.throw('The JWT public short name must be a non-empty string.'); }); }); @@ -264,14 +272,15 @@ describe('FirebaseTokenVerifier', () => { 'https://www.example.com/publicKeys', 'RS256', 'https://www.example.com/issuer/', - 'project_id', { url: 'https://docs.example.com/verify-tokens', verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: 'token', expiredErrorCode: invalidExpiredErrorCode as any, - }); + }, + app, + ); }).to.throw('The JWT expiration error code must be a non-null ErrorInfo object.'); }); }); @@ -315,15 +324,14 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - undefined as any, verifier.ID_TOKEN_INFO, + mocks.mockCredentialApp(), ); const mockIdToken = mocks.generateIdToken(); const expected = 'Must initialize app with a cert credential or set your Firebase project ID as ' + 'the GOOGLE_CLOUD_PROJECT environment variable to call verifyIdToken().'; - expect(() => { - tokenVerifierWithNoProjectId.verifyJWT(mockIdToken); - }).to.throw(expected); + return tokenVerifierWithNoProjectId.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith(expected); }); it('should be rejected given a Firebase JWT token with no kid', () => { @@ -408,7 +416,7 @@ describe('FirebaseTokenVerifier', () => { // Token should still be valid return tokenVerifier.verifyJWT(mockIdToken).then(() => { - clock.tick(1); + clock!.tick(1); // Token should now be invalid return tokenVerifier.verifyJWT(mockIdToken) @@ -423,8 +431,8 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - 'project_id', verifier.SESSION_COOKIE_INFO, + app, ); mockedRequests.push(mockFetchPublicKeys('/identitytoolkit/v3/relyingparty/publicKeys')); @@ -436,7 +444,7 @@ describe('FirebaseTokenVerifier', () => { // Cookie should still be valid return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie).then(() => { - clock.tick(1); + clock!.tick(1); // Cookie should now be invalid return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie) @@ -468,8 +476,8 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - 'project_id', verifier.SESSION_COOKIE_INFO, + app, ); return tokenGenerator.createCustomToken(mocks.uid) .then((customToken) => { @@ -494,8 +502,8 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', 'RS256', 'https://session.firebase.google.com/', - 'project_id', verifier.SESSION_COOKIE_INFO, + app, ); const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); const legacyCustomToken = legacyTokenGenerator.createToken({ @@ -527,6 +535,32 @@ describe('FirebaseTokenVerifier', () => { }); }); + it('should use the given HTTP Agent', () => { + const agent = new https.Agent(); + const appWithAgent = mocks.appWithOptions({ + credential: mocks.credential, + httpAgent: agent, + }); + tokenVerifier = new verifier.FirebaseTokenVerifier( + 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', + 'RS256', + 'https://securetoken.google.com/', + verifier.ID_TOKEN_INFO, + appWithAgent, + ); + mockedRequests.push(mockFetchPublicKeys()); + + clock = sinon.useFakeTimers(1000); + + const mockIdToken = mocks.generateIdToken(); + + return tokenVerifier.verifyJWT(mockIdToken) + .then(() => { + expect(https.request).to.have.been.calledOnce; + expect(httpsSpy.args[0][0].agent).to.equal(agent); + }); + }); + it('should not fetch the Google cert public keys until the first time verifyJWT() is called', () => { mockedRequests.push(mockFetchPublicKeys()); @@ -534,8 +568,8 @@ describe('FirebaseTokenVerifier', () => { 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', 'RS256', 'https://securetoken.google.com/', - 'project_id', verifier.ID_TOKEN_INFO, + app, ); expect(https.request).not.to.have.been.called; @@ -567,20 +601,20 @@ describe('FirebaseTokenVerifier', () => { return tokenVerifier.verifyJWT(mockIdToken).then(() => { expect(https.request).to.have.been.calledOnce; - clock.tick(999); + clock!.tick(999); return tokenVerifier.verifyJWT(mockIdToken); }).then(() => { expect(https.request).to.have.been.calledOnce; - clock.tick(1); + clock!.tick(1); return tokenVerifier.verifyJWT(mockIdToken); }).then(() => { // One second has passed expect(https.request).to.have.been.calledTwice; - clock.tick(999); + clock!.tick(999); return tokenVerifier.verifyJWT(mockIdToken); }).then(() => { expect(https.request).to.have.been.calledTwice; - clock.tick(1); + clock!.tick(1); return tokenVerifier.verifyJWT(mockIdToken); }).then(() => { // Two seconds have passed diff --git a/test/unit/auth/user-import-builder.spec.ts b/test/unit/auth/user-import-builder.spec.ts index 806a6f3c3e..828bbadb33 100755 --- a/test/unit/auth/user-import-builder.spec.ts +++ b/test/unit/auth/user-import-builder.spec.ts @@ -251,12 +251,34 @@ describe('UserImportBuilder', () => { md5ShaPbkdfAlgorithms.forEach((algorithm) => { describe(`${algorithm}`, () => { - const invalidRounds = [-1, 120001, 'invalid', undefined, null]; + let minRounds: number; + let maxRounds: number; + switch (algorithm) { + case 'MD5': + minRounds = 0; + maxRounds = 8192; + break; + case 'SHA1': + case 'SHA256': + case 'SHA512': + minRounds = 1; + maxRounds = 8192; + break; + case 'PBKDF_SHA1': + case 'PBKDF2_SHA256': + minRounds = 0; + maxRounds = 120000; + break; + default: + throw new Error('Unexpected algorithm: ' + algorithm); + } + const invalidRounds = [minRounds - 1, maxRounds + 1, 'invalid', undefined, null]; + invalidRounds.forEach((rounds) => { it(`should throw when ${JSON.stringify(rounds)} rounds provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ROUNDS, - `A valid "hash.rounds" number between 0 and 120000 must be provided for ` + + `A valid "hash.rounds" number between ${minRounds} and ${maxRounds} must be provided for ` + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -275,12 +297,12 @@ describe('UserImportBuilder', () => { const validOptions = { hash: { algorithm, - rounds: 120000, + rounds: maxRounds, }, }; const expectedRequest = { hashAlgorithm: algorithm, - rounds: 120000, + rounds: maxRounds, users: expectedUsersRequest, }; const userImportBuilder = @@ -603,7 +625,7 @@ describe('UserImportBuilder', () => { }); it('should return expected request with no multi-factor fields when not available', () => { - const noMultiFactorUsers: UserImportRecord[] = [ + const noMultiFactorUsers: any[] = [ {uid: '1234', email: 'user@example.com', multiFactor: null}, {uid: '5678', phoneNumber: '+16505550101', multiFactor: {enrolledFactors: []}}, ]; @@ -833,7 +855,7 @@ describe('UserImportBuilder', () => { index: 9, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLED_FACTORS, - `Unsupported second factor "${JSON.stringify(testUsers[9].multiFactor.enrolledFactors[0])}" provided.`), + `Unsupported second factor "${JSON.stringify(testUsers[9].multiFactor!.enrolledFactors[0])}" provided.`), }, ], }; diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index 8a8b6fd25e..43884a7d70 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -981,7 +981,7 @@ describe('UserRecord', () => { ], }); expect(userRecord.multiFactor).to.deep.equal(multiFactor); - expect(userRecord.multiFactor.enrolledFactors.length).to.equal(2); + expect(userRecord.multiFactor!.enrolledFactors.length).to.equal(2); }); it('should return undefined multiFactor when not available', () => { @@ -1008,11 +1008,11 @@ describe('UserRecord', () => { it('should throw when modifying readonly multiFactor internals', () => { expect(() => { - (userRecord.multiFactor.enrolledFactors[0] as any).displayName = 'Modified'; + (userRecord.multiFactor!.enrolledFactors[0] as any).displayName = 'Modified'; }).to.throw(Error); expect(() => { - (userRecord.multiFactor.enrolledFactors as any)[0] = new PhoneMultiFactorInfo({ + (userRecord.multiFactor!.enrolledFactors as any)[0] = new PhoneMultiFactorInfo({ mfaEnrollmentId: 'enrollmentId3', displayName: 'displayName3', enrolledAt: now.toISOString(), diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index c2bb365997..75262b1456 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -66,7 +66,7 @@ describe('FirebaseApp', () => { let getTokenStub: sinon.SinonStub; let firebaseNamespace: FirebaseNamespace; let firebaseNamespaceInternals: FirebaseNamespaceInternals; - let firebaseConfigVar: string; + let firebaseConfigVar: string | undefined; beforeEach(() => { getTokenStub = sinon.stub(CertCredential.prototype, 'getAccessToken').resolves({ @@ -747,7 +747,8 @@ describe('FirebaseApp', () => { return mockApp.INTERNAL.getToken(true).then((token1) => { // Stub the getToken() method to return a rejected promise. getTokenStub.restore(); - getTokenStub = sinon.stub(mockApp.options.credential, 'getAccessToken') + expect(mockApp.options.credential).to.exist; + getTokenStub = sinon.stub(mockApp.options.credential!, 'getAccessToken') .rejects(new Error('Intentionally rejected')); // Forward the clock to exactly five minutes before expiry. @@ -789,7 +790,8 @@ describe('FirebaseApp', () => { // Stub the credential's getAccessToken() method to always return a rejected promise. getTokenStub.restore(); - getTokenStub = sinon.stub(mockApp.options.credential, 'getAccessToken') + expect(mockApp.options.credential).to.exist; + getTokenStub = sinon.stub(mockApp.options.credential!, 'getAccessToken') .rejects(new Error('Intentionally rejected')); // Expect the call count to initially be zero. @@ -910,7 +912,8 @@ describe('FirebaseApp', () => { it('proactively refreshes the token at the next full minute if it expires in five minutes or less', () => { // Turn off default mocking of one hour access tokens and replace it with a short-lived token. getTokenStub.restore(); - getTokenStub = sinon.stub(mockApp.options.credential, 'getAccessToken').resolves({ + expect(mockApp.options.credential).to.exist; + getTokenStub = sinon.stub(mockApp.options.credential!, 'getAccessToken').resolves({ access_token: utils.generateRandomAccessToken(), expires_in: 3 * 60 + 10, }); diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 8680eac25a..f52b50bfc9 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -320,7 +320,7 @@ describe('FirebaseNamespace', () => { it('should throw given no service name', () => { expect(() => { - firebaseNamespace.INTERNAL.registerService(undefined, mocks.firebaseServiceFactory); + firebaseNamespace.INTERNAL.registerService(undefined as unknown as string, mocks.firebaseServiceFactory); }).to.throw(`No service name provided. Service name must be a non-empty string.`); }); diff --git a/test/unit/firebase.spec.ts b/test/unit/firebase.spec.ts index a38a2942ab..8d3f0cb1ec 100644 --- a/test/unit/firebase.spec.ts +++ b/test/unit/firebase.spec.ts @@ -127,7 +127,7 @@ describe('Firebase', () => { }); it('should initialize SDK given an application default credential', () => { - let credPath: string; + let credPath: string | undefined; credPath = process.env.GOOGLE_APPLICATION_CREDENTIALS; process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../resources/mock.key.json'); firebaseAdmin.initializeApp({ diff --git a/test/unit/firestore/firestore.spec.ts b/test/unit/firestore/firestore.spec.ts index 09d2193800..9105dab4e7 100644 --- a/test/unit/firestore/firestore.spec.ts +++ b/test/unit/firestore/firestore.spec.ts @@ -32,9 +32,9 @@ describe('Firestore', () => { let projectIdApp: FirebaseApp; let firestore: any; - let appCredentials: string; - let googleCloudProject: string; - let gcloudProject: string; + let appCredentials: string | undefined; + let googleCloudProject: string | undefined; + let gcloudProject: string | undefined; const invalidCredError = 'Failed to initialize Google Cloud Firestore client with the available ' + 'credentials. Must initialize the SDK with a certificate credential or application default ' @@ -62,7 +62,11 @@ describe('Firestore', () => { }); afterEach(() => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = appCredentials; + if (appCredentials) { + process.env.GOOGLE_APPLICATION_CREDENTIALS = appCredentials; + } else { + delete process.env.GOOGLE_APPLICATION_CREDENTIALS; + } if (googleCloudProject) { process.env.GOOGLE_CLOUD_PROJECT = googleCloudProject; } else { diff --git a/test/unit/instance-id/instance-id-request.spec.ts b/test/unit/instance-id/instance-id-request.spec.ts index 3bdfc86bb2..10c59c4b05 100644 --- a/test/unit/instance-id/instance-id-request.spec.ts +++ b/test/unit/instance-id/instance-id-request.spec.ts @@ -80,15 +80,13 @@ describe('FirebaseInstanceIdRequestHandler', () => { const timeout = 10000; it('should be fulfilled given a valid instance ID', () => { - const expectedResult = {}; const stub = sinon.stub(HttpClient.prototype, 'send') - .resolves(utils.responseFrom(expectedResult)); + .resolves(utils.responseFrom('')); stubs.push(stub); const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId); return requestHandler.deleteInstanceId('test-iid') - .then((result) => { - expect(result).to.deep.equal(expectedResult); + .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: httpMethod, url: `https://${host}${path}`, diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index d03bfea3ba..081e9ddb82 100644 --- a/test/unit/instance-id/instance-id.spec.ts +++ b/test/unit/instance-id/instance-id.spec.ts @@ -46,8 +46,8 @@ describe('InstanceId', () => { let malformedAccessTokenClient: InstanceId; let rejectedPromiseAccessTokenClient: InstanceId; - let googleCloudProject: string; - let gcloudProject: string; + let googleCloudProject: string | undefined; + let gcloudProject: string | undefined; const noProjectIdError = 'Failed to determine project ID for InstanceId. Initialize the SDK ' + 'with service account credentials or set project ID as an app option. Alternatively set the ' diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index 6793f4fe52..52219c4312 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -28,7 +28,7 @@ import * as mocks from '../../resources/mocks'; import {FirebaseApp} from '../../../src/firebase-app'; import { - Message, MessagingOptions, MessagingPayload, MessagingDevicesResponse, + Message, MessagingOptions, MessagingPayload, MessagingDevicesResponse, MessagingDeviceGroupResponse, MessagingTopicManagementResponse, BatchResponse, SendResponse, MulticastMessage, } from '../../../src/messaging/messaging-types'; import { @@ -366,13 +366,14 @@ describe('Messaging', () => { }).to.throw('First argument passed to admin.messaging() must be a valid Firebase app instance.'); }); - it('should throw given app without project ID', () => { - expect(() => { - const appWithoutProhectId = mocks.mockCredentialApp(); - return new Messaging(appWithoutProhectId); - }).to.throw('Failed to determine project ID for Messaging. Initialize the SDK with service ' - + 'account credentials or set project ID as an app option. Alternatively set the ' - + 'GOOGLE_CLOUD_PROJECT environment variable.'); + it('should reject given app without project ID', () => { + const appWithoutProjectId = mocks.mockCredentialApp(); + const messagingWithoutProjectId = new Messaging(appWithoutProjectId); + messagingWithoutProjectId.send({topic: 'test'}) + .should.eventually.be.rejectedWith( + 'Failed to determine project ID for Messaging. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'); }); it('should not throw given a valid app', () => { @@ -398,10 +399,10 @@ describe('Messaging', () => { describe('send()', () => { it('should throw given no message', () => { expect(() => { - messaging.send(undefined as Message); + messaging.send(undefined as any); }).to.throw('Message must be a non-null object'); expect(() => { - messaging.send(null); + messaging.send(null as any); }).to.throw('Message must be a non-null object'); }); @@ -504,6 +505,28 @@ describe('Messaging', () => { .and.have.property('code', 'messaging/registration-token-not-registered'); }); + ['THIRD_PARTY_AUTH_ERROR', 'APNS_AUTH_ERROR'].forEach((errorCode) => { + it(`should map ${errorCode} to third party auth error`, () => { + const resp = { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + details: [ + { + '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', + 'errorCode': errorCode, + }, + ], + }, + }; + mockedRequests.push(mockSendError(404, 'json', resp)); + return messaging.send( + {token: 'mock-token'}, + ).should.eventually.be.rejectedWith('test error message') + .and.have.property('code', 'messaging/third-party-auth-error'); + }); + }); + it('should map server error code to client-side error', () => { const resp = { error: { @@ -549,37 +572,36 @@ describe('Messaging', () => { expect(response.messageId).to.be.undefined; expect(response.error).to.have.property('code', code); if (msg) { - expect(response.error.toString()).to.contain(msg); + expect(response.error!.toString()).to.contain(msg); } } it('should throw given no messages', () => { expect(() => { - messaging.sendAll(undefined as Message[]); + messaging.sendAll(undefined as any); }).to.throw('messages must be a non-empty array'); expect(() => { - messaging.sendAll(null); + messaging.sendAll(null as any); }).to.throw('messages must be a non-empty array'); expect(() => { messaging.sendAll([]); }).to.throw('messages must be a non-empty array'); }); - it('should throw when called with more than 100 messages', () => { + it('should throw when called with more than 500 messages', () => { const messages: Message[] = []; - for (let i = 0; i < 101; i++) { + for (let i = 0; i < 501; i++) { messages.push(validMessage); } expect(() => { messaging.sendAll(messages); - }).to.throw('messages list must not contain more than 100 items'); + }).to.throw('messages list must not contain more than 500 items'); }); - it('should throw when a message is invalid', () => { + it('should reject when a message is invalid', () => { const invalidMessage: Message = {} as any; - expect(() => { - messaging.sendAll([validMessage, invalidMessage]); - }).to.throw('Exactly one of topic, token or condition is required'); + messaging.sendAll([validMessage, invalidMessage]) + .should.eventually.be.rejectedWith('Exactly one of topic, token or condition is required'); }); const invalidDryRun = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; @@ -804,7 +826,7 @@ describe('Messaging', () => { ], }; - let stub: sinon.SinonStub; + let stub: sinon.SinonStub | null; afterEach(() => { if (stub) { @@ -815,7 +837,7 @@ describe('Messaging', () => { it('should throw given no messages', () => { expect(() => { - messaging.sendMulticast(undefined as MulticastMessage); + messaging.sendMulticast(undefined as any); }).to.throw('MulticastMessage must be a non-null object'); expect(() => { messaging.sendMulticast({} as any); @@ -825,14 +847,14 @@ describe('Messaging', () => { }).to.throw('tokens must be a non-empty array'); }); - it('should throw when called with more than 100 messages', () => { + it('should throw when called with more than 500 messages', () => { const tokens: string[] = []; - for (let i = 0; i < 101; i++) { + for (let i = 0; i < 501; i++) { tokens.push(`token${i}`); } expect(() => { messaging.sendMulticast({tokens}); - }).to.throw('tokens list must not contain more than 100 items'); + }).to.throw('tokens list must not contain more than 500 items'); }); const invalidDryRun = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; @@ -851,9 +873,9 @@ describe('Messaging', () => { .then((response: BatchResponse) => { expect(response).to.deep.equal(mockResponse); expect(stub).to.have.been.calledOnce; - const messages: Message[] = stub.args[0][0]; + const messages: Message[] = stub!.args[0][0]; expect(messages.length).to.equal(3); - expect(stub.args[0][1]).to.be.undefined; + expect(stub!.args[0][1]).to.be.undefined; messages.forEach((message, idx) => { expect((message as any).token).to.equal(tokens[idx]); expect(message.android).to.be.undefined; @@ -875,14 +897,15 @@ describe('Messaging', () => { data: {key: 'value'}, notification: {title: 'test title'}, webpush: {data: {webKey: 'webValue'}}, + fcmOptions: {analyticsLabel: 'label'}, }; return messaging.sendMulticast(multicast) .then((response: BatchResponse) => { expect(response).to.deep.equal(mockResponse); expect(stub).to.have.been.calledOnce; - const messages: Message[] = stub.args[0][0]; + const messages: Message[] = stub!.args[0][0]; expect(messages.length).to.equal(3); - expect(stub.args[0][1]).to.be.undefined; + expect(stub!.args[0][1]).to.be.undefined; messages.forEach((message, idx) => { expect((message as any).token).to.equal(tokens[idx]); expect(message.android).to.deep.equal(multicast.android); @@ -890,6 +913,7 @@ describe('Messaging', () => { expect(message.data).to.be.deep.equal(multicast.data); expect(message.notification).to.deep.equal(multicast.notification); expect(message.webpush).to.deep.equal(multicast.webpush); + expect(message.fcmOptions).to.deep.equal(multicast.fcmOptions); }); }); }); @@ -901,7 +925,7 @@ describe('Messaging', () => { .then((response: BatchResponse) => { expect(response).to.deep.equal(mockResponse); expect(stub).to.have.been.calledOnce; - expect(stub.args[0][1]).to.be.true; + expect(stub!.args[0][1]).to.be.true; }); }); @@ -1095,7 +1119,7 @@ describe('Messaging', () => { expect(response.messageId).to.be.undefined; expect(response.error).to.have.property('code', code); if (msg) { - expect(response.error.toString()).to.contain(msg); + expect(response.error!.toString()).to.contain(msg); } } }); @@ -1116,7 +1140,7 @@ describe('Messaging', () => { it('should throw given no registration token(s) argument', () => { expect(() => { - messaging.sendToDevice(undefined as string, mocks.messaging.payloadDataOnly); + messaging.sendToDevice(undefined as any, mocks.messaging.payloadDataOnly); }).to.throw(invalidArgumentError); }); @@ -1309,10 +1333,11 @@ describe('Messaging', () => { mocks.messaging.registrationToken + '2', ], mocks.messaging.payload, - ).then((response: MessagingDevicesResponse) => { + ).then((response: MessagingDevicesResponse | MessagingDeviceGroupResponse) => { expect(response).to.have.keys([ 'failureCount', 'successCount', 'canonicalRegistrationTokenCount', 'multicastId', 'results', ]); + response = response as MessagingDevicesResponse; expect(response.failureCount).to.equal(2); expect(response.successCount).to.equal(1); expect(response.canonicalRegistrationTokenCount).to.equal(1); @@ -1437,7 +1462,7 @@ describe('Messaging', () => { it('should throw given no notification key argument', () => { expect(() => { - messaging.sendToDeviceGroup(undefined as string, mocks.messaging.payloadDataOnly); + messaging.sendToDeviceGroup(undefined as any, mocks.messaging.payloadDataOnly); }).to.throw(invalidArgumentError); }); @@ -1685,7 +1710,7 @@ describe('Messaging', () => { it('should throw given no topic argument', () => { expect(() => { - messaging.sendToTopic(undefined as string, mocks.messaging.payload); + messaging.sendToTopic(undefined as any, mocks.messaging.payload); }).to.throw(invalidArgumentError); }); @@ -1912,7 +1937,7 @@ describe('Messaging', () => { it('should throw given no condition argument', () => { expect(() => { - messaging.sendToCondition(undefined as string, mocks.messaging.payloadDataOnly); + messaging.sendToCondition(undefined as any, mocks.messaging.payloadDataOnly); }).to.throw(invalidArgumentError); }); @@ -2084,19 +2109,19 @@ describe('Messaging', () => { invalidPayloads.forEach((invalidPayload) => { it(`should throw given invalid type for payload argument: ${ JSON.stringify(invalidPayload) }`, () => { expect(() => { - messaging.sendToDevice(mocks.messaging.registrationToken, invalidPayload as MessagingPayload); + messaging.sendToDevice(mocks.messaging.registrationToken, invalidPayload as any); }).to.throw('Messaging payload must be an object'); expect(() => { - messaging.sendToDeviceGroup(mocks.messaging.notificationKey, invalidPayload as MessagingPayload); + messaging.sendToDeviceGroup(mocks.messaging.notificationKey, invalidPayload as any); }).to.throw('Messaging payload must be an object'); expect(() => { - messaging.sendToTopic(mocks.messaging.topic, invalidPayload as MessagingPayload); + messaging.sendToTopic(mocks.messaging.topic, invalidPayload as any); }).to.throw('Messaging payload must be an object'); expect(() => { - messaging.sendToCondition(mocks.messaging.condition, invalidPayload as MessagingPayload); + messaging.sendToCondition(mocks.messaging.condition, invalidPayload as any); }).to.throw('Messaging payload must be an object'); }); }); @@ -2393,6 +2418,96 @@ describe('Messaging', () => { }).to.throw('bodyLocKey is required when specifying bodyLocArgs'); }); + const invalidVibrateTimings = [[null, 500], [-100]]; + invalidVibrateTimings.forEach((vibrateTimingsMillisMaybeNull) => { + const vibrateTimingsMillis = vibrateTimingsMillisMaybeNull as number[]; + it(`should throw given an null or negative vibrateTimingsMillis: ${ vibrateTimingsMillis }`, () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + vibrateTimingsMillis, + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('android.notification.vibrateTimingsMillis must be non-negative durations in milliseconds'); + }); + }); + + it(`should throw given an empty vibrateTimingsMillis array`, () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + vibrateTimingsMillis: [], + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('android.notification.vibrateTimingsMillis must be a non-empty array of numbers'); + }); + + invalidColors.forEach((color) => { + it(`should throw given an invalid color: ${ color }`, () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + lightSettings: { + color, + lightOnDurationMillis: 100, + lightOffDurationMillis: 800, + }, + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('android.notification.lightSettings.color must be in the form #RRGGBB or #RRGGBBAA format'); + }); + }); + + it(`should throw given a negative light on duration`, () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + lightSettings: { + color: '#aabbcc', + lightOnDurationMillis: -1, + lightOffDurationMillis: 800, + }, + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw( + 'android.notification.lightSettings.lightOnDurationMillis must be a non-negative duration in milliseconds'); + }); + + it(`should throw given a negative light off duration`, () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + lightSettings: { + color: '#aabbcc', + lightOnDurationMillis: 100, + lightOffDurationMillis: -800, + }, + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw( + 'android.notification.lightSettings.lightOffDurationMillis must be a non-negative duration in milliseconds'); + }); + const invalidVolumes = [-0.1, 1.1]; invalidVolumes.forEach((volume) => { it(`should throw given invalid apns sound volume: ${volume}`, () => { @@ -2846,6 +2961,9 @@ describe('Messaging', () => { sound: 'test.sound', tag: 'test.tag', imageUrl: 'https://example.com/image.png', + ticker: 'test.ticker', + sticky: true, + visibility: 'private', }, }, }, @@ -2859,6 +2977,9 @@ describe('Messaging', () => { sound: 'test.sound', tag: 'test.tag', image: 'https://example.com/image.png', + ticker: 'test.ticker', + sticky: true, + visibility: 'PRIVATE', }, }, }, @@ -2876,6 +2997,19 @@ describe('Messaging', () => { bodyLocKey: 'body.loc.key', bodyLocArgs: ['arg1', 'arg2'], channelId: 'test.channel', + eventTimestamp: new Date('2019-10-20T12:00:00-06:30'), + localOnly: true, + priority: 'high', + vibrateTimingsMillis: [100, 50, 250], + defaultVibrateTimings: false, + defaultSound: true, + lightSettings: { + color: '#AABBCCDD', + lightOnDurationMillis: 200, + lightOffDurationMillis: 300, + }, + defaultLightSettings: false, + notificationCount: 1, }, }, }, @@ -2890,6 +3024,24 @@ describe('Messaging', () => { body_loc_key: 'body.loc.key', body_loc_args: ['arg1', 'arg2'], channel_id: 'test.channel', + event_time: '2019-10-20T18:30:00.000Z', + local_only: true, + notification_priority: 'PRIORITY_HIGH', + vibrate_timings: ['0.100000000s', '0.050000000s', '0.250000000s'], + default_vibrate_timings: false, + default_sound: true, + light_settings: { + color: { + red: 0.6666666666666666, + green: 0.7333333333333333, + blue: 0.8, + alpha: 0.8666666666666667, + }, + light_on_duration: '0.200000000s', + light_off_duration: '0.300000000s', + }, + default_light_settings: false, + notification_count: 1, }, }, }, @@ -2939,6 +3091,22 @@ describe('Messaging', () => { bodyLocKey: 'body.loc.key', bodyLocArgs: ['arg1', 'arg2'], channelId: 'test.channel', + ticker: 'test.ticker', + sticky: true, + visibility: 'private', + eventTimestamp: new Date('2019-10-20T12:00:00-06:30'), + localOnly: true, + priority: 'high', + vibrateTimingsMillis: [100, 50, 250], + defaultVibrateTimings: false, + defaultSound: true, + lightSettings: { + color: '#AABBCC', + lightOnDurationMillis: 200, + lightOffDurationMillis: 300, + }, + defaultLightSettings: false, + notificationCount: 1, }, fcmOptions: { analyticsLabel: 'test.analytics', @@ -2969,6 +3137,27 @@ describe('Messaging', () => { body_loc_key: 'body.loc.key', body_loc_args: ['arg1', 'arg2'], channel_id: 'test.channel', + ticker: 'test.ticker', + sticky: true, + visibility: 'PRIVATE', + event_time: '2019-10-20T18:30:00.000Z', + local_only: true, + notification_priority: 'PRIORITY_HIGH', + vibrate_timings: ['0.100000000s', '0.050000000s', '0.250000000s'], + default_vibrate_timings: false, + default_sound: true, + light_settings: { + color: { + red: 0.6666666666666666, + green: 0.7333333333333333, + blue: 0.8, + alpha: 1, + }, + light_on_duration: '0.200000000s', + light_off_duration: '0.300000000s', + }, + default_light_settings: false, + notification_count: 1, }, fcmOptions: { analyticsLabel: 'test.analytics', @@ -3423,7 +3612,7 @@ describe('Messaging', () => { it('should throw given no registration token(s) argument', () => { expect(() => { - messagingService[methodName](undefined as string, mocks.messaging.topic); + messagingService[methodName](undefined as any, mocks.messaging.topic); }).to.throw(invalidRegistrationTokensArgumentError); }); @@ -3478,7 +3667,7 @@ describe('Messaging', () => { it('should throw given no topic argument', () => { expect(() => { - messagingService[methodName](mocks.messaging.registrationToken, undefined as string); + messagingService[methodName](mocks.messaging.registrationToken, undefined as any); }).to.throw(invalidTopicArgumentError); }); diff --git a/test/unit/project-management/project-management-api-request.spec.ts b/test/unit/project-management/project-management-api-request.spec.ts index 9b9e6ec0c4..87a3e334af 100644 --- a/test/unit/project-management/project-management-api-request.spec.ts +++ b/test/unit/project-management/project-management-api-request.spec.ts @@ -435,8 +435,7 @@ describe('ProjectManagementRequestHandler', () => { displayName: newDisplayName, }; return requestHandler.setDisplayName(ANDROID_APP_RESOURCE_NAME, newDisplayName) - .then((result) => { - expect(result).to.deep.equal(null); + .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'PATCH', url, @@ -489,8 +488,7 @@ describe('ProjectManagementRequestHandler', () => { certType: 'SHA_1', }; return requestHandler.addAndroidShaCertificate(ANDROID_APP_RESOURCE_NAME, certificateToAdd) - .then((result) => { - expect(result).to.deep.equal(null); + .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'POST', url, @@ -567,8 +565,7 @@ describe('ProjectManagementRequestHandler', () => { const url = `https://${HOST}:${PORT}/v1beta1/${resourceName}`; return requestHandler.deleteResource(resourceName) - .then((result) => { - expect(result).to.deep.equal(null); + .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'DELETE', url, diff --git a/test/unit/security-rules/security-rules-api-client.spec.ts b/test/unit/security-rules/security-rules-api-client.spec.ts index d2a0de58ee..b0f506c5d5 100644 --- a/test/unit/security-rules/security-rules-api-client.spec.ts +++ b/test/unit/security-rules/security-rules-api-client.spec.ts @@ -55,7 +55,7 @@ describe('SecurityRulesApiClient', () => { describe('Constructor', () => { it('should throw when the HttpClient is null', () => { - expect(() => new SecurityRulesApiClient(null, 'test')) + expect(() => new SecurityRulesApiClient(null as unknown as HttpClient, 'test')) .to.throw('HttpClient must be a non-null object.'); }); diff --git a/test/unit/security-rules/security-rules.spec.ts b/test/unit/security-rules/security-rules.spec.ts index 2407c54a91..44b4655a0d 100644 --- a/test/unit/security-rules/security-rules.spec.ts +++ b/test/unit/security-rules/security-rules.spec.ts @@ -31,7 +31,15 @@ const expect = chai.expect; describe('SecurityRules', () => { const EXPECTED_ERROR = new FirebaseSecurityRulesError('internal-error', 'message'); - const FIRESTORE_RULESET_RESPONSE = { + const FIRESTORE_RULESET_RESPONSE: { + // This type is effectively a RulesetResponse, but with non-readonly fields + // to allow easier use from within the tests. An improvement would be to + // alter this into a helper that creates customized RulesetResponses based + // on the needs of the test, as that would ensure type-safety. + name: string, + createTime: string, + source: object | null, + } = { name: 'projects/test-project/rulesets/foo', createTime: '2019-03-08T23:45:23.288047Z', source: { diff --git a/test/unit/utils/api-request.spec.ts b/test/unit/utils/api-request.spec.ts index 9480ee8d0a..440a9de044 100644 --- a/test/unit/utils/api-request.spec.ts +++ b/test/unit/utils/api-request.spec.ts @@ -106,9 +106,9 @@ function testRetryConfig(): RetryConfig { describe('HttpClient', () => { let mockedRequests: nock.Scope[] = []; - let transportSpy: sinon.SinonSpy = null; - let delayStub: sinon.SinonStub = null; - let clock: sinon.SinonFakeTimers = null; + let transportSpy: sinon.SinonSpy | null = null; + let delayStub: sinon.SinonStub | null = null; + let clock: sinon.SinonFakeTimers | null = null; const sampleMultipartData = '--boundary\r\n' + 'Content-type: application/json\r\n\r\n' @@ -239,7 +239,7 @@ describe('HttpClient', () => { expect(resp.status).to.equal(200); expect(resp.headers['content-type']).to.equal('multipart/mixed; boundary=boundary'); expect(resp.multipart).to.not.be.undefined; - expect(resp.multipart.length).to.equal(0); + expect(resp.multipart!.length).to.equal(0); expect(() => { resp.text; }).to.throw('Unable to parse multipart payload as text'); expect(() => { resp.data; }).to.throw('Unable to parse multipart payload as JSON'); expect(resp.isJson()).to.be.false; @@ -260,10 +260,8 @@ describe('HttpClient', () => { }).then((resp) => { expect(resp.status).to.equal(200); expect(resp.headers['content-type']).to.equal('multipart/mixed; boundary=boundary'); - expect(resp.multipart).to.not.be.undefined; - expect(resp.multipart.length).to.equal(2); - expect(resp.multipart[0].toString('utf-8')).to.equal('{"foo": 1}'); - expect(resp.multipart[1].toString('utf-8')).to.equal('foo bar'); + expect(resp.multipart).to.exist; + expect(resp.multipart!.map((buffer) => buffer.toString('utf-8'))).to.deep.equal(['{"foo": 1}', 'foo bar']); expect(() => { resp.text; }).to.throw('Unable to parse multipart payload as text'); expect(() => { resp.data; }).to.throw('Unable to parse multipart payload as JSON'); expect(resp.isJson()).to.be.false; @@ -284,10 +282,8 @@ describe('HttpClient', () => { }).then((resp) => { expect(resp.status).to.equal(200); expect(resp.headers['content-type']).to.equal('multipart/something; boundary=boundary'); - expect(resp.multipart).to.not.be.undefined; - expect(resp.multipart.length).to.equal(2); - expect(resp.multipart[0].toString('utf-8')).to.equal('{"foo": 1}'); - expect(resp.multipart[1].toString('utf-8')).to.equal('foo bar'); + expect(resp.multipart).to.exist; + expect(resp.multipart!.map((buffer) => buffer.toString('utf-8'))).to.deep.equal(['{"foo": 1}', 'foo bar']); expect(() => { resp.text; }).to.throw('Unable to parse multipart payload as text'); expect(() => { resp.data; }).to.throw('Unable to parse multipart payload as JSON'); expect(resp.isJson()).to.be.false; @@ -358,8 +354,8 @@ describe('HttpClient', () => { httpAgent, }).then((resp) => { expect(resp.status).to.equal(200); - expect(transportSpy.callCount).to.equal(1); - const options = transportSpy.args[0][0]; + expect(transportSpy!.callCount).to.equal(1); + const options = transportSpy!.args[0][0]; expect(options.agent).to.equal(httpAgent); }); }); @@ -645,10 +641,8 @@ describe('HttpClient', () => { const resp = err.response; expect(resp.status).to.equal(500); expect(resp.headers['content-type']).to.equal('multipart/mixed; boundary=boundary'); - expect(resp.multipart).to.not.be.undefined; - expect(resp.multipart.length).to.equal(2); - expect(resp.multipart[0].toString('utf-8')).to.equal('{"foo": 1}'); - expect(resp.multipart[1].toString('utf-8')).to.equal('foo bar'); + expect(resp.multipart).to.exist; + expect(resp.multipart!.map((buffer) => buffer.toString('utf-8'))).to.deep.equal(['{"foo": 1}', 'foo bar']); expect(() => { resp.text; }).to.throw('Unable to parse multipart payload as text'); expect(() => { resp.data; }).to.throw('Unable to parse multipart payload as JSON'); expect(resp.isJson()).to.be.false; @@ -925,8 +919,8 @@ describe('HttpClient', () => { expect(resp.headers['content-type']).to.equal('application/json'); expect(resp.data).to.deep.equal({}); expect(resp.isJson()).to.be.true; - expect(delayStub.callCount).to.equal(4); - const delays = delayStub.args.map((args) => args[0]); + expect(delayStub!.callCount).to.equal(4); + const delays = delayStub!.args.map((args) => args[0]); expect(delays).to.deep.equal([0, 1000, 2000, 4000]); }); }); @@ -956,8 +950,8 @@ describe('HttpClient', () => { expect(resp.headers['content-type']).to.equal('application/json'); expect(resp.data).to.deep.equal({}); expect(resp.isJson()).to.be.true; - expect(delayStub.callCount).to.equal(4); - const delays = delayStub.args.map((args) => args[0]); + expect(delayStub!.callCount).to.equal(4); + const delays = delayStub!.args.map((args) => args[0]); expect(delays).to.deep.equal([0, 2000, 4000, 4000]); }); }); @@ -986,8 +980,8 @@ describe('HttpClient', () => { expect(resp.headers['content-type']).to.equal('application/json'); expect(resp.data).to.deep.equal({}); expect(resp.isJson()).to.be.true; - expect(delayStub.callCount).to.equal(4); - const delays = delayStub.args.map((args) => args[0]); + expect(delayStub!.callCount).to.equal(4); + const delays = delayStub!.args.map((args) => args[0]); expect(delays).to.deep.equal([0, 0, 0, 0]); }); }); @@ -1019,8 +1013,8 @@ describe('HttpClient', () => { expect(resp.headers['content-type']).to.equal('application/json'); expect(resp.data).to.deep.equal(respData); expect(resp.isJson()).to.be.true; - expect(delayStub.callCount).to.equal(1); - expect(delayStub.args[0][0]).to.equal(30 * 1000); + expect(delayStub!.callCount).to.equal(1); + expect(delayStub!.args[0][0]).to.equal(30 * 1000); }); }); @@ -1055,8 +1049,8 @@ describe('HttpClient', () => { expect(resp.headers['content-type']).to.equal('application/json'); expect(resp.data).to.deep.equal(respData); expect(resp.isJson()).to.be.true; - expect(delayStub.callCount).to.equal(1); - expect(delayStub.args[0][0]).to.equal(30 * 1000); + expect(delayStub!.callCount).to.equal(1); + expect(delayStub!.args[0][0]).to.equal(30 * 1000); }); }); @@ -1089,8 +1083,8 @@ describe('HttpClient', () => { expect(resp.headers['content-type']).to.equal('application/json'); expect(resp.data).to.deep.equal(respData); expect(resp.isJson()).to.be.true; - expect(delayStub.callCount).to.equal(1); - expect(delayStub.args[0][0]).to.equal(0); + expect(delayStub!.callCount).to.equal(1); + expect(delayStub!.args[0][0]).to.equal(0); }); }); @@ -1121,8 +1115,8 @@ describe('HttpClient', () => { expect(resp.headers['content-type']).to.equal('application/json'); expect(resp.data).to.deep.equal(respData); expect(resp.isJson()).to.be.true; - expect(delayStub.callCount).to.equal(1); - expect(delayStub.args[0][0]).to.equal(0); + expect(delayStub!.callCount).to.equal(1); + expect(delayStub!.args[0][0]).to.equal(0); }); }); @@ -1223,7 +1217,7 @@ describe('AuthorizedHttpClient', () => { }); describe('HTTP Agent', () => { - let transportSpy: sinon.SinonSpy = null; + let transportSpy: sinon.SinonSpy | null = null; let mockAppWithAgent: FirebaseApp; let agentForApp: Agent; @@ -1237,7 +1231,7 @@ describe('AuthorizedHttpClient', () => { }); afterEach(() => { - transportSpy.restore(); + transportSpy!.restore(); transportSpy = null; return mockAppWithAgent.delete(); }); @@ -1258,8 +1252,8 @@ describe('AuthorizedHttpClient', () => { httpAgent, }).then((resp) => { expect(resp.status).to.equal(200); - expect(transportSpy.callCount).to.equal(1); - const options = transportSpy.args[0][0]; + expect(transportSpy!.callCount).to.equal(1); + const options = transportSpy!.args[0][0]; expect(options.agent).to.equal(httpAgent); }); }); @@ -1278,8 +1272,8 @@ describe('AuthorizedHttpClient', () => { url: mockUrl, }).then((resp) => { expect(resp.status).to.equal(200); - expect(transportSpy.callCount).to.equal(1); - const options = transportSpy.args[0][0]; + expect(transportSpy!.callCount).to.equal(1); + const options = transportSpy!.args[0][0]; expect(options.agent).to.equal(agentForApp); }); }); diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 1bdfc27069..985f3b5a8c 100755 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -19,7 +19,7 @@ import {expect} from 'chai'; import * as mocks from '../../resources/mocks'; import { - addReadonlyGetter, getProjectId, toWebSafeBase64, formatString, generateUpdateMask, + addReadonlyGetter, getProjectId, findProjectId, toWebSafeBase64, formatString, generateUpdateMask, } from '../../../src/utils/index'; import {isNonEmptyString} from '../../../src/utils/validator'; import {FirebaseApp, FirebaseAppOptions} from '../../../src/firebase-app'; @@ -67,8 +67,8 @@ describe('toWebSafeBase64()', () => { }); describe('getProjectId()', () => { - let googleCloudProject: string; - let gcloudProject: string; + let googleCloudProject: string | undefined; + let gcloudProject: string | undefined; before(() => { googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; @@ -123,6 +123,63 @@ describe('getProjectId()', () => { }); }); +describe('findProjectId()', () => { + let googleCloudProject: string | undefined; + let gcloudProject: string | undefined; + + before(() => { + googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; + gcloudProject = process.env.GCLOUD_PROJECT; + }); + + after(() => { + if (isNonEmptyString(googleCloudProject)) { + process.env.GOOGLE_CLOUD_PROJECT = googleCloudProject; + } else { + delete process.env.GOOGLE_CLOUD_PROJECT; + } + + if (isNonEmptyString(gcloudProject)) { + process.env.GCLOUD_PROJECT = gcloudProject; + } else { + delete process.env.GCLOUD_PROJECT; + } + }); + + it('should return the explicitly specified project ID from app options', () => { + const options: FirebaseAppOptions = { + credential: new mocks.MockCredential(), + projectId: 'explicit-project-id', + }; + const app: FirebaseApp = mocks.appWithOptions(options); + return findProjectId(app).should.eventually.equal(options.projectId); + }); + + it('should return the project ID from service account', () => { + const app: FirebaseApp = mocks.app(); + return findProjectId(app).should.eventually.equal('project_id'); + }); + + it('should return the project ID set in GOOGLE_CLOUD_PROJECT environment variable', () => { + process.env.GOOGLE_CLOUD_PROJECT = 'env-var-project-id'; + const app: FirebaseApp = mocks.mockCredentialApp(); + return findProjectId(app).should.eventually.equal('env-var-project-id'); + }); + + it('should return the project ID set in GCLOUD_PROJECT environment variable', () => { + process.env.GCLOUD_PROJECT = 'env-var-project-id'; + const app: FirebaseApp = mocks.mockCredentialApp(); + return findProjectId(app).should.eventually.equal('env-var-project-id'); + }); + + it('should return null when project ID is not set', () => { + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const app: FirebaseApp = mocks.mockCredentialApp(); + return findProjectId(app).should.eventually.be.null; + }); +}); + describe('formatString()', () => { it('should keep string as is if not parameters are provided', () => { const str = 'projects/{projectId}/{api}/path/api/projectId'; diff --git a/tsconfig.json b/tsconfig.json index 938016bb3a..e13292b408 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,13 @@ "target": "es5", "noImplicitAny": true, "noUnusedLocals": true, + // TODO(rsgowman): enable `"strict": true,` and remove explicit setting of: noImplicitAny, noImplicitThis, alwaysStrict, strictBindCallApply, strictNullChecks, strictFunctionTypes, strictPropertyInitialization. + "noImplicitThis": true, + "alwaysStrict": true, + "strictBindCallApply": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + //"strictPropertyInitialization": true, "lib": ["es2015"], "outDir": "lib", // We manually craft typings in src/index.d.ts instead of auto-generating them.