From f855b1f60497060f916d5085a3f3bf6623d0c9ac Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Mon, 25 Apr 2022 13:37:50 -0400 Subject: [PATCH 01/27] chore: Update database-compat and database-types (#1660) - Update `@firebase/database-types` to `0.9.7` - Update `@firebase/database-compat` to `0.1.8` --- package-lock.json | 84 +++++++++++++++++++++++++++++++++++++---------- package.json | 4 +-- 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88ab154a62..f5cbc07f8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "10.0.2", + "version": "10.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -503,44 +503,93 @@ "version": "0.5.12", "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.12.tgz", "integrity": "sha512-gAKwxo0Ev+rp7Px+Yr71WbcC0CM9Tevhv0g38ORp2p57HqGjY65D3MD+jTKGZl58N/0nmX6MRRKym3bq/3k1gw==", + "dev": true, "requires": { "@firebase/util": "1.5.1", "tslib": "^2.1.0" } }, "@firebase/database": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.7.tgz", - "integrity": "sha512-HL2NMQ3Ce5YNM2MdEuACHmU9NQEwq2F64R0XK+CReph40skxp+A7TvlJDO5bTAC0s3l3ebgCA9VmxfJu5R6UAA==", + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.8.tgz", + "integrity": "sha512-JBQVfFLzfhxlQbl4OU6ov9fdsddkytBQdtSSR49cz48homj38ccltAhK6seum+BI7f28cV2LFHF9672lcN+qxA==", "requires": { "@firebase/auth-interop-types": "0.1.6", - "@firebase/component": "0.5.12", + "@firebase/component": "0.5.13", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.1", + "@firebase/util": "1.5.2", "faye-websocket": "0.11.4", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", + "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", + "requires": { + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/database-compat": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.7.tgz", - "integrity": "sha512-T1mleRv2A8wyTV/jUuOdkN9Tl2lz0RGauqGc9nxP3AUzS9m3gIDN7u4CahZSdJlkR6tSU/MEWlfs5Q/oZStqxg==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.8.tgz", + "integrity": "sha512-dhXr5CSieBuKNdU96HgeewMQCT9EgOIkfF1GNy+iRrdl7BWLxmlKuvLfK319rmIytSs/vnCzcD9uqyxTeU/A3A==", "requires": { - "@firebase/component": "0.5.12", - "@firebase/database": "0.12.7", - "@firebase/database-types": "0.9.6", + "@firebase/component": "0.5.13", + "@firebase/database": "0.12.8", + "@firebase/database-types": "0.9.7", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.1", + "@firebase/util": "1.5.2", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", + "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", + "requires": { + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/database-types": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.6.tgz", - "integrity": "sha512-E7U28X+FtVtug7EkIkaOXbdP8ghCPno21WWgEiDKsneY28N5WOwccfXqSzHgAAezkR40ht/ZqXlCsUhEpv6JXw==", + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.7.tgz", + "integrity": "sha512-EFhgL89Fz6DY3kkB8TzdHvdu8XaqqvzcF2DLVOXEnQ3Ms7L755p5EO42LfxXoJqb9jKFvgLpFmKicyJG25WFWw==", "requires": { "@firebase/app-types": "0.7.0", - "@firebase/util": "1.5.1" + "@firebase/util": "1.5.2" + }, + "dependencies": { + "@firebase/util": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/logger": { @@ -555,6 +604,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.1.tgz", "integrity": "sha512-ojwPg8sKVcoU/kC1QdTrD+eUDyjQkZyiH9tlouXeZdAeDddCYNvHgIeBQhZt62WIcjlNhy1zro/xdV5nUUU38A==", + "dev": true, "requires": { "tslib": "^2.1.0" } diff --git a/package.json b/package.json index 2f557f9a95..d53cdb03ec 100644 --- a/package.json +++ b/package.json @@ -159,8 +159,8 @@ } }, "dependencies": { - "@firebase/database-compat": "^0.1.1", - "@firebase/database-types": "^0.9.3", + "@firebase/database-compat": "^0.1.8", + "@firebase/database-types": "^0.9.7", "@types/node": ">=12.12.47", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", From b87752b193da99c9af7fc926b9956e053a5fda3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 12:01:59 -0400 Subject: [PATCH 02/27] build(deps-dev): bump @firebase/auth-compat from 0.2.11 to 0.2.12 (#1661) Bumps [@firebase/auth-compat](https://github.com/firebase/firebase-js-sdk/tree/HEAD/packages/auth-compat) from 0.2.11 to 0.2.12. - [Release notes](https://github.com/firebase/firebase-js-sdk/releases) - [Changelog](https://github.com/firebase/firebase-js-sdk/blob/master/packages/auth-compat/CHANGELOG.md) - [Commits](https://github.com/firebase/firebase-js-sdk/commits/@firebase/auth-compat@0.2.12/packages/auth-compat) --- updated-dependencies: - dependency-name: "@firebase/auth-compat" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index f5cbc07f8f..ea8cd2c7c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -446,14 +446,14 @@ "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" }, "@firebase/auth": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.19.11.tgz", - "integrity": "sha512-9bctXmQA5pRhLL03wkbg6ibmhoTMa8QRHm3uDnb+iyMcHTJ5AyILRc5AVPS9FsnpWPDOLiVjtuMC28D6iC+zew==", + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.19.12.tgz", + "integrity": "sha512-39/eJBmq5Ne+HoCJuQXlhaOH2e8qySxYUa5Z25mhcam8nmAMrBh7Ph1yZjUeSfLsSJiSXANMHK5dnVE+1TROXw==", "dev": true, "requires": { - "@firebase/component": "0.5.12", + "@firebase/component": "0.5.13", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.1", + "@firebase/util": "1.5.2", "node-fetch": "2.6.7", "selenium-webdriver": "4.0.0-rc-1", "tslib": "^2.1.0" @@ -474,15 +474,15 @@ } }, "@firebase/auth-compat": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.2.11.tgz", - "integrity": "sha512-6C42yXevri3F7H1LS3h524UsQsUlzGuszlIL3YsDuS+WJFqBe8I5AHOEM+Opi/VtIpWaXxPhWsp75TQndaCjKA==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.2.12.tgz", + "integrity": "sha512-LKeKylktRj03xgW5ilSOW1c4AsMig15ogf5hDKa820t6Bp6MNabj8yq2TV0/Q4SP4Ox/yrTISJGVvk+TJuBecQ==", "dev": true, "requires": { - "@firebase/auth": "0.19.11", + "@firebase/auth": "0.19.12", "@firebase/auth-types": "0.11.0", - "@firebase/component": "0.5.12", - "@firebase/util": "1.5.1", + "@firebase/component": "0.5.13", + "@firebase/util": "1.5.2", "node-fetch": "2.6.7", "selenium-webdriver": "^4.0.0-beta.2", "tslib": "^2.1.0" @@ -500,12 +500,12 @@ "dev": true }, "@firebase/component": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.12.tgz", - "integrity": "sha512-gAKwxo0Ev+rp7Px+Yr71WbcC0CM9Tevhv0g38ORp2p57HqGjY65D3MD+jTKGZl58N/0nmX6MRRKym3bq/3k1gw==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", + "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", "dev": true, "requires": { - "@firebase/util": "1.5.1", + "@firebase/util": "1.5.2", "tslib": "^2.1.0" } }, @@ -601,9 +601,9 @@ } }, "@firebase/util": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.1.tgz", - "integrity": "sha512-ojwPg8sKVcoU/kC1QdTrD+eUDyjQkZyiH9tlouXeZdAeDddCYNvHgIeBQhZt62WIcjlNhy1zro/xdV5nUUU38A==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", "dev": true, "requires": { "tslib": "^2.1.0" @@ -5787,9 +5787,9 @@ } }, "jszip": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.8.0.tgz", - "integrity": "sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.9.1.tgz", + "integrity": "sha512-H9A60xPqJ1CuC4Ka6qxzXZeU8aNmgOeP5IFqwJbQQwtu2EUYxota3LdsiZWplF7Wgd9tkAd0mdu36nceSaPuYw==", "dev": true, "requires": { "lie": "~3.3.0", From 85a7bd8d68ee337278f0adefbd5857a010e74d9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 12:44:18 -0400 Subject: [PATCH 03/27] build(deps): bump @types/node from 17.0.25 to 17.0.27 (#1662) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 17.0.25 to 17.0.27. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea8cd2c7c4..2ed59b963c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1449,9 +1449,9 @@ } }, "@types/node": { - "version": "17.0.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz", - "integrity": "sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==" + "version": "17.0.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.27.tgz", + "integrity": "sha512-4/Ke7bbWOasuT3kceBZFGakP1dYN2XFd8v2l9bqF2LNWrmeU07JLpp56aEeG6+Q3olqO5TvXpW0yaiYnZJ5CXg==" }, "@types/qs": { "version": "6.9.7", From 2d8848e16d2a56771cb96e92dcec4cb6a9790b1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 13:21:48 -0400 Subject: [PATCH 04/27] build(deps-dev): bump @microsoft/api-extractor from 7.21.3 to 7.23.0 (#1663) Bumps [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) from 7.21.3 to 7.23.0. - [Release notes](https://github.com/microsoft/rushstack/releases) - [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md) - [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.23.0/apps/api-extractor) --- updated-dependencies: - dependency-name: "@microsoft/api-extractor" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 60 +++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ed59b963c..7c36b89960 100644 --- a/package-lock.json +++ b/package-lock.json @@ -870,23 +870,23 @@ } }, "@microsoft/api-extractor": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.21.3.tgz", - "integrity": "sha512-ZQwuu5QbJq/TMDGr8NGmt4p/kcceaQAhQYQ4DszwNKDaCi/IhGVtO2zRcjSt8DEI2XD40s/CAOPYyF2C+Y99Ow==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.23.0.tgz", + "integrity": "sha512-fbdX05RVE1EMA7nvyRHuS9nx1pryhjgURDx6pQlE/9yOXQ5PO7MpYdfWGaRsQwyYuU3+tPxgro819c0R3AK6KA==", "dev": true, "requires": { - "@microsoft/api-extractor-model": "7.16.2", + "@microsoft/api-extractor-model": "7.17.2", "@microsoft/tsdoc": "0.14.1", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.45.2", - "@rushstack/rig-package": "0.3.9", - "@rushstack/ts-command-line": "4.10.8", + "@rushstack/node-core-library": "3.45.4", + "@rushstack/rig-package": "0.3.11", + "@rushstack/ts-command-line": "4.10.10", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.17.0", "semver": "~7.3.0", "source-map": "~0.6.1", - "typescript": "~4.5.2" + "typescript": "~4.6.3" }, "dependencies": { "@microsoft/tsdoc": { @@ -896,9 +896,9 @@ "dev": true }, "@rushstack/node-core-library": { - "version": "3.45.2", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.2.tgz", - "integrity": "sha512-MJKdB6mxOoIkks3htGVCo7aiTzllm2I6Xua+KbTSb0cp7rBp8gTCOF/4d8R4HFMwpRdEdwzKgqMM6k9rAK73iw==", + "version": "3.45.4", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.4.tgz", + "integrity": "sha512-FMoEQWjK7nWAO2uFgV1eVpVhY9ZDGOdIIomi9zTej64cKJ+8/Nvu+ny0xKaUDEjw/ALftN2D2ml7L0RDpW/Z9g==", "dev": true, "requires": { "@types/node": "12.20.24", @@ -913,9 +913,9 @@ } }, "@rushstack/ts-command-line": { - "version": "4.10.8", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.8.tgz", - "integrity": "sha512-G7CQYY/m3aZU5fVxbebv35yDeua7sSumrDAB2pJp0d60ZEsxGkUQW8771CeMcGWwSKqT9PxPzKpmIakiWv54sA==", + "version": "4.10.10", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.10.tgz", + "integrity": "sha512-F+MH7InPDXqX40qvvcEsnvPpmg566SBpfFqj2fcCh8RjM6AyOoWlXc8zx7giBD3ZN85NVAEjZAgrcLU0z+R2yg==", "dev": true, "requires": { "@types/argparse": "1.0.38", @@ -940,9 +940,9 @@ } }, "typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", "dev": true }, "validator": { @@ -966,14 +966,14 @@ } }, "@microsoft/api-extractor-model": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.16.2.tgz", - "integrity": "sha512-hnqKsG89iIiQlLDXasxFw8QR0LwakPVXejNVmlGpFL008R+IExgc1f+tBrUAm1in6Oq76t7Ea0TFhER56Qfhaw==", + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.17.2.tgz", + "integrity": "sha512-fYfCeBeLm7jnZligC64qHiH4/vzswFLDfyPpX+uKO36OI2kIeMHrYG0zaezmuinKvE4vg1dAz38zZeDbPvBKGg==", "dev": true, "requires": { "@microsoft/tsdoc": "0.14.1", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.45.2" + "@rushstack/node-core-library": "3.45.4" }, "dependencies": { "@microsoft/tsdoc": { @@ -983,9 +983,9 @@ "dev": true }, "@rushstack/node-core-library": { - "version": "3.45.2", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.2.tgz", - "integrity": "sha512-MJKdB6mxOoIkks3htGVCo7aiTzllm2I6Xua+KbTSb0cp7rBp8gTCOF/4d8R4HFMwpRdEdwzKgqMM6k9rAK73iw==", + "version": "3.45.4", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.4.tgz", + "integrity": "sha512-FMoEQWjK7nWAO2uFgV1eVpVhY9ZDGOdIIomi9zTej64cKJ+8/Nvu+ny0xKaUDEjw/ALftN2D2ml7L0RDpW/Z9g==", "dev": true, "requires": { "@types/node": "12.20.24", @@ -1200,9 +1200,9 @@ } }, "@rushstack/rig-package": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.9.tgz", - "integrity": "sha512-z3Oxpfb4n9mGXwseX+ifpkmUf9B8Fy8oieVwg8eFgpCbzllkgOwEiwLKEnRWVQ8owFcd46NCKz+7ICH35CRsAw==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.11.tgz", + "integrity": "sha512-uI1/g5oQPtyrT9nStoyX/xgZSLa2b+srRFaDk3r1eqC7zA5th4/bvTGl2QfV3C9NcP+coSqmk5mFJkUfH6i3Lw==", "dev": true, "requires": { "resolve": "~1.17.0", @@ -5250,9 +5250,9 @@ "dev": true }, "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", "dev": true, "requires": { "has": "^1.0.3" From c8733a553ca8602d81ba2e19fc2961b4128b890a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 13:36:29 -0400 Subject: [PATCH 05/27] build(deps-dev): bump eslint from 8.13.0 to 8.14.0 (#1664) Bumps [eslint](https://github.com/eslint/eslint) from 8.13.0 to 8.14.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.13.0...v8.14.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c36b89960..25a9822147 100644 --- a/package-lock.json +++ b/package-lock.json @@ -324,9 +324,9 @@ } }, "@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", + "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -3438,12 +3438,12 @@ "dev": true }, "eslint": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.13.0.tgz", - "integrity": "sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", + "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.1", + "@eslint/eslintrc": "^1.2.2", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", From 2b6a033c9e851a2b521fe9f75b89259e40e6dbb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 14:20:28 -0400 Subject: [PATCH 06/27] build(deps): bump @google-cloud/storage from 5.19.2 to 5.19.3 (#1665) Bumps [@google-cloud/storage](https://github.com/googleapis/nodejs-storage) from 5.19.2 to 5.19.3. - [Release notes](https://github.com/googleapis/nodejs-storage/releases) - [Changelog](https://github.com/googleapis/nodejs-storage/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/nodejs-storage/compare/v5.19.2...v5.19.3) --- updated-dependencies: - dependency-name: "@google-cloud/storage" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25a9822147..25f02fe410 100644 --- a/package-lock.json +++ b/package-lock.json @@ -644,9 +644,9 @@ "optional": true }, "@google-cloud/storage": { - "version": "5.19.2", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.19.2.tgz", - "integrity": "sha512-0saiMeQkDALf9pvDD1rDZGV5aktUbnIWjm9jYiFKlxiMROJhODzaaltvyKg5oY6ujHv7Fzc5pzQVFo3R8vENpw==", + "version": "5.19.3", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.19.3.tgz", + "integrity": "sha512-l+8X0BoA7rg9jyZaS4p2DwMg1Ivju+VAL6PeQZE1u2q52LQ0KemrZmdQWhtrplHYo8UdYtqpbj4A6Fc5fKDZdg==", "optional": true, "requires": { "@google-cloud/paginator": "^3.0.7", From ddec0f57089c2408b0e5f61c42a1a0b2ec5d2761 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 14:30:11 -0400 Subject: [PATCH 07/27] build(deps-dev): bump @typescript-eslint/parser from 5.19.0 to 5.21.0 (#1667) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.19.0 to 5.21.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.21.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25f02fe410..0e98d97c09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1580,25 +1580,25 @@ } }, "@typescript-eslint/parser": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.19.0.tgz", - "integrity": "sha512-yhktJjMCJX8BSBczh1F/uY8wGRYrBeyn84kH6oyqdIJwTGKmzX5Qiq49LRQ0Jh0LXnWijEziSo6BRqny8nqLVQ==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.21.0.tgz", + "integrity": "sha512-8RUwTO77hstXUr3pZoWZbRQUxXcSXafZ8/5gpnQCfXvgmP9gpNlRGlWzvfbEQ14TLjmtU8eGnONkff8U2ui2Eg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.19.0", - "@typescript-eslint/types": "5.19.0", - "@typescript-eslint/typescript-estree": "5.19.0", + "@typescript-eslint/scope-manager": "5.21.0", + "@typescript-eslint/types": "5.21.0", + "@typescript-eslint/typescript-estree": "5.21.0", "debug": "^4.3.2" } }, "@typescript-eslint/scope-manager": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.19.0.tgz", - "integrity": "sha512-Fz+VrjLmwq5fbQn5W7cIJZ066HxLMKvDEmf4eu1tZ8O956aoX45jAuBB76miAECMTODyUxH61AQM7q4/GOMQ5g==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.21.0.tgz", + "integrity": "sha512-XTX0g0IhvzcH/e3393SvjRCfYQxgxtYzL3UREteUneo72EFlt7UNoiYnikUtmGVobTbhUDByhJ4xRBNe+34kOQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.19.0", - "@typescript-eslint/visitor-keys": "5.19.0" + "@typescript-eslint/types": "5.21.0", + "@typescript-eslint/visitor-keys": "5.21.0" } }, "@typescript-eslint/type-utils": { @@ -1613,19 +1613,19 @@ } }, "@typescript-eslint/types": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.19.0.tgz", - "integrity": "sha512-zR1ithF4Iyq1wLwkDcT+qFnhs8L5VUtjgac212ftiOP/ZZUOCuuF2DeGiZZGQXGoHA50OreZqLH5NjDcDqn34w==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.21.0.tgz", + "integrity": "sha512-XnOOo5Wc2cBlq8Lh5WNvAgHzpjnEzxn4CJBwGkcau7b/tZ556qrWXQz4DJyChYg8JZAD06kczrdgFPpEQZfDsA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.19.0.tgz", - "integrity": "sha512-dRPuD4ocXdaE1BM/dNR21elSEUPKaWgowCA0bqJ6YbYkvtrPVEvZ+zqcX5a8ECYn3q5iBSSUcBBD42ubaOp0Hw==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.21.0.tgz", + "integrity": "sha512-Y8Y2T2FNvm08qlcoSMoNchh9y2Uj3QmjtwNMdRQkcFG7Muz//wfJBGBxh8R7HAGQFpgYpdHqUpEoPQk+q9Kjfg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.19.0", - "@typescript-eslint/visitor-keys": "5.19.0", + "@typescript-eslint/types": "5.21.0", + "@typescript-eslint/visitor-keys": "5.21.0", "debug": "^4.3.2", "globby": "^11.0.4", "is-glob": "^4.0.3", @@ -1711,12 +1711,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.19.0.tgz", - "integrity": "sha512-Ym7zZoMDZcAKWsULi2s7UMLREdVQdScPQ/fKWMYefarCztWlHPFVJo8racf8R0Gc8FAEJ2eD4of8As1oFtnQlQ==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.21.0.tgz", + "integrity": "sha512-SX8jNN+iHqAF0riZQMkm7e8+POXa/fXw5cxL+gjpyP+FI+JVNhii53EmQgDAfDcBpFekYSlO0fGytMQwRiMQCA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.19.0", + "@typescript-eslint/types": "5.21.0", "eslint-visitor-keys": "^3.0.0" } }, From c641379359796246396a3206ec63331e1ff0bbc4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 14:42:28 -0400 Subject: [PATCH 08/27] build(deps-dev): bump @types/mocha from 9.1.0 to 9.1.1 (#1668) Bumps [@types/mocha](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mocha) from 9.1.0 to 9.1.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mocha) --- updated-dependencies: - dependency-name: "@types/mocha" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e98d97c09..80c6327377 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1434,9 +1434,9 @@ "dev": true }, "@types/mocha": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz", - "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, "@types/nock": { From 7adf65e01b4d28212cc947178d8e70485b12e744 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 15:09:01 -0400 Subject: [PATCH 09/27] build(deps-dev): bump @types/lodash from 4.14.181 to 4.14.182 (#1672) Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.181 to 4.14.182. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) --- updated-dependencies: - dependency-name: "@types/lodash" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 80c6327377..0c1cca58ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1405,9 +1405,9 @@ } }, "@types/lodash": { - "version": "4.14.181", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", - "integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==", + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", "dev": true }, "@types/long": { From 48430f76e12e2d88d78a0ef402f24dde62a820ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 15:21:55 -0400 Subject: [PATCH 10/27] build(deps-dev): bump @typescript-eslint/eslint-plugin (#1669) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.20.0 to 5.21.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.21.0/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 110 +++++++--------------------------------------- 1 file changed, 16 insertions(+), 94 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0c1cca58ae..984ee8901a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1526,14 +1526,14 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.20.0.tgz", - "integrity": "sha512-fapGzoxilCn3sBtC6NtXZX6+P/Hef7VDbyfGqTTpzYydwhlkevB+0vE0EnmHPVTVSy68GUncyJ/2PcrFBeCo5Q==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.21.0.tgz", + "integrity": "sha512-fTU85q8v5ZLpoZEyn/u1S2qrFOhi33Edo2CZ0+q1gDaWWm0JuPh3bgOyU8lM0edIEYgKLDkPFiZX2MOupgjlyg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.20.0", - "@typescript-eslint/type-utils": "5.20.0", - "@typescript-eslint/utils": "5.20.0", + "@typescript-eslint/scope-manager": "5.21.0", + "@typescript-eslint/type-utils": "5.21.0", + "@typescript-eslint/utils": "5.21.0", "debug": "^4.3.2", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", @@ -1542,32 +1542,6 @@ "tsutils": "^3.21.0" }, "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz", - "integrity": "sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.20.0", - "@typescript-eslint/visitor-keys": "5.20.0" - } - }, - "@typescript-eslint/types": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.20.0.tgz", - "integrity": "sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg==", - "dev": true - }, - "@typescript-eslint/visitor-keys": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz", - "integrity": "sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.20.0", - "eslint-visitor-keys": "^3.0.0" - } - }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -1602,12 +1576,12 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.20.0.tgz", - "integrity": "sha512-WxNrCwYB3N/m8ceyoGCgbLmuZwupvzN0rE8NBuwnl7APgjv24ZJIjkNzoFBXPRCGzLNkoU/WfanW0exvp/+3Iw==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.21.0.tgz", + "integrity": "sha512-MxmLZj0tkGlkcZCSE17ORaHl8Th3JQwBzyXL/uvC6sNmu128LsgjTX0NIzy+wdH2J7Pd02GN8FaoudJntFvSOw==", "dev": true, "requires": { - "@typescript-eslint/utils": "5.20.0", + "@typescript-eslint/utils": "5.21.0", "debug": "^4.3.2", "tsutils": "^3.21.0" } @@ -1645,69 +1619,17 @@ } }, "@typescript-eslint/utils": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.20.0.tgz", - "integrity": "sha512-lHONGJL1LIO12Ujyx8L8xKbwWSkoUKFSO+0wDAqGXiudWB2EO7WEUT+YZLtVbmOmSllAjLb9tpoIPwpRe5Tn6w==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.21.0.tgz", + "integrity": "sha512-q/emogbND9wry7zxy7VYri+7ydawo2HDZhRZ5k6yggIvXa7PvBbAAZ4PFH/oZLem72ezC4Pr63rJvDK/sTlL8Q==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.20.0", - "@typescript-eslint/types": "5.20.0", - "@typescript-eslint/typescript-estree": "5.20.0", + "@typescript-eslint/scope-manager": "5.21.0", + "@typescript-eslint/types": "5.21.0", + "@typescript-eslint/typescript-estree": "5.21.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz", - "integrity": "sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.20.0", - "@typescript-eslint/visitor-keys": "5.20.0" - } - }, - "@typescript-eslint/types": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.20.0.tgz", - "integrity": "sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz", - "integrity": "sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.20.0", - "@typescript-eslint/visitor-keys": "5.20.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz", - "integrity": "sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.20.0", - "eslint-visitor-keys": "^3.0.0" - } - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@typescript-eslint/visitor-keys": { From c142e22b1a2d2949c2d25b0e7e4abbf252950acc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:06:57 -0400 Subject: [PATCH 11/27] build(deps-dev): bump @types/chai from 4.3.0 to 4.3.1 (#1670) Bumps [@types/chai](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chai) from 4.3.0 to 4.3.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chai) --- updated-dependencies: - dependency-name: "@types/chai" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 984ee8901a..711f08d1ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1323,9 +1323,9 @@ "dev": true }, "@types/chai": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", - "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", + "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==", "dev": true }, "@types/chai-as-promised": { From 75407f4035f188576edf8a63412f0e6fff43bf09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:14:16 -0400 Subject: [PATCH 12/27] build(deps): bump jwks-rsa from 2.0.5 to 2.1.0 (#1671) Bumps [jwks-rsa](https://github.com/auth0/node-jwks-rsa) from 2.0.5 to 2.1.0. - [Release notes](https://github.com/auth0/node-jwks-rsa/releases) - [Changelog](https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md) - [Commits](https://github.com/auth0/node-jwks-rsa/compare/v2.0.5...v2.1.0) --- updated-dependencies: - dependency-name: jwks-rsa dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 711f08d1ab..2ea034749e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5776,12 +5776,12 @@ } }, "jwks-rsa": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.5.tgz", - "integrity": "sha512-fliHfsiBRzEU0nXzSvwnh0hynzGB0WihF+CinKbSRlaqRxbqqKf2xbBPgwc8mzf18/WgwlG8e5eTpfSTBcU4DQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.0.tgz", + "integrity": "sha512-GKOSDBWWBCiQTzawei6mEdRQvji5gecj8F9JwMt0ZOPnBPSmTjo5CKFvvbhE7jGPkU159Cpi0+OTLuABFcNOQQ==", "requires": { "@types/express-jwt": "0.0.42", - "debug": "^4.3.2", + "debug": "^4.3.4", "jose": "^2.0.5", "limiter": "^1.1.5", "lru-memoizer": "^2.1.4" From b2a28aee5cac172ed183bc47f71a55b31f79696a Mon Sep 17 00:00:00 2001 From: Liubin Jiang <56564857+Xiaoshouzi-gh@users.noreply.github.com> Date: Thu, 28 Apr 2022 09:17:41 -0700 Subject: [PATCH 13/27] feat(auth): Support generate oob code request type VERIFY_AND_CHANGE_EMAIL (#1633) * Supported generate OOB code from VERIFY_AND_CHANGE_EMAIL request type. * Added integration tests. --- etc/firebase-admin.auth.api.md | 1 + src/auth/auth-api-request.ts | 26 +++++++-- src/auth/base-auth.ts | 29 ++++++++++ src/utils/error.ts | 6 +++ test/integration/auth.spec.ts | 26 +++++++++ test/unit/auth/auth-api-request.spec.ts | 56 ++++++++++++++++++- test/unit/auth/auth.spec.ts | 71 +++++++++++++++++++++---- 7 files changed, 201 insertions(+), 14 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 16fd6a64e1..2986869ef2 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -55,6 +55,7 @@ export abstract class BaseAuth { generateEmailVerificationLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise; generatePasswordResetLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise; generateSignInWithEmailLink(email: string, actionCodeSettings: ActionCodeSettings): Promise; + generateVerifyAndChangeEmailLink(email: string, newEmail: string, actionCodeSettings?: ActionCodeSettings): Promise; getProviderConfig(providerId: string): Promise; getUser(uid: string): Promise; getUserByEmail(email: string): Promise; diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 13018337da..a962a4f719 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -59,7 +59,7 @@ export const RESERVED_CLAIMS = [ /** List of supported email action request types. */ export const EMAIL_ACTION_REQUEST_TYPES = [ - 'PASSWORD_RESET', 'VERIFY_EMAIL', 'EMAIL_SIGNIN', + 'PASSWORD_RESET', 'VERIFY_EMAIL', 'EMAIL_SIGNIN', 'VERIFY_AND_CHANGE_EMAIL', ]; /** Maximum allowed number of characters in the custom claims payload. */ @@ -817,6 +817,11 @@ const FIREBASE_AUTH_GET_OOB_CODE = new ApiSettings('/accounts:sendOobCode', 'POS AuthClientErrorCode.INVALID_EMAIL, ); } + if (typeof request.newEmail !== 'undefined' && !validator.isEmail(request.newEmail)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_NEW_EMAIL, + ); + } if (EMAIL_ACTION_REQUEST_TYPES.indexOf(request.requestType) === -1) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -1599,12 +1604,19 @@ export abstract class AbstractAuthRequestHandler { * @param 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. Required when requestType == 'EMAIL_SIGNIN' + * @param newEmail - The email address the account is being updated to. + * Required only for VERIFY_AND_CHANGE_EMAIL requests. * @returns A promise that resolves with the email action link. */ public getEmailActionLink( requestType: string, email: string, - actionCodeSettings?: ActionCodeSettings): Promise { - let request = { requestType, email, returnOobLink: true }; + actionCodeSettings?: ActionCodeSettings, newEmail?: string): Promise { + let request = { + requestType, + email, + returnOobLink: true, + ...(typeof newEmail !== 'undefined') && { newEmail }, + }; // 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') { @@ -1623,6 +1635,14 @@ export abstract class AbstractAuthRequestHandler { return Promise.reject(e); } } + if (requestType === 'VERIFY_AND_CHANGE_EMAIL' && typeof newEmail === 'undefined') { + return Promise.reject( + new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "`newEmail` is required when `requestType` === 'VERIFY_AND_CHANGE_EMAIL'", + ), + ); + } return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_OOB_CODE, request) .then((response: any) => { // Return the link. diff --git a/src/auth/base-auth.ts b/src/auth/base-auth.ts index 7119dd2514..6f77e088f8 100644 --- a/src/auth/base-auth.ts +++ b/src/auth/base-auth.ts @@ -834,6 +834,35 @@ export abstract class BaseAuth { return this.authRequestHandler.getEmailActionLink('VERIFY_EMAIL', email, actionCodeSettings); } + /** + * Generates an out-of-band email action link to verify the user's ownership + * of the specified email. The {@link ActionCodeSettings} object provided + * as an argument to this method defines whether the link is to be handled by a + * mobile app or browser along with additional state information to be passed in + * the deep link, etc. + * + * @param email - The current email account. + * @param newEmail - The email address the account is being updated to. + * @param actionCodeSettings - The action + * code settings. If specified, the state/continue URL is set as the + * "continueUrl" parameter in the email verification link. The default email + * verification landing page will use this to display a link to go back to + * the app if it is installed. + * If the actionCodeSettings is not specified, no URL is appended to the + * action URL. + * The state URL provided must belong to a domain that is authorized + * in the console, or an error will be thrown. + * Mobile app redirects are only applicable if the developer configures + * and accepts the Firebase Dynamic Links terms of service. + * The Android package name and iOS bundle ID are respected only if they + * are configured in the same Firebase Auth project. + * @returns A promise that resolves with the generated link. + */ + public generateVerifyAndChangeEmailLink(email: string, newEmail: string, + actionCodeSettings?: ActionCodeSettings): Promise { + return this.authRequestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email, actionCodeSettings, newEmail); + } + /** * Generates the out of band email action link to verify the user's ownership * of the specified email. The {@link ActionCodeSettings} object provided diff --git a/src/utils/error.ts b/src/utils/error.ts index 7989e7ecad..6c74748ed1 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -452,6 +452,10 @@ export class AuthClientErrorCode { code: 'invalid-email', message: 'The email address is improperly formatted.', }; + public static INVALID_NEW_EMAIL = { + code: 'invalid-new-email', + message: 'The new email address is improperly formatted.', + }; public static INVALID_ENROLLED_FACTORS = { code: 'invalid-enrolled-factors', message: 'The enrolled factors must be a valid array of MultiFactorInfo objects.', @@ -908,6 +912,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { INVALID_DURATION: 'INVALID_SESSION_COOKIE_DURATION', // Invalid email provided. INVALID_EMAIL: 'INVALID_EMAIL', + // Invalid new email provided. + INVALID_NEW_EMAIL: 'INVALID_NEW_EMAIL', // Invalid tenant display name. This can be thrown on CreateTenant and UpdateTenant. INVALID_DISPLAY_NAME: 'INVALID_DISPLAY_NAME', // Invalid ID token provided. diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 2308ca6879..e1005d9c4a 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -1073,6 +1073,7 @@ describe('admin.auth', () => { describe('Link operations', () => { const uid = generateRandomString(20).toLowerCase(); const email = uid + '@example.com'; + const newEmail = uid + 'new@example.com'; const newPassword = 'newPassword'; const userData = { uid, @@ -1152,6 +1153,31 @@ describe('admin.auth', () => { expect(result.user!.emailVerified).to.be.true; }); }); + + it('generateVerifyAndChangeEmailLink() should return a verification link', function() { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + // Ensure the user's email is verified. + return getAuth().updateUser(uid, { password: 'password', emailVerified: true }) + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.true; + return getAuth().generateVerifyAndChangeEmailLink(email, newEmail, actionCodeSettings); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrl(link)).equal(actionCodeSettings.url); + return clientAuth().applyActionCode(code); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(newEmail, 'password'); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(newEmail); + expect(result.user!.emailVerified).to.be.true; + }); + }); }); describe('Tenant management operations', () => { diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index f948b27b17..574962df53 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -3065,6 +3065,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const path = handler.path('v1', '/accounts:sendOobCode', 'project_id'); const method = 'POST'; const email = 'user@example.com'; + const newEmail = 'usernew@example.com'; const actionCodeSettings = { url: 'https://www.example.com/path/file?a=1&b=2', handleCodeInApp: true, @@ -3110,12 +3111,14 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { requestType, email, returnOobLink: true, + ...(requestType === 'VERIFY_AND_CHANGE_EMAIL') && { newEmail }, }, expectedActionCodeSettingsRequest); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); const requestHandler = handler.init(mockApp); - return requestHandler.getEmailActionLink(requestType, email, actionCodeSettings) + return requestHandler.getEmailActionLink(requestType, email, actionCodeSettings, + (requestType === 'VERIFY_AND_CHANGE_EMAIL') ? newEmail: undefined) .then((oobLink: string) => { expect(oobLink).to.be.equal(expectedLink); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); @@ -3124,7 +3127,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); EMAIL_ACTION_REQUEST_TYPES.forEach((requestType) => { - if (requestType === 'EMAIL_SIGNIN') { + if (requestType === 'EMAIL_SIGNIN' || requestType === 'VERIFY_AND_CHANGE_EMAIL') { return; } it('should be fulfilled given requestType:' + requestType + ' and no ActionCodeSettings', () => { @@ -3145,6 +3148,25 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + it('should be fulfilled given a valid requestType: VERIFY_AND_CHANGE_EMAIL and no ActionCodeSettings', () => { + const VERIFY_AND_CHANGE_EMAIL = 'VERIFY_AND_CHANGE_EMAIL'; + const requestData = { + requestType: VERIFY_AND_CHANGE_EMAIL, + email, + returnOobLink: true, + newEmail, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink(VERIFY_AND_CHANGE_EMAIL, email, undefined, newEmail) + .then((oobLink: string) => { + expect(oobLink).to.be.equal(expectedLink); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + it('should be rejected given requestType:EMAIL_SIGNIN and no ActionCodeSettings', () => { const invalidRequestType = 'EMAIL_SIGNIN'; const requestHandler = handler.init(mockApp); @@ -3153,6 +3175,22 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); + it('should be rejected given requestType: VERIFY_AND_CHANGE and no new Email address', () => { + const requestHandler = handler.init(mockApp); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '`newEmail` is required when `requestType` === \'VERIFY_AND_CHANGE_EMAIL\'', + ) + + return requestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid argument error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + it('should be rejected given an invalid email', () => { const invalidEmail = 'invalid'; const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); @@ -3167,6 +3205,20 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + it('should be rejected given an invalid new email', () => { + const invalidNewEmail = 'invalid'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_NEW_EMAIL); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email, actionCodeSettings, invalidNewEmail) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid new email error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + it('should be rejected given an invalid request type', () => { const invalidRequestType = 'invalid'; const expectedError = new FirebaseAuthError( diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 1e596c2721..0995a310d4 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -2869,10 +2869,12 @@ AUTH_CONFIGS.forEach((testConfig) => { { api: 'generatePasswordResetLink', requestType: 'PASSWORD_RESET', requiresSettings: false }, { api: 'generateEmailVerificationLink', requestType: 'VERIFY_EMAIL', requiresSettings: false }, { api: 'generateSignInWithEmailLink', requestType: 'EMAIL_SIGNIN', requiresSettings: true }, + { api: 'generateVerifyAndChangeEmailLink', requestType: 'VERIFY_AND_CHANGE_EMAIL', requiresSettings: false }, ]; emailActionFlows.forEach((emailActionFlow) => { describe(`${emailActionFlow.api}()`, () => { const email = 'user@example.com'; + const newEmail = 'usernew@example.com'; const actionCodeSettings = { url: 'https://www.example.com/path/file?a=1&b=2', handleCodeInApp: true, @@ -2898,32 +2900,71 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be rejected given no email', () => { - return (auth as any)[emailActionFlow.api](undefined, actionCodeSettings) + let args: any = [ undefined, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ undefined, newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-email'); }); it('should be rejected given an invalid email', () => { - return (auth as any)[emailActionFlow.api]('invalid', actionCodeSettings) + let args: any = [ 'invalid', actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ 'invalid', newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-email'); }); + it('should be rejected given no new email when request type is `generateVerifyAndChangeEmailLink`', () => { + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + return (auth as any)[emailActionFlow.api](email) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + } + }); + + it('should be rejected given an invalid new email when request type is `generateVerifyAndChangeEmailLink`', + () => { + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + return (auth as any)[emailActionFlow.api](email, 'invalid') + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-new-email'); + } + }); + it('should be rejected given an invalid ActionCodeSettings object', () => { - return (auth as any)[emailActionFlow.api](email, 'invalid') + let args: any = [ email, 'invalid' ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, 'invalid' ]; + } + return (auth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); it('should be rejected given an app which returns null access tokens', () => { - return (nullAccessTokenAuth as any)[emailActionFlow.api](email, actionCodeSettings) + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (nullAccessTokenAuth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); it('should be rejected given an app which returns invalid access tokens', () => { - return (malformedAccessTokenAuth as any)[emailActionFlow.api](email, actionCodeSettings) + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (malformedAccessTokenAuth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); it('should be rejected given an app which fails to generate access tokens', () => { - return (rejectedPromiseAccessTokenAuth as any)[emailActionFlow.api](email, actionCodeSettings) + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (rejectedPromiseAccessTokenAuth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); @@ -2932,7 +2973,11 @@ AUTH_CONFIGS.forEach((testConfig) => { const getEmailActionLinkStub = sinon.stub(testConfig.RequestHandler.prototype, 'getEmailActionLink') .resolves(expectedLink); stubs.push(getEmailActionLinkStub); - return (auth as any)[emailActionFlow.api](email, actionCodeSettings) + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) .then((actualLink: string) => { // Confirm underlying API called with expected parameters. expect(getEmailActionLinkStub).to.have.been.calledOnce.and.calledWith( @@ -2953,7 +2998,11 @@ AUTH_CONFIGS.forEach((testConfig) => { const getEmailActionLinkStub = sinon.stub(testConfig.RequestHandler.prototype, 'getEmailActionLink') .resolves(expectedLink); stubs.push(getEmailActionLinkStub); - return (auth as any)[emailActionFlow.api](email) + let args: any = [ email ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail ]; + } + return (auth as any)[emailActionFlow.api](...args) .then((actualLink: string) => { // Confirm underlying API called with expected parameters. expect(getEmailActionLinkStub).to.have.been.calledOnce.and.calledWith( @@ -2969,7 +3018,11 @@ AUTH_CONFIGS.forEach((testConfig) => { const getEmailActionLinkStub = sinon.stub(testConfig.RequestHandler.prototype, 'getEmailActionLink') .rejects(expectedError); stubs.push(getEmailActionLinkStub); - return (auth as any)[emailActionFlow.api](email, actionCodeSettings) + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) .then(() => { throw new Error('Unexpected success'); }, (error: any) => { From 35df364f53d10badffbb4bd42dc7afc56ab9d3e9 Mon Sep 17 00:00:00 2001 From: Liubin Jiang <56564857+Xiaoshouzi-gh@users.noreply.github.com> Date: Thu, 28 Apr 2022 13:34:47 -0700 Subject: [PATCH 14/27] feat(auth): Support sms region config change on Tenant and Project level. (#1673) * Supported SMS regions config update on a project and a tenant level. * Added integration tests. --- etc/firebase-admin.auth.api.md | 47 +++++ src/auth/auth-api-request.ts | 72 ++++++- src/auth/auth-config.ts | 143 +++++++++++++ src/auth/auth.ts | 12 ++ src/auth/index.ts | 14 ++ src/auth/project-config-manager.ts | 67 ++++++ src/auth/project-config.ts | 131 ++++++++++++ src/auth/tenant.ts | 31 ++- test/integration/auth.spec.ts | 85 +++++++- test/unit/auth/project-config-manager.spec.ts | 196 ++++++++++++++++++ test/unit/auth/project-config.spec.ts | 192 +++++++++++++++++ test/unit/auth/tenant.spec.ts | 137 ++++++++++++ 12 files changed, 1115 insertions(+), 12 deletions(-) create mode 100644 src/auth/project-config-manager.ts create mode 100644 src/auth/project-config.ts create mode 100644 test/unit/auth/project-config-manager.spec.ts create mode 100644 test/unit/auth/project-config.spec.ts diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 2986869ef2..c7090af304 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -23,10 +23,35 @@ export interface ActionCodeSettings { url: string; } +// @public +export interface AllowByDefault { + disallowedRegions: string[]; +} + +// @public +export interface AllowByDefaultWrap { + allowByDefault: AllowByDefault; + // @alpha (undocumented) + allowlistOnly?: never; +} + +// @public +export interface AllowlistOnly { + allowedRegions: string[]; +} + +// @public +export interface AllowlistOnlyWrap { + // @alpha (undocumented) + allowByDefault?: never; + allowlistOnly: AllowlistOnly; +} + // @public export class Auth extends BaseAuth { // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts get app(): App; + projectConfigManager(): ProjectConfigManager; tenantManager(): TenantManager; } @@ -309,6 +334,18 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { toJSON(): object; } +// @public +export class ProjectConfig { + readonly smsRegionConfig?: SmsRegionConfig; + toJSON(): object; +} + +// @public +export class ProjectConfigManager { + getProjectConfig(): Promise; + updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise; +} + // @public export interface ProviderIdentifier { // (undocumented) @@ -342,6 +379,9 @@ export interface SessionCookieOptions { expiresIn: number; } +// @public +export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap; + // @public export class Tenant { // (undocumented) @@ -349,6 +389,7 @@ export class Tenant { readonly displayName?: string; get emailSignInConfig(): EmailSignInProviderConfig | undefined; get multiFactorConfig(): MultiFactorConfig | undefined; + readonly smsRegionConfig?: SmsRegionConfig; readonly tenantId: string; readonly testPhoneNumbers?: { [phoneNumber: string]: string; @@ -391,6 +432,11 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor phoneNumber: string; } +// @public +export interface UpdateProjectConfigRequest { + smsRegionConfig?: SmsRegionConfig; +} + // @public export interface UpdateRequest { disabled?: boolean; @@ -411,6 +457,7 @@ export interface UpdateTenantRequest { displayName?: string; emailSignInConfig?: EmailSignInProviderConfig; multiFactorConfig?: MultiFactorConfig; + smsRegionConfig?: SmsRegionConfig; testPhoneNumbers?: { [phoneNumber: string]: string; } | null; diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index a962a4f719..2893d49a9d 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -42,6 +42,7 @@ import { OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest, SAMLUpdateAuthProviderRequest } from './auth-config'; +import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { @@ -102,7 +103,6 @@ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace( const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace( 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); - /** Maximum allowed number of tenants to download at one time. */ const MAX_LIST_TENANT_PAGE_SIZE = 1000; @@ -1981,6 +1981,29 @@ export abstract class AbstractAuthRequestHandler { } } +/** Instantiates the getConfig endpoint settings. */ +const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + +/** Instantiates the updateConfig endpoint settings. */ +const UPDATE_PROJECT_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update project config', + ); + } + }); /** Instantiates the getTenant endpoint settings. */ const GET_TENANT = new ApiSettings('/tenants/{tenantId}', 'GET') @@ -2049,13 +2072,13 @@ const CREATE_TENANT = new ApiSettings('/tenants', 'POST') /** - * Utility for sending requests to Auth server that are Auth instance related. This includes user and - * tenant management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines + * Utility for sending requests to Auth server that are Auth instance related. This includes user, tenant, + * and project config management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines * additional tenant management related APIs. */ export class AuthRequestHandler extends AbstractAuthRequestHandler { - protected readonly tenantMgmtResourceBuilder: AuthResourceUrlBuilder; + protected readonly authResourceUrlBuilder: AuthResourceUrlBuilder; /** * The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp. @@ -2065,7 +2088,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ constructor(app: App) { super(app); - this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(app, 'v2'); + this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2'); } /** @@ -2082,6 +2105,35 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { return new AuthResourceUrlBuilder(this.app, 'v2'); } + /** + * Get the current project's config + * @returns A promise that resolves with the project config information. + */ + public getProjectConfig(): Promise { + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_PROJECT_CONFIG, {}, {}) + .then((response: any) => { + return response as ProjectConfigServerResponse; + }); + } + + /** + * Update the current project's config. + * @returns A promise that resolves with the project config information. + */ + public updateProjectConfig(options: UpdateProjectConfigRequest): Promise { + try { + const request = ProjectConfig.buildServerRequest(options); + const updateMask = utils.generateUpdateMask(request); + return this.invokeRequestHandler( + this.authResourceUrlBuilder, UPDATE_PROJECT_CONFIG, request, { updateMask: updateMask.join(',') }) + .then((response: any) => { + return response as ProjectConfigServerResponse; + }); + } catch (e) { + return Promise.reject(e); + } + } + /** * Looks up a tenant by tenant ID. * @@ -2092,7 +2144,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, GET_TENANT, {}, { tenantId }) + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT, {}, { tenantId }) .then((response: any) => { return response as TenantServerResponse; }); @@ -2122,7 +2174,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (typeof request.pageToken === 'undefined') { delete request.pageToken; } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, LIST_TENANTS, request) + return this.invokeRequestHandler(this.authResourceUrlBuilder, LIST_TENANTS, request) .then((response: any) => { if (!response.tenants) { response.tenants = []; @@ -2142,7 +2194,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, DELETE_TENANT, undefined, { tenantId }) + return this.invokeRequestHandler(this.authResourceUrlBuilder, DELETE_TENANT, undefined, { tenantId }) .then(() => { // Return nothing. }); @@ -2158,7 +2210,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { try { // Construct backend request. const request = Tenant.buildServerRequest(tenantOptions, true); - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, CREATE_TENANT, request) + return this.invokeRequestHandler(this.authResourceUrlBuilder, CREATE_TENANT, request) .then((response: any) => { return response as TenantServerResponse; }); @@ -2184,7 +2236,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { // Do not traverse deep into testPhoneNumbers. The entire content should be replaced // and not just specific phone numbers. const updateMask = utils.generateUpdateMask(request, ['testPhoneNumbers']); - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, UPDATE_TENANT, request, + return this.invokeRequestHandler(this.authResourceUrlBuilder, UPDATE_TENANT, request, { tenantId, updateMask: updateMask.join(',') }) .then((response: any) => { return response as TenantServerResponse; diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index ce45713f97..45ca3ef2d0 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1451,3 +1451,146 @@ export class OIDCConfig implements OIDCAuthProviderConfig { }; } } + +/** + * The request interface for updating a SMS Region Config. + * Configures the regions where users are allowed to send verification SMS. + * This is based on the calling code of the destination phone number. + */ +export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap; + +/** + * Mutual exclusive SMS Region Config of AllowByDefault interface + */ +export interface AllowByDefaultWrap { + /** + * Allow every region by default. + */ + allowByDefault: AllowByDefault; + /** @alpha */ + allowlistOnly?: never; +} + +/** + * Mutually exclusive SMS Region Config of AllowlistOnly interface + */ +export interface AllowlistOnlyWrap { + /** + * Only allowing regions by explicitly adding them to an + * allowlist. + */ + allowlistOnly: AllowlistOnly; + /** @alpha */ + allowByDefault?: never; +} + +/** + * Defines a policy of allowing every region by default and adding disallowed + * regions to a disallow list. + */ +export interface AllowByDefault { + /** + * Two letter unicode region codes to disallow as defined by + * https://cldr.unicode.org/ + * The full list of these region codes is here: + * https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json + */ + disallowedRegions: string[]; +} + +/** + * Defines a policy of only allowing regions by explicitly adding them to an + * allowlist. + */ +export interface AllowlistOnly { + /** + * Two letter unicode region codes to allow as defined by + * https://cldr.unicode.org/ + * The full list of these region codes is here: + * https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json + */ + allowedRegions: string[]; +} + +/** + * Defines the SMSRegionConfig class used for validation. + * + * @internal + */ +export class SmsRegionsAuthConfig { + public static validate(options: SmsRegionConfig): void { + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SmsRegionConfig" must be a non-null object.', + ); + } + + const validKeys = { + allowlistOnly: true, + allowByDefault: true, + }; + + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid SmsRegionConfig parameter.`, + ); + } + } + + // validate mutual exclusiveness of allowByDefault and allowlistOnly + if (typeof options.allowByDefault !== 'undefined' && typeof options.allowlistOnly !== 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + 'SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.', + ); + } + // validation for allowByDefault type + if (typeof options.allowByDefault !== 'undefined') { + const allowByDefaultValidKeys = { + disallowedRegions: true, + } + for (const key in options.allowByDefault) { + if (!(key in allowByDefaultValidKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid SmsRegionConfig.allowByDefault parameter.`, + ); + } + } + // disallowedRegion can be empty. + if (typeof options.allowByDefault.disallowedRegions !== 'undefined' + && !validator.isArray(options.allowByDefault.disallowedRegions)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.', + ); + } + } + + if (typeof options.allowlistOnly !== 'undefined') { + const allowListOnlyValidKeys = { + allowedRegions: true, + } + for (const key in options.allowlistOnly) { + if (!(key in allowListOnlyValidKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid SmsRegionConfig.allowlistOnly parameter.`, + ); + } + } + + // allowedRegions can be empty + if (typeof options.allowlistOnly.allowedRegions !== 'undefined' + && !validator.isArray(options.allowlistOnly.allowedRegions)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.', + ); + } + } + } +} diff --git a/src/auth/auth.ts b/src/auth/auth.ts index d9b5aa7978..4808fbbdc0 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -19,6 +19,7 @@ import { App } from '../app/index'; import { AuthRequestHandler } from './auth-api-request'; import { TenantManager } from './tenant-manager'; import { BaseAuth } from './base-auth'; +import { ProjectConfigManager } from './project-config-manager'; /** * Auth service bound to the provided app. @@ -27,6 +28,7 @@ import { BaseAuth } from './base-auth'; export class Auth extends BaseAuth { private readonly tenantManager_: TenantManager; + private readonly projectConfigManager_: ProjectConfigManager; private readonly app_: App; /** @@ -38,6 +40,7 @@ export class Auth extends BaseAuth { super(app, new AuthRequestHandler(app)); this.app_ = app; this.tenantManager_ = new TenantManager(app); + this.projectConfigManager_ = new ProjectConfigManager(app); } /** @@ -57,4 +60,13 @@ export class Auth extends BaseAuth { public tenantManager(): TenantManager { return this.tenantManager_; } + + /** + * Returns the project config manager instance associated with the current project. + * + * @returns The project config manager instance associated with the current project. + */ + public projectConfigManager(): ProjectConfigManager { + return this.projectConfigManager_; + } } diff --git a/src/auth/index.ts b/src/auth/index.ts index 5a7e668244..7dec658473 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -61,6 +61,10 @@ export { } from './auth'; export { + AllowByDefault, + AllowByDefaultWrap, + AllowlistOnly, + AllowlistOnlyWrap, AuthFactorType, AuthProviderConfig, AuthProviderConfigFilter, @@ -81,6 +85,7 @@ export { OIDCUpdateAuthProviderRequest, SAMLAuthProviderConfig, SAMLUpdateAuthProviderRequest, + SmsRegionConfig, UserProvider, UpdateAuthProviderRequest, UpdateMultiFactorInfoRequest, @@ -116,6 +121,15 @@ export { TenantManager, } from './tenant-manager'; +export { + UpdateProjectConfigRequest, + ProjectConfig, +} from './project-config'; + +export { + ProjectConfigManager, +} from './project-config-manager'; + export { DecodedIdToken, DecodedAuthBlockingToken diff --git a/src/auth/project-config-manager.ts b/src/auth/project-config-manager.ts new file mode 100644 index 0000000000..030b64a779 --- /dev/null +++ b/src/auth/project-config-manager.ts @@ -0,0 +1,67 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { App } from '../app'; +import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; +import { + AuthRequestHandler, +} from './auth-api-request'; + +/** + * Defines the project config manager used to help manage project config related operations. + * This includes: + *
    + *
  • The ability to update and get project config.
  • + */ +export class ProjectConfigManager { + private readonly authRequestHandler: AuthRequestHandler; + + /** + * Initializes a ProjectConfigManager instance for a specified FirebaseApp. + * + * @param app - The app for this ProjectConfigManager instance. + * + * @constructor + * @internal + */ + constructor(app: App) { + this.authRequestHandler = new AuthRequestHandler(app); + } + + /** + * Get the project configuration. + * + * @returns A promise fulfilled with the project configuration. + */ + public getProjectConfig(): Promise { + return this.authRequestHandler.getProjectConfig() + .then((response: ProjectConfigServerResponse) => { + return new ProjectConfig(response); + }) + } + /** + * Updates an existing project configuration. + * + * @param projectConfigOptions - The properties to update on the project. + * + * @returns A promise fulfilled with the updated project config. + */ + public updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise { + return this.authRequestHandler.updateProjectConfig(projectConfigOptions) + .then((response: ProjectConfigServerResponse) => { + return new ProjectConfig(response); + }) + } +} diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts new file mode 100644 index 0000000000..54dcfb3b9c --- /dev/null +++ b/src/auth/project-config.ts @@ -0,0 +1,131 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as validator from '../utils/validator'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { + SmsRegionsAuthConfig, + SmsRegionConfig, +} from './auth-config'; +import { deepCopy } from '../utils/deep-copy'; + +/** + * Interface representing the properties to update on the provided project config. + */ +export interface UpdateProjectConfigRequest { + /** + * The SMS configuration to update on the project. + */ + smsRegionConfig?: SmsRegionConfig; +} + +/** + * Response received from getting or updating a project config. + * This object currently exposes only the SMS Region config. + */ +export interface ProjectConfigServerResponse { + smsRegionConfig?: SmsRegionConfig; +} + +/** + * Request sent to update project config. + * This object currently exposes only the SMS Region config. + */ +export interface ProjectConfigClientRequest { + smsRegionConfig?: SmsRegionConfig; +} + +/** +* Represents a project configuration. +*/ +export class ProjectConfig { + /** + * The SMS Regions Config for the project. + * Configures the regions where users are allowed to send verification SMS. + * This is based on the calling code of the destination phone number. + */ + public readonly smsRegionConfig?: SmsRegionConfig; + + /** + * Validates a project config options object. Throws an error on failure. + * + * @param request - The project config options object to validate. + */ + private static validate(request: ProjectConfigClientRequest): void { + if (!validator.isNonNullObject(request)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"UpdateProjectConfigRequest" must be a valid non-null object.', + ); + } + const validKeys = { + smsRegionConfig: true, + } + // Check for unsupported top level attributes. + for (const key in request) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${key}" is not a valid UpdateProjectConfigRequest parameter.`, + ); + } + } + // Validate SMS Regions Config if provided. + if (typeof request.smsRegionConfig !== 'undefined') { + SmsRegionsAuthConfig.validate(request.smsRegionConfig); + } + } + + /** + * Build the corresponding server request for a UpdateProjectConfigRequest object. + * @param configOptions - The properties to convert to a server request. + * @returns The equivalent server request. + * + * @internal + */ + public static buildServerRequest(configOptions: UpdateProjectConfigRequest): ProjectConfigClientRequest { + ProjectConfig.validate(configOptions); + return configOptions as ProjectConfigClientRequest; + } + + /** + * The Project Config object constructor. + * + * @param response - The server side response used to initialize the Project Config object. + * @constructor + * @internal + */ + constructor(response: ProjectConfigServerResponse) { + if (typeof response.smsRegionConfig !== 'undefined') { + this.smsRegionConfig = response.smsRegionConfig; + } + } + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ + public toJSON(): object { + // JSON serialization + const json = { + smsRegionConfig: deepCopy(this.smsRegionConfig), + }; + if (typeof json.smsRegionConfig === 'undefined') { + delete json.smsRegionConfig; + } + return json; + } +} + diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index e489fa3b09..56cf2abd8d 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -21,7 +21,7 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig, - MultiFactorAuthConfig, + MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig } from './auth-config'; /** @@ -54,6 +54,11 @@ export interface UpdateTenantRequest { * Passing null clears the previously save phone number / code pairs. */ testPhoneNumbers?: { [phoneNumber: string]: string } | null; + + /** + * The SMS configuration to update on the project. + */ + smsRegionConfig?: SmsRegionConfig; } /** @@ -68,6 +73,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; + smsRegionConfig?: SmsRegionConfig; } /** The tenant server response interface. */ @@ -79,6 +85,7 @@ export interface TenantServerResponse { enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; + smsRegionConfig?: SmsRegionConfig; } /** @@ -123,6 +130,13 @@ export class Tenant { private readonly emailSignInConfig_?: EmailSignInConfig; private readonly multiFactorConfig_?: MultiFactorAuthConfig; + /** + * The SMS Regions Config to update a tenant. + * Configures the regions where users are allowed to send verification SMS. + * This is based on the calling code of the destination phone number. + */ + public readonly smsRegionConfig?: SmsRegionConfig; + /** * Builds the corresponding server request for a TenantOptions object. * @@ -152,6 +166,9 @@ export class Tenant { // null will clear existing test phone numbers. Translate to empty object. request.testPhoneNumbers = tenantOptions.testPhoneNumbers ?? {}; } + if (typeof tenantOptions.smsRegionConfig !== 'undefined') { + request.smsRegionConfig = tenantOptions.smsRegionConfig; + } return request; } @@ -185,6 +202,7 @@ export class Tenant { anonymousSignInEnabled: true, multiFactorConfig: true, testPhoneNumbers: true, + smsRegionConfig: true, }; const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; if (!validator.isNonNullObject(request)) { @@ -231,6 +249,10 @@ export class Tenant { // This will throw an error if invalid. MultiFactorAuthConfig.buildServerRequest(request.multiFactorConfig); } + // Validate SMS Regions Config if provided. + if (typeof request.smsRegionConfig != 'undefined') { + SmsRegionsAuthConfig.validate(request.smsRegionConfig); + } } /** @@ -265,6 +287,9 @@ export class Tenant { if (typeof response.testPhoneNumbers !== 'undefined') { this.testPhoneNumbers = deepCopy(response.testPhoneNumbers || {}); } + if (typeof response.smsRegionConfig !== 'undefined') { + this.smsRegionConfig = deepCopy(response.smsRegionConfig); + } } /** @@ -294,6 +319,7 @@ export class Tenant { multiFactorConfig: this.multiFactorConfig_?.toJSON(), anonymousSignInEnabled: this.anonymousSignInEnabled, testPhoneNumbers: this.testPhoneNumbers, + smsRegionConfig: deepCopy(this.smsRegionConfig), }; if (typeof json.multiFactorConfig === 'undefined') { delete json.multiFactorConfig; @@ -301,6 +327,9 @@ export class Tenant { if (typeof json.testPhoneNumbers === 'undefined') { delete json.testPhoneNumbers; } + if (typeof json.smsRegionConfig === 'undefined') { + delete json.smsRegionConfig; + } return json; } } diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index e1005d9c4a..9245ac5211 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -31,7 +31,7 @@ import { deepExtend, deepCopy } from '../../src/utils/deep-copy'; import { AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, - UserImportRecord, UserRecord, getAuth, + UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, } from '../../lib/auth/index'; const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires @@ -1180,6 +1180,61 @@ describe('admin.auth', () => { }); }); + describe('Project config management operations', () => { + before(function() { + if (authEmulatorHost) { + this.skip(); // getConfig is not supported in Auth Emulator + } + }); + const projectConfigOption1: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + } + }, + }; + const projectConfigOption2: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + } + }, + }; + const expectedProjectConfig1: any = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + } + }, + }; + const expectedProjectConfig2: any = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + } + }, + }; + + it('updateProjectConfig() should resolve with the updated project config', () => { + return getAuth().projectConfigManager().updateProjectConfig(projectConfigOption1) + .then((actualProjectConfig) => { + expect(actualProjectConfig.toJSON()).to.deep.equal(expectedProjectConfig1); + return getAuth().projectConfigManager().updateProjectConfig(projectConfigOption2); + }) + .then((actualProjectConfig) => { + expect(actualProjectConfig.toJSON()).to.deep.equal(expectedProjectConfig2); + }); + }); + + it('getProjectConfig() should resolve with expected project config', () => { + return getAuth().projectConfigManager().getProjectConfig() + .then((actualConfig) => { + const actualConfigObj = actualConfig.toJSON(); + expect(actualConfigObj).to.deep.equal(expectedProjectConfig2); + }); + }); + }); + describe('Tenant management operations', () => { let createdTenantId: string; const createdTenants: string[] = []; @@ -1248,6 +1303,11 @@ describe('admin.auth', () => { state: 'ENABLED', factorIds: ['phone'], }, + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + } + }, }; // https://mochajs.org/ @@ -1669,6 +1729,7 @@ describe('admin.auth', () => { multiFactorConfig: deepCopy(expectedUpdatedTenant2.multiFactorConfig), // Test clearing of phone numbers. testPhoneNumbers: null, + smsRegionConfig: deepCopy(expectedUpdatedTenant2.smsRegionConfig), }; if (authEmulatorHost) { return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions) @@ -1698,6 +1759,28 @@ describe('admin.auth', () => { }); }); + it('updateTenant() should not update tenant when SMS region config is undefined', () => { + expectedUpdatedTenant.tenantId = createdTenantId; + const updatedOptions2: UpdateTenantRequest = { + displayName: expectedUpdatedTenant2.displayName, + smsRegionConfig: undefined, + }; + if (authEmulatorHost) { + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2) + .then((actualTenant) => { + const actualTenantObj = actualTenant.toJSON(); + // Not supported in Auth Emulator + delete (actualTenantObj as {testPhoneNumbers: Record}).testPhoneNumbers; + delete expectedUpdatedTenant2.testPhoneNumbers; + expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant2); + }); + } + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2) + .then((actualTenant) => { + expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenant2); + }); + }); + it('updateTenant() should be able to enable/disable anon provider', async () => { const tenantManager = getAuth().tenantManager(); let tenant = await tenantManager.createTenant({ diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts new file mode 100644 index 0000000000..d06b24fa80 --- /dev/null +++ b/test/unit/auth/project-config-manager.spec.ts @@ -0,0 +1,196 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { ProjectConfigManager } from '../../../src/auth/project-config-manager'; +import { + ProjectConfig, + ProjectConfigServerResponse, + UpdateProjectConfigRequest +} from '../../../src/auth/project-config'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('ProjectConfigManager', () => { + let mockApp: FirebaseApp; + let projectConfigManager: ProjectConfigManager; + let nullAccessTokenProjectConfigManager: ProjectConfigManager; + let malformedAccessTokenProjectConfigManager: ProjectConfigManager; + let rejectedPromiseAccessTokenProjectConfigManager: ProjectConfigManager; + const GET_CONFIG_RESPONSE: ProjectConfigServerResponse = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + + before(() => { + mockApp = mocks.app(); + projectConfigManager = new ProjectConfigManager(mockApp); + nullAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appReturningNullAccessToken()); + malformedAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appRejectedWhileFetchingAccessToken()); + }); + + after(() => { + return mockApp.delete(); + }); + + describe('getProjectConfig()', () => { + const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + return rejectedPromiseAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a Project Config on success', () => { + // Stub getProjectConfig to return expected result. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getProjectConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(stub); + return projectConfigManager.getProjectConfig() + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce; + // Confirm expected project config returned. + expect(result).to.deep.equal(expectedProjectConfig); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getConfig to throw a backend error. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getProjectConfig') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return projectConfigManager.getProjectConfig() + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce; + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('updateProjectConfig()', () => { + const projectConfigOptions: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to update the config provided.'); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no projectConfigOptions', () => { + return (projectConfigManager as any).updateProjectConfig(null as unknown as UpdateProjectConfigRequest) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + return rejectedPromiseAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a ProjectConfig on updateProjectConfig request success', () => { + // Stub updateProjectConfig to return expected result. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateProjectConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(updateConfigStub); + return projectConfigManager.updateProjectConfig(projectConfigOptions) + .then((actualProjectConfig) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(projectConfigOptions); + // Confirm expected Project Config object returned. + expect(actualProjectConfig).to.deep.equal(expectedProjectConfig); + }); + }); + + it('should throw an error when updateProjectConfig returns an error', () => { + // Stub updateProjectConfig to throw a backend error. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateProjectConfig') + .returns(Promise.reject(expectedError)); + stubs.push(updateConfigStub); + return projectConfigManager.updateProjectConfig(projectConfigOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(projectConfigOptions); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts new file mode 100644 index 0000000000..19cc8f420d --- /dev/null +++ b/test/unit/auth/project-config.spec.ts @@ -0,0 +1,192 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import { deepCopy } from '../../../src/utils/deep-copy'; +import { + ProjectConfig, + ProjectConfigServerResponse, + UpdateProjectConfigRequest, +} from '../../../src/auth/project-config'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('ProjectConfig', () => { + const serverResponse: ProjectConfigServerResponse = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + + const updateProjectConfigRequest1: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + + const updateProjectConfigRequest2: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + + const updateProjectConfigRequest3: any = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + allowByDefault: { + disallowedRegions: ['AC', 'AD'], + }, + }, + }; + + describe('buildServerRequest()', () => { + + describe('for an update request', () => { + it('should throw on null SmsRegionConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"SmsRegionConfig" must be a non-null object.'); + }); + + it('should throw on invalid SmsRegionConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); + }); + + it('should throw on invalid allowlistOnly attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest2) as any; + configOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); + }); + + it('should throw on invalid allowByDefault attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); + }); + + it('should throw on non-array disallowedRegions attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); + }); + + it('should throw on non-array allowedRegions attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest2) as any; + configOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); + }); + + it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest3) as any; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); + }); + + it('should not throw on valid client request object', () => { + const configOptionsClientRequest1 = deepCopy(updateProjectConfigRequest1); + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest1); + }).not.to.throw; + const configOptionsClientRequest2 = deepCopy(updateProjectConfigRequest2); + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest2); + }).not.to.throw; + }); + + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + nonObjects.forEach((request) => { + it('should throw on invalid UpdateProjectConfigRequest:' + JSON.stringify(request), () => { + expect(() => { + ProjectConfig.buildServerRequest(request as any); + }).to.throw('"UpdateProjectConfigRequest" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute for update request', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.unsupported = 'value'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"unsupported" is not a valid UpdateProjectConfigRequest parameter.'); + }); + }); + }); + + describe('constructor', () => { + const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + const projectConfig = new ProjectConfig(serverResponseCopy); + + it('should not throw on valid initialization', () => { + expect(() => new ProjectConfig(serverResponse)).not.to.throw(); + }); + + it('should set readonly property smsRegionConfig', () => { + const expectedSmsRegionConfig = { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }; + expect(projectConfig.smsRegionConfig).to.deep.equal(expectedSmsRegionConfig); + }); + }); + + describe('toJSON()', () => { + const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + it('should return the expected object representation of project config', () => { + expect(new ProjectConfig(serverResponseCopy).toJSON()).to.deep.equal({ + smsRegionConfig: deepCopy(serverResponse.smsRegionConfig) + }); + }); + + it('should not populate optional fields if not available', () => { + const serverResponseOptionalCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + delete serverResponseOptionalCopy.smsRegionConfig; + + expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({}); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 0f14856faa..4780cbb473 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -33,6 +33,18 @@ chai.use(chaiAsPromised); const expect = chai.expect; describe('Tenant', () => { + const smsAllowByDefault = { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }; + + const smsAllowlistOnly = { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + }; + const serverRequest: TenantServerResponse = { name: 'projects/project1/tenants/TENANT-ID', displayName: 'TENANT-DISPLAY-NAME', @@ -46,6 +58,7 @@ describe('Tenant', () => { '+16505551234': '019287', '+16505550676': '985235', }, + smsRegionConfig: smsAllowByDefault, }; const clientRequest: UpdateTenantRequest = { @@ -62,6 +75,7 @@ describe('Tenant', () => { '+16505551234': '019287', '+16505550676': '985235', }, + smsRegionConfig: smsAllowByDefault, }; const serverRequestWithoutMfa: TenantServerResponse = { @@ -141,6 +155,64 @@ describe('Tenant', () => { .to.deep.equal(tenantOptionsServerRequest); }); + it('should throw on null SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"SmsRegionConfig" must be a non-null object.'); + }); + + it('should throw on invalid SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); + }); + + it('should throw on invalid allowlistOnly attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); + }); + + it('should throw on invalid allowByDefault attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); + }); + + it('should throw on non-array disallowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); + }); + + it('should throw on non-array allowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); + }); + + it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = { ...smsAllowByDefault, ...smsAllowlistOnly }; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); + }); + it('should not throw on valid client request object', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); expect(() => { @@ -232,6 +304,64 @@ describe('Tenant', () => { }).to.throw('"CreateTenantRequest.testPhoneNumbers" must be a non-null object.'); }); + it('should throw on null SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"SmsRegionConfig" must be a non-null object.'); + }); + + it('should throw on invalid SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); + }); + + it('should throw on invalid allowlistOnly attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); + }); + + it('should throw on invalid allowByDefault attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); + }); + + it('should throw on non-array disallowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); + }); + + it('should throw on non-array allowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); + }); + + it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = { ...smsAllowByDefault, ...smsAllowlistOnly }; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { @@ -314,6 +444,11 @@ describe('Tenant', () => { deepCopy(clientRequest.testPhoneNumbers)); }); + it('should set readonly property smsRegionConfig', () => { + expect(tenant.smsRegionConfig).to.deep.equal( + deepCopy(clientRequest.smsRegionConfig)); + }); + it('should throw when no tenant ID is provided', () => { const invalidOptions = deepCopy(serverRequest); // Use resource name that does not include a tenant ID. @@ -352,6 +487,7 @@ describe('Tenant', () => { anonymousSignInEnabled: false, multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), + smsRegionConfig: deepCopy(clientRequest.smsRegionConfig), }); }); @@ -359,6 +495,7 @@ describe('Tenant', () => { const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverRequest); delete serverRequestCopyWithoutMfa.mfaConfig; delete serverRequestCopyWithoutMfa.testPhoneNumbers; + delete serverRequestCopyWithoutMfa.smsRegionConfig; expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({ tenantId: 'TENANT-ID', From abdfb00aa6a100ebfd3659c5818f162585af7eb8 Mon Sep 17 00:00:00 2001 From: Liubin Jiang <56564857+Xiaoshouzi-gh@users.noreply.github.com> Date: Mon, 2 May 2022 11:51:59 -0700 Subject: [PATCH 15/27] Revert "feat(auth): Support sms region config change on Tenant and Project level." (#1676) * Revert "feat(auth): Support sms region config change on Tenant and Project level. (#1673)" This reverts commit 35df364f53d10badffbb4bd42dc7afc56ab9d3e9. * Trigger CI --- etc/firebase-admin.auth.api.md | 47 ----- src/auth/auth-api-request.ts | 72 +------ src/auth/auth-config.ts | 143 ------------- src/auth/auth.ts | 12 -- src/auth/index.ts | 14 -- src/auth/project-config-manager.ts | 67 ------ src/auth/project-config.ts | 131 ------------ src/auth/tenant.ts | 31 +-- test/integration/auth.spec.ts | 85 +------- test/unit/auth/project-config-manager.spec.ts | 196 ------------------ test/unit/auth/project-config.spec.ts | 192 ----------------- test/unit/auth/tenant.spec.ts | 137 ------------ 12 files changed, 12 insertions(+), 1115 deletions(-) delete mode 100644 src/auth/project-config-manager.ts delete mode 100644 src/auth/project-config.ts delete mode 100644 test/unit/auth/project-config-manager.spec.ts delete mode 100644 test/unit/auth/project-config.spec.ts diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index c7090af304..2986869ef2 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -23,35 +23,10 @@ export interface ActionCodeSettings { url: string; } -// @public -export interface AllowByDefault { - disallowedRegions: string[]; -} - -// @public -export interface AllowByDefaultWrap { - allowByDefault: AllowByDefault; - // @alpha (undocumented) - allowlistOnly?: never; -} - -// @public -export interface AllowlistOnly { - allowedRegions: string[]; -} - -// @public -export interface AllowlistOnlyWrap { - // @alpha (undocumented) - allowByDefault?: never; - allowlistOnly: AllowlistOnly; -} - // @public export class Auth extends BaseAuth { // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts get app(): App; - projectConfigManager(): ProjectConfigManager; tenantManager(): TenantManager; } @@ -334,18 +309,6 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { toJSON(): object; } -// @public -export class ProjectConfig { - readonly smsRegionConfig?: SmsRegionConfig; - toJSON(): object; -} - -// @public -export class ProjectConfigManager { - getProjectConfig(): Promise; - updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise; -} - // @public export interface ProviderIdentifier { // (undocumented) @@ -379,9 +342,6 @@ export interface SessionCookieOptions { expiresIn: number; } -// @public -export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap; - // @public export class Tenant { // (undocumented) @@ -389,7 +349,6 @@ export class Tenant { readonly displayName?: string; get emailSignInConfig(): EmailSignInProviderConfig | undefined; get multiFactorConfig(): MultiFactorConfig | undefined; - readonly smsRegionConfig?: SmsRegionConfig; readonly tenantId: string; readonly testPhoneNumbers?: { [phoneNumber: string]: string; @@ -432,11 +391,6 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor phoneNumber: string; } -// @public -export interface UpdateProjectConfigRequest { - smsRegionConfig?: SmsRegionConfig; -} - // @public export interface UpdateRequest { disabled?: boolean; @@ -457,7 +411,6 @@ export interface UpdateTenantRequest { displayName?: string; emailSignInConfig?: EmailSignInProviderConfig; multiFactorConfig?: MultiFactorConfig; - smsRegionConfig?: SmsRegionConfig; testPhoneNumbers?: { [phoneNumber: string]: string; } | null; diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 2893d49a9d..a962a4f719 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -42,7 +42,6 @@ import { OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest, SAMLUpdateAuthProviderRequest } from './auth-config'; -import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { @@ -103,6 +102,7 @@ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace( const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace( 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); + /** Maximum allowed number of tenants to download at one time. */ const MAX_LIST_TENANT_PAGE_SIZE = 1000; @@ -1981,29 +1981,6 @@ export abstract class AbstractAuthRequestHandler { } } -/** Instantiates the getConfig endpoint settings. */ -const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET') - .setResponseValidator((response: any) => { - // Response should always contain at least the config name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get project config', - ); - } - }); - -/** Instantiates the updateConfig endpoint settings. */ -const UPDATE_PROJECT_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH') - .setResponseValidator((response: any) => { - // Response should always contain at least the config name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to update project config', - ); - } - }); /** Instantiates the getTenant endpoint settings. */ const GET_TENANT = new ApiSettings('/tenants/{tenantId}', 'GET') @@ -2072,13 +2049,13 @@ const CREATE_TENANT = new ApiSettings('/tenants', 'POST') /** - * Utility for sending requests to Auth server that are Auth instance related. This includes user, tenant, - * and project config management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines + * Utility for sending requests to Auth server that are Auth instance related. This includes user and + * tenant management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines * additional tenant management related APIs. */ export class AuthRequestHandler extends AbstractAuthRequestHandler { - protected readonly authResourceUrlBuilder: AuthResourceUrlBuilder; + protected readonly tenantMgmtResourceBuilder: AuthResourceUrlBuilder; /** * The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp. @@ -2088,7 +2065,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ constructor(app: App) { super(app); - this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2'); + this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(app, 'v2'); } /** @@ -2105,35 +2082,6 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { return new AuthResourceUrlBuilder(this.app, 'v2'); } - /** - * Get the current project's config - * @returns A promise that resolves with the project config information. - */ - public getProjectConfig(): Promise { - return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_PROJECT_CONFIG, {}, {}) - .then((response: any) => { - return response as ProjectConfigServerResponse; - }); - } - - /** - * Update the current project's config. - * @returns A promise that resolves with the project config information. - */ - public updateProjectConfig(options: UpdateProjectConfigRequest): Promise { - try { - const request = ProjectConfig.buildServerRequest(options); - const updateMask = utils.generateUpdateMask(request); - return this.invokeRequestHandler( - this.authResourceUrlBuilder, UPDATE_PROJECT_CONFIG, request, { updateMask: updateMask.join(',') }) - .then((response: any) => { - return response as ProjectConfigServerResponse; - }); - } catch (e) { - return Promise.reject(e); - } - } - /** * Looks up a tenant by tenant ID. * @@ -2144,7 +2092,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT, {}, { tenantId }) + return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, GET_TENANT, {}, { tenantId }) .then((response: any) => { return response as TenantServerResponse; }); @@ -2174,7 +2122,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (typeof request.pageToken === 'undefined') { delete request.pageToken; } - return this.invokeRequestHandler(this.authResourceUrlBuilder, LIST_TENANTS, request) + return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, LIST_TENANTS, request) .then((response: any) => { if (!response.tenants) { response.tenants = []; @@ -2194,7 +2142,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.authResourceUrlBuilder, DELETE_TENANT, undefined, { tenantId }) + return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, DELETE_TENANT, undefined, { tenantId }) .then(() => { // Return nothing. }); @@ -2210,7 +2158,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { try { // Construct backend request. const request = Tenant.buildServerRequest(tenantOptions, true); - return this.invokeRequestHandler(this.authResourceUrlBuilder, CREATE_TENANT, request) + return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, CREATE_TENANT, request) .then((response: any) => { return response as TenantServerResponse; }); @@ -2236,7 +2184,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { // Do not traverse deep into testPhoneNumbers. The entire content should be replaced // and not just specific phone numbers. const updateMask = utils.generateUpdateMask(request, ['testPhoneNumbers']); - return this.invokeRequestHandler(this.authResourceUrlBuilder, UPDATE_TENANT, request, + return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, UPDATE_TENANT, request, { tenantId, updateMask: updateMask.join(',') }) .then((response: any) => { return response as TenantServerResponse; diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 45ca3ef2d0..ce45713f97 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1451,146 +1451,3 @@ export class OIDCConfig implements OIDCAuthProviderConfig { }; } } - -/** - * The request interface for updating a SMS Region Config. - * Configures the regions where users are allowed to send verification SMS. - * This is based on the calling code of the destination phone number. - */ -export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap; - -/** - * Mutual exclusive SMS Region Config of AllowByDefault interface - */ -export interface AllowByDefaultWrap { - /** - * Allow every region by default. - */ - allowByDefault: AllowByDefault; - /** @alpha */ - allowlistOnly?: never; -} - -/** - * Mutually exclusive SMS Region Config of AllowlistOnly interface - */ -export interface AllowlistOnlyWrap { - /** - * Only allowing regions by explicitly adding them to an - * allowlist. - */ - allowlistOnly: AllowlistOnly; - /** @alpha */ - allowByDefault?: never; -} - -/** - * Defines a policy of allowing every region by default and adding disallowed - * regions to a disallow list. - */ -export interface AllowByDefault { - /** - * Two letter unicode region codes to disallow as defined by - * https://cldr.unicode.org/ - * The full list of these region codes is here: - * https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json - */ - disallowedRegions: string[]; -} - -/** - * Defines a policy of only allowing regions by explicitly adding them to an - * allowlist. - */ -export interface AllowlistOnly { - /** - * Two letter unicode region codes to allow as defined by - * https://cldr.unicode.org/ - * The full list of these region codes is here: - * https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json - */ - allowedRegions: string[]; -} - -/** - * Defines the SMSRegionConfig class used for validation. - * - * @internal - */ -export class SmsRegionsAuthConfig { - public static validate(options: SmsRegionConfig): void { - if (!validator.isNonNullObject(options)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"SmsRegionConfig" must be a non-null object.', - ); - } - - const validKeys = { - allowlistOnly: true, - allowByDefault: true, - }; - - for (const key in options) { - if (!(key in validKeys)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - `"${key}" is not a valid SmsRegionConfig parameter.`, - ); - } - } - - // validate mutual exclusiveness of allowByDefault and allowlistOnly - if (typeof options.allowByDefault !== 'undefined' && typeof options.allowlistOnly !== 'undefined') { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - 'SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.', - ); - } - // validation for allowByDefault type - if (typeof options.allowByDefault !== 'undefined') { - const allowByDefaultValidKeys = { - disallowedRegions: true, - } - for (const key in options.allowByDefault) { - if (!(key in allowByDefaultValidKeys)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - `"${key}" is not a valid SmsRegionConfig.allowByDefault parameter.`, - ); - } - } - // disallowedRegion can be empty. - if (typeof options.allowByDefault.disallowedRegions !== 'undefined' - && !validator.isArray(options.allowByDefault.disallowedRegions)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.', - ); - } - } - - if (typeof options.allowlistOnly !== 'undefined') { - const allowListOnlyValidKeys = { - allowedRegions: true, - } - for (const key in options.allowlistOnly) { - if (!(key in allowListOnlyValidKeys)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - `"${key}" is not a valid SmsRegionConfig.allowlistOnly parameter.`, - ); - } - } - - // allowedRegions can be empty - if (typeof options.allowlistOnly.allowedRegions !== 'undefined' - && !validator.isArray(options.allowlistOnly.allowedRegions)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, - '"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.', - ); - } - } - } -} diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 4808fbbdc0..d9b5aa7978 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -19,7 +19,6 @@ import { App } from '../app/index'; import { AuthRequestHandler } from './auth-api-request'; import { TenantManager } from './tenant-manager'; import { BaseAuth } from './base-auth'; -import { ProjectConfigManager } from './project-config-manager'; /** * Auth service bound to the provided app. @@ -28,7 +27,6 @@ import { ProjectConfigManager } from './project-config-manager'; export class Auth extends BaseAuth { private readonly tenantManager_: TenantManager; - private readonly projectConfigManager_: ProjectConfigManager; private readonly app_: App; /** @@ -40,7 +38,6 @@ export class Auth extends BaseAuth { super(app, new AuthRequestHandler(app)); this.app_ = app; this.tenantManager_ = new TenantManager(app); - this.projectConfigManager_ = new ProjectConfigManager(app); } /** @@ -60,13 +57,4 @@ export class Auth extends BaseAuth { public tenantManager(): TenantManager { return this.tenantManager_; } - - /** - * Returns the project config manager instance associated with the current project. - * - * @returns The project config manager instance associated with the current project. - */ - public projectConfigManager(): ProjectConfigManager { - return this.projectConfigManager_; - } } diff --git a/src/auth/index.ts b/src/auth/index.ts index 7dec658473..5a7e668244 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -61,10 +61,6 @@ export { } from './auth'; export { - AllowByDefault, - AllowByDefaultWrap, - AllowlistOnly, - AllowlistOnlyWrap, AuthFactorType, AuthProviderConfig, AuthProviderConfigFilter, @@ -85,7 +81,6 @@ export { OIDCUpdateAuthProviderRequest, SAMLAuthProviderConfig, SAMLUpdateAuthProviderRequest, - SmsRegionConfig, UserProvider, UpdateAuthProviderRequest, UpdateMultiFactorInfoRequest, @@ -121,15 +116,6 @@ export { TenantManager, } from './tenant-manager'; -export { - UpdateProjectConfigRequest, - ProjectConfig, -} from './project-config'; - -export { - ProjectConfigManager, -} from './project-config-manager'; - export { DecodedIdToken, DecodedAuthBlockingToken diff --git a/src/auth/project-config-manager.ts b/src/auth/project-config-manager.ts deleted file mode 100644 index 030b64a779..0000000000 --- a/src/auth/project-config-manager.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*! - * Copyright 2022 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { App } from '../app'; -import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; -import { - AuthRequestHandler, -} from './auth-api-request'; - -/** - * Defines the project config manager used to help manage project config related operations. - * This includes: - *
      - *
    • The ability to update and get project config.
    • - */ -export class ProjectConfigManager { - private readonly authRequestHandler: AuthRequestHandler; - - /** - * Initializes a ProjectConfigManager instance for a specified FirebaseApp. - * - * @param app - The app for this ProjectConfigManager instance. - * - * @constructor - * @internal - */ - constructor(app: App) { - this.authRequestHandler = new AuthRequestHandler(app); - } - - /** - * Get the project configuration. - * - * @returns A promise fulfilled with the project configuration. - */ - public getProjectConfig(): Promise { - return this.authRequestHandler.getProjectConfig() - .then((response: ProjectConfigServerResponse) => { - return new ProjectConfig(response); - }) - } - /** - * Updates an existing project configuration. - * - * @param projectConfigOptions - The properties to update on the project. - * - * @returns A promise fulfilled with the updated project config. - */ - public updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise { - return this.authRequestHandler.updateProjectConfig(projectConfigOptions) - .then((response: ProjectConfigServerResponse) => { - return new ProjectConfig(response); - }) - } -} diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts deleted file mode 100644 index 54dcfb3b9c..0000000000 --- a/src/auth/project-config.ts +++ /dev/null @@ -1,131 +0,0 @@ -/*! - * Copyright 2022 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import * as validator from '../utils/validator'; -import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; -import { - SmsRegionsAuthConfig, - SmsRegionConfig, -} from './auth-config'; -import { deepCopy } from '../utils/deep-copy'; - -/** - * Interface representing the properties to update on the provided project config. - */ -export interface UpdateProjectConfigRequest { - /** - * The SMS configuration to update on the project. - */ - smsRegionConfig?: SmsRegionConfig; -} - -/** - * Response received from getting or updating a project config. - * This object currently exposes only the SMS Region config. - */ -export interface ProjectConfigServerResponse { - smsRegionConfig?: SmsRegionConfig; -} - -/** - * Request sent to update project config. - * This object currently exposes only the SMS Region config. - */ -export interface ProjectConfigClientRequest { - smsRegionConfig?: SmsRegionConfig; -} - -/** -* Represents a project configuration. -*/ -export class ProjectConfig { - /** - * The SMS Regions Config for the project. - * Configures the regions where users are allowed to send verification SMS. - * This is based on the calling code of the destination phone number. - */ - public readonly smsRegionConfig?: SmsRegionConfig; - - /** - * Validates a project config options object. Throws an error on failure. - * - * @param request - The project config options object to validate. - */ - private static validate(request: ProjectConfigClientRequest): void { - if (!validator.isNonNullObject(request)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - '"UpdateProjectConfigRequest" must be a valid non-null object.', - ); - } - const validKeys = { - smsRegionConfig: true, - } - // Check for unsupported top level attributes. - for (const key in request) { - if (!(key in validKeys)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `"${key}" is not a valid UpdateProjectConfigRequest parameter.`, - ); - } - } - // Validate SMS Regions Config if provided. - if (typeof request.smsRegionConfig !== 'undefined') { - SmsRegionsAuthConfig.validate(request.smsRegionConfig); - } - } - - /** - * Build the corresponding server request for a UpdateProjectConfigRequest object. - * @param configOptions - The properties to convert to a server request. - * @returns The equivalent server request. - * - * @internal - */ - public static buildServerRequest(configOptions: UpdateProjectConfigRequest): ProjectConfigClientRequest { - ProjectConfig.validate(configOptions); - return configOptions as ProjectConfigClientRequest; - } - - /** - * The Project Config object constructor. - * - * @param response - The server side response used to initialize the Project Config object. - * @constructor - * @internal - */ - constructor(response: ProjectConfigServerResponse) { - if (typeof response.smsRegionConfig !== 'undefined') { - this.smsRegionConfig = response.smsRegionConfig; - } - } - /** - * Returns a JSON-serializable representation of this object. - * - * @returns A JSON-serializable representation of this object. - */ - public toJSON(): object { - // JSON serialization - const json = { - smsRegionConfig: deepCopy(this.smsRegionConfig), - }; - if (typeof json.smsRegionConfig === 'undefined') { - delete json.smsRegionConfig; - } - return json; - } -} - diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 56cf2abd8d..e489fa3b09 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -21,7 +21,7 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig, - MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig + MultiFactorAuthConfig, } from './auth-config'; /** @@ -54,11 +54,6 @@ export interface UpdateTenantRequest { * Passing null clears the previously save phone number / code pairs. */ testPhoneNumbers?: { [phoneNumber: string]: string } | null; - - /** - * The SMS configuration to update on the project. - */ - smsRegionConfig?: SmsRegionConfig; } /** @@ -73,7 +68,6 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; - smsRegionConfig?: SmsRegionConfig; } /** The tenant server response interface. */ @@ -85,7 +79,6 @@ export interface TenantServerResponse { enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; - smsRegionConfig?: SmsRegionConfig; } /** @@ -130,13 +123,6 @@ export class Tenant { private readonly emailSignInConfig_?: EmailSignInConfig; private readonly multiFactorConfig_?: MultiFactorAuthConfig; - /** - * The SMS Regions Config to update a tenant. - * Configures the regions where users are allowed to send verification SMS. - * This is based on the calling code of the destination phone number. - */ - public readonly smsRegionConfig?: SmsRegionConfig; - /** * Builds the corresponding server request for a TenantOptions object. * @@ -166,9 +152,6 @@ export class Tenant { // null will clear existing test phone numbers. Translate to empty object. request.testPhoneNumbers = tenantOptions.testPhoneNumbers ?? {}; } - if (typeof tenantOptions.smsRegionConfig !== 'undefined') { - request.smsRegionConfig = tenantOptions.smsRegionConfig; - } return request; } @@ -202,7 +185,6 @@ export class Tenant { anonymousSignInEnabled: true, multiFactorConfig: true, testPhoneNumbers: true, - smsRegionConfig: true, }; const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; if (!validator.isNonNullObject(request)) { @@ -249,10 +231,6 @@ export class Tenant { // This will throw an error if invalid. MultiFactorAuthConfig.buildServerRequest(request.multiFactorConfig); } - // Validate SMS Regions Config if provided. - if (typeof request.smsRegionConfig != 'undefined') { - SmsRegionsAuthConfig.validate(request.smsRegionConfig); - } } /** @@ -287,9 +265,6 @@ export class Tenant { if (typeof response.testPhoneNumbers !== 'undefined') { this.testPhoneNumbers = deepCopy(response.testPhoneNumbers || {}); } - if (typeof response.smsRegionConfig !== 'undefined') { - this.smsRegionConfig = deepCopy(response.smsRegionConfig); - } } /** @@ -319,7 +294,6 @@ export class Tenant { multiFactorConfig: this.multiFactorConfig_?.toJSON(), anonymousSignInEnabled: this.anonymousSignInEnabled, testPhoneNumbers: this.testPhoneNumbers, - smsRegionConfig: deepCopy(this.smsRegionConfig), }; if (typeof json.multiFactorConfig === 'undefined') { delete json.multiFactorConfig; @@ -327,9 +301,6 @@ export class Tenant { if (typeof json.testPhoneNumbers === 'undefined') { delete json.testPhoneNumbers; } - if (typeof json.smsRegionConfig === 'undefined') { - delete json.smsRegionConfig; - } return json; } } diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 9245ac5211..e1005d9c4a 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -31,7 +31,7 @@ import { deepExtend, deepCopy } from '../../src/utils/deep-copy'; import { AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, - UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, + UserImportRecord, UserRecord, getAuth, } from '../../lib/auth/index'; const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires @@ -1180,61 +1180,6 @@ describe('admin.auth', () => { }); }); - describe('Project config management operations', () => { - before(function() { - if (authEmulatorHost) { - this.skip(); // getConfig is not supported in Auth Emulator - } - }); - const projectConfigOption1: UpdateProjectConfigRequest = { - smsRegionConfig: { - allowByDefault: { - disallowedRegions: [ 'AC', 'AD' ], - } - }, - }; - const projectConfigOption2: UpdateProjectConfigRequest = { - smsRegionConfig: { - allowlistOnly: { - allowedRegions: [ 'AC', 'AD' ], - } - }, - }; - const expectedProjectConfig1: any = { - smsRegionConfig: { - allowByDefault: { - disallowedRegions: [ 'AC', 'AD' ], - } - }, - }; - const expectedProjectConfig2: any = { - smsRegionConfig: { - allowlistOnly: { - allowedRegions: [ 'AC', 'AD' ], - } - }, - }; - - it('updateProjectConfig() should resolve with the updated project config', () => { - return getAuth().projectConfigManager().updateProjectConfig(projectConfigOption1) - .then((actualProjectConfig) => { - expect(actualProjectConfig.toJSON()).to.deep.equal(expectedProjectConfig1); - return getAuth().projectConfigManager().updateProjectConfig(projectConfigOption2); - }) - .then((actualProjectConfig) => { - expect(actualProjectConfig.toJSON()).to.deep.equal(expectedProjectConfig2); - }); - }); - - it('getProjectConfig() should resolve with expected project config', () => { - return getAuth().projectConfigManager().getProjectConfig() - .then((actualConfig) => { - const actualConfigObj = actualConfig.toJSON(); - expect(actualConfigObj).to.deep.equal(expectedProjectConfig2); - }); - }); - }); - describe('Tenant management operations', () => { let createdTenantId: string; const createdTenants: string[] = []; @@ -1303,11 +1248,6 @@ describe('admin.auth', () => { state: 'ENABLED', factorIds: ['phone'], }, - smsRegionConfig: { - allowByDefault: { - disallowedRegions: [ 'AC', 'AD' ], - } - }, }; // https://mochajs.org/ @@ -1729,7 +1669,6 @@ describe('admin.auth', () => { multiFactorConfig: deepCopy(expectedUpdatedTenant2.multiFactorConfig), // Test clearing of phone numbers. testPhoneNumbers: null, - smsRegionConfig: deepCopy(expectedUpdatedTenant2.smsRegionConfig), }; if (authEmulatorHost) { return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions) @@ -1759,28 +1698,6 @@ describe('admin.auth', () => { }); }); - it('updateTenant() should not update tenant when SMS region config is undefined', () => { - expectedUpdatedTenant.tenantId = createdTenantId; - const updatedOptions2: UpdateTenantRequest = { - displayName: expectedUpdatedTenant2.displayName, - smsRegionConfig: undefined, - }; - if (authEmulatorHost) { - return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2) - .then((actualTenant) => { - const actualTenantObj = actualTenant.toJSON(); - // Not supported in Auth Emulator - delete (actualTenantObj as {testPhoneNumbers: Record}).testPhoneNumbers; - delete expectedUpdatedTenant2.testPhoneNumbers; - expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant2); - }); - } - return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2) - .then((actualTenant) => { - expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenant2); - }); - }); - it('updateTenant() should be able to enable/disable anon provider', async () => { const tenantManager = getAuth().tenantManager(); let tenant = await tenantManager.createTenant({ diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts deleted file mode 100644 index d06b24fa80..0000000000 --- a/test/unit/auth/project-config-manager.spec.ts +++ /dev/null @@ -1,196 +0,0 @@ -/*! - * Copyright 2022 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -import * as _ from 'lodash'; -import * as chai from 'chai'; -import * as sinon from 'sinon'; -import * as sinonChai from 'sinon-chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import * as mocks from '../../resources/mocks'; -import { FirebaseApp } from '../../../src/app/firebase-app'; -import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; -import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; -import { ProjectConfigManager } from '../../../src/auth/project-config-manager'; -import { - ProjectConfig, - ProjectConfigServerResponse, - UpdateProjectConfigRequest -} from '../../../src/auth/project-config'; - -chai.should(); -chai.use(sinonChai); -chai.use(chaiAsPromised); - -const expect = chai.expect; - -describe('ProjectConfigManager', () => { - let mockApp: FirebaseApp; - let projectConfigManager: ProjectConfigManager; - let nullAccessTokenProjectConfigManager: ProjectConfigManager; - let malformedAccessTokenProjectConfigManager: ProjectConfigManager; - let rejectedPromiseAccessTokenProjectConfigManager: ProjectConfigManager; - const GET_CONFIG_RESPONSE: ProjectConfigServerResponse = { - smsRegionConfig: { - allowlistOnly: { - allowedRegions: [ 'AC', 'AD' ], - }, - }, - }; - - before(() => { - mockApp = mocks.app(); - projectConfigManager = new ProjectConfigManager(mockApp); - nullAccessTokenProjectConfigManager = new ProjectConfigManager( - mocks.appReturningNullAccessToken()); - malformedAccessTokenProjectConfigManager = new ProjectConfigManager( - mocks.appReturningMalformedAccessToken()); - rejectedPromiseAccessTokenProjectConfigManager = new ProjectConfigManager( - mocks.appRejectedWhileFetchingAccessToken()); - }); - - after(() => { - return mockApp.delete(); - }); - - describe('getProjectConfig()', () => { - const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); - // Stubs used to simulate underlying API calls. - let stubs: sinon.SinonStub[] = []; - afterEach(() => { - _.forEach(stubs, (stub) => stub.restore()); - stubs = []; - }); - - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenProjectConfigManager.getProjectConfig() - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); - - it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenProjectConfigManager.getProjectConfig() - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); - - it('should be rejected given an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenProjectConfigManager.getProjectConfig() - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); - - it('should resolve with a Project Config on success', () => { - // Stub getProjectConfig to return expected result. - const stub = sinon.stub(AuthRequestHandler.prototype, 'getProjectConfig') - .returns(Promise.resolve(GET_CONFIG_RESPONSE)); - stubs.push(stub); - return projectConfigManager.getProjectConfig() - .then((result) => { - // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce; - // Confirm expected project config returned. - expect(result).to.deep.equal(expectedProjectConfig); - }); - }); - - it('should throw an error when the backend returns an error', () => { - // Stub getConfig to throw a backend error. - const stub = sinon.stub(AuthRequestHandler.prototype, 'getProjectConfig') - .returns(Promise.reject(expectedError)); - stubs.push(stub); - return projectConfigManager.getProjectConfig() - .then(() => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce; - // Confirm expected error returned. - expect(error).to.equal(expectedError); - }); - }); - }); - - describe('updateProjectConfig()', () => { - const projectConfigOptions: UpdateProjectConfigRequest = { - smsRegionConfig: { - allowByDefault: { - disallowedRegions: [ 'AC', 'AD' ], - }, - }, - }; - const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'Unable to update the config provided.'); - // Stubs used to simulate underlying API calls. - let stubs: sinon.SinonStub[] = []; - afterEach(() => { - _.forEach(stubs, (stub) => stub.restore()); - stubs = []; - }); - - it('should be rejected given no projectConfigOptions', () => { - return (projectConfigManager as any).updateProjectConfig(null as unknown as UpdateProjectConfigRequest) - .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); - }); - - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); - - it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); - - it('should be rejected given an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); - - it('should resolve with a ProjectConfig on updateProjectConfig request success', () => { - // Stub updateProjectConfig to return expected result. - const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateProjectConfig') - .returns(Promise.resolve(GET_CONFIG_RESPONSE)); - stubs.push(updateConfigStub); - return projectConfigManager.updateProjectConfig(projectConfigOptions) - .then((actualProjectConfig) => { - // Confirm underlying API called with expected parameters. - expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(projectConfigOptions); - // Confirm expected Project Config object returned. - expect(actualProjectConfig).to.deep.equal(expectedProjectConfig); - }); - }); - - it('should throw an error when updateProjectConfig returns an error', () => { - // Stub updateProjectConfig to throw a backend error. - const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateProjectConfig') - .returns(Promise.reject(expectedError)); - stubs.push(updateConfigStub); - return projectConfigManager.updateProjectConfig(projectConfigOptions) - .then(() => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(projectConfigOptions); - // Confirm expected error returned. - expect(error).to.equal(expectedError); - }); - }); - }); -}); \ No newline at end of file diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts deleted file mode 100644 index 19cc8f420d..0000000000 --- a/test/unit/auth/project-config.spec.ts +++ /dev/null @@ -1,192 +0,0 @@ -/*! - * Copyright 2022 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as _ from 'lodash'; -import * as chai from 'chai'; -import * as sinonChai from 'sinon-chai'; -import * as chaiAsPromised from 'chai-as-promised'; - -import { deepCopy } from '../../../src/utils/deep-copy'; -import { - ProjectConfig, - ProjectConfigServerResponse, - UpdateProjectConfigRequest, -} from '../../../src/auth/project-config'; - -chai.should(); -chai.use(sinonChai); -chai.use(chaiAsPromised); - -const expect = chai.expect; - -describe('ProjectConfig', () => { - const serverResponse: ProjectConfigServerResponse = { - smsRegionConfig: { - allowByDefault: { - disallowedRegions: [ 'AC', 'AD' ], - }, - }, - }; - - const updateProjectConfigRequest1: UpdateProjectConfigRequest = { - smsRegionConfig: { - allowByDefault: { - disallowedRegions: [ 'AC', 'AD' ], - }, - }, - }; - - const updateProjectConfigRequest2: UpdateProjectConfigRequest = { - smsRegionConfig: { - allowlistOnly: { - allowedRegions: [ 'AC', 'AD' ], - }, - }, - }; - - const updateProjectConfigRequest3: any = { - smsRegionConfig: { - allowlistOnly: { - allowedRegions: [ 'AC', 'AD' ], - }, - allowByDefault: { - disallowedRegions: ['AC', 'AD'], - }, - }, - }; - - describe('buildServerRequest()', () => { - - describe('for an update request', () => { - it('should throw on null SmsRegionConfig attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - configOptionsClientRequest.smsRegionConfig = null; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"SmsRegionConfig" must be a non-null object.'); - }); - - it('should throw on invalid SmsRegionConfig attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - configOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); - }); - - it('should throw on invalid allowlistOnly attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest2) as any; - configOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); - }); - - it('should throw on invalid allowByDefault attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - configOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); - }); - - it('should throw on non-array disallowedRegions attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - configOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); - }); - - it('should throw on non-array allowedRegions attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest2) as any; - configOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); - }); - - it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest3) as any; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); - }); - - it('should not throw on valid client request object', () => { - const configOptionsClientRequest1 = deepCopy(updateProjectConfigRequest1); - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest1); - }).not.to.throw; - const configOptionsClientRequest2 = deepCopy(updateProjectConfigRequest2); - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest2); - }).not.to.throw; - }); - - const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; - nonObjects.forEach((request) => { - it('should throw on invalid UpdateProjectConfigRequest:' + JSON.stringify(request), () => { - expect(() => { - ProjectConfig.buildServerRequest(request as any); - }).to.throw('"UpdateProjectConfigRequest" must be a valid non-null object.'); - }); - }); - - it('should throw on unsupported attribute for update request', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; - configOptionsClientRequest.unsupported = 'value'; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"unsupported" is not a valid UpdateProjectConfigRequest parameter.'); - }); - }); - }); - - describe('constructor', () => { - const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); - const projectConfig = new ProjectConfig(serverResponseCopy); - - it('should not throw on valid initialization', () => { - expect(() => new ProjectConfig(serverResponse)).not.to.throw(); - }); - - it('should set readonly property smsRegionConfig', () => { - const expectedSmsRegionConfig = { - allowByDefault: { - disallowedRegions: [ 'AC', 'AD' ], - }, - }; - expect(projectConfig.smsRegionConfig).to.deep.equal(expectedSmsRegionConfig); - }); - }); - - describe('toJSON()', () => { - const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); - it('should return the expected object representation of project config', () => { - expect(new ProjectConfig(serverResponseCopy).toJSON()).to.deep.equal({ - smsRegionConfig: deepCopy(serverResponse.smsRegionConfig) - }); - }); - - it('should not populate optional fields if not available', () => { - const serverResponseOptionalCopy: ProjectConfigServerResponse = deepCopy(serverResponse); - delete serverResponseOptionalCopy.smsRegionConfig; - - expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({}); - }); - }); -}); \ No newline at end of file diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 4780cbb473..0f14856faa 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -33,18 +33,6 @@ chai.use(chaiAsPromised); const expect = chai.expect; describe('Tenant', () => { - const smsAllowByDefault = { - allowByDefault: { - disallowedRegions: [ 'AC', 'AD' ], - }, - }; - - const smsAllowlistOnly = { - allowlistOnly: { - allowedRegions: [ 'AC', 'AD' ], - }, - }; - const serverRequest: TenantServerResponse = { name: 'projects/project1/tenants/TENANT-ID', displayName: 'TENANT-DISPLAY-NAME', @@ -58,7 +46,6 @@ describe('Tenant', () => { '+16505551234': '019287', '+16505550676': '985235', }, - smsRegionConfig: smsAllowByDefault, }; const clientRequest: UpdateTenantRequest = { @@ -75,7 +62,6 @@ describe('Tenant', () => { '+16505551234': '019287', '+16505550676': '985235', }, - smsRegionConfig: smsAllowByDefault, }; const serverRequestWithoutMfa: TenantServerResponse = { @@ -155,64 +141,6 @@ describe('Tenant', () => { .to.deep.equal(tenantOptionsServerRequest); }); - it('should throw on null SmsRegionConfig attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig = null; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"SmsRegionConfig" must be a non-null object.'); - }); - - it('should throw on invalid SmsRegionConfig attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); - }); - - it('should throw on invalid allowlistOnly attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); - tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); - }); - - it('should throw on invalid allowByDefault attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); - }); - - it('should throw on non-array disallowedRegions attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); - }); - - it('should throw on non-array allowedRegions attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); - tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); - }); - - it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig = { ...smsAllowByDefault, ...smsAllowlistOnly }; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); - }); - it('should not throw on valid client request object', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); expect(() => { @@ -304,64 +232,6 @@ describe('Tenant', () => { }).to.throw('"CreateTenantRequest.testPhoneNumbers" must be a non-null object.'); }); - it('should throw on null SmsRegionConfig attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig = null; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"SmsRegionConfig" must be a non-null object.'); - }); - - it('should throw on invalid SmsRegionConfig attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); - }); - - it('should throw on invalid allowlistOnly attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); - tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); - }); - - it('should throw on invalid allowByDefault attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); - }); - - it('should throw on non-array disallowedRegions attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); - }); - - it('should throw on non-array allowedRegions attribute', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); - tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); - }); - - it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { - const tenantOptionsClientRequest = deepCopy(clientRequest) as any; - tenantOptionsClientRequest.smsRegionConfig = { ...smsAllowByDefault, ...smsAllowlistOnly }; - expect(() => { - Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); - }); - const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { @@ -444,11 +314,6 @@ describe('Tenant', () => { deepCopy(clientRequest.testPhoneNumbers)); }); - it('should set readonly property smsRegionConfig', () => { - expect(tenant.smsRegionConfig).to.deep.equal( - deepCopy(clientRequest.smsRegionConfig)); - }); - it('should throw when no tenant ID is provided', () => { const invalidOptions = deepCopy(serverRequest); // Use resource name that does not include a tenant ID. @@ -487,7 +352,6 @@ describe('Tenant', () => { anonymousSignInEnabled: false, multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), - smsRegionConfig: deepCopy(clientRequest.smsRegionConfig), }); }); @@ -495,7 +359,6 @@ describe('Tenant', () => { const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverRequest); delete serverRequestCopyWithoutMfa.mfaConfig; delete serverRequestCopyWithoutMfa.testPhoneNumbers; - delete serverRequestCopyWithoutMfa.smsRegionConfig; expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({ tenantId: 'TENANT-ID', From f19eb4ffbc9f6378e201823201d984325e152d89 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 3 May 2022 14:36:32 -0400 Subject: [PATCH 16/27] chore: Update App Check to V1 endpoints (#1632) --- src/app-check/app-check-api-client-internal.ts | 4 ++-- src/app-check/token-generator.ts | 2 +- src/app-check/token-verifier.ts | 2 +- test/unit/app-check/app-check-api-client-internal.spec.ts | 8 ++++---- test/unit/app-check/token-generator.spec.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app-check/app-check-api-client-internal.ts b/src/app-check/app-check-api-client-internal.ts index e9ba97ab97..aae736cf63 100644 --- a/src/app-check/app-check-api-client-internal.ts +++ b/src/app-check/app-check-api-client-internal.ts @@ -26,7 +26,7 @@ import * as validator from '../utils/validator'; import { AppCheckToken } from './app-check-api' // App Check backend constants -const FIREBASE_APP_CHECK_V1_API_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1beta/projects/{projectId}/apps/{appId}:exchangeCustomToken'; +const FIREBASE_APP_CHECK_V1_API_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken'; const FIREBASE_APP_CHECK_CONFIG_HEADERS = { 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}` @@ -144,7 +144,7 @@ export class AppCheckApiClient { * @returns An AppCheckToken instance. */ private toAppCheckToken(resp: HttpResponse): AppCheckToken { - const token = resp.data.attestationToken; + const token = resp.data.token; // `ttl` is a string with the suffix "s" preceded by the number of seconds, // with nanoseconds expressed as fractional seconds. const ttlMillis = this.stringToMilliseconds(resp.data.ttl); diff --git a/src/app-check/token-generator.ts b/src/app-check/token-generator.ts index c2a76c8640..2cfe3ca14c 100644 --- a/src/app-check/token-generator.ts +++ b/src/app-check/token-generator.ts @@ -31,7 +31,7 @@ const ONE_MINUTE_IN_MILLIS = ONE_MINUTE_IN_SECONDS * 1000; const ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000; // Audience to use for Firebase App Check Custom tokens -const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1beta.TokenExchangeService'; +const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService'; /** * Class for generating Firebase App Check tokens. diff --git a/src/app-check/token-verifier.ts b/src/app-check/token-verifier.ts index 313d7945d3..6c6503f05d 100644 --- a/src/app-check/token-verifier.ts +++ b/src/app-check/token-verifier.ts @@ -26,7 +26,7 @@ import { DecodedAppCheckToken } from './app-check-api' import { App } from '../app'; const APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/'; -const JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1beta/jwks'; +const JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1/jwks'; /** * Class for verifying Firebase App Check tokens. diff --git a/test/unit/app-check/app-check-api-client-internal.spec.ts b/test/unit/app-check/app-check-api-client-internal.spec.ts index b846fd1c68..65bc64d69d 100644 --- a/test/unit/app-check/app-check-api-client-internal.spec.ts +++ b/test/unit/app-check/app-check-api-client-internal.spec.ts @@ -56,7 +56,7 @@ describe('AppCheckApiClient', () => { const TEST_TOKEN_TO_EXCHANGE = 'signed-custom-token'; const TEST_RESPONSE = { - attestationToken: 'token', + token: 'token', ttl: '3s' }; @@ -203,11 +203,11 @@ describe('AppCheckApiClient', () => { stubs.push(stub); return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) .then((resp) => { - expect(resp.token).to.deep.equal(TEST_RESPONSE.attestationToken); + expect(resp.token).to.deep.equal(TEST_RESPONSE.token); expect(resp.ttlMillis).to.deep.equal(3000); expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'POST', - url: `https://firebaseappcheck.googleapis.com/v1beta/projects/test-project/apps/${APP_ID}:exchangeCustomToken`, + url: `https://firebaseappcheck.googleapis.com/v1/projects/test-project/apps/${APP_ID}:exchangeCustomToken`, headers: EXPECTED_HEADERS, data: { customToken: TEST_TOKEN_TO_EXCHANGE } }); @@ -229,7 +229,7 @@ describe('AppCheckApiClient', () => { stubs.push(stub); return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) .then((resp) => { - expect(resp.token).to.deep.equal(response.attestationToken); + expect(resp.token).to.deep.equal(response.token); expect(resp.ttlMillis).to.deep.equal(ttlMillis); }); }); diff --git a/test/unit/app-check/token-generator.spec.ts b/test/unit/app-check/token-generator.spec.ts index 7fea6b8594..f892c9fa08 100644 --- a/test/unit/app-check/token-generator.spec.ts +++ b/test/unit/app-check/token-generator.spec.ts @@ -44,7 +44,7 @@ const expect = chai.expect; const ALGORITHM = 'RS256'; const FIVE_MIN_IN_SECONDS = 5 * 60; -const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1beta.TokenExchangeService'; +const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService'; /** * Verifies a token is signed with the private key corresponding to the provided public key. From 3f21d0939d1756e8ecbc4786a5ea32c49b04fae5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 May 2022 14:48:58 -0400 Subject: [PATCH 17/27] build(deps-dev): bump @typescript-eslint/parser from 5.21.0 to 5.22.0 (#1677) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.21.0 to 5.22.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.22.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lahiru Maramba --- package-lock.json | 64 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ea034749e..86c404f9fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1554,15 +1554,67 @@ } }, "@typescript-eslint/parser": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.21.0.tgz", - "integrity": "sha512-8RUwTO77hstXUr3pZoWZbRQUxXcSXafZ8/5gpnQCfXvgmP9gpNlRGlWzvfbEQ14TLjmtU8eGnONkff8U2ui2Eg==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.22.0.tgz", + "integrity": "sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.21.0", - "@typescript-eslint/types": "5.21.0", - "@typescript-eslint/typescript-estree": "5.21.0", + "@typescript-eslint/scope-manager": "5.22.0", + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/typescript-estree": "5.22.0", "debug": "^4.3.2" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", + "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/visitor-keys": "5.22.0" + } + }, + "@typescript-eslint/types": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", + "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz", + "integrity": "sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/visitor-keys": "5.22.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", + "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.22.0", + "eslint-visitor-keys": "^3.0.0" + } + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "@typescript-eslint/scope-manager": { From 727684e09904bb6013183998b213746bfd4002a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 May 2022 14:53:58 -0400 Subject: [PATCH 18/27] build(deps): bump @types/node from 17.0.27 to 17.0.31 (#1678) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 17.0.27 to 17.0.31. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86c404f9fd..02ff05a5fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1449,9 +1449,9 @@ } }, "@types/node": { - "version": "17.0.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.27.tgz", - "integrity": "sha512-4/Ke7bbWOasuT3kceBZFGakP1dYN2XFd8v2l9bqF2LNWrmeU07JLpp56aEeG6+Q3olqO5TvXpW0yaiYnZJ5CXg==" + "version": "17.0.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz", + "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==" }, "@types/qs": { "version": "6.9.7", From decb472857fe6da2a340624f6eda64815f71635b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 May 2022 16:05:20 -0400 Subject: [PATCH 19/27] build(deps): bump @google-cloud/storage from 5.19.3 to 5.19.4 (#1679) Bumps [@google-cloud/storage](https://github.com/googleapis/nodejs-storage) from 5.19.3 to 5.19.4. - [Release notes](https://github.com/googleapis/nodejs-storage/releases) - [Changelog](https://github.com/googleapis/nodejs-storage/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/nodejs-storage/compare/v5.19.3...v5.19.4) --- updated-dependencies: - dependency-name: "@google-cloud/storage" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 02ff05a5fe..a6fea337ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -644,9 +644,9 @@ "optional": true }, "@google-cloud/storage": { - "version": "5.19.3", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.19.3.tgz", - "integrity": "sha512-l+8X0BoA7rg9jyZaS4p2DwMg1Ivju+VAL6PeQZE1u2q52LQ0KemrZmdQWhtrplHYo8UdYtqpbj4A6Fc5fKDZdg==", + "version": "5.19.4", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.19.4.tgz", + "integrity": "sha512-Jz7ugcPHhsEmMVvIxM9uoBsdEbKIYwDkh3u07tifsIymEWs47F4/D6+/Tv/W7kLhznqjyOjVJ/0frtBeIC0lJA==", "optional": true, "requires": { "@google-cloud/paginator": "^3.0.7", From 517a589c7ad8e6f4991434fef8b4156b545fe718 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 May 2022 16:55:41 -0400 Subject: [PATCH 20/27] build(deps-dev): bump @firebase/app-compat from 0.1.22 to 0.1.23 (#1680) Bumps [@firebase/app-compat](https://github.com/firebase/firebase-js-sdk/tree/HEAD/packages/app-compat) from 0.1.22 to 0.1.23. - [Release notes](https://github.com/firebase/firebase-js-sdk/releases) - [Changelog](https://github.com/firebase/firebase-js-sdk/blob/master/packages/app-compat/CHANGELOG.md) - [Commits](https://github.com/firebase/firebase-js-sdk/commits/@firebase/app-compat@0.1.23/packages/app-compat) --- updated-dependencies: - dependency-name: "@firebase/app-compat" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 56 ++++++----------------------------------------- 1 file changed, 7 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6fea337ee..6c281fef6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -374,70 +374,28 @@ } }, "@firebase/app": { - "version": "0.7.21", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.21.tgz", - "integrity": "sha512-b1COyw4HwajJ4zQCtL7w+d4GCQDmEaVO957eLLlBwz4QuDlx3eQIirpQhzkkPH17BJFZ6x0qyYEt6Wbhakn0kg==", + "version": "0.7.22", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.22.tgz", + "integrity": "sha512-v3AXSCwAvZyIFzOGgPAYtzjltm1M9R4U4yqsIBPf5B4ryaT1EGK+3ETZUOckNl5y2YwdKRJVPDDore+B2xg0Ug==", "dev": true, "requires": { "@firebase/component": "0.5.13", "@firebase/logger": "0.3.2", "@firebase/util": "1.5.2", "tslib": "^2.1.0" - }, - "dependencies": { - "@firebase/component": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", - "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", - "dev": true, - "requires": { - "@firebase/util": "1.5.2", - "tslib": "^2.1.0" - } - }, - "@firebase/util": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", - "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - } } }, "@firebase/app-compat": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.22.tgz", - "integrity": "sha512-InzQWdIKXsioZb6Ll/uynvopFbq9k3Qpi3gEUq+f8q0yr8/KQVuH2lIDmN70z11LRKXlsziU49qRwtV9tcEYhA==", + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.23.tgz", + "integrity": "sha512-c0QOhU2UVxZ7N5++nLQgKZ899ZC8+/ESa8VCzsQDwBw1T3MFAD1cG40KhB+CGtp/uYk/w6Jtk8k0xyZu6O2LOg==", "dev": true, "requires": { - "@firebase/app": "0.7.21", + "@firebase/app": "0.7.22", "@firebase/component": "0.5.13", "@firebase/logger": "0.3.2", "@firebase/util": "1.5.2", "tslib": "^2.1.0" - }, - "dependencies": { - "@firebase/component": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", - "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", - "dev": true, - "requires": { - "@firebase/util": "1.5.2", - "tslib": "^2.1.0" - } - }, - "@firebase/util": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", - "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - } } }, "@firebase/app-types": { From 844479a00860f11cbf5469115a99636a70684f82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 May 2022 11:50:46 -0400 Subject: [PATCH 21/27] build(deps-dev): bump mocha from 9.2.2 to 10.0.0 (#1681) Bumps [mocha](https://github.com/mochajs/mocha) from 9.2.2 to 10.0.0. - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v9.2.2...v10.0.0) --- updated-dependencies: - dependency-name: mocha dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 77 ++++++++++++++++------------------------------- package.json | 2 +- 2 files changed, 27 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c281fef6c..d6a48e21cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4562,12 +4562,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, "gtoken": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", @@ -6394,32 +6388,30 @@ "dev": true }, "mocha": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", - "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", + "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.3", - "debug": "4.3.3", + "debug": "4.3.4", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", "glob": "7.2.0", - "growl": "1.10.5", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", - "minimatch": "4.2.1", + "minimatch": "5.0.1", "ms": "2.1.3", - "nanoid": "3.3.1", + "nanoid": "3.3.3", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", + "workerpool": "6.2.1", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" @@ -6453,6 +6445,15 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -6469,23 +6470,6 @@ "readdirp": "~3.6.0" } }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6522,12 +6506,12 @@ } }, "minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" } }, "ms": { @@ -6560,15 +6544,6 @@ "has-flag": "^4.0.0" } }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -6635,9 +6610,9 @@ "optional": true }, "nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", "dev": true }, "nanomatch": { @@ -9652,9 +9627,9 @@ "dev": true }, "workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", "dev": true }, "wrap-ansi": { diff --git a/package.json b/package.json index d53cdb03ec..1a4d967a34 100644 --- a/package.json +++ b/package.json @@ -207,7 +207,7 @@ "http-message-parser": "^0.0.34", "lodash": "^4.17.15", "minimist": "^1.2.6", - "mocha": "^9.1.2", + "mocha": "^10.0.0", "mz": "^2.7.0", "nock": "^13.0.0", "npm-run-all": "^4.1.5", From b4b5e03afb4e1a429562450c1564c69efff6bfc3 Mon Sep 17 00:00:00 2001 From: Pavel Jbanov Date: Wed, 4 May 2022 14:08:02 -0400 Subject: [PATCH 22/27] Implemented eventarc event publishing API (#1617) (#1644) --- entrypoints.json | 4 + etc/firebase-admin.eventarc.api.md | 51 ++ package-lock.json | 1010 ++++++++++++--------- package.json | 12 +- src/eventarc/cloudevent.ts | 95 ++ src/eventarc/eventarc-client-internal.ts | 156 ++++ src/eventarc/eventarc-utils.ts | 138 +++ src/eventarc/eventarc.ts | 191 ++++ src/eventarc/index.ts | 65 ++ test/unit/eventarc/eventarc-utils.spec.ts | 193 ++++ test/unit/eventarc/eventarc.spec.ts | 572 ++++++++++++ test/unit/index.spec.ts | 4 + 12 files changed, 2049 insertions(+), 442 deletions(-) create mode 100644 etc/firebase-admin.eventarc.api.md create mode 100644 src/eventarc/cloudevent.ts create mode 100644 src/eventarc/eventarc-client-internal.ts create mode 100644 src/eventarc/eventarc-utils.ts create mode 100644 src/eventarc/eventarc.ts create mode 100644 src/eventarc/index.ts create mode 100644 test/unit/eventarc/eventarc-utils.spec.ts create mode 100644 test/unit/eventarc/eventarc.spec.ts diff --git a/entrypoints.json b/entrypoints.json index 975db81888..55f2d766b2 100644 --- a/entrypoints.json +++ b/entrypoints.json @@ -55,5 +55,9 @@ "firebase-admin/remote-config": { "typings": "./lib/remote-config/index.d.ts", "dist": "./lib/remote-config/index.js" + }, + "firebase-admin/eventarc": { + "typings": "./lib/eventarc/index.d.ts", + "dist": "./lib/eventarc/index.js" } } diff --git a/etc/firebase-admin.eventarc.api.md b/etc/firebase-admin.eventarc.api.md new file mode 100644 index 0000000000..a070f8b391 --- /dev/null +++ b/etc/firebase-admin.eventarc.api.md @@ -0,0 +1,51 @@ +## API Report File for "firebase-admin.eventarc" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export class Channel { + readonly allowedEventTypes?: string[]; + get eventarc(): Eventarc; + get name(): string; + publish(events: CloudEvent | CloudEvent[]): Promise; +} + +// @public +export interface ChannelOptions { + allowedEventTypes?: string[] | string | undefined; +} + +// @public +export interface CloudEvent { + [key: string]: any; + data?: object | string; + datacontenttype?: string; + id?: string; + source?: string; + specversion?: CloudEventVersion; + subject?: string; + time?: string; + type: string; +} + +// @public +export type CloudEventVersion = '1.0'; + +// @public +export class Eventarc { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts + get app(): App; + channel(name: string, options?: ChannelOptions): Channel; + channel(options?: ChannelOptions): Channel; +} + +// @public +export function getEventarc(app?: App): Eventarc; + +``` diff --git a/package-lock.json b/package-lock.json index d6a48e21cf..bb4447ca91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,60 +4,59 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@ampproject/remapping": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", - "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.0" - } - }, - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, "@babel/compat-data": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz", - "integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.8.tgz", + "integrity": "sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q==", "dev": true }, "@babel/core": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz", - "integrity": "sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.7.tgz", + "integrity": "sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==", "dev": true, "requires": { - "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.7", - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.8", - "@babel/parser": "^7.17.8", + "@babel/generator": "^7.16.7", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helpers": "^7.16.7", + "@babel/parser": "^7.16.7", "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0", + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", - "semver": "^6.3.0" + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } } }, "@babel/generator": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", - "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz", + "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==", "dev": true, "requires": { - "@babel/types": "^7.17.0", + "@babel/types": "^7.16.8", "jsesc": "^2.5.1", "source-map": "^0.5.0" }, @@ -71,12 +70,12 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz", - "integrity": "sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", + "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", "dev": true, "requires": { - "@babel/compat-data": "^7.17.7", + "@babel/compat-data": "^7.16.4", "@babel/helper-validator-option": "^7.16.7", "browserslist": "^4.17.5", "semver": "^6.3.0" @@ -130,28 +129,28 @@ } }, "@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", + "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-simple-access": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7", "@babel/helper-validator-identifier": "^7.16.7", "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" } }, "@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", + "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", "dev": true, "requires": { - "@babel/types": "^7.17.0" + "@babel/types": "^7.16.7" } }, "@babel/helper-split-export-declaration": { @@ -176,20 +175,20 @@ "dev": true }, "@babel/helpers": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.8.tgz", - "integrity": "sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.7.tgz", + "integrity": "sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==", "dev": true, "requires": { "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" } }, "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz", + "integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.16.7", @@ -256,9 +255,9 @@ } }, "@babel/parser": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz", - "integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", + "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==", "dev": true }, "@babel/template": { @@ -270,26 +269,46 @@ "@babel/code-frame": "^7.16.7", "@babel/parser": "^7.16.7", "@babel/types": "^7.16.7" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + } } }, "@babel/traverse": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", - "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.8.tgz", + "integrity": "sha512-xe+H7JlvKsDQwXRsBhSnq1/+9c+LlQcCK3Tn/l5sbx02HYns/cn7ibp9+RV1sIUqu7hKg91NWsgHurO9dowITQ==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", + "@babel/generator": "^7.16.8", "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-function-name": "^7.16.7", "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.3", - "@babel/types": "^7.17.0", + "@babel/parser": "^7.16.8", + "@babel/types": "^7.16.8", "debug": "^4.1.0", "globals": "^11.1.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -299,9 +318,9 @@ } }, "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz", + "integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.16.7", @@ -374,28 +393,71 @@ } }, "@firebase/app": { - "version": "0.7.22", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.22.tgz", - "integrity": "sha512-v3AXSCwAvZyIFzOGgPAYtzjltm1M9R4U4yqsIBPf5B4ryaT1EGK+3ETZUOckNl5y2YwdKRJVPDDore+B2xg0Ug==", + "version": "0.7.18", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.18.tgz", + "integrity": "sha512-jomDaPaEQEWfFUqvxQw4TYSs2gCT2BN0Ec1//3CdMsc1NcppduS31bxsjhn3KdPbtx4opkaZ2FcA+buHtdw9dw==", "dev": true, "requires": { - "@firebase/component": "0.5.13", + "@firebase/component": "0.5.10", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.2", + "@firebase/util": "1.4.3", + "idb": "3.0.2", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.10.tgz", + "integrity": "sha512-mzUpg6rsBbdQJvAdu1rNWabU3O7qdd+B+/ubE1b+pTbBKfw5ySRpRRE6sKcZ/oQuwLh0HHB6FRJHcylmI7jDzA==", + "dev": true, + "requires": { + "@firebase/util": "1.4.3", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.3.tgz", + "integrity": "sha512-gQJl6r0a+MElLQEyU8Dx0kkC2coPj67f/zKZrGR7z7WpLgVanhaCUqEsptwpwoxi9RMFIaebleG+C9xxoARq+Q==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/app-compat": { - "version": "0.1.23", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.23.tgz", - "integrity": "sha512-c0QOhU2UVxZ7N5++nLQgKZ899ZC8+/ESa8VCzsQDwBw1T3MFAD1cG40KhB+CGtp/uYk/w6Jtk8k0xyZu6O2LOg==", + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.19.tgz", + "integrity": "sha512-a0TgAXcjF3htSdi10mRwAks1+73nwbmSMXzjlOQDYJ8t3HE7FvHxfB4hjuwHKfgr3MWZjcarsGKVr7LWhUAE8w==", "dev": true, "requires": { - "@firebase/app": "0.7.22", - "@firebase/component": "0.5.13", + "@firebase/app": "0.7.18", + "@firebase/component": "0.5.10", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.2", + "@firebase/util": "1.4.3", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.10.tgz", + "integrity": "sha512-mzUpg6rsBbdQJvAdu1rNWabU3O7qdd+B+/ubE1b+pTbBKfw5ySRpRRE6sKcZ/oQuwLh0HHB6FRJHcylmI7jDzA==", + "dev": true, + "requires": { + "@firebase/util": "1.4.3", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.3.tgz", + "integrity": "sha512-gQJl6r0a+MElLQEyU8Dx0kkC2coPj67f/zKZrGR7z7WpLgVanhaCUqEsptwpwoxi9RMFIaebleG+C9xxoARq+Q==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/app-types": { @@ -404,14 +466,14 @@ "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" }, "@firebase/auth": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.19.12.tgz", - "integrity": "sha512-39/eJBmq5Ne+HoCJuQXlhaOH2e8qySxYUa5Z25mhcam8nmAMrBh7Ph1yZjUeSfLsSJiSXANMHK5dnVE+1TROXw==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.19.8.tgz", + "integrity": "sha512-pU3U8k70gXDYHjrIDlxnnfPkt6Eq1/61KikF7aps1ny8xmSyeUhbXxUbl2pvX5k7eK8uVQvm4uWFlPNJWMitww==", "dev": true, "requires": { - "@firebase/component": "0.5.13", + "@firebase/component": "0.5.10", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.2", + "@firebase/util": "1.4.3", "node-fetch": "2.6.7", "selenium-webdriver": "4.0.0-rc-1", "tslib": "^2.1.0" @@ -432,15 +494,15 @@ } }, "@firebase/auth-compat": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.2.12.tgz", - "integrity": "sha512-LKeKylktRj03xgW5ilSOW1c4AsMig15ogf5hDKa820t6Bp6MNabj8yq2TV0/Q4SP4Ox/yrTISJGVvk+TJuBecQ==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.2.8.tgz", + "integrity": "sha512-6gG8agS3LlSxnyObZ7TR1Ze41cJargpP+rGTuBz0WiOvrFcrMoZUjv+5oA5VvF2GiYVMvAzJImxmgYJhMse+GA==", "dev": true, "requires": { - "@firebase/auth": "0.19.12", + "@firebase/auth": "0.19.8", "@firebase/auth-types": "0.11.0", - "@firebase/component": "0.5.13", - "@firebase/util": "1.5.2", + "@firebase/component": "0.5.10", + "@firebase/util": "1.4.3", "node-fetch": "2.6.7", "selenium-webdriver": "^4.0.0-beta.2", "tslib": "^2.1.0" @@ -458,12 +520,12 @@ "dev": true }, "@firebase/component": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", - "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.10.tgz", + "integrity": "sha512-mzUpg6rsBbdQJvAdu1rNWabU3O7qdd+B+/ubE1b+pTbBKfw5ySRpRRE6sKcZ/oQuwLh0HHB6FRJHcylmI7jDzA==", "dev": true, "requires": { - "@firebase/util": "1.5.2", + "@firebase/util": "1.4.3", "tslib": "^2.1.0" } }, @@ -559,9 +621,9 @@ } }, "@firebase/util": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", - "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.3.tgz", + "integrity": "sha512-gQJl6r0a+MElLQEyU8Dx0kkC2coPj67f/zKZrGR7z7WpLgVanhaCUqEsptwpwoxi9RMFIaebleG+C9xxoARq+Q==", "dev": true, "requires": { "tslib": "^2.1.0" @@ -635,9 +697,9 @@ } }, "@grpc/grpc-js": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.5.10.tgz", - "integrity": "sha512-++oAubX/7rJzlqH0ShyzDENNNDHYrlttdc3NM40KlaVQDcgGqQknuPoavmyTC+oNUDyxPCX5dHceKhfcgN3tiw==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.5.2.tgz", + "integrity": "sha512-JlBkWqm2qVtZTg6OQU9g0o9C3jR6Us0TekZlTVCESxq5wUbFUjrW5GijXPDpwLqdmabCRJ0xm9Ayyj+b9T9pow==", "optional": true, "requires": { "@grpc/proto-loader": "^0.6.4", @@ -777,38 +839,16 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, - "@jridgewell/resolve-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", - "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", - "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "@mapbox/node-pre-gyp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz", - "integrity": "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz", + "integrity": "sha512-CMGKi28CF+qlbXh26hDe6NxCd7amqeAzEqnS6IHeO6LoaKyM/n+Xw3HT1COdq8cuioOdlKdqn/hCmqPUOMOywg==", "dev": true, "requires": { - "detect-libc": "^2.0.0", + "detect-libc": "^1.0.3", "https-proxy-agent": "^5.0.0", "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.6.5", "nopt": "^5.0.0", "npmlog": "^5.0.1", "rimraf": "^3.0.2", @@ -828,35 +868,35 @@ } }, "@microsoft/api-extractor": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.23.0.tgz", - "integrity": "sha512-fbdX05RVE1EMA7nvyRHuS9nx1pryhjgURDx6pQlE/9yOXQ5PO7MpYdfWGaRsQwyYuU3+tPxgro819c0R3AK6KA==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.19.4.tgz", + "integrity": "sha512-iehC6YA3DGJvxTUaK7HUtQmP6hAQU07+Q/OR8TG4dVR6KpqCi9UPEVk8AgCvQkiK+6FbVEFQTx0qLuYk4EeuHg==", "dev": true, "requires": { - "@microsoft/api-extractor-model": "7.17.2", - "@microsoft/tsdoc": "0.14.1", - "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.45.4", - "@rushstack/rig-package": "0.3.11", - "@rushstack/ts-command-line": "4.10.10", + "@microsoft/api-extractor-model": "7.15.3", + "@microsoft/tsdoc": "0.13.2", + "@microsoft/tsdoc-config": "~0.15.2", + "@rushstack/node-core-library": "3.45.0", + "@rushstack/rig-package": "0.3.7", + "@rushstack/ts-command-line": "4.10.6", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.17.0", "semver": "~7.3.0", "source-map": "~0.6.1", - "typescript": "~4.6.3" + "typescript": "~4.5.2" }, "dependencies": { "@microsoft/tsdoc": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz", - "integrity": "sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz", + "integrity": "sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg==", "dev": true }, "@rushstack/node-core-library": { - "version": "3.45.4", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.4.tgz", - "integrity": "sha512-FMoEQWjK7nWAO2uFgV1eVpVhY9ZDGOdIIomi9zTej64cKJ+8/Nvu+ny0xKaUDEjw/ALftN2D2ml7L0RDpW/Z9g==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.0.tgz", + "integrity": "sha512-YMuIJl19vQT1+g/OU9mLY6T5ZBT9uDlmeXExDQACpGuxTJW+LHNbk/lRX+eCApQI2eLBlaL4U68r3kZlqwbdmw==", "dev": true, "requires": { "@types/node": "12.20.24", @@ -871,9 +911,9 @@ } }, "@rushstack/ts-command-line": { - "version": "4.10.10", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.10.tgz", - "integrity": "sha512-F+MH7InPDXqX40qvvcEsnvPpmg566SBpfFqj2fcCh8RjM6AyOoWlXc8zx7giBD3ZN85NVAEjZAgrcLU0z+R2yg==", + "version": "4.10.6", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.6.tgz", + "integrity": "sha512-Y3GkUag39sTIlukDg9mUp8MCHrrlJ27POrBNRQGc/uF+VVgX8M7zMzHch5zP6O1QVquWgD7Engdpn2piPYaS/g==", "dev": true, "requires": { "@types/argparse": "1.0.38", @@ -889,18 +929,18 @@ "dev": true }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" } }, "typescript": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", - "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", + "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", "dev": true }, "validator": { @@ -910,12 +950,12 @@ "dev": true }, "z-schema": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.3.tgz", - "integrity": "sha512-sGvEcBOTNum68x9jCpCVGPFJ6mWnkD0YxOcddDlJHRx3tKdB2q8pCHExMVZo/AV/6geuVJXG7hljDaWG8+5GDw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.2.tgz", + "integrity": "sha512-40TH47ukMHq5HrzkeVE40Ad7eIDKaRV2b+Qpi2prLc9X9eFJFzV7tMe5aH12e6avaSS/u5l653EQOv+J9PirPw==", "dev": true, "requires": { - "commander": "^2.20.3", + "commander": "^2.7.1", "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", "validator": "^13.7.0" @@ -924,26 +964,26 @@ } }, "@microsoft/api-extractor-model": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.17.2.tgz", - "integrity": "sha512-fYfCeBeLm7jnZligC64qHiH4/vzswFLDfyPpX+uKO36OI2kIeMHrYG0zaezmuinKvE4vg1dAz38zZeDbPvBKGg==", + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.15.3.tgz", + "integrity": "sha512-NkSjolmSI7NGvbdz0Y7kjQfdpD+j9E5CwXTxEyjDqxd10MI7GXV8DnAsQ57GFJcgHKgTjf2aUnYfMJ9w3aMicw==", "dev": true, "requires": { - "@microsoft/tsdoc": "0.14.1", - "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.45.4" + "@microsoft/tsdoc": "0.13.2", + "@microsoft/tsdoc-config": "~0.15.2", + "@rushstack/node-core-library": "3.45.0" }, "dependencies": { "@microsoft/tsdoc": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz", - "integrity": "sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz", + "integrity": "sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg==", "dev": true }, "@rushstack/node-core-library": { - "version": "3.45.4", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.4.tgz", - "integrity": "sha512-FMoEQWjK7nWAO2uFgV1eVpVhY9ZDGOdIIomi9zTej64cKJ+8/Nvu+ny0xKaUDEjw/ALftN2D2ml7L0RDpW/Z9g==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.0.tgz", + "integrity": "sha512-YMuIJl19vQT1+g/OU9mLY6T5ZBT9uDlmeXExDQACpGuxTJW+LHNbk/lRX+eCApQI2eLBlaL4U68r3kZlqwbdmw==", "dev": true, "requires": { "@types/node": "12.20.24", @@ -964,9 +1004,9 @@ "dev": true }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -979,12 +1019,12 @@ "dev": true }, "z-schema": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.3.tgz", - "integrity": "sha512-sGvEcBOTNum68x9jCpCVGPFJ6mWnkD0YxOcddDlJHRx3tKdB2q8pCHExMVZo/AV/6geuVJXG7hljDaWG8+5GDw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.2.tgz", + "integrity": "sha512-40TH47ukMHq5HrzkeVE40Ad7eIDKaRV2b+Qpi2prLc9X9eFJFzV7tMe5aH12e6avaSS/u5l653EQOv+J9PirPw==", "dev": true, "requires": { - "commander": "^2.20.3", + "commander": "^2.7.1", "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", "validator": "^13.7.0" @@ -999,21 +1039,21 @@ "dev": true }, "@microsoft/tsdoc-config": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.1.tgz", - "integrity": "sha512-2RqkwiD4uN6MLnHFljqBlZIXlt/SaUT6cuogU1w2ARw4nKuuppSmR0+s+NC+7kXBQykd9zzu0P4HtBpZT5zBpQ==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.15.2.tgz", + "integrity": "sha512-mK19b2wJHSdNf8znXSMYVShAHktVr/ib0Ck2FA3lsVBSEhSI/TfXT7DJQkAYgcztTuwazGcg58ZjYdk0hTCVrA==", "dev": true, "requires": { - "@microsoft/tsdoc": "0.14.1", + "@microsoft/tsdoc": "0.13.2", "ajv": "~6.12.6", "jju": "~1.4.0", "resolve": "~1.19.0" }, "dependencies": { "@microsoft/tsdoc": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz", - "integrity": "sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz", + "integrity": "sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg==", "dev": true }, "resolve": { @@ -1158,9 +1198,9 @@ } }, "@rushstack/rig-package": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.11.tgz", - "integrity": "sha512-uI1/g5oQPtyrT9nStoyX/xgZSLa2b+srRFaDk3r1eqC7zA5th4/bvTGl2QfV3C9NcP+coSqmk5mFJkUfH6i3Lw==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.7.tgz", + "integrity": "sha512-pzMsTSeTC8IiZ6EJLr53gGMvhT4oLWH+hxD7907cHyWuIUlEXFtu/2pK25vUQT13nKp5DJCWxXyYoGRk/h6rtA==", "dev": true, "requires": { "resolve": "~1.17.0", @@ -1189,9 +1229,9 @@ } }, "@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" @@ -1287,9 +1327,9 @@ "dev": true }, "@types/chai-as-promised": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz", - "integrity": "sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz", + "integrity": "sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA==", "dev": true, "requires": { "@types/chai": "*" @@ -1334,9 +1374,9 @@ } }, "@types/express-unless": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.3.tgz", - "integrity": "sha512-TyPLQaF6w8UlWdv4gj8i46B+INBVzURBNRahCozCSXfsK2VTlL1wNyTlMKw817VHygBtlcl5jfnPadlydr06Yw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.2.tgz", + "integrity": "sha512-Q74UyYRX/zIgl1HSp9tUX2PlG8glkVm+59r7aK4KGKzC5jqKIOX6rrVLRQrzpZUQ84VukHtRoeAuon2nIssHPQ==", "requires": { "@types/express": "*" } @@ -1348,9 +1388,9 @@ "dev": true }, "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, "@types/jsonwebtoken": { @@ -1363,9 +1403,9 @@ } }, "@types/lodash": { - "version": "4.14.182", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", - "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", "dev": true }, "@types/long": { @@ -1407,9 +1447,9 @@ } }, "@types/node": { - "version": "17.0.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz", - "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==" + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.10.tgz", + "integrity": "sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==" }, "@types/qs": { "version": "6.9.7", @@ -1453,12 +1493,12 @@ } }, "@types/sinon": { - "version": "10.0.11", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.11.tgz", - "integrity": "sha512-dmZsHlBsKUtBpHriNjlK0ndlvEh8dcb9uV9Afsbt89QIyydpC7NcR+nWlAhASfy3GHnxTl4FX/aKE7XZUt/B4g==", + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.6.tgz", + "integrity": "sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==", "dev": true, "requires": { - "@types/sinonjs__fake-timers": "*" + "@sinonjs/fake-timers": "^7.1.0" } }, "@types/sinon-chai": { @@ -1471,27 +1511,27 @@ "@types/sinon": "*" } }, - "@types/sinonjs__fake-timers": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", - "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", - "dev": true - }, "@types/tough-cookie": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==", "dev": true }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.21.0.tgz", - "integrity": "sha512-fTU85q8v5ZLpoZEyn/u1S2qrFOhi33Edo2CZ0+q1gDaWWm0JuPh3bgOyU8lM0edIEYgKLDkPFiZX2MOupgjlyg==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.11.0.tgz", + "integrity": "sha512-HJh33bgzXe6jGRocOj4FmefD7hRY4itgjzOrSs3JPrTNXsX7j5+nQPciAUj/1nZtwo2kAc3C75jZO+T23gzSGw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.21.0", - "@typescript-eslint/type-utils": "5.21.0", - "@typescript-eslint/utils": "5.21.0", + "@typescript-eslint/scope-manager": "5.11.0", + "@typescript-eslint/type-utils": "5.11.0", + "@typescript-eslint/utils": "5.11.0", "debug": "^4.3.2", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", @@ -1499,75 +1539,37 @@ "semver": "^7.3.5", "tsutils": "^3.21.0" }, - "dependencies": { - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "@typescript-eslint/parser": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.22.0.tgz", - "integrity": "sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.22.0", - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/typescript-estree": "5.22.0", - "debug": "^4.3.2" - }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", - "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.11.0.tgz", + "integrity": "sha512-z+K4LlahDFVMww20t/0zcA7gq/NgOawaLuxgqGRVKS0PiZlCTIUtX0EJbC0BK1JtR4CelmkPK67zuCgpdlF4EA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0" + "@typescript-eslint/types": "5.11.0", + "@typescript-eslint/visitor-keys": "5.11.0" } }, "@typescript-eslint/types": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", - "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.11.0.tgz", + "integrity": "sha512-cxgBFGSRCoBEhvSVLkKw39+kMzUKHlJGVwwMbPcTZX3qEhuXhrjwaZXWMxVfxDgyMm+b5Q5b29Llo2yow8Y7xQ==", "dev": true }, - "@typescript-eslint/typescript-estree": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz", - "integrity": "sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.22.0", - "@typescript-eslint/visitor-keys": "5.22.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "is-glob": "^4.0.3", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - } - }, "@typescript-eslint/visitor-keys": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", - "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.11.0.tgz", + "integrity": "sha512-E8w/vJReMGuloGxJDkpPlGwhxocxOpSVgSvjiLO5IxZPmxZF30weOeJYyPSEACwM+X4NziYS9q+WkN/2DHYQwA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/types": "5.11.0", "eslint-visitor-keys": "^3.0.0" } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -1575,41 +1577,53 @@ } } }, + "@typescript-eslint/parser": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.12.0.tgz", + "integrity": "sha512-MfSwg9JMBojMUoGjUmX+D2stoQj1CBYTCP0qnnVtu9A+YQXVKNtLjasYh+jozOcrb/wau8TCfWOkQTiOAruBog==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.12.0", + "@typescript-eslint/types": "5.12.0", + "@typescript-eslint/typescript-estree": "5.12.0", + "debug": "^4.3.2" + } + }, "@typescript-eslint/scope-manager": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.21.0.tgz", - "integrity": "sha512-XTX0g0IhvzcH/e3393SvjRCfYQxgxtYzL3UREteUneo72EFlt7UNoiYnikUtmGVobTbhUDByhJ4xRBNe+34kOQ==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.12.0.tgz", + "integrity": "sha512-GAMobtIJI8FGf1sLlUWNUm2IOkIjvn7laFWyRx7CLrv6nLBI7su+B7lbStqVlK5NdLvHRFiJo2HhiDF7Ki01WQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.21.0", - "@typescript-eslint/visitor-keys": "5.21.0" + "@typescript-eslint/types": "5.12.0", + "@typescript-eslint/visitor-keys": "5.12.0" } }, "@typescript-eslint/type-utils": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.21.0.tgz", - "integrity": "sha512-MxmLZj0tkGlkcZCSE17ORaHl8Th3JQwBzyXL/uvC6sNmu128LsgjTX0NIzy+wdH2J7Pd02GN8FaoudJntFvSOw==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.11.0.tgz", + "integrity": "sha512-wDqdsYO6ofLaD4DsGZ0jGwxp4HrzD2YKulpEZXmgN3xo4BHJwf7kq49JTRpV0Gx6bxkSUmc9s0EIK1xPbFFpIA==", "dev": true, "requires": { - "@typescript-eslint/utils": "5.21.0", + "@typescript-eslint/utils": "5.11.0", "debug": "^4.3.2", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.21.0.tgz", - "integrity": "sha512-XnOOo5Wc2cBlq8Lh5WNvAgHzpjnEzxn4CJBwGkcau7b/tZ556qrWXQz4DJyChYg8JZAD06kczrdgFPpEQZfDsA==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.12.0.tgz", + "integrity": "sha512-JowqbwPf93nvf8fZn5XrPGFBdIK8+yx5UEGs2QFAYFI8IWYfrzz+6zqlurGr2ctShMaJxqwsqmra3WXWjH1nRQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.21.0.tgz", - "integrity": "sha512-Y8Y2T2FNvm08qlcoSMoNchh9y2Uj3QmjtwNMdRQkcFG7Muz//wfJBGBxh8R7HAGQFpgYpdHqUpEoPQk+q9Kjfg==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.12.0.tgz", + "integrity": "sha512-Dd9gVeOqt38QHR0BEA8oRaT65WYqPYbIc5tRFQPkfLquVEFPD1HAtbZT98TLBkEcCkvwDYOAvuSvAD9DnQhMfQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.21.0", - "@typescript-eslint/visitor-keys": "5.21.0", + "@typescript-eslint/types": "5.12.0", + "@typescript-eslint/visitor-keys": "5.12.0", "debug": "^4.3.2", "globby": "^11.0.4", "is-glob": "^4.0.3", @@ -1618,9 +1632,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -1629,26 +1643,78 @@ } }, "@typescript-eslint/utils": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.21.0.tgz", - "integrity": "sha512-q/emogbND9wry7zxy7VYri+7ydawo2HDZhRZ5k6yggIvXa7PvBbAAZ4PFH/oZLem72ezC4Pr63rJvDK/sTlL8Q==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.11.0.tgz", + "integrity": "sha512-g2I480tFE1iYRDyMhxPAtLQ9HAn0jjBtipgTCZmd9I9s11OV8CTsG+YfFciuNDcHqm4csbAgC2aVZCHzLxMSUw==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.21.0", - "@typescript-eslint/types": "5.21.0", - "@typescript-eslint/typescript-estree": "5.21.0", + "@typescript-eslint/scope-manager": "5.11.0", + "@typescript-eslint/types": "5.11.0", + "@typescript-eslint/typescript-estree": "5.11.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.11.0.tgz", + "integrity": "sha512-z+K4LlahDFVMww20t/0zcA7gq/NgOawaLuxgqGRVKS0PiZlCTIUtX0EJbC0BK1JtR4CelmkPK67zuCgpdlF4EA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.11.0", + "@typescript-eslint/visitor-keys": "5.11.0" + } + }, + "@typescript-eslint/types": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.11.0.tgz", + "integrity": "sha512-cxgBFGSRCoBEhvSVLkKw39+kMzUKHlJGVwwMbPcTZX3qEhuXhrjwaZXWMxVfxDgyMm+b5Q5b29Llo2yow8Y7xQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.11.0.tgz", + "integrity": "sha512-yVH9hKIv3ZN3lw8m/Jy5I4oXO4ZBMqijcXCdA4mY8ull6TPTAoQnKKrcZ0HDXg7Bsl0Unwwx7jcXMuNZc0m4lg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.11.0", + "@typescript-eslint/visitor-keys": "5.11.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.11.0.tgz", + "integrity": "sha512-E8w/vJReMGuloGxJDkpPlGwhxocxOpSVgSvjiLO5IxZPmxZF30weOeJYyPSEACwM+X4NziYS9q+WkN/2DHYQwA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.11.0", + "eslint-visitor-keys": "^3.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "@typescript-eslint/visitor-keys": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.21.0.tgz", - "integrity": "sha512-SX8jNN+iHqAF0riZQMkm7e8+POXa/fXw5cxL+gjpyP+FI+JVNhii53EmQgDAfDcBpFekYSlO0fGytMQwRiMQCA==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.12.0.tgz", + "integrity": "sha512-cFwTlgnMV6TgezQynx2c/4/tx9Tufbuo9LPzmWqyRC3QC4qTGkAG1C6pBr0/4I10PAI/FlYunI3vJjIcu+ZHMg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.21.0", + "@typescript-eslint/types": "5.12.0", "eslint-visitor-keys": "^3.0.0" } }, @@ -1722,13 +1788,10 @@ } }, "ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, - "requires": { - "ansi-wrap": "^0.1.0" - } + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true }, "ansi-cyan": { "version": "0.1.1", @@ -2339,15 +2402,15 @@ "dev": true }, "browserslist": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", + "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", + "caniuse-lite": "^1.0.30001286", + "electron-to-chromium": "^1.4.17", "escalade": "^3.1.1", - "node-releases": "^2.0.2", + "node-releases": "^2.0.1", "picocolors": "^1.0.0" } }, @@ -2431,9 +2494,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001323", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001323.tgz", - "integrity": "sha512-e4BF2RlCVELKx8+RmklSEIVub1TWrmdhvA5kEUueummz1XyySW0DVk+3x9HyhU9MuWTa2BhqLgEuEmUwASAdCA==", + "version": "1.0.30001299", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz", + "integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==", "dev": true }, "caseless": { @@ -2443,16 +2506,15 @@ "dev": true }, "chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", "dev": true, "requires": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", "deep-eql": "^3.0.1", "get-func-name": "^2.0.0", - "loupe": "^2.3.1", "pathval": "^1.1.1", "type-detect": "^4.0.5" } @@ -2466,6 +2528,15 @@ "check-error": "^1.0.2" } }, + "chai-exclude": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chai-exclude/-/chai-exclude-2.1.0.tgz", + "integrity": "sha512-IBnm50Mvl3O1YhPpTgbU8MK0Gw7NHcb18WT2TxGdPKOMtdtZVKLHmQwdvOF7mTlHVQStbXuZKFwkevFtbHjpVg==", + "dev": true, + "requires": { + "fclone": "^1.0.11" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2984,15 +3055,15 @@ } }, "date-and-time": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-2.3.1.tgz", - "integrity": "sha512-OaIRmSJXifwEN21rMVVDs0Kz8uhJ3wWPYd86atkRiqN54liaMQYEbbrgjZQea75YXOBWL4ZFb3rG/waenw1TEg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-2.1.0.tgz", + "integrity": "sha512-X/b2gM7e8zQ7siiE0DhRLeNMpuCkIqec5Jnx4GMk/HWB71a6e5Lz48mH9/GIS/hpLsBRFZfMF1gjXBkY0vq5oA==", "optional": true }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -3149,9 +3220,9 @@ "dev": true }, "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true }, "dicer": { @@ -3236,9 +3307,9 @@ } }, "electron-to-chromium": { - "version": "1.4.103", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz", - "integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg==", + "version": "1.4.44", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.44.tgz", + "integrity": "sha512-tHGWiUUmY7GABK8+DNcr474cnZDTzD8x1736SlDosVH8+/vRJeqfaIBAEHFtMjddz/0T4rKKYsxEc8BwQRdBpw==", "dev": true }, "emoji-regex": { @@ -3270,9 +3341,9 @@ } }, "es-abstract": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.2.tgz", - "integrity": "sha512-gfSBJoZdlL2xRiOCy0g8gLMryhoe1TlimjzU99L/31Z8QEGIhVQI+EWwt5lT+AuU9SnorVupXFqqOGqGfsyO6w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -3281,15 +3352,15 @@ "get-intrinsic": "^1.1.1", "get-symbol-description": "^1.0.0", "has": "^1.0.3", - "has-symbols": "^1.0.3", + "has-symbols": "^1.0.2", "internal-slot": "^1.0.3", "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", + "is-negative-zero": "^2.0.1", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.1", "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", "string.prototype.trimend": "^1.0.4", @@ -3309,14 +3380,14 @@ } }, "es5-ext": { - "version": "0.10.59", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.59.tgz", - "integrity": "sha512-cOgyhW0tIJyQY1Kfw6Kr0viu9ZlUctVchRMZ7R0HiH3dxTSp5zJDLecwxUqPUrGKMsgBI1wd1FL+d9Jxfi4cLw==", + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "dev": true, "requires": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "next-tick": "^1.1.0" + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" } }, "es6-error": { @@ -3645,9 +3716,9 @@ }, "dependencies": { "type": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.6.0.tgz", - "integrity": "sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", + "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", "dev": true } } @@ -3767,9 +3838,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.10.tgz", + "integrity": "sha512-s9nFhFnvR63wls6/kM88kQqDhMu0AfdjqouE2l5GVQPbqLgyFjjU5ry/r2yKsJxpb9Py1EYNqieFrmMaX4v++A==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -3814,6 +3885,12 @@ "websocket-driver": ">=0.5.1" } }, + "fclone": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", + "integrity": "sha1-EOhdo4v+p/xZk0HClu4ddyZu5kA=", + "dev": true + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4019,9 +4096,9 @@ } }, "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, "flush-write-stream": { @@ -4528,9 +4605,9 @@ } }, "google-gax": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.30.1.tgz", - "integrity": "sha512-AR00wrunctUqwKQFl15Yq5bo9NuFLnT0zguZYCf8eAqoOUMbxn9V1L0ONCtV4+P9z7sLu+cjtgl+5b4eRZvktg==", + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.29.4.tgz", + "integrity": "sha512-3o6cByD2fE1yIc6i1gpKMQlJStqlvu8Sa/Ly/HCQ6GPHpltpVfkTT4KVj2YLVa7WTSDoGbsLBDmJ1KfN1uNiRw==", "optional": true, "requires": { "@grpc/grpc-js": "~1.5.0", @@ -4539,11 +4616,11 @@ "abort-controller": "^3.0.0", "duplexify": "^4.0.0", "fast-text-encoding": "^1.0.3", - "google-auth-library": "^7.14.0", + "google-auth-library": "^7.6.1", "is-stream-ended": "^0.1.4", "node-fetch": "^2.6.1", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^0.1.8", + "object-hash": "^2.1.1", + "proto3-json-serializer": "^0.1.7", "protobufjs": "6.11.2", "retry-request": "^4.0.0" } @@ -4563,13 +4640,13 @@ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, "gtoken": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", - "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.1.tgz", + "integrity": "sha512-yqOREjzLHcbzz1UrQoxhBtpk8KjrVhuqPE7od1K2uhyxG2BHjKZetlbLw/SPZak/QqTIQW+addS+EcjqQsZbwQ==", "optional": true, "requires": { "gaxios": "^4.0.0", - "google-p12-pem": "^3.1.3", + "google-p12-pem": "^3.0.3", "jws": "^4.0.0" } }, @@ -4585,6 +4662,15 @@ "vinyl-fs": "^3.0.0" }, "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -4838,9 +4924,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "dev": true }, "has-tostringtag": { @@ -5010,6 +5096,12 @@ "debug": "4" } }, + "idb": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz", + "integrity": "sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==", + "dev": true + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -5545,9 +5637,9 @@ } }, "istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.3.tgz", + "integrity": "sha512-x9LtDVtfm/t1GFiLl3NffC7hz+I1ragvgX1P/Lg1NlIagifZDKUkuuaAxH/qpwj2IuEfD8G2Bs/UKp+sZ/pKkg==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -5643,10 +5735,13 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } }, "jsonfile": { "version": "4.0.0", @@ -5713,9 +5808,9 @@ } }, "jszip": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.9.1.tgz", - "integrity": "sha512-H9A60xPqJ1CuC4Ka6qxzXZeU8aNmgOeP5IFqwJbQQwtu2EUYxota3LdsiZWplF7Wgd9tkAd0mdu36nceSaPuYw==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", + "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", "dev": true, "requires": { "lie": "~3.3.0", @@ -5789,6 +5884,16 @@ "jose": "^2.0.5", "limiter": "^1.1.5", "lru-memoizer": "^2.1.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } } }, "jws": { @@ -6061,15 +6166,6 @@ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", "optional": true }, - "loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", - "dev": true, - "requires": { - "get-func-name": "^2.0.0" - } - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -6298,13 +6394,13 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" + "braces": "^3.0.1", + "picomatch": "^2.2.3" } }, "mime": { @@ -6314,22 +6410,22 @@ "optional": true }, "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" }, "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "requires": { - "mime-db": "1.52.0" + "mime-db": "1.51.0" } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -6417,12 +6513,6 @@ "yargs-unparser": "2.0.0" }, "dependencies": { - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, "anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", @@ -6470,6 +6560,23 @@ "readdirp": "~3.6.0" } }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6641,9 +6748,9 @@ "dev": true }, "next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, "nice-try": { @@ -6666,9 +6773,9 @@ } }, "nock": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.4.tgz", - "integrity": "sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug==", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.2.tgz", + "integrity": "sha512-PcBHuvl9i6zfaJ50A7LS55oU+nFLv8htXIhffJO+FxyfibdZ4jEvd9kTuvkrJireBFIGMZ+oUIRpMK5gU9h//g==", "dev": true, "requires": { "debug": "^4.1.0", @@ -6706,9 +6813,9 @@ } }, "node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", "dev": true }, "node-version": { @@ -7162,9 +7269,9 @@ } }, "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "optional": true }, "object-inspect": { @@ -7596,6 +7703,17 @@ "arr-diff": "^4.0.0", "arr-union": "^3.1.0", "extend-shallow": "^3.0.2" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + } } }, "posix-character-classes": { @@ -7644,9 +7762,9 @@ "dev": true }, "proto3-json-serializer": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.8.tgz", - "integrity": "sha512-ACilkB6s1U1gWnl5jtICpnDai4VCxmI9GFxuEaYdxtDG2oVI3sVFIUsvUZcQbJgtPM6p+zqKbjTKQZp6Y4FpQw==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.7.tgz", + "integrity": "sha512-91Yn0rgRL/diKZABrQIVnOm7k3HttbxfP5nm0xMjHctDbCNqaLkGc6O25bwc5Y7WmpxfUdYfeidbhWoyO1aJfA==", "optional": true, "requires": { "protobufjs": "^6.11.2" @@ -8408,9 +8526,9 @@ } }, "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, "sinon": { "version": "13.0.2", @@ -8424,6 +8542,17 @@ "diff": "^5.0.0", "nise": "^5.1.1", "supports-color": "^7.2.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + } } }, "sinon-chai": { @@ -9085,9 +9214,9 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "ts-node": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", - "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.5.0.tgz", + "integrity": "sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==", "dev": true, "requires": { "@cspotcode/source-map-support": "0.7.0", @@ -9359,8 +9488,7 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "optional": true + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { "version": "2.3.0", @@ -9687,9 +9815,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "17.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", - "integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", + "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", "dev": true, "requires": { "cliui": "^7.0.2", @@ -9702,9 +9830,9 @@ }, "dependencies": { "yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==", "dev": true } } diff --git a/package.json b/package.json index 1a4d967a34..f0cd1c6e3f 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,9 @@ "auth": [ "lib/auth" ], + "eventarc": [ + "lib/eventarc" + ], "database": [ "lib/database" ], @@ -121,6 +124,10 @@ "require": "./lib/database/index.js", "import": "./lib/esm/database/index.js" }, + "./eventarc": { + "require": "./lib/eventarc/index.js", + "import": "./lib/esm/eventarc/index.js" + }, "./firestore": { "require": "./lib/firestore/index.js", "import": "./lib/esm/firestore/index.js" @@ -165,7 +172,8 @@ "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^2.0.2", - "node-forge": "^1.3.1" + "node-forge": "^1.3.1", + "uuid": "^8.3.2" }, "optionalDependencies": { "@google-cloud/firestore": "^4.15.1", @@ -190,11 +198,13 @@ "@types/request-promise": "^4.1.41", "@types/sinon": "^10.0.2", "@types/sinon-chai": "^3.0.0", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "bcrypt": "^5.0.0", "chai": "^4.2.0", "chai-as-promised": "^7.0.0", + "chai-exclude": "^2.1.0", "chalk": "^4.1.1", "child-process-promise": "^2.2.1", "del": "^6.0.0", diff --git a/src/eventarc/cloudevent.ts b/src/eventarc/cloudevent.ts new file mode 100644 index 0000000000..9fa0749f2e --- /dev/null +++ b/src/eventarc/cloudevent.ts @@ -0,0 +1,95 @@ + +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A CloudEvent version. + */ +export type CloudEventVersion = '1.0'; + +/** + * A CloudEvent describes event data. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md + */ +export interface CloudEvent { + + /** + * Identifier for the event. If not provided, it is auto-populated with a UUID. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#id + */ + id?: string; + + /** + * Identifies the context in which an event happened. If not provided, the value of `EVENTARC_CLOUD_EVENT_SOURCE` + * environment variable is used and if that is not set, a validation error is thrown. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#source-1 + */ + source?: string; + + /** + * The version of the CloudEvents specification which the event uses. If not provided, is set to `1.0` -- + * the only supported value. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#specversion + */ + specversion?: CloudEventVersion; + + /** + * Type of the event. Should be prefixed with a reverse-DNS name (`com.my-org.v1.something.happended`). + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#type + */ + type: string; + + /** + * Subject (context) of the event in the context of the event producer. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#subject + */ + subject?: string; + + /** + * MIME type of the data being sent with the event in the `data` field. Only `application/json` and `text/plain` + * are currently supported. If not specified, it is automatically inferred from the type of provided data. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#datacontenttype + */ + datacontenttype?: string; + + /** + * Timestamp of the event. Must be in ISO time format. If not specified, current time (at the moment of publishing) + * is used. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#time + */ + time?: string; + + /** + * Data payload of the event. Objects are stringified with JSON and strings are be passed along as-is. + */ + data?: object | string; + + /** + * Custom attributes/extensions. Must be strings. Added to the event as is. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#extension-context-attributes + */ + [key: string]: any; + } diff --git a/src/eventarc/eventarc-client-internal.ts b/src/eventarc/eventarc-client-internal.ts new file mode 100644 index 0000000000..e265480024 --- /dev/null +++ b/src/eventarc/eventarc-client-internal.ts @@ -0,0 +1,156 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as validator from '../utils/validator'; +import { FirebaseEventarcError, toCloudEventProtoFormat } from './eventarc-utils'; +import { App } from '../app'; +import { Channel } from './eventarc'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient +} from '../utils/api-request'; +import { FirebaseApp } from '../app/firebase-app'; +import * as utils from '../utils'; +import { PrefixedFirebaseError } from '../utils/error'; +import { CloudEvent } from './cloudevent'; + +const EVENTARC_API = 'https://eventarcpublishing.googleapis.com/v1'; +const FIREBASE_VERSION_HEADER = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, +}; +const CHANNEL_NAME_REGEX = /^(projects\/([^/]+)\/)?locations\/([^/]+)\/channels\/([^/]+)$/; +const DEFAULT_CHANNEL_REGION = 'us-central1'; + +/** + * Class that facilitates sending requests to the Eventarc backend API. + * + * @internal + */ +export class EventarcApiClient { + private readonly httpClient: HttpClient; + private projectId?: string; + private readonly resolvedChannelName: Promise; + + constructor(private readonly app: App, private readonly channel: Channel) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseEventarcError( + 'invalid-argument', + 'First argument passed to Channel() must be a valid Eventarc service instance.'); + } + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + this.resolvedChannelName = this.resolveChannelName(channel.name); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseEventarcError( + 'unknown-error', + '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; + return projectId; + }); + } + + /** + * Publishes provided events to this channel. If channel was created with `allowedEventsTypes` and event type + * is not on that list, the event is ignored. + * + * The following CloudEvent fields are auto-populated if not set: + * * specversion - `1.0` + * * id - uuidv4() + * * source - populated with `process.env.EVENTARC_CLOUD_EVENT_SOURCE` and + * if not set an error is thrown. + * + * @param events - CloudEvent to publish to the channel. + */ + public async publish(events: CloudEvent | CloudEvent[]): Promise { + if (!Array.isArray(events)) { + events = [events as CloudEvent]; + } + return this.publishToEventarcApi( + await this.resolvedChannelName, + events + .filter(e => typeof this.channel.allowedEventTypes === 'undefined' || + this.channel.allowedEventTypes.includes(e.type)) + .map(toCloudEventProtoFormat)); + } + + private async publishToEventarcApi(channel:string, events: CloudEvent[]): Promise { + if (events.length === 0) { + return; + } + const request: HttpRequestConfig = { + method: 'POST', + url: `${EVENTARC_API}/${channel}:publishEvents`, + data: JSON.stringify({ events }), + }; + return this.sendRequest(request); + } + + private sendRequest(request: HttpRequestConfig): Promise { + request.headers = FIREBASE_VERSION_HEADER; + return this.httpClient.send(request) + .then(() => undefined) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + return new FirebaseEventarcError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + private resolveChannelName(name: string): Promise { + if (!name.includes('/')) { + const location = DEFAULT_CHANNEL_REGION; + const channelId = name; + return this.resolveChannelNameProjectId(location, channelId); + } else { + const match = CHANNEL_NAME_REGEX.exec(name); + if (match === null || match.length < 4) { + throw new FirebaseEventarcError('invalid-argument', 'Invalid channel name format.'); + } + const projectId = match[2]; + const location = match[3]; + const channelId = match[4]; + if (validator.isNonEmptyString(projectId)) { + return Promise.resolve(`projects/${projectId}/locations/${location}/channels/${channelId}`); + } else { + return this.resolveChannelNameProjectId(location, channelId); + } + } + } + + private async resolveChannelNameProjectId(location: string, channelId: string): Promise { + const projectId = await this.getProjectId(); + return `projects/${projectId}/locations/${location}/channels/${channelId}`; + } +} diff --git a/src/eventarc/eventarc-utils.ts b/src/eventarc/eventarc-utils.ts new file mode 100644 index 0000000000..6bf6531285 --- /dev/null +++ b/src/eventarc/eventarc-utils.ts @@ -0,0 +1,138 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError } from '../utils/error'; +import { CloudEvent } from './cloudevent'; +import { v4 as uuid } from 'uuid'; +import * as validator from '../utils/validator'; + +// List of CloudEvent properties that are handled "by hand" and should be skipped by +// automatic attribute copy. +const TOP_LEVEL_CE_ATTRS: string[] = + ['id', 'type', 'specversion', 'source', 'data', 'time', 'datacontenttype', 'subject']; + +export type EventarcErrorCode = 'unknown-error' | 'invalid-argument' + +/** + * Firebase Eventarc error code structure. This extends PrefixedFirebaseError. + * + * @param code - The error code. + * @param message - The error message. + * @constructor + */ +export class FirebaseEventarcError extends PrefixedFirebaseError { + constructor(code: EventarcErrorCode, message: string) { + super('eventarc', code, message); + } +} + +export function toCloudEventProtoFormat(ce: CloudEvent): any { + const source = ce.source ?? process.env.EVENTARC_CLOUD_EVENT_SOURCE; + if (typeof source === 'undefined' || !validator.isNonEmptyString(source)) { + throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'source' is required."); + } + if (!validator.isNonEmptyString(ce.type)) { + throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'type' is required."); + } + const out: Record = { + '@type': 'type.googleapis.com/io.cloudevents.v1.CloudEvent', + 'id': ce.id ?? uuid(), + 'type': ce.type, + 'specVersion': ce.specversion ?? '1.0', + 'source': source + } + + if (typeof ce.time !== 'undefined') { + if (!validator.isISODateString(ce.time)) { + throw new FirebaseEventarcError( + 'invalid-argument', "CloudEvent 'tyme' must be in ISO date format."); + } + setAttribute(out, 'time', { + 'ceTimestamp': ce.time + }); + } else { + setAttribute(out, 'time', { + 'ceTimestamp': new Date().toISOString() + }); + } + if (typeof ce.datacontenttype !== 'undefined') { + if (!validator.isNonEmptyString(ce.datacontenttype)) { + throw new FirebaseEventarcError( + 'invalid-argument', + "CloudEvent 'datacontenttype' if specified must be non-empty string."); + } + setAttribute(out, 'datacontenttype', { + 'ceString': ce.datacontenttype + }); + } + if (ce.subject) { + if (!validator.isNonEmptyString(ce.subject)) { + throw new FirebaseEventarcError( + 'invalid-argument', + "CloudEvent 'subject' if specified must be non-empty string."); + } + setAttribute(out, 'subject', { + 'ceString': ce.subject + }); + } + + if (typeof ce.data === 'undefined') { + throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'data' is required."); + } + if (validator.isObject(ce.data)) { + out['textData'] = JSON.stringify(ce.data); + if (!ce.datacontenttype) { + setAttribute(out, 'datacontenttype', { + 'ceString': 'application/json' + }); + } + } else if (validator.isNonEmptyString(ce.data)) { + out['textData'] = ce.data; + if (!ce.datacontenttype) { + setAttribute(out, 'datacontenttype', { + 'ceString': 'text/plain' + }); + } + } else { + throw new FirebaseEventarcError( + 'invalid-argument', + `CloudEvent 'data' must be string or an object (which are converted to JSON), got '${typeof ce.data}'.`); + } + + for (const attr in ce) { + if (TOP_LEVEL_CE_ATTRS.includes(attr)) { + continue; + } + if (!validator.isNonEmptyString(ce[attr])) { + throw new FirebaseEventarcError( + 'invalid-argument', + `CloudEvent extension attributes ('${attr}') must be string.`); + } + setAttribute(out, attr, { + 'ceString': ce[attr] + }); + } + + return out; +} + +function setAttribute(event: any, attr: string, value: any): void { + if (!Object.prototype.hasOwnProperty.call(event, 'attributes')) { + event.attributes = {}; + } + event['attributes'][attr] = value; +} diff --git a/src/eventarc/eventarc.ts b/src/eventarc/eventarc.ts new file mode 100644 index 0000000000..363679a375 --- /dev/null +++ b/src/eventarc/eventarc.ts @@ -0,0 +1,191 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import * as validator from '../utils/validator'; +import { FirebaseEventarcError } from './eventarc-utils'; +import { CloudEvent } from './cloudevent'; +import { EventarcApiClient } from './eventarc-client-internal'; + +/** + * Channel options interface. + */ +export interface ChannelOptions { + /** + * An array of allowed event types. If specified, publishing events of + * unknown types is a no op. When not provided, no event filtering is + * performed. + */ + allowedEventTypes?: string[] | string | undefined +} + +/** + * Eventarc service bound to the provided app. + */ +export class Eventarc { + + private readonly appInternal: App; + + /** + * @internal + */ + constructor(app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseEventarcError( + 'invalid-argument', + 'First argument passed to Eventarc() must be a valid Firebase app instance.', + ); + } + + this.appInternal = app; + } + + /** + * The {@link firebase-admin.app#App} associated with the current Eventarc service + * instance. + * + * @example + * ```javascript + * var app = eventarc.app; + * ``` + */ + get app(): App { + return this.appInternal; + } + + /** + * Creates a reference to the Eventarc channel using the provided channel resource name. + * The channel resource name can be either: + * * A fully qualified channel resource name: + * `projects/{project}/locations/{location}/channels/{channel-id}` + * * A partial resource name with location and channel ID, in which case + * the runtime project ID of the function is used: + * `locations/{location}/channels/{channel-id}` + * * A partial channel ID, in which case the runtime project ID of the + * function and `us-central1` as location is used: + * `{channel-id}` + * + * @param name - Channel resource name. + * @param options - (optional) additional channel options + * @returns An Eventarc channel reference for publishing events. + */ + public channel(name: string, options?: ChannelOptions): Channel; + + /** + * Create a reference to the default Firebase channel: + * `locations/us-central1/channels/firebase` + * + * @param options - (optional) additional channel options + * @returns Eventarc channel reference for publishing events. + */ + public channel(options?: ChannelOptions): Channel; + + public channel(nameOrOptions?: string | ChannelOptions, options?: ChannelOptions): Channel { + let channel: string; + let opts: ChannelOptions; + if (validator.isNonEmptyString(nameOrOptions)) { + channel = nameOrOptions; + } else { + channel = 'locations/us-central1/channels/firebase'; + } + + if (validator.isNonNullObject(nameOrOptions)) { + opts = nameOrOptions as ChannelOptions; + } else { + opts = options as ChannelOptions; + } + let allowedEventTypes : string[] | undefined = undefined; + if (typeof opts?.allowedEventTypes === 'string') { + allowedEventTypes = opts.allowedEventTypes.split(','); + } else if (validator.isArray(opts?.allowedEventTypes)) { + allowedEventTypes = opts?.allowedEventTypes as string[]; + } else if (typeof opts?.allowedEventTypes !== 'undefined') { + throw new FirebaseEventarcError( + 'invalid-argument', + 'AllowedEventTypes must be either an array of strings or a comma separated string.', + ); + } + return new Channel(this, channel, allowedEventTypes); + } +} + +/** + * Eventarc Channel. + */ +export class Channel { + private readonly eventarcInternal: Eventarc; + private nameInternal: string; + + /** + * List of event types allowed by this channel for publishing. Other event types are ignored. + */ + public readonly allowedEventTypes?: string[] + + private readonly client: EventarcApiClient; + + /** + * @internal + */ + constructor(eventarc: Eventarc, name: string, allowedEventTypes?: string[]) { + if (!validator.isNonNullObject(eventarc)) { + throw new FirebaseEventarcError( + 'invalid-argument', + 'First argument passed to Channel() must be a valid Eventarc service instance.', + ); + } + if (!validator.isNonEmptyString(name)) { + throw new FirebaseEventarcError( + 'invalid-argument', 'name is required.', + ); + } + + this.nameInternal = name; + this.eventarcInternal = eventarc; + this.allowedEventTypes = allowedEventTypes; + this.client = new EventarcApiClient(eventarc.app, this); + } + + /** + * The {@link firebase-admin.eventarc#Eventarc} service instance associated with the current `Channel`. + * + * @example + * ```javascript + * var app = channel.eventarc; + * ``` + */ + get eventarc(): Eventarc { + return this.eventarcInternal; + } + + /** + * The channel name as provided during channel creation. If it was not specifed, the default channel name is returned + * ('locations/us-central1/channels/firebase'). + */ + get name(): string { + return this.nameInternal; + } + + /** + * Publishes provided events to this channel. If channel was created with `allowedEventTypes` and event type is not + * on that list, the event is ignored. + * + * @param events - CloudEvent to publish to the channel. + */ + public publish(events: CloudEvent | CloudEvent[]): Promise { + return this.client.publish(events); + } +} diff --git a/src/eventarc/index.ts b/src/eventarc/index.ts new file mode 100644 index 0000000000..d1e6fc79bd --- /dev/null +++ b/src/eventarc/index.ts @@ -0,0 +1,65 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Firebase Eventarc. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; + +import { Eventarc } from './eventarc'; + +export { CloudEvent, CloudEventVersion } from './cloudevent'; +export { Eventarc, Channel, ChannelOptions } from './eventarc'; + +/** + * Gets the {@link Eventarc} service for the default app or a given app. + * + * `getEventarc()` can be called with no arguments to access the default + * app's `Eventarc` service or as `getEventarc(app)` to access the + * `Eventarc` service associated with specific app. + * + * @example + * ```javascript + * // Get the Eventarc service for the default app + * const defaultEventarc = getEventarc(); + * ``` + * + * @example + * ```javascript + * // Get the Eventarc service for a given app + * const otherEventarc = getEventarc(otherApp); + * ``` + * + * @param app - Optional app whose `Eventarc` service will be returned. + * If not provided, the default `Eventarc` service will be returned. + * + * @returns The default `Eventarc` service if no + * app is provided or the `Eventarc` service associated with the provided + * app. + */ +export function getEventarc(app?: App): Eventarc { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('eventarc', (app) => new Eventarc(app)); +} diff --git a/test/unit/eventarc/eventarc-utils.spec.ts b/test/unit/eventarc/eventarc-utils.spec.ts new file mode 100644 index 0000000000..d2f18b7d96 --- /dev/null +++ b/test/unit/eventarc/eventarc-utils.spec.ts @@ -0,0 +1,193 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as sinon from 'sinon'; +import * as utils from '../../../src/eventarc/eventarc-utils'; +import * as chai from 'chai'; +import chaiExclude from 'chai-exclude'; + +const expect = chai.expect; +chai.use(chaiExclude); + +describe('eventarc-utils', () => { + before(() => { + sinon + .stub(Date.prototype, 'toISOString') + .returns('2022-03-16T20:20:42.212Z'); + }); + + after(() => { + sinon.restore(); + }); + + afterEach(() => { + delete process.env.EVENTARC_CLOUD_EVENT_SOURCE; + }); + + describe('toCloudEventProtoFormat', () => { + it('converts cloud event to proto format', () => { + expect(utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + specversion: '1.0', + subject: 'context', + datacontenttype: 'application/json', + id: 'user-provided-id', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + customattr: 'custom value', + })).to.deep.eq({ + '@type': 'type.googleapis.com/io.cloudevents.v1.CloudEvent', + 'attributes': { + 'customattr': { + 'ceString': 'custom value' + }, + 'datacontenttype': { + 'ceString': 'application/json' + }, + 'time': { + 'ceTimestamp': '2022-03-16T20:20:42.212Z' + }, + 'subject': { + 'ceString': 'context' + } + }, + 'id': 'user-provided-id', + 'source': '/my/functions', + 'specVersion': '1.0', + 'textData': '{"hello":"world"}', + 'type': 'some.custom.event', + }); + }); + + it('populates specversion if not provided', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + }); + expect(got['specVersion']).to.eq('1.0'); + }); + + it('populates time if not provided', () => { + const got = utils.toCloudEventProtoFormat({ + specversion: '1.0', + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + }); + expect(got['attributes']['time']).to.deep.eq({ + 'ceTimestamp': '2022-03-16T20:20:42.212Z' + }); + }); + + it('populates id if not provided', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + id: 'user-provided-id', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + }); + // Couldn't figure out how to stub uuid, so just checking for presense. + expect(got).to.haveOwnProperty('id'); + }); + + it('populates source from EVENTARC_CLOUD_EVENT_SOURCE env var if not set', () => { + process.env.EVENTARC_CLOUD_EVENT_SOURCE = '//source/from/env/var'; + const got = utils.toCloudEventProtoFormat({ + specversion: '1.0', + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + }); + expect(got['source']).to.eq('//source/from/env/var'); + }); + + it('throws invalid argument when source not set', () => { + expect(() => utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + time: new Date().toISOString(), + })).throws("CloudEvent 'source' is required."); + }); + + it('throws invalid argument when custom attr not string', () => { + expect(() => utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + customattr: 123, + })).throws("CloudEvent extension attributes ('customattr') must be string"); + }); + + it('populates converts object data to JSON and sets datacontenttype', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + id: 'user-provided-id', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + }); + // Couldn't figure out how to stub uuid, so just checking for presense. + expect(got['textData']).to.eq('{"hello":"world"}'); + expect(got['attributes']['datacontenttype']).to.deep.eq({ + 'ceString': 'application/json' + }); + }); + + it('populates string data and sets datacontenttype', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + id: 'user-provided-id', + data: 'hello world', + source: '/my/functions', + time: new Date().toISOString(), + }); + // Couldn't figure out how to stub uuid, so just checking for presense. + expect(got['textData']).to.eq('hello world'); + expect(got['attributes']['datacontenttype']).to.deep.eq({ + 'ceString': 'text/plain' + }); + }); + }); +}); diff --git a/test/unit/eventarc/eventarc.spec.ts b/test/unit/eventarc/eventarc.spec.ts new file mode 100644 index 0000000000..1bbddd5a4d --- /dev/null +++ b/test/unit/eventarc/eventarc.spec.ts @@ -0,0 +1,572 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as sinon from 'sinon'; +import { Channel, Eventarc } from '../../../src/eventarc'; +import { toCloudEventProtoFormat } from '../../../src/eventarc/eventarc-utils'; +import { CloudEvent } from '../../../src/eventarc/cloudevent'; +import { HttpClient } from '../../../src/utils/api-request'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import * as mocks from '../../resources/mocks'; +import * as utils from '../utils'; +import * as chai from 'chai'; +import chaiExclude from 'chai-exclude'; +import { getSdkVersion } from '../../../src/utils/index'; + +const expect = chai.expect; +chai.use(chaiExclude); + +const TEST_EVENT1 : CloudEvent = { + type: 'some.custom.event1', + specversion: '1.0', + id: 'user-provided-id-1', + data: 'hello world', + source: '/my/functions', + time: '2011-11-11T11:11:11.111Z', +}; +const TEST_EVENT1_SERIALIZED = JSON.stringify(toCloudEventProtoFormat(TEST_EVENT1)); + +const TEST_EVENT2 : CloudEvent = { + type: 'some.custom.event2', + specversion: '1.0', + id: 'user-provided-id-2', + data: 'hello world', + source: '/my/functions', + time: '2011-11-11T11:11:11.111Z', +}; +const TEST_EVENT2_SERIALIZED = JSON.stringify(toCloudEventProtoFormat(TEST_EVENT2)); + +describe('eventarc', () => { + let mockApp: FirebaseApp; + let eventarc: Eventarc; + + before(() => { + mockApp = mocks.app(); + eventarc = new Eventarc(mockApp); + }); + + after(() => { + sinon.restore(); + }); + + afterEach(() => { + delete process.env.EVENTARC_CLOUD_EVENT_SOURCE; + }); + + describe('Eventarc', () => { + it('inintializes Eventarc object', () => { + expect(eventarc.app).eq(mockApp); + }); + }); + + it('throws invalid argument with creating channel with invalid name', () => { + expect(() => eventarc.channel('foo/bar')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('foo/bar/baz')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('us-central1/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projectid/locations/us-central1/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('v1/projects/projectid/locations/us-central1/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projects/projectid/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projects/projectid/locations/us-central1')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projects/projectid/locations_us-central1/channels/foo')) + .throws('Invalid channel name format.'); + }); + + describe('default Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel(); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('locations/us-central1/channels/firebase'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('full resource name Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel('projects/other-project-id/locations/us-west1/channels/my-channel2'); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('projects/other-project-id/locations/us-west1/channels/my-channel2'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/other-project-id/locations/us-west1/channels/my-channel2:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/other-project-id/locations/us-west1/channels/my-channel2:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('partial (no project) Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel('locations/us-west1/channels/my-channel'); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('locations/us-west1/channels/my-channel'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-west1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-west1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('partial (channel id only) Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel('my-channel'); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('my-channel'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('Channel with empty allowed events', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel({ allowedEventTypes: [] }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).is.empty; + }); + + it('filters out event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.not.have.been.called; + }); + + it('filters out all event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { type: 'foo' }]); + + expect(httpStub).to.not.have.been.called; + }); + }); + + describe('Channel with channel and empty allowed events', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel( + 'adasdas', + { allowedEventTypes: [] }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).is.empty; + }); + + it('filters out event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.not.have.been.called; + }); + + it('filters out all event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { type: 'foo' }]); + + expect(httpStub).to.not.have.been.called; + }); + }); + + describe('Channel with allowed events', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel({ allowedEventTypes: ['some.custom.event1'] }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).deep.eq(['some.custom.event1']); + }); + + it('publishes events with allowed type', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes events with allowed type and filters out others', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { + type: 'some.custom.event2' + }]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('Channel with allowed events as string', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel({ allowedEventTypes: 'some.custom.event1,some.other.event.type' }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).deep.eq(['some.custom.event1', 'some.other.event.type']); + }); + + it('publishes events with allowed type', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes events with allowed type and filters out others', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { + type: 'some.custom.event2' + }]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); +}); diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index ade3503069..ca1f63adee 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -103,3 +103,7 @@ import './app-check/app-check.spec'; import './app-check/app-check-api-client-internal.spec'; import './app-check/token-generator.spec'; import './app-check/token-verifier.spec.ts'; + +// Eventarc +import './eventarc/eventarc.spec'; +import './eventarc/eventarc-utils.spec'; From 331777d3c3973ed39b975a06314d442d471d17f0 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 4 May 2022 14:29:20 -0400 Subject: [PATCH 23/27] feat: Add Task Queue API (#1674) * Add Task Queue Functions API --- entrypoints.json | 4 + etc/firebase-admin.functions.api.md | 56 +++ package.json | 7 + src/app/credential-internal.ts | 21 + .../functions-api-client-internal.ts | 334 +++++++++++++ src/functions/functions-api.ts | 71 +++ src/functions/functions.ts | 102 ++++ src/functions/index.ts | 73 +++ src/utils/index.ts | 84 ++++ test/integration/functions.spec.ts | 35 ++ .../integration/postcheck/esm/example.test.js | 7 + .../typescript/example-modular.test.ts | 7 + .../functions-api-client-internal.spec.ts | 447 ++++++++++++++++++ test/unit/functions/functions.spec.ts | 187 ++++++++ test/unit/functions/index.spec.ts | 76 +++ test/unit/index.spec.ts | 4 + test/unit/utils/index.spec.ts | 26 +- 17 files changed, 1540 insertions(+), 1 deletion(-) create mode 100644 etc/firebase-admin.functions.api.md create mode 100644 src/functions/functions-api-client-internal.ts create mode 100644 src/functions/functions-api.ts create mode 100644 src/functions/functions.ts create mode 100644 src/functions/index.ts create mode 100644 test/integration/functions.spec.ts create mode 100644 test/unit/functions/functions-api-client-internal.spec.ts create mode 100644 test/unit/functions/functions.spec.ts create mode 100644 test/unit/functions/index.spec.ts diff --git a/entrypoints.json b/entrypoints.json index 55f2d766b2..6b507911ae 100644 --- a/entrypoints.json +++ b/entrypoints.json @@ -24,6 +24,10 @@ "typings": "./lib/firestore/index.d.ts", "dist": "./lib/firestore/index.js" }, + "firebase-admin/functions": { + "typings": "./lib/functions/index.d.ts", + "dist": "./lib/functions/index.js" + }, "firebase-admin/installations": { "typings": "./lib/installations/index.d.ts", "dist": "./lib/installations/index.js" diff --git a/etc/firebase-admin.functions.api.md b/etc/firebase-admin.functions.api.md new file mode 100644 index 0000000000..2ed05d6f60 --- /dev/null +++ b/etc/firebase-admin.functions.api.md @@ -0,0 +1,56 @@ +## API Report File for "firebase-admin.functions" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export interface AbsoluteDelivery { + // @alpha (undocumented) + scheduleDelaySeconds?: never; + scheduleTime?: Date; +} + +// @public +export interface DelayDelivery { + scheduleDelaySeconds?: number; + // @alpha (undocumented) + scheduleTime?: never; +} + +// @public +export type DeliverySchedule = DelayDelivery | AbsoluteDelivery; + +// @public +export class Functions { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly app: App; + taskQueue>(functionName: string, extensionId?: string): TaskQueue; +} + +// @public +export function getFunctions(app?: App): Functions; + +// @public +export type TaskOptions = DeliverySchedule & TaskOptionsExperimental & { + dispatchDeadlineSeconds?: number; +}; + +// @public +export interface TaskOptionsExperimental { + // @beta + uri?: string; +} + +// @public +export class TaskQueue> { + enqueue(data: Args, opts?: TaskOptions): Promise; +} + +``` diff --git a/package.json b/package.json index f0cd1c6e3f..e5e08f7829 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,9 @@ "firestore": [ "lib/firestore" ], + "functions": [ + "lib/functions" + ], "installations": [ "lib/installations" ], @@ -132,6 +135,10 @@ "require": "./lib/firestore/index.js", "import": "./lib/esm/firestore/index.js" }, + "./functions": { + "require": "./lib/functions/index.js", + "import": "./lib/esm/functions/index.js" + }, "./installations": { "require": "./lib/installations/index.js", "import": "./lib/esm/installations/index.js" diff --git a/src/app/credential-internal.ts b/src/app/credential-internal.ts index 0817236038..28442ba8f7 100644 --- a/src/app/credential-internal.ts +++ b/src/app/credential-internal.ts @@ -33,6 +33,7 @@ const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token'; const GOOGLE_METADATA_SERVICE_HOST = 'metadata.google.internal'; const GOOGLE_METADATA_SERVICE_TOKEN_PATH = '/computeMetadata/v1/instance/service-accounts/default/token'; const GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH = '/computeMetadata/v1/project/project-id'; +const GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH = '/computeMetadata/v1/instance/service-accounts/default/email'; const configDir = (() => { // Windows has a dedicated low-rights location for apps at ~/Application Data @@ -197,6 +198,7 @@ export class ComputeEngineCredential implements Credential { private readonly httpClient = new HttpClient(); private readonly httpAgent?: Agent; private projectId?: string; + private accountId?: string; constructor(httpAgent?: Agent) { this.httpAgent = httpAgent; @@ -226,6 +228,25 @@ export class ComputeEngineCredential implements Credential { }); } + public getServiceAccountEmail(): Promise { + if (this.accountId) { + return Promise.resolve(this.accountId); + } + + const request = this.buildRequest(GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH); + return this.httpClient.send(request) + .then((resp) => { + this.accountId = resp.text!; + return this.accountId; + }) + .catch((err) => { + const detail: string = (err instanceof HttpError) ? getDetailFromResponse(err.response) : err.message; + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + `Failed to determine service account email: ${detail}`); + }); + } + private buildRequest(urlPath: string): HttpRequestConfig { return { method: 'GET', diff --git a/src/functions/functions-api-client-internal.ts b/src/functions/functions-api-client-internal.ts new file mode 100644 index 0000000000..36b7bf99c2 --- /dev/null +++ b/src/functions/functions-api-client-internal.ts @@ -0,0 +1,334 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient +} from '../utils/api-request'; +import { PrefixedFirebaseError } from '../utils/error'; +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; +import { TaskOptions } from './functions-api'; + +const CLOUD_TASKS_API_URL_FORMAT = 'https://cloudtasks.googleapis.com/v2/projects/{projectId}/locations/{locationId}/queues/{resourceId}/tasks'; +const FIREBASE_FUNCTION_URL_FORMAT = 'https://{locationId}-{projectId}.cloudfunctions.net/{resourceId}'; + +const FIREBASE_FUNCTIONS_CONFIG_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}` +}; + +// Default canonical location ID of the task queue. +const DEFAULT_LOCATION = 'us-central1'; + +/** + * Class that facilitates sending requests to the Firebase Functions backend API. + * + * @internal + */ +export class FunctionsApiClient { + private readonly httpClient: HttpClient; + private projectId?: string; + private accountId?: string; + + constructor(private readonly app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + 'First argument passed to getFunctions() must be a valid Firebase app instance.'); + } + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + } + + /** + * Creates a task and adds it to a queue. + * + * @param data - The data payload of the task. + * @param functionName - The functionName of the queue. + * @param extensionId - Optional canonical ID of the extension. + * @param opts - Optional options when enqueuing a new task. + */ + public enqueue(data: any, functionName: string, extensionId?: string, opts?: TaskOptions): Promise { + if (!validator.isNonEmptyString(functionName)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Function name must be a non empty string'); + } + + const task = this.validateTaskOptions(data, opts); + let resources: utils.ParsedResource; + try { + resources = utils.parseResourceName(functionName, 'functions'); + } + catch (err) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Function name must be a single string or a qualified resource name'); + } + + if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) { + resources.resourceId = `ext-${extensionId}-${resources.resourceId}`; + } + + return this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT) + .then((serviceUrl) => { + return this.updateTaskPayload(task, resources) + .then((task) => { + const request: HttpRequestConfig = { + method: 'POST', + url: serviceUrl, + headers: FIREBASE_FUNCTIONS_CONFIG_HEADERS, + data: { + task, + } + }; + return this.httpClient.send(request); + }) + }) + .then(() => { + return; + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + private getUrl(resourceName: utils.ParsedResource, urlFormat: string): Promise { + let { locationId } = resourceName; + const { projectId, resourceId } = resourceName; + if (typeof locationId === 'undefined' || !validator.isNonEmptyString(locationId)) { + locationId = DEFAULT_LOCATION; + } + return Promise.resolve() + .then(() => { + if (typeof projectId !== 'undefined' && validator.isNonEmptyString(projectId)) { + return projectId; + } + return this.getProjectId(); + }) + .then((projectId) => { + const urlParams = { + projectId, + locationId, + resourceId, + }; + // Formats a string of form 'project/{projectId}/{api}' and replaces + // with corresponding arguments {projectId: '1234', api: 'resource'} + // and returns output: 'project/1234/resource'. + return utils.formatString(urlFormat, urlParams); + }); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseFunctionsError( + 'unknown-error', + '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; + return projectId; + }); + } + + private getServiceAccount(): Promise { + if (this.accountId) { + return Promise.resolve(this.accountId); + } + return utils.findServiceAccountEmail(this.app) + .then((accountId) => { + if (!validator.isNonEmptyString(accountId)) { + throw new FirebaseFunctionsError( + 'unknown-error', + 'Failed to determine service account. Initialize the ' + + 'SDK with service account credentials or set service account ID as an app option.'); + } + this.accountId = accountId; + return accountId; + }); + } + + private validateTaskOptions(data: any, opts?: TaskOptions): Task { + const task: Task = { + httpRequest: { + url: '', + oidcToken: { + serviceAccountEmail: '', + }, + body: Buffer.from(JSON.stringify({ data })).toString('base64'), + headers: { 'Content-Type': 'application/json' } + } + } + + if (typeof opts !== 'undefined') { + if (!validator.isNonNullObject(opts)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'TaskOptions must be a non-null object'); + } + if ('scheduleTime' in opts && 'scheduleDelaySeconds' in opts) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Both scheduleTime and scheduleDelaySeconds are provided. ' + + 'Only one value should be set.'); + } + if ('scheduleTime' in opts && typeof opts.scheduleTime !== 'undefined') { + if (!(opts.scheduleTime instanceof Date)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'scheduleTime must be a valid Date object.'); + } + task.scheduleTime = opts.scheduleTime.toISOString(); + } + if ('scheduleDelaySeconds' in opts && typeof opts.scheduleDelaySeconds !== 'undefined') { + if (!validator.isNumber(opts.scheduleDelaySeconds) || opts.scheduleDelaySeconds < 0) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'scheduleDelaySeconds must be a non-negative duration in seconds.'); + } + const date = new Date(); + date.setSeconds(date.getSeconds() + opts.scheduleDelaySeconds); + task.scheduleTime = date.toISOString(); + } + if (typeof opts.dispatchDeadlineSeconds !== 'undefined') { + if (!validator.isNumber(opts.dispatchDeadlineSeconds) || opts.dispatchDeadlineSeconds < 15 + || opts.dispatchDeadlineSeconds > 1800) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'dispatchDeadlineSeconds must be a non-negative duration in seconds ' + + 'and must be in the range of 15s to 30 mins.'); + } + task.dispatchDeadline = `${opts.dispatchDeadlineSeconds}s`; + } + if (typeof opts.uri !== 'undefined') { + if (!validator.isURL(opts.uri)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'uri must be a valid URL string.'); + } + task.httpRequest.url = opts.uri; + } + } + return task; + } + + private updateTaskPayload(task: Task, resources: utils.ParsedResource): Promise { + return Promise.resolve() + .then(() => { + if (validator.isNonEmptyString(task.httpRequest.url)) { + return task.httpRequest.url; + } + return this.getUrl(resources, FIREBASE_FUNCTION_URL_FORMAT); + }) + .then((functionUrl) => { + return this.getServiceAccount() + .then((account) => { + task.httpRequest.oidcToken.serviceAccountEmail = account; + task.httpRequest.url = functionUrl; + return task; + }) + }); + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + if (!response.isJson()) { + return new FirebaseFunctionsError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + const error: Error = (response.data as ErrorResponse).error || {}; + let code: FunctionsErrorCode = 'unknown-error'; + if (error.status && error.status in FUNCTIONS_ERROR_CODE_MAPPING) { + code = FUNCTIONS_ERROR_CODE_MAPPING[error.status]; + } + const message = error.message || `Unknown server error: ${response.text}`; + return new FirebaseFunctionsError(code, message); + } +} + +interface ErrorResponse { + error?: Error; +} + +interface Error { + code?: number; + message?: string; + status?: string; +} + +interface Task { + // A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional + // digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z". + scheduleTime?: string; + // A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". + dispatchDeadline?: string; + httpRequest: { + url: string; + oidcToken: { + serviceAccountEmail: string; + }; + // A base64-encoded string. + body: string; + headers: { [key: string]: string }; + }; +} + +export const FUNCTIONS_ERROR_CODE_MAPPING: { [key: string]: FunctionsErrorCode } = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL: 'internal-error', + FAILED_PRECONDITION: 'failed-precondition', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + UNKNOWN: 'unknown-error', +}; + +export type FunctionsErrorCode = + 'aborted' + | 'invalid-argument' + | 'invalid-credential' + | 'internal-error' + | 'failed-precondition' + | 'permission-denied' + | 'unauthenticated' + | 'not-found' + | 'unknown-error'; + +/** + * Firebase Functions error code structure. This extends PrefixedFirebaseError. + * + * @param code - The error code. + * @param message - The error message. + * @constructor + */ +export class FirebaseFunctionsError extends PrefixedFirebaseError { + constructor(code: FunctionsErrorCode, message: string) { + super('functions', code, message); + + /* tslint:disable:max-line-length */ + // Set the prototype explicitly. See the following link for more details: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + /* tslint:enable:max-line-length */ + (this as any).__proto__ = FirebaseFunctionsError.prototype; + } +} diff --git a/src/functions/functions-api.ts b/src/functions/functions-api.ts new file mode 100644 index 0000000000..1383495aa7 --- /dev/null +++ b/src/functions/functions-api.ts @@ -0,0 +1,71 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Interface representing task options with delayed delivery. + */ +export interface DelayDelivery { + /** + * The duration of delay of the time when the task is scheduled to be attempted or retried. + * This delay is added to the current time. + */ + scheduleDelaySeconds?: number; + /** @alpha */ + scheduleTime?: never; +} + +/** + * Interface representing task options with absolute delivery. + */ +export interface AbsoluteDelivery { + /** + * The time when the task is scheduled to be attempted or retried. + */ + scheduleTime?: Date; + /** @alpha */ + scheduleDelaySeconds?: never; +} + +/** + * Type representing delivery schedule options. + */ +export type DeliverySchedule = DelayDelivery | AbsoluteDelivery + +/** + * Type representing task options. + */ +export type TaskOptions = DeliverySchedule & TaskOptionsExperimental & { + + /** + * The deadline for requests sent to the worker. If the worker does not respond by this deadline + * then the request is cancelled and the attempt is marked as a DEADLINE_EXCEEDED failure. + * Cloud Tasks will retry the task according to the `RetryConfig`. + * The default is 10 minutes. The deadline must be in the range of 15 seconds and 30 minutes. + */ + dispatchDeadlineSeconds?: number; +} + +/** + * Type representing experimental (beta) task options. + */ +export interface TaskOptionsExperimental { + /** + * The full URL path that the request will be sent to. Must be a valid URL. + * @beta + */ + uri?: string; +} diff --git a/src/functions/functions.ts b/src/functions/functions.ts new file mode 100644 index 0000000000..648f593297 --- /dev/null +++ b/src/functions/functions.ts @@ -0,0 +1,102 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseFunctionsError, FunctionsApiClient } from './functions-api-client-internal'; +import { TaskOptions } from './functions-api'; +import * as validator from '../utils/validator'; + +/** + * The Firebase `Functions` service interface. + */ +export class Functions { + + private readonly client: FunctionsApiClient; + + /** + * @param app - The app for this `Functions` service. + * @constructor + * @internal + */ + constructor(readonly app: App) { + this.client = new FunctionsApiClient(app); + } + + /** + * Creates a reference to a {@link TaskQueue} for a given function name. + * The function name can be either: + * * A fully qualified function resource name: + * `projects/{project}/locations/{location}/functions/{functionName}` + * * A partial resource name with location and function name, in which case + * the runtime project ID is used: + * `locations/{location}/functions/{functionName}` + * * A partial function name, in which case the runtime project ID and the default location, + * `us-central1`, is used: + * `{functionName}` + * + * @param functionName - The name of the function. + * @param extensionId - Optional Firebase extension ID. + * @returns A promise that fulfills with a `TaskQueue`. + */ + public taskQueue>(functionName: string, extensionId?: string): TaskQueue { + return new TaskQueue(functionName, this.client, extensionId); + } +} + +/** + * The `TaskQueue` interface. + */ +export class TaskQueue> { + + /** + * @param functionName - The name of the function. + * @param client - The `FunctionsApiClient` instance. + * @param extensionId - Optional canonical ID of the extension. + * @constructor + * @internal + */ + constructor(private readonly functionName: string, private readonly client: FunctionsApiClient, + private readonly extensionId?: string) { + if (!validator.isNonEmptyString(functionName)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + '`functionName` must be a non-empty string.'); + } + if (!validator.isNonNullObject(client) || !('enqueue' in client)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + 'Must provide a valid FunctionsApiClient instance to create a new TaskQueue.'); + } + if (typeof extensionId !== 'undefined' && !validator.isString(extensionId)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + '`extensionId` must be a string.'); + } + } + + /** + * Creates a task and adds it to the queue. Tasks cannot be updated after creation. + * This action requires `cloudtasks.tasks.create` IAM permission on the service account. + * + * @param data - The data payload of the task. + * @param opts - Optional options when enqueuing a new task. + * @returns A promise that resolves when the task has successfully been added to the queue. + */ + public enqueue(data: Args, opts?: TaskOptions): Promise { + return this.client.enqueue(data, this.functionName, this.extensionId, opts); + } +} diff --git a/src/functions/index.ts b/src/functions/index.ts new file mode 100644 index 0000000000..a046c4dd93 --- /dev/null +++ b/src/functions/index.ts @@ -0,0 +1,73 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Firebase Functions service. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { Functions } from './functions'; + +export { + DelayDelivery, + AbsoluteDelivery, + DeliverySchedule, + TaskOptions, + TaskOptionsExperimental +} from './functions-api'; +export { + Functions, + TaskQueue +} from './functions'; + +/** + * Gets the {@link Functions} service for the default app + * or a given app. + * + * `getFunctions()` can be called with no arguments to access the default + * app's `Functions` service or as `getFunctions(app)` to access the + * `Functions` service associated with a specific app. + * + * @example + * ```javascript + * // Get the `Functions` service for the default app + * const defaultFunctions = getFunctions(); + * ``` + * + * @example + * ```javascript + * // Get the `Functions` service for a given app + * const otherFunctions = getFunctions(otherApp); + * ``` + * + * @param app - Optional app for which to return the `Functions` service. + * If not provided, the default `Functions` service is returned. + * + * @returns The default `Functions` service if no app is provided, or the `Functions` + * service associated with the provided app. + */ +export function getFunctions(app?: App): Functions { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('functions', (app) => new Functions(app)); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index ed9b2bc80e..824d41c0f9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -120,6 +120,53 @@ export function findProjectId(app: App): Promise { return Promise.resolve(null); } +/** + * Returns the service account email associated with a Firebase app, if it's explicitly + * specified in either the Firebase app options, credentials or the local environment. + * Otherwise returns null. + * + * @param app - A Firebase app to get the service account email from. + * + * @returns A service account email string or null. + */ +export function getExplicitServiceAccountEmail(app: App): string | null { + const options = app.options; + if (validator.isNonEmptyString(options.serviceAccountId)) { + return options.serviceAccountId; + } + + const credential = app.options.credential; + if (credential instanceof ServiceAccountCredential) { + return credential.clientEmail; + } + return null; +} + +/** + * Determines the service account email associated with a Firebase app. This method first + * checks if a service account email is explicitly specified in either the Firebase app options, + * credentials or the local environment in that order. If no explicit service account email is + * configured, but the SDK has been initialized with ComputeEngineCredentials, this + * method attempts to discover the service account email from the local metadata service. + * + * @param app - A Firebase app to get the service account email from. + * + * @returns A service account email ID string or null. + */ +export function findServiceAccountEmail(app: App): Promise { + const accountId = getExplicitServiceAccountEmail(app); + if (accountId) { + return Promise.resolve(accountId); + } + + const credential = app.options.credential; + if (credential instanceof ComputeEngineCredential) { + return credential.getServiceAccountEmail(); + } + + return Promise.resolve(null); +} + /** * Encodes data using web-safe-base64. * @@ -217,3 +264,40 @@ export function transformMillisecondsToSecondsString(milliseconds: number): stri } return duration; } + +/** + * Internal type to represent a resource name + */ +export type ParsedResource = { + projectId?: string; + locationId?: string; + resourceId: string; +} + +/** + * Parses the top level resources of a given resource name. + * Supports both full and partial resources names, example: + * `locations/{location}/functions/{functionName}`, + * `projects/{project}/locations/{location}/functions/{functionName}`, or {functionName} + * Does not support deeply nested resource names. + * + * @param resourceName - The resource name string. + * @param resourceIdKey - The key of the resource name to be parsed. + * @returns A parsed resource name object. + */ +export function parseResourceName(resourceName: string, resourceIdKey: string): ParsedResource { + if (!resourceName.includes('/')) { + return { resourceId: resourceName }; + } + const CHANNEL_NAME_REGEX = + new RegExp(`^(projects/([^/]+)/)?locations/([^/]+)/${resourceIdKey}/([^/]+)$`); + const match = CHANNEL_NAME_REGEX.exec(resourceName); + if (match === null) { + throw new Error('Invalid resource name format.'); + } + const projectId = match[2]; + const locationId = match[3]; + const resourceId = match[4]; + + return { projectId, locationId, resourceId }; +} diff --git a/test/integration/functions.spec.ts b/test/integration/functions.spec.ts new file mode 100644 index 0000000000..9b1f5277f9 --- /dev/null +++ b/test/integration/functions.spec.ts @@ -0,0 +1,35 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { getFunctions } from '../../lib/functions/index'; + +chai.should(); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('getFunctions()', () => { + + describe('taskQueue()', () => { + it('successfully returns a taskQueue', () => { + const factorizeQueue = getFunctions().taskQueue('queue-name'); + expect(factorizeQueue).to.be.not.undefined; + expect(typeof factorizeQueue.enqueue).to.equal('function'); + }); + }); +}); diff --git a/test/integration/postcheck/esm/example.test.js b/test/integration/postcheck/esm/example.test.js index c731577f66..29d0654374 100644 --- a/test/integration/postcheck/esm/example.test.js +++ b/test/integration/postcheck/esm/example.test.js @@ -22,6 +22,7 @@ import { getAppCheck, AppCheck } from 'firebase-admin/app-check'; import { getAuth, Auth } from 'firebase-admin/auth'; import { getDatabase, getDatabaseWithUrl, ServerValue } from 'firebase-admin/database'; import { getFirestore, DocumentReference, Firestore, FieldValue } from 'firebase-admin/firestore'; +import { getFunctions } from 'firebase-admin/functions'; import { getInstanceId, InstanceId } from 'firebase-admin/instance-id'; import { getMachineLearning, MachineLearning } from 'firebase-admin/machine-learning'; import { getMessaging, Messaging } from 'firebase-admin/messaging'; @@ -110,6 +111,12 @@ describe('ESM entry points', () => { expect(ref).to.be.instanceOf(DocumentReference); }); + it('Should return a Functions client', () => { + const fn = getFunctions(app); + expect(fn).to.be.not.undefined; + expect(typeof fn.taskQueue).to.equal('function'); + }); + it('Should return an InstanceId client', () => { const client = getInstanceId(app); expect(client).to.be.instanceOf(InstanceId); diff --git a/test/integration/postcheck/typescript/example-modular.test.ts b/test/integration/postcheck/typescript/example-modular.test.ts index 0d67abade4..c5ccf3c8a2 100644 --- a/test/integration/postcheck/typescript/example-modular.test.ts +++ b/test/integration/postcheck/typescript/example-modular.test.ts @@ -22,6 +22,7 @@ import { getAppCheck, AppCheck } from 'firebase-admin/app-check'; import { getAuth, Auth } from 'firebase-admin/auth'; import { getDatabase, getDatabaseWithUrl, Database, ServerValue } from 'firebase-admin/database'; import { getFirestore, DocumentReference, Firestore, FieldValue } from 'firebase-admin/firestore'; +import { getFunctions, Functions } from 'firebase-admin/functions'; import { getInstanceId, InstanceId } from 'firebase-admin/instance-id'; import { getMachineLearning, MachineLearning } from 'firebase-admin/machine-learning'; import { getMessaging, Messaging } from 'firebase-admin/messaging'; @@ -116,6 +117,12 @@ describe('Modular API', () => { expect(ref).to.be.instanceOf(DocumentReference); }); + it('Should return a Functions client', () => { + const fn: Functions = getFunctions(app); + expect(fn).to.be.not.undefined; + expect(typeof fn.taskQueue).to.equal('function'); + }); + it('Should return an InstanceId client', () => { const client = getInstanceId(app); expect(client).to.be.instanceOf(InstanceId); diff --git a/test/unit/functions/functions-api-client-internal.spec.ts b/test/unit/functions/functions-api-client-internal.spec.ts new file mode 100644 index 0000000000..8f5cc1b6f7 --- /dev/null +++ b/test/unit/functions/functions-api-client-internal.spec.ts @@ -0,0 +1,447 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as utils from '../utils'; +import * as mocks from '../../resources/mocks'; +import { getSdkVersion } from '../../../src/utils'; + +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { FirebaseFunctionsError, FunctionsApiClient } from '../../../src/functions/functions-api-client-internal'; +import { HttpClient } from '../../../src/utils/api-request'; +import { FirebaseAppError } from '../../../src/utils/error'; +import { deepCopy } from '../../../src/utils/deep-copy'; + +const expect = chai.expect; + +describe('FunctionsApiClient', () => { + + const ERROR_RESPONSE = { + error: { + code: 404, + message: 'Requested entity not found', + status: 'NOT_FOUND', + }, + }; + + const EXPECTED_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, + 'Authorization': 'Bearer mock-token' + }; + + const noProjectId = '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.'; + + const DEFAULT_REGION = 'us-central1'; + const CUSTOM_REGION = 'us-west1'; + const FUNCTION_NAME = 'function-name'; + const CUSTOM_PROJECT_ID = 'taskq-project'; + const EXTENSION_ID = 'image-resize'; + const PARTIAL_RESOURCE_NAME = `locations/${CUSTOM_REGION}/functions/${FUNCTION_NAME}`; + const FULL_RESOURCE_NAME = `projects/${CUSTOM_PROJECT_ID}/locations/${CUSTOM_REGION}/functions/${FUNCTION_NAME}`; + + const mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + serviceAccountId: 'service-acct@email.com' + }; + + const TEST_TASK_PAYLOAD = { + httpRequest: { + url: `https://${DEFAULT_REGION}-${mockOptions.projectId}.cloudfunctions.net/${FUNCTION_NAME}`, + oidcToken: { + serviceAccountEmail: mockOptions.serviceAccountId, + }, + body: Buffer.from(JSON.stringify({ data: {} })).toString('base64'), + headers: { 'Content-Type' : 'application/json' } + } + } + + const CLOUD_TASKS_URL = `https://cloudtasks.googleapis.com/v2/projects/${mockOptions.projectId}/locations/${DEFAULT_REGION}/queues/${FUNCTION_NAME}/tasks`; + + const CLOUD_TASKS_URL_EXT = `https://cloudtasks.googleapis.com/v2/projects/${mockOptions.projectId}/locations/${DEFAULT_REGION}/queues/ext-${EXTENSION_ID}-${FUNCTION_NAME}/tasks`; + + const CLOUD_TASKS_URL_FULL_RESOURCE = `https://cloudtasks.googleapis.com/v2/projects/${CUSTOM_PROJECT_ID}/locations/${CUSTOM_REGION}/queues/${FUNCTION_NAME}/tasks`; + + const CLOUD_TASKS_URL_PARTIAL_RESOURCE = `https://cloudtasks.googleapis.com/v2/projects/${mockOptions.projectId}/locations/${CUSTOM_REGION}/queues/${FUNCTION_NAME}/tasks`; + + const clientWithoutProjectId = new FunctionsApiClient(mocks.mockCredentialApp()); + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + let app: FirebaseApp; + let apiClient: FunctionsApiClient; + + beforeEach(() => { + app = mocks.appWithOptions(mockOptions); + apiClient = new FunctionsApiClient(app); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + return app.delete(); + }); + + describe('Constructor', () => { + it('should reject when the app is null', () => { + expect(() => new FunctionsApiClient(null as unknown as FirebaseApp)) + .to.throw('First argument passed to getFunctions() must be a valid Firebase app instance.'); + }); + }); + + describe('enqueue', () => { + let clock: sinon.SinonFakeTimers | undefined; + + afterEach(() => { + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + it('should reject when project id is not available', () => { + return clientWithoutProjectId.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should reject when project id is not available in partial resource name', () => { + return clientWithoutProjectId.enqueue({}, PARTIAL_RESOURCE_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + for (const invalidName of [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop, undefined]) { + it(`should throw if functionName is ${invalidName}`, () => { + expect(() => apiClient.enqueue({}, invalidName as any)) + .to.throw('Function name must be a non empty string'); + }); + } + + for (const invalidName of ['project/abc/locations/east/fname', 'location/west/', '//']) { + it(`should throw if functionName is ${invalidName}`, () => { + expect(() => apiClient.enqueue({}, invalidName as any)) + .to.throw('Function name must be a single string or a qualified resource name'); + }); + } + + for (const invalidOption of [null, 'abc', '', [], true, 102, 1.2]) { + it(`should throw if options is ${invalidOption}`, () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', invalidOption as any)) + .to.throw('TaskOptions must be a non-null object'); + }); + } + + for (const invalidScheduleTime of [null, '', 'abc', 102, 1.2, [], {}, true, NaN]) { + it(`should throw if scheduleTime is ${invalidScheduleTime}`, () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', { scheduleTime: invalidScheduleTime } as any)) + .to.throw('scheduleTime must be a valid Date object.'); + }); + } + + for (const invalidScheduleDelaySeconds of [null, 'abc', '', [], {}, true, NaN, -1]) { + it(`should throw if scheduleDelaySeconds is ${invalidScheduleDelaySeconds}`, () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', + { scheduleDelaySeconds: invalidScheduleDelaySeconds } as any)) + .to.throw('scheduleDelaySeconds must be a non-negative duration in seconds.'); + }); + } + + for (const invalidDispatchDeadlineSeconds of [null, 'abc', '', [], {}, true, NaN, -1, 14, 1801]) { + it(`should throw if dispatchDeadlineSeconds is ${invalidDispatchDeadlineSeconds}`, () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', + { dispatchDeadlineSeconds: invalidDispatchDeadlineSeconds } as any)) + .to.throw('dispatchDeadlineSeconds must be a non-negative duration in seconds ' + + 'and must be in the range of 15s to 30 mins.'); + }); + } + + for (const invalidUri of [null, '', 'a', 'foo', 'image.jpg', [], {}, true, NaN]) { + it(`should throw given an invalid uri: ${invalidUri}`, () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', + { uri: invalidUri } as any)) + .to.throw('uri must be a valid URL string.'); + }); + } + + it('should throw when both scheduleTime and scheduleDelaySeconds are provided', () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', { + scheduleTime: new Date(), + scheduleDelaySeconds: 1000 + } as any)) + .to.throw('Both scheduleTime and scheduleDelaySeconds are provided. Only one value should be set.'); + }); + + it('should reject when a full platform error response is received', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + stubs.push(stub); + const expected = new FirebaseFunctionsError('not-found', 'Requested entity not found'); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error when error code is not present', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 404)); + stubs.push(stub); + const expected = new FirebaseFunctionsError('unknown-error', 'Unknown server error: {}'); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error for non-json response', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom('not json', 404)); + stubs.push(stub); + const expected = new FirebaseFunctionsError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject when rejected with a FirebaseAppError', () => { + const expected = new FirebaseAppError('network-error', 'socket hang up'); + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(expected); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: TEST_TASK_PAYLOAD + } + }); + }); + }); + + it('should resolve the projectId and location from the full resource name', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${CUSTOM_REGION}-${CUSTOM_PROJECT_ID}.cloudfunctions.net/${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FULL_RESOURCE_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_FULL_RESOURCE, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + }); + }); + + it('should resolve the location from the partial resource name', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${CUSTOM_REGION}-${mockOptions.projectId}.cloudfunctions.net/${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, PARTIAL_RESOURCE_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_PARTIAL_RESOURCE, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + }); + }); + + it('should update the function name when the extension-id is provided', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${DEFAULT_REGION}-${mockOptions.projectId}.cloudfunctions.net/ext-${EXTENSION_ID}-${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, EXTENSION_ID) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_EXT, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + }); + }); + + it('should use the default projectId following a request with a full resource name', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${CUSTOM_REGION}-${CUSTOM_PROJECT_ID}.cloudfunctions.net/${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + // pass the full resource name. SDK should not use the default values + return apiClient.enqueue({}, FULL_RESOURCE_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_FULL_RESOURCE, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + + // passing just the function name. SDK should deffer to default values + return apiClient.enqueue({}, FUNCTION_NAME); + }) + .then(() => { + expect(stub).to.have.been.calledTwice.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: TEST_TASK_PAYLOAD + } + }); + }); + }); + + + + // tests for Task Options + it('should convert scheduleTime to ISO string', () => { + const scheduleTime = new Date(); + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + // timestamps should be converted to ISO strings + (expectedPayload as any).scheduleTime = scheduleTime.toISOString(); + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, '', { scheduleTime }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + + it('should set scheduleTime based on scheduleDelaySeconds', () => { + clock = sinon.useFakeTimers(1000); + + const scheduleDelaySeconds = 1800; + const scheduleTime = new Date(); // '1970-01-01T00:00:01.000Z' + scheduleTime.setSeconds(scheduleTime.getSeconds() + scheduleDelaySeconds); // '1970-01-01T00:30:01.000Z' + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + // timestamps should be converted to ISO strings + (expectedPayload as any).scheduleTime = scheduleTime.toISOString(); + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, '', { scheduleDelaySeconds }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + + it('should convert dispatchDeadline to a duration with `s` prefix', () => { + const dispatchDeadlineSeconds = 1800; + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + (expectedPayload as any).dispatchDeadline = `${dispatchDeadlineSeconds}s`; + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, '', { dispatchDeadlineSeconds }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + + it('should encode data in the payload', () => { + const data = { privateKey: '~/.ssh/id_rsa.pub' }; + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.body = Buffer.from(JSON.stringify({ data })).toString('base64'); + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue(data, FUNCTION_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + }); +}); diff --git a/test/unit/functions/functions.spec.ts b/test/unit/functions/functions.spec.ts new file mode 100644 index 0000000000..36e6098d42 --- /dev/null +++ b/test/unit/functions/functions.spec.ts @@ -0,0 +1,187 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as mocks from '../../resources/mocks'; + +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { FunctionsApiClient, FirebaseFunctionsError } from '../../../src/functions/functions-api-client-internal'; +import { HttpClient } from '../../../src/utils/api-request'; +import { Functions, TaskQueue } from '../../../src/functions/functions'; + +const expect = chai.expect; + +describe('Functions', () => { + const mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + }; + + let functions: Functions; + + let mockApp: FirebaseApp; + let mockCredentialApp: FirebaseApp; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + before(() => { + mockApp = mocks.appWithOptions(mockOptions); + mockCredentialApp = mocks.mockCredentialApp(); + functions = new Functions(mockApp); + }); + + after(() => { + return mockApp.delete(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + describe('Constructor', () => { + for (const invalidApp of [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop, + undefined]) { + it('should throw given invalid app: ' + JSON.stringify(invalidApp), () => { + expect(() => { + const functionsAny: any = Functions; + return new functionsAny(invalidApp); + }).to.throw( + 'First argument passed to getFunctions() must be a valid Firebase app ' + + 'instance.'); + }); + } + + it('should reject when initialized without project ID', () => { + // Remove Project ID from the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const noProjectId = '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.'; + const functionsWithoutProjectId = new Functions(mockCredentialApp); + return functionsWithoutProjectId.taskQueue('task-name').enqueue({}) + .should.eventually.rejectedWith(noProjectId); + }); + + it('should reject when failed to contact the Metadata server for service account email', () => { + const functionsWithProjectId = new Functions(mockApp); + const stub = sinon.stub(HttpClient.prototype, 'send') + .rejects(new Error('network error.')); + stubs.push(stub); + const expected = 'Failed to determine service account. Initialize the ' + + 'SDK with service account credentials or set service account ID as an app option.'; + return functionsWithProjectId.taskQueue('task-name').enqueue({}) + .should.eventually.be.rejectedWith(expected); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new Functions(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(functions.app).to.equal(mockApp); + }); + }); +}); + +describe('TaskQueue', () => { + const INTERNAL_ERROR = new FirebaseFunctionsError('internal-error', 'message'); + const FUNCTION_NAME = 'function-name'; + + let taskQueue: TaskQueue; + let mockClient: FunctionsApiClient; + + let mockApp: FirebaseApp; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + before(() => { + mockApp = mocks.app(); + mockClient = new FunctionsApiClient(mockApp); + taskQueue = new TaskQueue(FUNCTION_NAME, mockClient); + }); + + after(() => { + return mockApp.delete(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + describe('Constructor', () => { + for (const invalidClient of [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop, + undefined]) { + it('should throw given invalid client: ' + JSON.stringify(invalidClient), () => { + expect(() => { + const taskQueueAny: any = TaskQueue; + return new taskQueueAny(FUNCTION_NAME, invalidClient); + }).to.throw( + 'Must provide a valid FunctionsApiClient instance to create a new TaskQueue.'); + }); + } + + for (const invalidFunctionName of [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop, + undefined]) { + it('should throw given invalid name: ' + JSON.stringify(invalidFunctionName), () => { + expect(() => { + const taskQueueAny: any = TaskQueue; + return new taskQueueAny(invalidFunctionName, mockClient); + }).to.throw('`functionName` must be a non-empty string.'); + }); + } + + it('should not throw given a valid name and client', () => { + expect(() => { + return new TaskQueue(FUNCTION_NAME, mockClient); + }).not.to.throw(); + }); + }); + + describe('enqueue', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(FunctionsApiClient.prototype, 'enqueue') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return taskQueue.enqueue({}) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + it('should propagate API errors with task options', () => { + const stub = sinon + .stub(FunctionsApiClient.prototype, 'enqueue') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return taskQueue.enqueue({}, { scheduleDelaySeconds: 3600 }) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + }); +}); diff --git a/test/unit/functions/index.spec.ts b/test/unit/functions/index.spec.ts new file mode 100644 index 0000000000..8bfcaa5dfe --- /dev/null +++ b/test/unit/functions/index.spec.ts @@ -0,0 +1,76 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getFunctions, Functions } from '../../../src/functions/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Functions', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = '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.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getFunctions()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getFunctions(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const functions = getFunctions(mockCredentialApp); + const factorizedQueue = functions.taskQueue('task-name'); + return factorizedQueue.enqueue({}) + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getFunctions(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const fn1: Functions = getFunctions(mockApp); + const fn2: Functions = getFunctions(mockApp); + expect(fn1).to.equal(fn2); + }); + }); +}); diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index ca1f63adee..aa7b262111 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -107,3 +107,7 @@ import './app-check/token-verifier.spec.ts'; // Eventarc import './eventarc/eventarc.spec'; import './eventarc/eventarc-utils.spec'; +// Functions +import './functions/index.spec'; +import './functions/functions.spec'; +import './functions/functions-api-client-internal.spec'; diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 7d007f2f78..7105726e67 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -22,7 +22,7 @@ import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; import { addReadonlyGetter, getExplicitProjectId, findProjectId, - toWebSafeBase64, formatString, generateUpdateMask, transformMillisecondsToSecondsString, + toWebSafeBase64, formatString, generateUpdateMask, transformMillisecondsToSecondsString, parseResourceName, } from '../../../src/utils/index'; import { isNonEmptyString } from '../../../src/utils/validator'; import { FirebaseApp } from '../../../src/app/firebase-app'; @@ -396,3 +396,27 @@ describe('transformMillisecondsToSecondsString()', () => { }); }); }); + +describe('parseResourceName()', () => { + + const FULL_RESOURCE_NAME = 'projects/abc/locations/us/functions/f1'; + const PARTIAL_RESOURCE_NAME = 'locations/us/functions/f1'; + const projectId = 'abc'; + const locationId = 'us'; + const resourceId = 'f1'; + + it('should return projectId, location, and resource when given a full resource name', () => { + expect(parseResourceName(FULL_RESOURCE_NAME, 'functions')) + .to.deep.equal({ projectId, locationId, resourceId }); + }); + + it('should return location and resource when given a partial resource name', () => { + expect(parseResourceName(PARTIAL_RESOURCE_NAME, 'functions')) + .to.deep.equal({ projectId: undefined, locationId, resourceId }); + }); + + it('should return the resource when given only the resource name', () => { + expect(parseResourceName('f1', 'functions')) + .to.deep.equal({ resourceId }); + }); +}); From c21112e969a26308a733fa3e18bf730734307826 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 May 2022 14:37:23 -0400 Subject: [PATCH 24/27] build(deps-dev): bump @typescript-eslint/eslint-plugin (#1682) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.21.0 to 5.22.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.22.0/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 108 +++++++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb4447ca91..a00b138421 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1388,9 +1388,9 @@ "dev": true }, "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, "@types/jsonwebtoken": { @@ -1524,14 +1524,14 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.11.0.tgz", - "integrity": "sha512-HJh33bgzXe6jGRocOj4FmefD7hRY4itgjzOrSs3JPrTNXsX7j5+nQPciAUj/1nZtwo2kAc3C75jZO+T23gzSGw==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.22.0.tgz", + "integrity": "sha512-YCiy5PUzpAeOPGQ7VSGDEY2NeYUV1B0swde2e0HzokRsHBYjSdF6DZ51OuRZxVPHx0032lXGLvOMls91D8FXlg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.11.0", - "@typescript-eslint/type-utils": "5.11.0", - "@typescript-eslint/utils": "5.11.0", + "@typescript-eslint/scope-manager": "5.22.0", + "@typescript-eslint/type-utils": "5.22.0", + "@typescript-eslint/utils": "5.22.0", "debug": "^4.3.2", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", @@ -1541,35 +1541,35 @@ }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.11.0.tgz", - "integrity": "sha512-z+K4LlahDFVMww20t/0zcA7gq/NgOawaLuxgqGRVKS0PiZlCTIUtX0EJbC0BK1JtR4CelmkPK67zuCgpdlF4EA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", + "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.11.0", - "@typescript-eslint/visitor-keys": "5.11.0" + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/visitor-keys": "5.22.0" } }, "@typescript-eslint/types": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.11.0.tgz", - "integrity": "sha512-cxgBFGSRCoBEhvSVLkKw39+kMzUKHlJGVwwMbPcTZX3qEhuXhrjwaZXWMxVfxDgyMm+b5Q5b29Llo2yow8Y7xQ==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", + "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", "dev": true }, "@typescript-eslint/visitor-keys": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.11.0.tgz", - "integrity": "sha512-E8w/vJReMGuloGxJDkpPlGwhxocxOpSVgSvjiLO5IxZPmxZF30weOeJYyPSEACwM+X4NziYS9q+WkN/2DHYQwA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", + "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.11.0", + "@typescript-eslint/types": "5.22.0", "eslint-visitor-keys": "^3.0.0" } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -1600,12 +1600,12 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.11.0.tgz", - "integrity": "sha512-wDqdsYO6ofLaD4DsGZ0jGwxp4HrzD2YKulpEZXmgN3xo4BHJwf7kq49JTRpV0Gx6bxkSUmc9s0EIK1xPbFFpIA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.22.0.tgz", + "integrity": "sha512-iqfLZIsZhK2OEJ4cQ01xOq3NaCuG5FQRKyHicA3xhZxMgaxQazLUHbH/B2k9y5i7l3+o+B5ND9Mf1AWETeMISA==", "dev": true, "requires": { - "@typescript-eslint/utils": "5.11.0", + "@typescript-eslint/utils": "5.22.0", "debug": "^4.3.2", "tsutils": "^3.21.0" } @@ -1643,43 +1643,43 @@ } }, "@typescript-eslint/utils": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.11.0.tgz", - "integrity": "sha512-g2I480tFE1iYRDyMhxPAtLQ9HAn0jjBtipgTCZmd9I9s11OV8CTsG+YfFciuNDcHqm4csbAgC2aVZCHzLxMSUw==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.22.0.tgz", + "integrity": "sha512-HodsGb037iobrWSUMS7QH6Hl1kppikjA1ELiJlNSTYf/UdMEwzgj0WIp+lBNb6WZ3zTwb0tEz51j0Wee3iJ3wQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.11.0", - "@typescript-eslint/types": "5.11.0", - "@typescript-eslint/typescript-estree": "5.11.0", + "@typescript-eslint/scope-manager": "5.22.0", + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/typescript-estree": "5.22.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.11.0.tgz", - "integrity": "sha512-z+K4LlahDFVMww20t/0zcA7gq/NgOawaLuxgqGRVKS0PiZlCTIUtX0EJbC0BK1JtR4CelmkPK67zuCgpdlF4EA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", + "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.11.0", - "@typescript-eslint/visitor-keys": "5.11.0" + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/visitor-keys": "5.22.0" } }, "@typescript-eslint/types": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.11.0.tgz", - "integrity": "sha512-cxgBFGSRCoBEhvSVLkKw39+kMzUKHlJGVwwMbPcTZX3qEhuXhrjwaZXWMxVfxDgyMm+b5Q5b29Llo2yow8Y7xQ==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", + "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.11.0.tgz", - "integrity": "sha512-yVH9hKIv3ZN3lw8m/Jy5I4oXO4ZBMqijcXCdA4mY8ull6TPTAoQnKKrcZ0HDXg7Bsl0Unwwx7jcXMuNZc0m4lg==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz", + "integrity": "sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.11.0", - "@typescript-eslint/visitor-keys": "5.11.0", + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/visitor-keys": "5.22.0", "debug": "^4.3.2", "globby": "^11.0.4", "is-glob": "^4.0.3", @@ -1688,19 +1688,19 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.11.0.tgz", - "integrity": "sha512-E8w/vJReMGuloGxJDkpPlGwhxocxOpSVgSvjiLO5IxZPmxZF30weOeJYyPSEACwM+X4NziYS9q+WkN/2DHYQwA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", + "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.11.0", + "@typescript-eslint/types": "5.22.0", "eslint-visitor-keys": "^3.0.0" } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, "requires": { "lru-cache": "^6.0.0" From 2a76539fbcbd831430e4d3151afd987b6d84d15d Mon Sep 17 00:00:00 2001 From: Alex Pascal Date: Wed, 4 May 2022 12:39:00 -0700 Subject: [PATCH 25/27] Added support for calling Eventarc emulator (#1686) --- src/eventarc/eventarc-client-internal.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/eventarc/eventarc-client-internal.ts b/src/eventarc/eventarc-client-internal.ts index e265480024..ce02967c93 100644 --- a/src/eventarc/eventarc-client-internal.ts +++ b/src/eventarc/eventarc-client-internal.ts @@ -102,7 +102,7 @@ export class EventarcApiClient { } const request: HttpRequestConfig = { method: 'POST', - url: `${EVENTARC_API}/${channel}:publishEvents`, + url: `${this.getEventarcHost()}/${channel}:publishEvents`, data: JSON.stringify({ events }), }; return this.sendRequest(request); @@ -153,4 +153,8 @@ export class EventarcApiClient { const projectId = await this.getProjectId(); return `projects/${projectId}/locations/${location}/channels/${channelId}`; } + + private getEventarcHost(): string { + return process.env.CLOUD_EVENTARC_EMULATOR_HOST ?? EVENTARC_API; + } } From ccf7579ec7075fd0756850af5570ce7d4abc1a1d Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 5 May 2022 15:41:07 -0400 Subject: [PATCH 26/27] Fix markdown lists in reference docs (#1687) --- src/eventarc/eventarc.ts | 9 ++++++--- src/functions/functions-api.ts | 1 + src/functions/functions.ts | 9 ++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/eventarc/eventarc.ts b/src/eventarc/eventarc.ts index 363679a375..a1edc4a011 100644 --- a/src/eventarc/eventarc.ts +++ b/src/eventarc/eventarc.ts @@ -70,12 +70,15 @@ export class Eventarc { /** * Creates a reference to the Eventarc channel using the provided channel resource name. * The channel resource name can be either: - * * A fully qualified channel resource name: + * + * - A fully qualified channel resource name: * `projects/{project}/locations/{location}/channels/{channel-id}` - * * A partial resource name with location and channel ID, in which case + * + * - A partial resource name with location and channel ID, in which case * the runtime project ID of the function is used: * `locations/{location}/channels/{channel-id}` - * * A partial channel ID, in which case the runtime project ID of the + * + * - A partial channel ID, in which case the runtime project ID of the * function and `us-central1` as location is used: * `{channel-id}` * diff --git a/src/functions/functions-api.ts b/src/functions/functions-api.ts index 1383495aa7..60351211e9 100644 --- a/src/functions/functions-api.ts +++ b/src/functions/functions-api.ts @@ -42,6 +42,7 @@ export interface AbsoluteDelivery { /** * Type representing delivery schedule options. + * `DeliverySchedule` is a union type of {@link DelayDelivery} and {@link AbsoluteDelivery} types. */ export type DeliverySchedule = DelayDelivery | AbsoluteDelivery diff --git a/src/functions/functions.ts b/src/functions/functions.ts index 648f593297..08a38ab7ac 100644 --- a/src/functions/functions.ts +++ b/src/functions/functions.ts @@ -39,12 +39,15 @@ export class Functions { /** * Creates a reference to a {@link TaskQueue} for a given function name. * The function name can be either: - * * A fully qualified function resource name: + * + * 1) A fully qualified function resource name: * `projects/{project}/locations/{location}/functions/{functionName}` - * * A partial resource name with location and function name, in which case + * + * 2) A partial resource name with location and function name, in which case * the runtime project ID is used: * `locations/{location}/functions/{functionName}` - * * A partial function name, in which case the runtime project ID and the default location, + * + * 3) A partial function name, in which case the runtime project ID and the default location, * `us-central1`, is used: * `{functionName}` * From a32195daa9848b261fe892d9f606152a40ff2915 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 5 May 2022 16:03:41 -0400 Subject: [PATCH 27/27] [chore] Release 10.2.0 (#1688) - Bumped version to v10.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5e08f7829..135c6d5d84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "10.1.0", + "version": "10.2.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0",