From c93893bdf24f515fa771b3a03accc8a6a198a106 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Mon, 6 Jul 2020 19:12:30 +0000 Subject: [PATCH 001/160] Add null to databaseAuthVariableOverride (#926) --- src/firebase-app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 1f01d61986..d9fce0055b 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -47,7 +47,7 @@ export type AppHook = (event: string, app: FirebaseApp) => void; */ export interface FirebaseAppOptions { credential?: Credential; - databaseAuthVariableOverride?: object; + databaseAuthVariableOverride?: object | null; databaseURL?: string; serviceAccountId?: string; storageBucket?: string; From db8be2620a5c9cfc5b9824d9d2487ab9815f5baf Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Wed, 8 Jul 2020 11:26:46 -0700 Subject: [PATCH 002/160] Adds scrypt support in node 12 (#739) * Adds scrypt support in node 12 Replaces scrypt npm module (no longer maintained and doesn't work for node 12) for testing standard scrypt with `crypto.scryptSync` which has been supported since node v10.5. * Fixes http timeout issues in node 12. * Updated CI config; Re-generated the package lock file Co-authored-by: hiranya911 --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 4 +- CONTRIBUTING.md | 2 +- README.md | 2 +- package-lock.json | 1567 +++++++++++++++------------------ package.json | 8 +- src/utils/api-request.ts | 10 +- test/integration/auth.spec.ts | 11 +- 8 files changed, 727 insertions(+), 879 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9461c44f43..3b4b53aba7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - node-version: [8.x, 10.x] + node-version: [10.x] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dbe21dc5e4..9b344f22ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 8.x + node-version: 10.x - name: Install and build run: | @@ -112,7 +112,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 8.x + node-version: 10.x - name: Publish preflight check id: preflight diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e263ffadc2..42d573c035 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,7 +87,7 @@ information on using pull requests. ### Prerequisites -1. Node.js 8.13.0 or higher. +1. Node.js 10.10.0 or higher. 2. NPM 5 or higher (NPM 6 recommended). 3. Google Cloud SDK ([`gcloud`](https://cloud.google.com/sdk/downloads) utility) diff --git a/README.md b/README.md index d7155fb512..4e9b7df5e0 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ requests, code review feedback, and also pull requests. ## Supported Environments -We support Node.js 8.13.0 and higher. +We support Node.js 10.10.0 and higher. Please also note that the Admin SDK should only be used in server-side/back-end environments controlled by the app developer. diff --git a/package-lock.json b/package-lock.json index ff44fd069a..2a82b424d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "8.12.1", + "version": "8.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -383,7 +383,8 @@ "@google-cloud/promisify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.2.tgz", - "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g==" + "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g==", + "optional": true }, "@google-cloud/storage": { "version": "4.1.2", @@ -472,6 +473,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -481,12 +483,14 @@ "safe-buffer": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "optional": true }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, "requires": { "safe-buffer": "~5.2.0" } @@ -523,27 +527,32 @@ "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "optional": true }, "@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true }, "@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true }, "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "optional": true }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -552,27 +561,32 @@ "@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "optional": true }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "optional": true }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "optional": true }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "optional": true }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "optional": true }, "@sinonjs/commons": { "version": "1.4.0", @@ -696,7 +710,8 @@ "@types/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", - "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" + "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==", + "optional": true }, "@types/minimatch": { "version": "3.0.3", @@ -726,9 +741,9 @@ } }, "@types/node": { - "version": "8.10.59", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", - "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==" + "version": "10.17.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.26.tgz", + "integrity": "sha512-myMwkO2Cr82kirHY8uknNRHEVtn0wV3DTQfkrjx17jmkstDRZ24gNUdl8AHXVyVclTYI/bNjgTPTAWvWLqXqkw==" }, "@types/promises-a-plus": { "version": "0.0.27", @@ -758,15 +773,6 @@ "@types/request": "*" } }, - "@types/scrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@types/scrypt/-/scrypt-6.0.0.tgz", - "integrity": "sha512-kiQtYPL3YOOliArRkiE58O5DK7NyURo81hU4G43+wyQp4UiqGfM7muHGAZ/nHOU8LdAICiwcm28y5BPBZXWIYg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/sinon": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz", @@ -903,10 +909,17 @@ "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", "dev": true }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, "requires": { "event-target-shim": "^5.0.0" } @@ -951,6 +964,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "optional": true, "requires": { "es6-promisify": "^5.0.0" } @@ -1046,12 +1060,60 @@ "default-require-extensions": "^2.0.0" } }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, "archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1225,758 +1287,199 @@ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "async-done": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", - "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^2.0.0", - "stream-exhaust": "^1.0.1" - } - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, - "async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", - "dev": true, - "requires": { - "async-done": "^1.2.2" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", - "dev": true, - "requires": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" - } - }, - "backbone": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.0.tgz", - "integrity": "sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==", - "dev": true, - "requires": { - "underscore": ">=1.8.3" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "bcrypt": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.2.tgz", - "integrity": "sha512-kE1IaaRchCgdrmzQX/eBQKcsuL4jRHZ+O11sMvEUrI/HgFTQYAGvxlj9z7kb3zfFuwljQ5y8/NrbnXtgx5oJLg==", - "dev": true, - "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.11.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "dev": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "minimatch": { - "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" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "minipass": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz", - "integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true - } - } - }, - "minizlib": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", - "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", - "dev": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "needle": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.3.tgz", - "integrity": "sha512-GPL22d/U9cai87FcCPO6e+MT3vyHS2j+zwotakDc7kE2DtUAqFKMXLJCTtRp+5S75vXIwQPvIxkvlctxf9q4gQ==", - "dev": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "dev": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", - "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", - "dev": true - }, - "npm-packlist": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.11.tgz", - "integrity": "sha512-CxKlZ24urLkJk+9kCm48RTQ7L4hsmgSVzEk0TLGPzzyuFxD7VNgy5Sl24tOLMzQv773a/NeJ1ce1DKeacqffEA==", - "dev": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "readable-stream": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", - "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.0.3", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async-done": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, + "async-settle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "dev": true, + "requires": { + "async-done": "^1.2.2" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "bach": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", + "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", + "dev": true, + "requires": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + } + }, + "backbone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.0.tgz", + "integrity": "sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==", + "dev": true, + "requires": { + "underscore": ">=1.8.3" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "is-descriptor": "^1.0.0" } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "ansi-regex": "^2.0.0" + "kind-of": "^6.0.0" } }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "tar": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", - "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.3", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true - } + "kind-of": "^6.0.0" } }, - "util-deprecate": { + "is-descriptor": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "string-width": "^1.0.2 || 2" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true } } }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "bcrypt": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.8.tgz", + "integrity": "sha512-jKV6RvLhI36TQnPDvUFqBEnGX9c8dRRygKxCZu7E+MgLfKZbmmXL8a7/SFFOyHoPNX9nV81cKRC5tbQfvEQtpw==", + "dev": true, + "requires": { + "nan": "2.14.0", + "node-pre-gyp": "0.14.0" + } + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -1995,7 +1498,8 @@ "bignumber.js": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", - "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==", + "optional": true }, "binary-extensions": { "version": "1.13.1", @@ -2293,6 +1797,12 @@ } } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -2590,6 +2100,12 @@ } } }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, "convert-source-map": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", @@ -2770,6 +2286,12 @@ "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", "dev": true }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -2894,12 +2416,24 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", "dev": true }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true + }, "dicer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", @@ -3034,6 +2568,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", + "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -3122,12 +2657,14 @@ "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "optional": true }, "es6-promisify": { "version": "5.0.0", "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "optional": true, "requires": { "es6-promise": "^4.0.3" } @@ -3437,7 +2974,8 @@ "event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true }, "execa": { "version": "1.0.0", @@ -3647,7 +3185,8 @@ "fast-text-encoding": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", - "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==" + "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==", + "optional": true }, "faye-websocket": { "version": "0.11.3", @@ -3942,6 +3481,15 @@ } } }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "requires": { + "minipass": "^2.6.0" + } + }, "fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", @@ -4013,24 +3561,29 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "dev": true + "resolved": false, + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "optional": true, "requires": { @@ -4040,13 +3593,17 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "dev": true + "resolved": false, + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", - "bundled": true, + "resolved": false, + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4054,34 +3611,43 @@ }, "chownr": { "version": "1.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": false, + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "optional": true }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, "concat-map": { "version": "0.0.1", - "bundled": true, - "dev": true + "resolved": false, + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "dev": true + "resolved": false, + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "4.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "optional": true, "requires": { @@ -4090,25 +3656,29 @@ }, "deep-extend": { "version": "0.6.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, + "resolved": false, + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, "requires": { @@ -4117,13 +3687,15 @@ }, "fs.realpath": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": false, + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, "requires": { @@ -4139,7 +3711,8 @@ }, "glob": { "version": "7.1.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "optional": true, "requires": { @@ -4153,13 +3726,15 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", - "bundled": true, + "resolved": false, + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "optional": true, "requires": { @@ -4168,7 +3743,8 @@ }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, "requires": { @@ -4177,7 +3753,8 @@ }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": false, + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, "requires": { @@ -4187,46 +3764,58 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "dev": true + "resolved": false, + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "dev": true + "resolved": false, + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4234,7 +3823,8 @@ }, "minizlib": { "version": "1.2.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "dev": true, "optional": true, "requires": { @@ -4243,15 +3833,18 @@ }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } }, "ms": { "version": "2.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "optional": true }, @@ -4264,7 +3857,8 @@ }, "needle": { "version": "2.3.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "optional": true, "requires": { @@ -4275,7 +3869,8 @@ }, "node-pre-gyp": { "version": "0.12.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "optional": true, "requires": { @@ -4293,7 +3888,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, "requires": { @@ -4303,13 +3899,15 @@ }, "npm-bundled": { "version": "1.0.6", - "bundled": true, + "resolved": false, + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "dev": true, "optional": true, "requires": { @@ -4319,7 +3917,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, "requires": { @@ -4331,38 +3930,46 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "dev": true + "resolved": false, + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "optional": true, "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, "requires": { @@ -4372,19 +3979,22 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { "version": "1.2.8", - "bundled": true, + "resolved": false, + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "optional": true, "requires": { @@ -4396,7 +4006,8 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true } @@ -4404,7 +4015,8 @@ }, "readable-stream": { "version": "2.3.6", - "bundled": true, + "resolved": false, + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, "requires": { @@ -4419,7 +4031,8 @@ }, "rimraf": { "version": "2.6.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "optional": true, "requires": { @@ -4428,43 +4041,52 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "dev": true + "resolved": false, + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.7.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4473,7 +4095,8 @@ }, "string_decoder": { "version": "1.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, "requires": { @@ -4482,21 +4105,25 @@ }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true }, "tar": { "version": "4.4.8", - "bundled": true, + "resolved": false, + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "optional": true, "requires": { @@ -4511,13 +4138,15 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "optional": true, "requires": { @@ -4526,13 +4155,17 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "dev": true + "resolved": false, + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "dev": true + "resolved": false, + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "dev": true, + "optional": true } } }, @@ -4546,10 +4179,27 @@ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, "gaxios": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.1.0.tgz", "integrity": "sha512-Gtpb5sdQmb82sgVkT2GnS2n+Kx4dlFwbeMYcDlD395aEvsLCSQXJJcHt7oJ2LrGxDEAeiOkK79Zv2A8Pzt6CFg==", + "optional": true, "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", @@ -4561,7 +4211,8 @@ "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "optional": true } } }, @@ -4569,6 +4220,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.2.1.tgz", "integrity": "sha512-JjDedBWnbXVXWwTpjBdpb9RpVLiowXG4/50rra4hPH8REXAi2si6Xbb48B2SwkQBLz9Wu6+o32GDTvVy2kkLoQ==", + "optional": true, "requires": { "gaxios": "^2.1.0", "json-bigint": "^0.3.0" @@ -4867,6 +4519,7 @@ "version": "5.5.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.1.tgz", "integrity": "sha512-zCtjQccWS/EHYyFdXRbfeSGM/gW+d7uMAcVnvXRnjBXON5ijo6s0nsObP0ifqileIDSbZjTlLtgo+UoN8IFJcg==", + "optional": true, "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", @@ -4881,7 +4534,8 @@ "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true } } }, @@ -4919,6 +4573,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.2.tgz", "integrity": "sha512-UfnEARfJKI6pbmC1hfFFm+UAcZxeIwTiEcHfqKe/drMsXD/ilnVjF7zgOGpHXyhuvX6jNJK3S8A0hOQjwtFxEw==", + "optional": true, "requires": { "node-forge": "^0.9.0" }, @@ -4926,7 +4581,8 @@ "node-forge": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", - "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" + "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==", + "optional": true } } }, @@ -4945,6 +4601,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.1.tgz", "integrity": "sha512-2FEmEDGi4NdM6u+mtaLjSDDtHiw5wT+nBsI+yrSeFO6fVqPEytYVF6uiIpRaOaZhRP+ozjYWuwwtMlrjAyTcYA==", + "optional": true, "requires": { "gaxios": "^2.1.0", "google-p12-pem": "^2.0.0", @@ -5350,6 +5007,12 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -5521,6 +5184,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "optional": true, "requires": { "agent-base": "^4.3.0", "debug": "^3.1.0" @@ -5547,6 +5211,15 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -6265,6 +5938,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", + "optional": true, "requires": { "bignumber.js": "^7.0.0" } @@ -6393,6 +6067,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", "integrity": "sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw==", + "optional": true, "requires": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.10", @@ -6403,6 +6078,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", "integrity": "sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ==", + "optional": true, "requires": { "jwa": "^1.1.5", "safe-buffer": "^5.0.1" @@ -6752,12 +6428,14 @@ "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "optional": true, "requires": { "yallist": "^3.0.2" } @@ -6923,7 +6601,8 @@ "mime": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "optional": true }, "mime-db": { "version": "1.37.0", @@ -6958,6 +6637,25 @@ "integrity": "sha512-+bMdgqjMN/Z77a6NlY/I3U5LlRDbnmaAk6lDveAPKwSpcPM4tKAuYsvYF8xjhOPXhOYGe/73vVLVez5PW+jqhw==", "dev": true }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "requires": { + "minipass": "^2.9.0" + } + }, "mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -7078,9 +6776,9 @@ } }, "nan": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", - "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", "dev": true }, "nanomatch": { @@ -7108,6 +6806,34 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "needle": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz", + "integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", @@ -7208,19 +6934,48 @@ "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "optional": true }, "node-forge": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" }, + "node-pre-gyp": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", + "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", + "dev": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + } + }, "node-version": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/node-version/-/node-version-1.2.0.tgz", "integrity": "sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==", "dev": true }, + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -7251,6 +7006,32 @@ "once": "^1.3.2" } }, + "npm-bundled": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", + "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "npm-packlist": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", + "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", + "dev": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, "npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -7308,6 +7089,18 @@ "path-key": "^2.0.0" } }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -7638,6 +7431,16 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -7932,6 +7735,7 @@ "version": "6.8.8", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", + "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -7951,7 +7755,8 @@ "@types/node": { "version": "10.17.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.11.tgz", - "integrity": "sha512-dNd2pp8qTzzNLAs3O8nH3iU9DG9866KHq9L3ISPB7DOGERZN81nW/5/g/KzMJpCU8jrbCiMRBzV9/sCEdRosig==" + "integrity": "sha512-dNd2pp8qTzzNLAs3O8nH3iU9DG9866KHq9L3ISPB7DOGERZN81nW/5/g/KzMJpCU8jrbCiMRBzV9/sCEdRosig==", + "optional": true } } }, @@ -8000,6 +7805,18 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -8621,6 +8438,12 @@ "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", "dev": true }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, "saxes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.9.tgz", @@ -8630,15 +8453,6 @@ "xmlchars": "^1.3.1" } }, - "scrypt": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/scrypt/-/scrypt-6.0.3.tgz", - "integrity": "sha1-BOAUpWgrU/pQwtXM4WfXGcBthw0=", - "dev": true, - "requires": { - "nan": "^2.0.8" - } - }, "semver": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", @@ -9074,6 +8888,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, "requires": { "stubs": "^3.0.0" } @@ -9174,7 +8989,8 @@ "stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "optional": true }, "supports-color": { "version": "2.0.0", @@ -9262,6 +9078,21 @@ } } }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, "teeny-request": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-5.3.1.tgz", @@ -10242,6 +10073,15 @@ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", "dev": true }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -10301,7 +10141,8 @@ "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "optional": true }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index d8057c1056..1725f3ec3b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "homepage": "https://firebase.google.com/", "engines": { - "node": "^8.13.0 || >=10.10.0" + "node": ">=10.10.0" }, "scripts": { "build": "gulp build", @@ -55,7 +55,7 @@ "types": "./lib/index.d.ts", "dependencies": { "@firebase/database": "^0.6.0", - "@types/node": "^8.10.59", + "@types/node": "^10.10.0", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", "node-forge": "^0.7.6" @@ -79,12 +79,11 @@ "@types/nock": "^9.1.0", "@types/request": "^2.47.0", "@types/request-promise": "^4.1.41", - "@types/scrypt": "^6.0.0", "@types/sinon": "^4.1.3", "@types/sinon-chai": "^2.7.27", "@typescript-eslint/eslint-plugin": "^2.20.0", "@typescript-eslint/parser": "^2.20.0", - "bcrypt": "^3.0.0", + "bcrypt": "^3.0.6", "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "chalk": "^1.1.3", @@ -108,7 +107,6 @@ "request": "^2.75.0", "request-promise": "^4.1.1", "run-sequence": "^1.1.5", - "scrypt": "^6.0.3", "sinon": "^4.5.0", "sinon-chai": "^2.14.0", "ts-node": "^3.3.0", diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index 9f1b8c18ca..0597d8d9f3 100644 --- a/src/utils/api-request.ts +++ b/src/utils/api-request.ts @@ -496,11 +496,15 @@ class AsyncHttpCall { }); const timeout: number | undefined = this.config.timeout; + const timeoutCallback: () => void = () => { + req.abort(); + this.rejectWithError(`timeout of ${timeout}ms exceeded`, 'ETIMEDOUT', req); + }; if (timeout) { // Listen to timeouts and throw an error. - req.setTimeout(timeout, () => { - req.abort(); - this.rejectWithError(`timeout of ${timeout}ms exceeded`, 'ETIMEDOUT', req); + req.setTimeout(timeout, timeoutCallback); + req.on('socket', (socket) => { + socket.setTimeout(timeout, timeoutCallback); }); } diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 4031618174..ac7d8bde08 100755 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -19,7 +19,6 @@ import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as crypto from 'crypto'; import * as bcrypt from 'bcrypt'; -import * as scrypt from 'scrypt'; import firebase from '@firebase/app'; import '@firebase/auth'; import {clone} from 'lodash'; @@ -1745,8 +1744,14 @@ describe('admin.auth', () => { expect(userImportTest.importOptions.hash.derivedKeyLength).to.exist; const dkLen = userImportTest.importOptions.hash.derivedKeyLength!; - return Buffer.from(scrypt.hashSync( - currentRawPassword, {N, r, p}, dkLen, Buffer.from(currentRawSalt))); + return Buffer.from( + crypto.scryptSync( + currentRawPassword, + Buffer.from(currentRawSalt), + dkLen, + { + N, r, p, + })); }, rawPassword, rawSalt, From 489d78ac361243e2172271743ab5c84d0c50357a Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 9 Jul 2020 10:59:17 -0700 Subject: [PATCH 003/160] change: Dropped Node 8 support and upgraded Firestore/Storage dependencies --- .github/workflows/ci.yml | 2 +- package-lock.json | 5364 +++++++++++++------------------------- package.json | 8 +- 3 files changed, 1885 insertions(+), 3489 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b4b53aba7..7615a6ed62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - node-version: [10.x] + node-version: [10.x, 12.x, 14.x] steps: - uses: actions/checkout@v1 diff --git a/package-lock.json b/package-lock.json index 2a82b424d5..05a01eafa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,64 +5,69 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.0.0" + "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", - "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", + "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", "dev": true, "requires": { - "@babel/types": "^7.4.4", + "@babel/types": "^7.10.4", "jsesc": "^2.5.1", - "lodash": "^4.17.11", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" + "lodash": "^4.17.13", + "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.0.0" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", + "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", "dev": true, "requires": { - "@babel/types": "^7.4.4" + "@babel/types": "^7.10.4" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -98,316 +103,239 @@ } }, "@babel/parser": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz", - "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", + "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", "dev": true }, "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.5.tgz", - "integrity": "sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", + "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.4", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.4.5", - "@babel/types": "^7.4.4", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.11" + "lodash": "^4.17.13" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true } } }, "@babel/types": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", - "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", + "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", "dev": true, "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.11", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } }, "@firebase/app": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.2.tgz", - "integrity": "sha512-rAxc90+82GAPpUxS02EO0dys4+TeQ6XjFjCwQz/OVptGeLgxN9ZoXYAf/bxyeYOdLxJW0kbEKE/0xXaJDt5gsg==", + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.7.tgz", + "integrity": "sha512-6NpIZ3iMrCR2XOShK5oi3YYB0GXX5yxVD8p3+2N+X4CF5cERyIrDRf8+YXOFgr+bDHSbVcIyzpWv6ijhg4MJlw==", "dev": true, "requires": { - "@firebase/app-types": "0.6.0", - "@firebase/component": "0.1.10", - "@firebase/logger": "0.2.2", - "@firebase/util": "0.2.45", + "@firebase/app-types": "0.6.1", + "@firebase/component": "0.1.15", + "@firebase/logger": "0.2.5", + "@firebase/util": "0.2.50", "dom-storage": "2.1.0", - "tslib": "1.11.1", + "tslib": "^1.11.1", "xmlhttprequest": "1.8.0" - }, - "dependencies": { - "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", - "dev": true - } } }, "@firebase/app-types": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.0.tgz", - "integrity": "sha512-ld6rzjXk/SUauHiQZJkeuSJpxIZ5wdnWuF5fWBFQNPaxsaJ9kyYg9GqEvwZ1z2e6JP5cU9gwRBlfW1WkGtGDYA==" + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", + "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==" }, "@firebase/auth": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.13.3.tgz", - "integrity": "sha512-Ks+6PdLzuxrlkbnSbrMKpOdCbvrfJEBwXe2/GfHCDuJWsxUEx2qFcda+g04pgXnlf1qCjPeNEJM8U0WzTvGHyA==", + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.13.6.tgz", + "integrity": "sha512-ERlda/t5RimNw5Err+5HJATC/qFkC64zR40G+4nK5b9eFJEm0MB+/DaismCwp6J6GoVL3NmejoVbuWU7sV4G1w==", "dev": true, "requires": { - "@firebase/auth-types": "0.9.3" + "@firebase/auth-types": "0.9.6" } }, "@firebase/auth-interop-types": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.4.tgz", - "integrity": "sha512-CLKNS84KGAv5lRnHTQZFWoR11Ti7gIPFirDDXWek/fSU+TdYdnxJFR5XSD4OuGyzUYQ3Dq7aVj5teiRdyBl9hA==" + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", + "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==" }, "@firebase/auth-types": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.9.3.tgz", - "integrity": "sha512-eS9BEuZ1XxBQReUhG6lbus9ScOgHwqYPT7a645PKa/tBb1BWsgivwRFzH0BATPGLP+JTtRvy5JqEsQ25S7J4ig==", + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.9.6.tgz", + "integrity": "sha512-HB1yXe5hgiwPMukLBEfC3TQX22U9qKczj8kEclKhL7rnds3FKZWMM0+EpKbcJREbU9Sj/rgwgaio7ovSN4ZQFA==", "dev": true }, "@firebase/component": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.10.tgz", - "integrity": "sha512-Iy1+f8wp6mROz19oxWUd31NxMlGxtW1IInGHITnVa6eZtXOg0lxcbgYeLp9W3PKzvvNfshHU0obDkcMY97zRAw==", + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.15.tgz", + "integrity": "sha512-HqFb1qQl1vtlUMIzPM15plNz27jqM8DWjuQQuGeDfG+4iRRflwKfgNw1BOyoP4kQ8vOBCL7t/71yPXSomNdJdQ==", "requires": { - "@firebase/util": "0.2.45", - "tslib": "1.11.1" - }, - "dependencies": { - "@firebase/util": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.45.tgz", - "integrity": "sha512-k3IqXaIgwlPg7m5lXmMUtkqA/p+LMFkFQIqBuDtdT0iyWB6kQDokyjw2Sgd3GoTybs6tWqUKFZupZpV6r73UHw==", - "requires": { - "tslib": "1.11.1" - } - }, - "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" - } + "@firebase/util": "0.2.50", + "tslib": "^1.11.1" } }, "@firebase/database": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.1.tgz", - "integrity": "sha512-7XqUbj3nK2vEdFjGOXBfKISmpLrM0caIwwfDPxhn6i7X/g6AIH+D1limH+Jit4QeKMh/IJZDNqO7P+Fz+e8q1Q==", - "requires": { - "@firebase/auth-interop-types": "0.1.4", - "@firebase/component": "0.1.10", - "@firebase/database-types": "0.5.0", - "@firebase/logger": "0.2.2", - "@firebase/util": "0.2.45", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.6.tgz", + "integrity": "sha512-TqUJOaCATF/h3wpqhPT9Fz1nZI6gBv/M2pHZztUjX4A9o9Bq93NyqUurYiZnGB7zpSkEADFCVT4f0VBrWdHlNw==", + "requires": { + "@firebase/auth-interop-types": "0.1.5", + "@firebase/component": "0.1.15", + "@firebase/database-types": "0.5.1", + "@firebase/logger": "0.2.5", + "@firebase/util": "0.2.50", "faye-websocket": "0.11.3", - "tslib": "1.11.1" - }, - "dependencies": { - "@firebase/logger": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.2.tgz", - "integrity": "sha512-MbEy17Ha1w/DlLtvxG89ScQ+0+yoElGKJ1nUCQHHLjeMNsRwd2wnUPOVCsZvtBzQp8Z0GaFmD4a2iG2v91lEbA==" - }, - "@firebase/util": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.45.tgz", - "integrity": "sha512-k3IqXaIgwlPg7m5lXmMUtkqA/p+LMFkFQIqBuDtdT0iyWB6kQDokyjw2Sgd3GoTybs6tWqUKFZupZpV6r73UHw==", - "requires": { - "tslib": "1.11.1" - } - }, - "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" - } + "tslib": "^1.11.1" } }, "@firebase/database-types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.0.tgz", - "integrity": "sha512-6/W3frFznYOALtw2nrWVPK2ytgdl89CzTqVBHCCGf22wT6uKU63iDBo+Nw+7olFGpD15O0zwYalFIcMZ27tkew==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.1.tgz", + "integrity": "sha512-onQxom1ZBYBJ648w/VNRzUewovEDAH7lvnrrpCd69ukkyrMk6rGEO/PQ9BcNEbhlNtukpsqRS0oNOFlHs0FaSA==", "requires": { - "@firebase/app-types": "0.6.0" + "@firebase/app-types": "0.6.1" } }, "@firebase/logger": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.2.tgz", - "integrity": "sha512-MbEy17Ha1w/DlLtvxG89ScQ+0+yoElGKJ1nUCQHHLjeMNsRwd2wnUPOVCsZvtBzQp8Z0GaFmD4a2iG2v91lEbA==", - "dev": true + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.5.tgz", + "integrity": "sha512-qqw3m0tWs/qrg7axTZG/QZq24DIMdSY6dGoWuBn08ddq7+GLF5HiqkRj71XznYeUUbfRq5W9C/PSFnN4JxX+WA==" }, "@firebase/util": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.45.tgz", - "integrity": "sha512-k3IqXaIgwlPg7m5lXmMUtkqA/p+LMFkFQIqBuDtdT0iyWB6kQDokyjw2Sgd3GoTybs6tWqUKFZupZpV6r73UHw==", - "dev": true, + "version": "0.2.50", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.50.tgz", + "integrity": "sha512-vFE6+Jfc25u0ViSpFxxq0q5s+XmuJ/y7CL3ud79RQe+WLFFg+j0eH1t23k0yNSG9vZNM7h3uHRIXbV97sYLAyw==", "requires": { - "tslib": "1.11.1" - }, - "dependencies": { - "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", - "dev": true - } + "tslib": "^1.11.1" } }, "@google-cloud/common": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.2.3.tgz", - "integrity": "sha512-lvw54mGKn8VqVIy2NzAk0l5fntBFX4UwQhHk6HaqkyCQ7WBl5oz4XhzKMtMilozF/3ObPcDogqwuyEWyZ6rnQQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.3.1.tgz", + "integrity": "sha512-bJamcNvZ2j5xS01uFBT1GqfHIKrtwpyUhIU/Xn3uwMZkK/t6JA3mlID0wuZlo7XjbjFSRT2iLBEmDWv9T2hP8g==", "optional": true, "requires": { - "@google-cloud/projectify": "^1.0.0", - "@google-cloud/promisify": "^1.0.0", - "arrify": "^2.0.0", - "duplexify": "^3.6.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", "ent": "^2.2.0", "extend": "^3.0.2", - "google-auth-library": "^5.5.0", - "retry-request": "^4.0.0", - "teeny-request": "^5.2.1" + "google-auth-library": "^6.0.0", + "retry-request": "^4.1.1", + "teeny-request": "^7.0.0" }, "dependencies": { - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "optional": true + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } } } }, "@google-cloud/firestore": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-3.0.0.tgz", - "integrity": "sha512-Os6rXW6z9bd2sVdjDJRUneF5u7keH+vpWX/Uddq0dlFyNbwBSgCBFWt+0VYXkgQE+O8B8i1p+FdaleTjFFuRVA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.0.0.tgz", + "integrity": "sha512-TU+MPOjvbouJM+xyQFJpSFYZoL1/nQhBR+9sqMiEwXkwa1CZKskSz0JspNE906cMBYgsyl6x1jH87QjGGwkV2w==", "optional": true, "requires": { - "bun": "^0.0.12", - "deep-equal": "^1.1.1", + "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", - "google-gax": "^1.12.0", - "through2": "^3.0.0" - }, - "dependencies": { - "deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "optional": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "optional": true - } + "google-gax": "^2.2.0" } }, "@google-cloud/paginator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.1.tgz", - "integrity": "sha512-HZ6UTGY/gHGNriD7OCikYWL/Eu0sTEur2qqse2w6OVsz+57se3nTkqH14JIPxtf0vlEJ8IJN5w3BdZ22pjCB8g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.2.tgz", + "integrity": "sha512-kXK+Dbz4pNvv8bKU80Aw5HsIdgOe0WuMTd8/fI6tkANUxzvJOVJQQRsWVqcHSWK2RXHPTA9WBniUCwY6gAJDXw==", "optional": true, "requires": { "arrify": "^2.0.0", "extend": "^3.0.2" - }, - "dependencies": { - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "optional": true - } } }, "@google-cloud/projectify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.1.tgz", - "integrity": "sha512-xknDOmsMgOYHksKc1GPbwDLsdej8aRNIA17SlSZgQdyrcC0lx0OGo4VZgYfwoEU1YS8oUxF9Y+6EzDOb0eB7Xg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==", "optional": true }, "@google-cloud/promisify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.2.tgz", - "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.1.tgz", + "integrity": "sha512-82EQzwrNauw1fkbUSr3f+50Bcq7g4h0XvLOk8C5e9ABkXYHei7ZPi9tiMMD7Vh3SfcdH97d1ibJ3KBWp2o1J+w==", "optional": true }, "@google-cloud/storage": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.1.2.tgz", - "integrity": "sha512-kYP7h2SMx5KmbIbeQ4qHoBm9uYFRZOR96BCYpzGWYO8ii157sA1nmmULai0lcrVwOhfVXoReZUpflTc5lFN80g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.1.1.tgz", + "integrity": "sha512-w/64V+eJl+vpYUXT15sBcO8pX0KTmb9Ni2ZNuQQ8HmyhAbEA3//G8JFaLPCXGBWO2/b0OQZytUT6q2wII9a9aQ==", "optional": true, "requires": { - "@google-cloud/common": "^2.1.1", - "@google-cloud/paginator": "^2.0.0", - "@google-cloud/promisify": "^1.0.0", + "@google-cloud/common": "^3.0.0", + "@google-cloud/paginator": "^3.0.0", + "@google-cloud/promisify": "^2.0.0", "arrify": "^2.0.0", "compressible": "^2.0.12", "concat-stream": "^2.0.0", - "date-and-time": "^0.10.0", + "date-and-time": "^0.13.0", "duplexify": "^3.5.0", "extend": "^3.0.2", - "gaxios": "^2.0.1", - "gcs-resumable-upload": "^2.2.4", + "gaxios": "^3.0.0", + "gcs-resumable-upload": "^3.0.0", "hash-stream-validation": "^0.2.2", "mime": "^2.2.0", "mime-types": "^2.0.8", "onetime": "^5.1.0", - "p-limit": "^2.2.0", + "p-limit": "^3.0.1", "pumpify": "^2.0.0", "readable-stream": "^3.4.0", "snakeize": "^0.1.0", @@ -416,108 +344,32 @@ "xdg-basedir": "^4.0.0" }, "dependencies": { - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "optional": true - }, - "concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "optional": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "optional": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "optional": true, - "requires": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - }, - "dependencies": { - "duplexify": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", - "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", - "optional": true, - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - } - } - }, "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "optional": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "optional": true, - "requires": { - "safe-buffer": "~5.2.0" - } } } }, "@grpc/grpc-js": { - "version": "0.6.14", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.6.14.tgz", - "integrity": "sha512-M6q3MtHzk0NQPs1PB+SXSJtkDtK8WXJh+1B1WVJQp5HTURadzj9t1bUb/Fjhq+K57lKsOgL60r8WGmE7vks1eg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.1.tgz", + "integrity": "sha512-mhZRszS0SKwnWPJaNyrECePZ9U7vaHFGqrzxQbWinWR3WznBIU+nmh2L5J3elF+lp5DEUIzARXkifbs6LQVAHA==", "optional": true, "requires": { "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "optional": true - } } }, "@grpc/proto-loader": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.3.tgz", - "integrity": "sha512-8qvUtGg77G2ZT2HqdqYoM/OY97gQd/0crSG34xNmZ4ZOsv3aQT/FQV9QfZPazTGna6MIoyUd+u6AxsoZjJ/VMQ==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.4.tgz", + "integrity": "sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA==", "optional": true, "requires": { "lodash.camelcase": "^4.3.0", @@ -589,9 +441,9 @@ "optional": true }, "@sinonjs/commons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", - "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", + "integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==", "dev": true, "requires": { "type-detect": "4.0.8" @@ -615,14 +467,14 @@ } }, "@sinonjs/samsam": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", - "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", "dev": true, "requires": { - "@sinonjs/commons": "^1.0.2", + "@sinonjs/commons": "^1.3.0", "array-from": "^2.1.1", - "lodash": "^4.17.11" + "lodash": "^4.17.15" } }, "@sinonjs/text-encoding": { @@ -631,6 +483,12 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true + }, "@types/bcrypt": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-2.0.0.tgz", @@ -638,21 +496,21 @@ "dev": true }, "@types/bluebird": { - "version": "3.5.24", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.24.tgz", - "integrity": "sha512-YeQoDpq4Lm8ppSBqAnAeF/xy1cYp/dMTif2JFcvmAbETMRlvKHT2iLcWu+WyYiJO3b3Ivokwo7EQca/xfLVJmg==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.32.tgz", + "integrity": "sha512-dIOxFfI0C+jz89g6lQ+TqhGgPQ0MxSnh/E4xuC0blhFtyW269+mPG5QeLgbdwst/LvdP8o1y0o/Gz5EHXLec/g==", "dev": true }, "@types/caseless": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", - "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", "dev": true }, "@types/chai": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-3.5.2.tgz", - "integrity": "sha1-wRzSgX06QBt7oPWkIPNcVhObHB4=", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.11.tgz", + "integrity": "sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==", "dev": true }, "@types/chai-as-promised": { @@ -665,6 +523,12 @@ "@types/promises-a-plus": "*" } }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -677,19 +541,10 @@ "integrity": "sha1-Z1VIHZMk4mt6XItFXWgUg3aCw5Y=", "dev": true }, - "@types/form-data": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/json-schema": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", - "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", "dev": true }, "@types/jsonwebtoken": { @@ -702,15 +557,15 @@ } }, "@types/lodash": { - "version": "4.14.118", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.118.tgz", - "integrity": "sha512-iiJbKLZbhSa6FYRip/9ZDX6HXhayXLDGY2Fqws9cOkEQ6XeKfaxB0sC541mowZJueYyMnVUmmG+al5/4fCDrgw==", + "version": "4.14.157", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.157.tgz", + "integrity": "sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ==", "dev": true }, "@types/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", - "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", "optional": true }, "@types/minimatch": { @@ -721,7 +576,7 @@ }, "@types/minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", "dev": true }, @@ -732,9 +587,9 @@ "dev": true }, "@types/nock": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.3.0.tgz", - "integrity": "sha512-ZHf/X8rTQ5Tb1rHjxIJYqm55uO265agE3G7NoSXVa2ep+EcJXgB2fsme+zBvK7MhrxTwkC/xkB6THyv50u0MGw==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.3.1.tgz", + "integrity": "sha512-eOVHXS5RnWOjTVhu3deCM/ruy9E6JCgeix2g7wpFiekQh3AaEAK1cz43tZDukKmtSmQnwvSySq7ubijCA32I7Q==", "dev": true, "requires": { "@types/node": "*" @@ -752,21 +607,21 @@ "dev": true }, "@types/request": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", - "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", + "version": "2.48.5", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz", + "integrity": "sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==", "dev": true, "requires": { "@types/caseless": "*", - "@types/form-data": "*", "@types/node": "*", - "@types/tough-cookie": "*" + "@types/tough-cookie": "*", + "form-data": "^2.5.0" } }, "@types/request-promise": { - "version": "4.1.42", - "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.42.tgz", - "integrity": "sha512-b8li55sEZ00BXZstZ3d8WOi48dnapTqB1VufEG9Qox0nVI2JVnTVT1Mw4JbBa1j+1sGVX/qJ0R4WDv4v2GjT0w==", + "version": "4.1.46", + "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.46.tgz", + "integrity": "sha512-3Thpj2Va5m0ji3spaCk8YKrjkZyZc6RqUVOphA0n/Xet66AW/AiOAs5vfXhQIL5NmkaO7Jnun7Nl9NEjJ2zBaw==", "dev": true, "requires": { "@types/bluebird": "*", @@ -780,9 +635,9 @@ "dev": true }, "@types/sinon-chai": { - "version": "2.7.34", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-2.7.34.tgz", - "integrity": "sha512-1AxQR0Tk4Q0FYGoJrj66UKhiMmlmxg78SuC5rBJp55Nex4cd948GGJca1Qf2bf1X7P4YKcAW3uKiQB9yvJP4rA==", + "version": "2.7.37", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-2.7.37.tgz", + "integrity": "sha512-blMkVSMl8FrrodQfYzKAxs+T1ET0ylTbmYkyytMAJokYdbLw7jMgI8/YliM9y5z93yIB1ZYjwmXFBWJDZRM2RA==", "dev": true, "requires": { "@types/chai": "*", @@ -790,62 +645,51 @@ } }, "@types/tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-Set5ZdrAaKI/qHdFlVMgm/GsAv/wkXhSTuZFkJ+JI7HK+wIkIlOaUXSXieIvJ0+OvGIqtREFoE+NHJtEq0gtEw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==", "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.20.0.tgz", - "integrity": "sha512-cimIdVDV3MakiGJqMXw51Xci6oEDEoPkvh8ggJe2IIzcc0fYqAxOXN6Vbeanahz6dLZq64W+40iUEc9g32FLDQ==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", + "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.20.0", - "eslint-utils": "^1.4.3", + "@typescript-eslint/experimental-utils": "2.34.0", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", "tsutils": "^3.17.1" - }, - "dependencies": { - "tsutils": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", - "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - } } }, "@typescript-eslint/experimental-utils": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.20.0.tgz", - "integrity": "sha512-fEBy9xYrwG9hfBLFEwGW2lKwDRTmYzH3DwTmYbT+SMycmxAoPl0eGretnBFj/s+NfYBG63w/5c3lsvqqz5mYag==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", + "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.20.0", - "eslint-scope": "^5.0.0" + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.20.0.tgz", - "integrity": "sha512-o8qsKaosLh2qhMZiHNtaHKTHyCHc3Triq6aMnwnWj7budm3xAY9owSZzV1uon5T9cWmJRJGzTFa90aex4m77Lw==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", + "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.20.0", - "@typescript-eslint/typescript-estree": "2.20.0", + "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/typescript-estree": "2.34.0", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/typescript-estree": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.20.0.tgz", - "integrity": "sha512-WlFk8QtI8pPaE7JGQGxU7nGcnk1ccKAJkhbVookv94ZcAef3m6oCE/jEDL6dGte3JcD7reKrA0o55XhBRiVT3A==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", "dev": true, "requires": { "debug": "^4.1.1", @@ -853,60 +697,22 @@ "glob": "^7.1.6", "is-glob": "^4.0.1", "lodash": "^4.17.15", - "semver": "^6.3.0", + "semver": "^7.3.2", "tsutils": "^3.17.1" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "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 - }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true - }, - "tsutils": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", - "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } } } }, "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", "dev": true }, "abbrev": { @@ -925,15 +731,15 @@ } }, "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", + "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", "dev": true }, "acorn-globals": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", - "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", "dev": true, "requires": { "acorn": "^6.0.1", @@ -949,33 +755,33 @@ } }, "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", "dev": true }, "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", "optional": true, "requires": { - "es6-promisify": "^5.0.0" + "debug": "4" } }, "ajv": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", - "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -991,12 +797,20 @@ } }, "ansi-escapes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", - "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } } }, "ansi-gray": { @@ -1080,38 +894,6 @@ "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "argparse": { @@ -1177,12 +959,6 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, - "array-filter": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", - "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", - "dev": true - }, "array-from": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", @@ -1224,18 +1000,6 @@ } } }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", - "dev": true - }, - "array-reduce": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", - "dev": true - }, "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -1283,10 +1047,10 @@ "dev": true }, "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true }, "asn1": { "version": "0.2.4", @@ -1339,12 +1103,6 @@ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", "dev": true }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, "async-settle": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", @@ -1373,9 +1131,9 @@ "dev": true }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", "dev": true }, "bach": { @@ -1466,9 +1224,9 @@ } }, "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "bcrypt": { "version": "3.0.8", @@ -1496,9 +1254,9 @@ "dev": true }, "bignumber.js": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", - "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", "optional": true }, "binary-extensions": { @@ -1513,13 +1271,23 @@ "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", "dev": true }, - "bluebird": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", - "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==", - "dev": true - }, - "brace-expansion": { + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", @@ -1559,9 +1327,9 @@ } }, "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, "browser-stdout": { @@ -1571,22 +1339,14 @@ "dev": true }, "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", "dev": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - } } }, "buffer-equal": { @@ -1605,21 +1365,6 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "bun": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/bun/-/bun-0.0.12.tgz", - "integrity": "sha512-Toms18J9DqnT+IfWkwxVTB2EaBprHvjlMWrTIsfX4xbu3ZBqVBwrERU0em1IgtRe04wT+wJxMlKHZok24hrcSQ==", - "optional": true, - "requires": { - "readable-stream": "~1.0.32" - } - }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -1664,6 +1409,23 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } } } }, @@ -1674,9 +1436,9 @@ "dev": true }, "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", "dev": true }, "caseless": { @@ -1739,40 +1501,12 @@ "cross-spawn": "^4.0.2", "node-version": "^1.0.0", "promise-polyfill": "^6.0.1" - }, - "dependencies": { - "cross-spawn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", - "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } } }, "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -1789,6 +1523,27 @@ "upath": "^1.1.1" }, "dependencies": { + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1836,9 +1591,9 @@ } }, "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true }, "cliui": { @@ -1853,9 +1608,9 @@ } }, "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", "dev": true }, "clone-buffer": { @@ -1865,52 +1620,20 @@ "dev": true }, "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", "dev": true }, "cloneable-readable": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz", - "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", "dev": true, "requires": { "inherits": "^2.0.1", "process-nextick-args": "^2.0.0", "readable-stream": "^2.3.5" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "code-point-at": { @@ -1962,9 +1685,9 @@ "dev": true }, "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "requires": { "delayed-stream": "~1.0.0" @@ -1989,20 +1712,12 @@ "dev": true }, "compressible": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", - "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "optional": true, "requires": { - "mime-db": ">= 1.40.0 < 2" - }, - "dependencies": { - "mime-db": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", - "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==", - "optional": true - } + "mime-db": ">= 1.43.0 < 2" } }, "concat-map": { @@ -2012,45 +1727,26 @@ "dev": true }, "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "optional": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, "requires": { - "safe-buffer": "~5.1.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } } } @@ -2073,31 +1769,17 @@ } }, "configstore": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.0.tgz", - "integrity": "sha512-eE/hvMs7qw7DlcB5JPRnthmrITuHMmACUJAp89v6PT6iOqzoLS7HRWhBtuHMlhNHo2AhUSA/3Dh1bKNJHcublQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", "optional": true, "requires": { - "dot-prop": "^5.1.0", + "dot-prop": "^5.2.0", "graceful-fs": "^4.1.2", "make-dir": "^3.0.0", "unique-string": "^2.0.0", "write-file-atomic": "^3.0.0", "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "write-file-atomic": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", - "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", - "optional": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - } } }, "console-control-strings": { @@ -2107,12 +1789,20 @@ "dev": true }, "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", "dev": true, "requires": { "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } } }, "copy-descriptor": { @@ -2164,20 +1854,41 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", + "lru-cache": "^4.0.1", "which": "^1.2.9" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } } }, "crypto-random-string": { @@ -2187,18 +1898,26 @@ "optional": true }, "cssom": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", "dev": true }, "cssstyle": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.2.tgz", - "integrity": "sha512-43wY3kl1CVQSvL7wUY1qXkxVGkStjpkDmVjiIKX8R97uhajy8Bybay78uOtqvh7Q5GK75dNPfW0geWjE6qQQow==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "dev": true, "requires": { - "cssom": "0.3.x" + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } } }, "d": { @@ -2232,9 +1951,9 @@ } }, "date-and-time": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.10.0.tgz", - "integrity": "sha512-IbIzxtvK80JZOVsWF6+NOjunTaoFVYxkAQoyzmflJyuRCJAJebehy48mPiCAedcGp4P7/UO3QYRWa0fe6INftg==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.13.1.tgz", + "integrity": "sha512-/Uge9DJAT+s+oAcDxtBhyR8+sKjUnZbYmyhbmWjTHNtX7B7oWD8YyYdeXcBRbwSj6hVvj+IQegJam7m7czhbFw==", "optional": true }, "dateformat": { @@ -2244,11 +1963,11 @@ "dev": true }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "decamelize": { @@ -2281,10 +2000,18 @@ } }, "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } }, "deep-extend": { "version": "0.6.0", @@ -2342,6 +2069,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -2400,14 +2128,6 @@ "pify": "^2.0.0", "pinkie-promise": "^2.0.0", "rimraf": "^2.2.8" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } } }, "delayed-stream": { @@ -2490,6 +2210,12 @@ "readable-stream": "~1.1.9" }, "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, "readable-stream": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", @@ -2501,47 +2227,24 @@ "isarray": "0.0.1", "string_decoder": "~0.10.x" } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true } } }, "duplexify": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", - "integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "each-props": { @@ -2565,24 +2268,23 @@ } }, "ecdsa-sig-formatter": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", - "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", - "optional": true, + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "requires": { "safe-buffer": "^5.0.1" } }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "requires": { "once": "^1.4.0" } @@ -2603,22 +2305,28 @@ } }, "es-abstract": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "dev": true, "requires": { - "es-to-primitive": "^1.1.1", + "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" } }, "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { "is-callable": "^1.1.4", @@ -2627,14 +2335,14 @@ } }, "es5-ext": { - "version": "0.10.50", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", - "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", + "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.1", - "next-tick": "^1.0.0" + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" } }, "es6-error": { @@ -2654,29 +2362,14 @@ "es6-symbol": "^3.1.1" } }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "optional": true - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "optional": true, - "requires": { - "es6-promise": "^4.0.3" - } - }, "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, "requires": { - "d": "1", - "es5-ext": "~0.10.14" + "d": "^1.0.1", + "ext": "^1.1.2" } }, "es6-weak-map": { @@ -2698,24 +2391,18 @@ "dev": true }, "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", "dev": true, "requires": { - "esprima": "^3.1.3", + "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" }, "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2770,18 +2457,6 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", @@ -2808,57 +2483,34 @@ "supports-color": "^5.3.0" } }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true - }, - "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", - "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, - "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 - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "dev": true, "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" + "eslint-visitor-keys": "^1.1.0" } }, "regexpp": { @@ -2867,12 +2519,6 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -2883,9 +2529,9 @@ } }, "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", + "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", "dev": true }, "supports-color": { @@ -2900,9 +2546,9 @@ } }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -2910,28 +2556,28 @@ } }, "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" } }, "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true }, "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.1.0" } }, @@ -2942,12 +2588,20 @@ "dev": true }, "esquery": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz", - "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } } }, "esrecurse": { @@ -2960,15 +2614,15 @@ } }, "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, "event-target-shim": { @@ -2977,21 +2631,6 @@ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "optional": true }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -3033,6 +2672,12 @@ "requires": { "is-extendable": "^0.1.0" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -3045,6 +2690,23 @@ "homedir-polyfill": "^1.0.1" } }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "dev": true, + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==", + "dev": true + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3154,26 +2816,26 @@ "dev": true }, "fancy-log": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz", - "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", "dev": true, "requires": { "ansi-gray": "^0.1.1", "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", "time-stamp": "^1.0.0" } }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, "fast-levenshtein": { @@ -3183,9 +2845,9 @@ "dev": true }, "fast-text-encoding": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", - "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", "optional": true }, "faye-websocket": { @@ -3214,6 +2876,13 @@ "flat-cache": "^2.0.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -3263,6 +2932,12 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -3336,9 +3011,9 @@ } }, "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, "flush-write-stream": { @@ -3349,38 +3024,6 @@ "requires": { "inherits": "^2.0.3", "readable-stream": "^2.3.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "for-in": { @@ -3406,34 +3049,6 @@ "requires": { "cross-spawn": "^4", "signal-exit": "^3.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", - "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } } }, "forever-agent": { @@ -3443,9 +3058,9 @@ "dev": true }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", "dev": true, "requires": { "asynckit": "^0.4.0", @@ -3471,14 +3086,6 @@ "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - } } }, "fs-minipass": { @@ -3500,36 +3107,6 @@ "through2": "^2.0.3" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -3549,836 +3126,165 @@ "dev": true }, "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "gaxios": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.0.3.tgz", + "integrity": "sha512-PkzQludeIFhd535/yucALT/Wxyj/y2zLyrMwPcJmnLHDugmV49NvAi/vb+VUq/eWztATZCNcb8ue+ywPG+oLuw==", + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.1.0.tgz", + "integrity": "sha512-r57SV28+olVsflPlKyVig3Muo/VDlcsObMtvDGOEtEJXj+DDE8bEl0coIkXh//hbkSDTvo+f5lbihZOndYXQQQ==", + "optional": true, + "requires": { + "gaxios": "^3.0.0", + "json-bigint": "^0.3.0" + } + }, + "gcs-resumable-upload": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.0.tgz", + "integrity": "sha512-gB8xH6EjYCv9lfBEL4FK5+AMgTY0feYoNHAYOV5nCuOrDPhy5MOiyJE8WosgxhbKBPS361H7fkwv6CTufEh9bg==", "optional": true, "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" + "abort-controller": "^3.0.0", + "configstore": "^5.0.0", + "extend": "^3.0.2", + "gaxios": "^3.0.0", + "google-auth-library": "^6.0.0", + "pumpify": "^2.0.0", + "stream-events": "^1.0.4" + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-prop": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/get-prop/-/get-prop-0.0.10.tgz", + "integrity": "sha1-p2tANdFw3XKKZkVpVbyBkjZzCNI=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "dev": true, + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" }, "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": false, - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, - "optional": true, "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" } }, - "balanced-match": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": false, - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "resolved": false, - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": false, - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "resolved": false, - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": false, - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "resolved": false, - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "resolved": false, - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": false, - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": false, - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": false, - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "resolved": false, - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": false, - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": false, - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": false, - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "resolved": false, - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "resolved": false, - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": false, - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true, - "optional": true - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "resolved": false, - "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "resolved": false, - "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": false, - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "resolved": false, - "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "resolved": false, - "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": false, - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": false, - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": false, - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": false, - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": false, - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "resolved": false, - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": false, - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": false, - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": false, - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": false, - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": false, - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "resolved": false, - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "resolved": false, - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": false, - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": false, - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": false, - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": false, - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "resolved": false, - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": false, - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "resolved": false, - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true, - "optional": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "gaxios": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.1.0.tgz", - "integrity": "sha512-Gtpb5sdQmb82sgVkT2GnS2n+Kx4dlFwbeMYcDlD395aEvsLCSQXJJcHt7oJ2LrGxDEAeiOkK79Zv2A8Pzt6CFg==", - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^3.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - }, - "dependencies": { - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "optional": true - } - } - }, - "gcp-metadata": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.2.1.tgz", - "integrity": "sha512-JjDedBWnbXVXWwTpjBdpb9RpVLiowXG4/50rra4hPH8REXAi2si6Xbb48B2SwkQBLz9Wu6+o32GDTvVy2kkLoQ==", - "optional": true, - "requires": { - "gaxios": "^2.1.0", - "json-bigint": "^0.3.0" - } - }, - "gcs-resumable-upload": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.0.tgz", - "integrity": "sha512-PclXJiEngrVx0c4K0LfE1XOxhmOkBEy39Rrhspdn6jAbbwe4OQMZfjo7Z1LHBrh57+bNZeIN4M+BooYppCoHSg==", - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "configstore": "^5.0.0", - "gaxios": "^2.0.0", - "google-auth-library": "^5.0.0", - "pumpify": "^2.0.0", - "stream-events": "^1.0.4" - }, - "dependencies": { - "duplexify": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", - "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", - "optional": true, - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "optional": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "optional": true, - "requires": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "optional": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "optional": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "optional": true, - "requires": { - "safe-buffer": "~5.2.0" - } - } - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "get-prop": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/get-prop/-/get-prop-0.0.10.tgz", - "integrity": "sha1-p2tANdFw3XKKZkVpVbyBkjZzCNI=", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - }, - "dependencies": { - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", @@ -4387,55 +3293,26 @@ "requires": { "is-extglob": "^2.1.0" } - } - } - }, - "glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" } } } @@ -4479,14 +3356,17 @@ } }, "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } }, "globby": { "version": "5.0.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", "dev": true, "requires": { @@ -4498,81 +3378,66 @@ "pinkie-promise": "^2.0.0" }, "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true } } }, "glogg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz", - "integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", "dev": true, "requires": { "sparkles": "^1.0.0" } }, "google-auth-library": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.1.tgz", - "integrity": "sha512-zCtjQccWS/EHYyFdXRbfeSGM/gW+d7uMAcVnvXRnjBXON5ijo6s0nsObP0ifqileIDSbZjTlLtgo+UoN8IFJcg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.0.3.tgz", + "integrity": "sha512-2Np6ojPmaJGXHSMsBhtTQEKfSMdLc8hefoihv7N2cwEr8E5bq39fhoat6TcXHwa0XoGO5Guh9sp3nxHFPmibMw==", "optional": true, "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^2.1.0", - "gcp-metadata": "^3.2.0", - "gtoken": "^4.1.0", - "jws": "^3.1.5", + "gaxios": "^3.0.0", + "gcp-metadata": "^4.1.0", + "gtoken": "^5.0.0", + "jws": "^4.0.0", "lru-cache": "^5.0.0" - }, - "dependencies": { - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "optional": true - } } }, "google-gax": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.12.0.tgz", - "integrity": "sha512-BeeoxVO6y9K20gUsexUwptutd0PfrTItrA02JWwwstlBIOAcvgFp86MHWufQsnrkPVhxBjHXq65aIkSejtJjDg==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.6.2.tgz", + "integrity": "sha512-Q5IydUQ+7sF072E72Cl1KDxHaQIa1a5CVqG80OHk3NXdT5ZvKAveBFp/f78E3HjVHGTgUDB16R4M2wDt0ia9mQ==", "optional": true, "requires": { - "@grpc/grpc-js": "^0.6.12", + "@grpc/grpc-js": "~1.1.1", "@grpc/proto-loader": "^0.5.1", "@types/long": "^4.0.0", "abort-controller": "^3.0.0", "duplexify": "^3.6.0", - "google-auth-library": "^5.0.0", + "google-auth-library": "^6.0.0", "is-stream-ended": "^0.1.4", "lodash.at": "^4.6.0", "lodash.has": "^4.5.2", "node-fetch": "^2.6.0", - "protobufjs": "^6.8.8", + "protobufjs": "^6.9.0", "retry-request": "^4.0.0", "semver": "^6.0.0", "walkdir": "^0.4.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "optional": true - } } }, "google-p12-pem": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.2.tgz", - "integrity": "sha512-UfnEARfJKI6pbmC1hfFFm+UAcZxeIwTiEcHfqKe/drMsXD/ilnVjF7zgOGpHXyhuvX6jNJK3S8A0hOQjwtFxEw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.1.tgz", + "integrity": "sha512-VlQgtozgNVVVcYTXS36eQz4PXPt9gIPqLOhHN0QiV6W6h4qSCNVKPtKC5INtJsaHHF2r7+nOIa26MJeJMTaZEQ==", "optional": true, "requires": { "node-forge": "^0.9.0" @@ -4587,9 +3452,9 @@ } }, "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, "growl": { "version": "1.10.5", @@ -4598,14 +3463,14 @@ "dev": true }, "gtoken": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.1.tgz", - "integrity": "sha512-2FEmEDGi4NdM6u+mtaLjSDDtHiw5wT+nBsI+yrSeFO6fVqPEytYVF6uiIpRaOaZhRP+ozjYWuwwtMlrjAyTcYA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.1.tgz", + "integrity": "sha512-33w4FNDkUcyIOq/TqyC+drnKdI4PdXmWp9lZzssyEQKuvu9ZFN3KttaSnDKo52U3E51oujVGop93mKxmqO8HHg==", "optional": true, "requires": { - "gaxios": "^2.1.0", - "google-p12-pem": "^2.0.0", - "jws": "^3.1.5", + "gaxios": "^3.0.0", + "google-p12-pem": "^3.0.0", + "jws": "^4.0.0", "mime": "^2.2.0" } }, @@ -4621,16 +3486,22 @@ "vinyl-fs": "^3.0.0" }, "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } }, "gulp-cli": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", - "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", "dev": true, "requires": { "ansi-colors": "^1.0.1", @@ -4641,7 +3512,7 @@ "copy-props": "^2.0.1", "fancy-log": "^1.3.2", "gulplog": "^1.0.0", - "interpret": "^1.1.0", + "interpret": "^1.4.0", "isobject": "^3.0.1", "liftoff": "^3.1.0", "matchdep": "^2.0.0", @@ -4649,14 +3520,14 @@ "pretty-hrtime": "^1.0.0", "replace-homedir": "^1.0.0", "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.0.1", + "v8flags": "^3.2.0", "yargs": "^7.1.0" } }, "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", + "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", "dev": true, "requires": { "camelcase": "^3.0.0", @@ -4671,7 +3542,7 @@ "string-width": "^1.0.2", "which-module": "^1.0.0", "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" + "yargs-parser": "5.0.0-security.0" } } } @@ -4687,55 +3558,6 @@ "through2": "^2.0.0" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "lodash.template": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", - "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", - "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -4757,38 +3579,6 @@ "istextorbinary": "1.0.2", "readable-stream": "^2.0.1", "replacestream": "^4.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "gulp-typescript": { @@ -4811,43 +3601,11 @@ "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", "dev": true }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, "source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } } } }, @@ -4876,43 +3634,58 @@ "through2": "^2.0.0", "vinyl": "^0.5.0" }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dependencies": { + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", "dev": true }, - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", "dev": true }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "lodash.template": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "lodash._basecopy": "^3.0.0", + "lodash._basetostring": "^3.0.0", + "lodash._basevalues": "^3.0.0", + "lodash._isiterateecall": "^3.0.0", + "lodash._reinterpolate": "^3.0.0", + "lodash.escape": "^3.0.0", + "lodash.keys": "^3.0.0", + "lodash.restparam": "^3.0.0", + "lodash.templatesettings": "^3.0.0" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "lodash.templatesettings": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "lodash._reinterpolate": "^3.0.0", + "lodash.escape": "^3.0.0" } }, + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -4922,6 +3695,17 @@ "readable-stream": "~2.3.6", "xtend": "~4.0.1" } + }, + "vinyl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", + "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "dev": true, + "requires": { + "clone": "^1.0.0", + "clone-stats": "^0.0.1", + "replace-ext": "0.0.1" + } } } }, @@ -4935,15 +3719,16 @@ } }, "handlebars": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", - "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", "dev": true, "requires": { + "minimist": "^1.2.5", "neo-async": "^2.6.0", - "optimist": "^0.6.1", "source-map": "^0.6.1", - "uglify-js": "^3.1.4" + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" }, "dependencies": { "source-map": { @@ -4974,6 +3759,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -5003,9 +3789,10 @@ } }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true }, "has-unicode": { "version": "2.0.1", @@ -5046,44 +3833,14 @@ } }, "hash-stream-validation": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.2.tgz", - "integrity": "sha512-cMlva5CxWZOrlS/cY0C+9qAzesn5srhFA8IT1VPiHc9bWWBLkJfEUIZr7MWoi89oOOGmpg8ymchaOjiArsGu5A==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.3.tgz", + "integrity": "sha512-OEohGLoUOh+bwsIpHpdvhIXFyRGjeLqJbT8Yc5QTZPbRM7LKywagTQxnX/6mghLDOrD9YGz88hy5mLN2eKflYQ==", "optional": true, "requires": { "through2": "^2.0.0" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -5103,6 +3860,14 @@ "dev": true, "requires": { "is-stream": "^1.0.1" + }, + "dependencies": { + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + } } }, "he": { @@ -5112,24 +3877,24 @@ "dev": true }, "highlight.js": { - "version": "9.15.10", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.10.tgz", - "integrity": "sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw==", + "version": "9.18.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz", + "integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg==", "dev": true }, "homedir-polyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", - "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dev": true, "requires": { "parse-passwd": "^1.0.0" } }, "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, "html-encoding-sniffer": { @@ -5141,6 +3906,12 @@ "whatwg-encoding": "^1.0.1" } }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "http-message-parser": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/http-message-parser/-/http-message-parser-0.0.34.tgz", @@ -5152,21 +3923,36 @@ "get-prop": "0.0.10", "minimist": "^1.2.0", "stream-buffers": "^3.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + } } }, "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", + "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==" }, "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "optional": true, "requires": { - "agent-base": "4", - "debug": "3.1.0" + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" } }, "http-signature": { @@ -5181,13 +3967,13 @@ } }, "https-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", - "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "optional": true, "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" + "agent-base": "6", + "debug": "4" } }, "iconv-lite": { @@ -5200,9 +3986,9 @@ } }, "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", "dev": true }, "ignore": { @@ -5246,9 +4032,9 @@ } }, "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", @@ -5257,23 +4043,23 @@ "dev": true }, "inquirer": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", - "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.0.tgz", + "integrity": "sha512-K+LZp6L/6eE5swqIcVXrxl21aGDU4S50gKH0/d96OMQnSBCyGyZl/oZhbkVmdp5sBoINHd4xZvFSARh2dk6DWA==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", + "chalk": "^4.1.0", "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.15", "mute-stream": "0.0.8", - "run-async": "^2.2.0", - "rxjs": "^6.5.3", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { @@ -5284,29 +4070,44 @@ "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, "is-fullwidth-code-point": { @@ -5324,51 +4125,32 @@ "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } + "ansi-regex": "^5.0.0" } }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } } } }, "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, "invert-kv": { @@ -5417,7 +4199,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "optional": true + "dev": true }, "is-arrayish": { "version": "0.2.1", @@ -5440,19 +4222,11 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "requires": { - "builtin-modules": "^1.0.0" - } - }, "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true }, "is-data-descriptor": { "version": "0.1.4", @@ -5475,9 +4249,10 @@ } }, "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true }, "is-descriptor": { "version": "0.1.6", @@ -5593,18 +4368,13 @@ "isobject": "^3.0.1" } }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, "requires": { - "has": "^1.0.1" + "has-symbols": "^1.0.1" } }, "is-relative": { @@ -5617,10 +4387,10 @@ } }, "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "optional": true }, "is-stream-ended": { "version": "0.1.4", @@ -5629,11 +4399,12 @@ "optional": true }, "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, "requires": { - "has-symbols": "^1.0.0" + "has-symbols": "^1.0.1" } }, "is-typedarray": { @@ -5669,9 +4440,9 @@ "dev": true }, "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -5719,14 +4490,6 @@ "@babel/types": "^7.4.0", "istanbul-lib-coverage": "^2.0.5", "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", - "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==", - "dev": true - } } }, "istanbul-lib-report": { @@ -5756,6 +4519,12 @@ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", @@ -5780,15 +4549,6 @@ "source-map": "^0.6.1" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -5799,26 +4559,17 @@ "semver": "^5.6.0" } }, - "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 - }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true }, "source-map": { "version": "0.6.1", @@ -5829,12 +4580,12 @@ } }, "istanbul-reports": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", - "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", "dev": true, "requires": { - "handlebars": "^4.1.2" + "html-escaper": "^2.0.0" } }, "istextorbinary": { @@ -5848,9 +4599,9 @@ } }, "jquery": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz", - "integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==", "dev": true }, "js-tokens": { @@ -5860,9 +4611,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -5876,22 +4627,22 @@ "dev": true }, "jsdom": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.1.1.tgz", - "integrity": "sha512-cQZRBB33arrDAeCrAEWn1U3SvrvC8XysBua9Oqg1yWrsY/gYcusloJC3RZJXuY5eehSCmws8f2YeliCqGSkrtQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", "dev": true, "requires": { "abab": "^2.0.0", - "acorn": "^6.1.1", + "acorn": "^7.1.0", "acorn-globals": "^4.3.2", "array-equal": "^1.0.0", - "cssom": "^0.3.6", - "cssstyle": "^1.2.2", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", "data-urls": "^1.1.0", "domexception": "^1.0.1", "escodegen": "^1.11.1", "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.1.4", + "nwsapi": "^2.2.0", "parse5": "5.1.0", "pn": "^1.1.0", "request": "^2.88.0", @@ -5907,25 +4658,6 @@ "whatwg-url": "^7.0.0", "ws": "^7.0.0", "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true - }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "dev": true, - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } } }, "jsesc": { @@ -5935,12 +4667,12 @@ "dev": true }, "json-bigint": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", - "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz", + "integrity": "sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ==", "optional": true, "requires": { - "bignumber.js": "^7.0.0" + "bignumber.js": "^9.0.0" } }, "json-parse-better-errors": { @@ -5982,12 +4714,6 @@ "graceful-fs": "^4.1.6" } }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, "jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", @@ -6005,14 +4731,6 @@ "semver": "^5.6.0" }, "dependencies": { - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -6032,10 +4750,10 @@ "safe-buffer": "^5.0.1" } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, @@ -6058,36 +4776,36 @@ "dev": true }, "just-extend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", - "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", + "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", "dev": true }, "jwa": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", - "integrity": "sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", "optional": true, "requires": { "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.10", + "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "jws": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", - "integrity": "sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", "optional": true, "requires": { - "jwa": "^1.1.5", + "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, "last-run": { @@ -6107,38 +4825,6 @@ "dev": true, "requires": { "readable-stream": "^2.0.5" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "lcid": { @@ -6186,23 +4872,16 @@ } }, "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" } }, "locate-path": { @@ -6224,9 +4903,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "lodash._basecopy": { @@ -6393,30 +5072,22 @@ "dev": true }, "lodash.template": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", "dev": true, "requires": { - "lodash._basecopy": "^3.0.0", - "lodash._basetostring": "^3.0.0", - "lodash._basevalues": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0", - "lodash.keys": "^3.0.0", - "lodash.restparam": "^3.0.0", - "lodash.templatesettings": "^3.0.0" + "lodash.templatesettings": "^4.0.0" } }, "lodash.templatesettings": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", - "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", "dev": true, "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0" + "lodash._reinterpolate": "^3.0.0" } }, "lolex": { @@ -6441,32 +5112,24 @@ } }, "lunr": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.6.tgz", - "integrity": "sha512-swStvEyDqQ85MGpABCMBclZcLI/pBIlu8FFDtmX197+oEgKloJ67QnB+Tidh0340HmLMs39c4GrkPY3cmkXp6Q==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.8.tgz", + "integrity": "sha512-oxMeX/Y35PNFuZoHp+jUj5OSEmLCaIH4KTFJh7a93cHBoFmpw2IoPs22VIz7vyO2YUnx2Tn9dzIwO2P/4quIRg==", "dev": true }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "optional": true, - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "optional": true - } + "requires": { + "semver": "^6.0.0" } }, "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, "make-iterator": { @@ -6478,15 +5141,6 @@ "kind-of": "^6.0.2" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -6503,9 +5157,9 @@ } }, "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz", + "integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==", "dev": true }, "matchdep": { @@ -6543,17 +5197,6 @@ } } }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -6599,22 +5242,22 @@ } }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", "optional": true }, "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" }, "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", "requires": { - "mime-db": "~1.37.0" + "mime-db": "1.44.0" } }, "mimic-fn": { @@ -6632,9 +5275,9 @@ } }, "minimist": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.3.tgz", - "integrity": "sha512-+bMdgqjMN/Z77a6NlY/I3U5LlRDbnmaAk6lDveAPKwSpcPM4tKAuYsvYF8xjhOPXhOYGe/73vVLVez5PW+jqhw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "minipass": { @@ -6678,20 +5321,12 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } + "minimist": "^1.2.5" } }, "mocha": { @@ -6713,6 +5348,15 @@ "supports-color": "5.4.0" }, "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -6727,6 +5371,27 @@ "path-is-absolute": "^1.0.0" } }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, "supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", @@ -6739,9 +5404,9 @@ } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "multipipe": { "version": "0.1.2", @@ -6825,12 +5490,6 @@ "requires": { "ms": "^2.1.1" } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true } } }, @@ -6859,27 +5518,36 @@ "dev": true }, "nise": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", - "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", + "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", "dev": true, "requires": { - "@sinonjs/formatio": "^3.1.0", + "@sinonjs/formatio": "^3.2.1", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", - "lolex": "^2.3.2", + "lolex": "^5.0.1", "path-to-regexp": "^1.7.0" }, "dependencies": { "@sinonjs/formatio": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", - "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", "dev": true, "requires": { "@sinonjs/commons": "^1", "@sinonjs/samsam": "^3.1.0" } + }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } } } }, @@ -6914,6 +5582,15 @@ "type-detect": "^4.0.5" } }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -6923,6 +5600,12 @@ "type-detect": "^4.0.0" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -6958,6 +5641,14 @@ "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4.4.2" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "node-version": { @@ -6977,15 +5668,23 @@ } }, "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "requires": { "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", + "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "normalize-path": { @@ -6998,9 +5697,9 @@ } }, "now-and-later": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.0.tgz", - "integrity": "sha1-vGHLtFbXnLMiB85HygUTb/Ln1u4=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", "dev": true, "requires": { "once": "^1.3.2" @@ -7059,9 +5758,9 @@ } }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", @@ -7069,6 +5768,79 @@ "supports-color": "^5.3.0" } }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -7080,15 +5852,6 @@ } } }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -7108,9 +5871,9 @@ "dev": true }, "nwsapi": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", - "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", "dev": true }, "nyc": { @@ -7146,9 +5909,15 @@ "yargs-parser": "^13.0.0" }, "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "find-up": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { @@ -7171,19 +5940,22 @@ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, - "rimraf": { - "version": "2.6.3", - "resolved": false, - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true }, "yargs-parser": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.0.tgz", - "integrity": "sha512-Yq+32PrijHRri0vVKQEm+ys8mbqWjLiwQkMFNXEENutzLPP0bE4Lcd4iA3OQY5HF+GD3xXxf0MEHb8E4/SA3AA==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -7236,21 +6008,26 @@ } }, "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "optional": true + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true }, "object-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", - "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==", - "optional": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", + "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } }, "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object-visit": { "version": "1.0.1", @@ -7265,6 +6042,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -7329,44 +6107,18 @@ "mimic-fn": "^2.1.0" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - } - } - }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", + "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } + "word-wrap": "~1.2.3" } }, "ordered-read-streams": { @@ -7376,38 +6128,6 @@ "dev": true, "requires": { "readable-stream": "^2.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "os-homedir": { @@ -7441,28 +6161,11 @@ "os-tmpdir": "^1.0.0" } }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.1.tgz", + "integrity": "sha512-mw/p92EyOzl2MhauKodw54Rx5ZK4624rNfgNaBguFZkHzyUG9WsDzFF5/yQVEJinbJDdP4jEfMN+uBquiGnaLg==", + "optional": true, "requires": { "p-try": "^2.0.0" } @@ -7474,6 +6177,17 @@ "dev": true, "requires": { "p-limit": "^2.0.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } } }, "p-try": { @@ -7514,15 +6228,20 @@ } }, "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "error-ex": "^1.2.0" } }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", @@ -7596,21 +6315,31 @@ "dev": true }, "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", "dev": true, "requires": { "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } } }, "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { - "pify": "^3.0.0" + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "pathval": { @@ -7626,15 +6355,15 @@ "dev": true }, "pidtree": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", - "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", "dev": true }, "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, "pinkie": { @@ -7709,9 +6438,9 @@ "dev": true }, "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { "version": "2.0.3", @@ -7732,9 +6461,9 @@ "dev": true }, "protobufjs": { - "version": "6.8.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", - "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.9.0.tgz", + "integrity": "sha512-LlGVfEWDXoI/STstRDdZZKb/qusoAWUnmLg9R8OLSO473mBLWHowx8clbX5/+mKDEI+v7GzjoK9tRPZMMcoTrg==", "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", @@ -7747,15 +6476,15 @@ "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.0", - "@types/node": "^10.1.0", + "@types/long": "^4.0.1", + "@types/node": "^13.7.0", "long": "^4.0.0" }, "dependencies": { "@types/node": { - "version": "10.17.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.11.tgz", - "integrity": "sha512-dNd2pp8qTzzNLAs3O8nH3iU9DG9866KHq9L3ISPB7DOGERZN81nW/5/g/KzMJpCU8jrbCiMRBzV9/sCEdRosig==", + "version": "13.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.13.tgz", + "integrity": "sha512-UfvBE9oRCAJVzfR+3eWm/sdLFe/qroAPEXP3GPJ1SehQiEVgZT6NQZWYbPMiJ3UdcKM06v4j+S1lTcdWCmw+3g==", "optional": true } } @@ -7767,30 +6496,55 @@ "dev": true }, "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "dev": true }, "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "optional": true, "requires": { - "duplexify": "^3.6.0", + "duplexify": "^4.1.1", "inherits": "^2.0.3", - "pump": "^2.0.0" + "pump": "^3.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "punycode": { @@ -7818,14 +6572,14 @@ } }, "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, "requires": { - "load-json-file": "^4.0.0", + "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" + "path-type": "^1.0.0" } }, "read-pkg-up": { @@ -7836,70 +6590,27 @@ "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" - }, - "dependencies": { - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - } } }, "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "optional": true, + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } } }, "readdirp": { @@ -7911,38 +6622,6 @@ "graceful-fs": "^4.1.11", "micromatch": "^3.1.10", "readable-stream": "^2.0.2" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "rechoir": { @@ -7968,60 +6647,16 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "optional": true, + "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0-next.1.tgz", - "integrity": "sha512-7MmGr03N7Rnuid6+wyhD9sHNE2n4tFSwExnU2lQl3lIo2ShXWGePY80zYaoMOmILWv57H0amMjZGHNzzGG70Rw==", - "optional": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "optional": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "optional": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "optional": true - } } }, "regexpp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", - "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, "release-zalgo": { @@ -8054,36 +6689,6 @@ "through2": "^2.0.3" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -8115,9 +6720,9 @@ "dev": true }, "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "dev": true }, "replace-homedir": { @@ -8140,44 +6745,12 @@ "escape-string-regexp": "^1.0.3", "object-assign": "^4.0.1", "readable-stream": "^2.0.2" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -8187,7 +6760,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -8197,50 +6770,92 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "request-promise": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", - "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.5.tgz", + "integrity": "sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg==", "dev": true, "requires": { "bluebird": "^3.5.0", - "request-promise-core": "1.1.1", - "stealthy-require": "^1.1.0", - "tough-cookie": ">=2.3.3" + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } } }, "request-promise-core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", - "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", "dev": true, "requires": { - "lodash": "^4.13.1" + "lodash": "^4.17.15" } }, "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", "dev": true, "requires": { - "request-promise-core": "1.1.2", + "request-promise-core": "1.1.3", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" }, "dependencies": { - "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, "requires": { - "lodash": "^4.17.11" + "psl": "^1.1.28", + "punycode": "^2.1.1" } } } @@ -8258,12 +6873,12 @@ "dev": true }, "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", "dev": true, "requires": { - "path-parse": "^1.0.5" + "path-parse": "^1.0.6" } }, "resolve-dir": { @@ -8321,77 +6936,22 @@ "requires": { "debug": "^4.1.1", "through2": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "optional": true - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "optional": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "optional": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "optional": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "through2": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", - "optional": true, - "requires": { - "readable-stream": "2 || 3" - } - } } }, "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true }, "run-sequence": { "version": "1.2.2", @@ -8404,18 +6964,18 @@ } }, "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz", + "integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==", "dev": true, "requires": { "tslib": "^1.9.0" } }, "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safe-regex": { "version": "1.1.0", @@ -8445,18 +7005,18 @@ "dev": true }, "saxes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.9.tgz", - "integrity": "sha512-FZeKhJglhJHk7eWG5YM0z46VHmI3KJpMBAQm3xa9meDvd+wevB5GuBB0wc0exPInZiBBHqi00DbS8AcvCGCFMw==", + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", "dev": true, "requires": { - "xmlchars": "^1.3.1" + "xmlchars": "^2.1.1" } }, "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "semver-greatest-satisfied-range": { "version": "1.1.0", @@ -8512,21 +7072,15 @@ "dev": true }, "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "dev": true, - "requires": { - "array-filter": "~0.0.0", - "array-map": "~0.0.0", - "array-reduce": "~0.0.0", - "jsonify": "~0.0.0" - } + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true }, "shelljs": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", - "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", "dev": true, "requires": { "glob": "^7.0.0", @@ -8535,9 +7089,9 @@ } }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "sinon": { "version": "4.5.0", @@ -8653,6 +7207,12 @@ "requires": { "is-extendable": "^0.1.0" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -8734,12 +7294,12 @@ "dev": true }, "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", "dev": true, "requires": { - "atob": "^2.1.1", + "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", @@ -8768,9 +7328,9 @@ "dev": true }, "spawn-wrap": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", - "integrity": "sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", + "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==", "dev": true, "requires": { "foreground-child": "^1.5.6", @@ -8782,9 +7342,9 @@ } }, "spdx-correct": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz", - "integrity": "sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -8792,15 +7352,15 @@ } }, "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -8808,9 +7368,9 @@ } }, "spdx-license-ids": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz", - "integrity": "sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", "dev": true }, "split-string": { @@ -8829,9 +7389,9 @@ "dev": true }, "sshpk": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", - "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "dev": true, "requires": { "asn1": "~0.2.3", @@ -8900,9 +7460,9 @@ "dev": true }, "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "streamsearch": { "version": "0.1.2", @@ -8921,41 +7481,50 @@ } }, "string.prototype.padend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", - "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz", + "integrity": "sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.4.3", - "function-bind": "^1.0.2" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" } }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "optional": true, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5" } }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "optional": true, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -8974,12 +7543,6 @@ "is-utf8": "^0.2.0" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -9009,9 +7572,9 @@ } }, "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, "table": { @@ -9026,28 +7589,16 @@ "string-width": "^3.0.0" }, "dependencies": { - "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, "is-fullwidth-code-point": { @@ -9094,16 +7645,16 @@ } }, "teeny-request": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-5.3.1.tgz", - "integrity": "sha512-hnUeun3xryzv92FbrnprltcdeDfSVaGFBlFPRvKJ2fO/ioQx9N0aSUbbXSfTO+ArRXine1gSWdWFWcgfrggWXw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.0.tgz", + "integrity": "sha512-kWD3sdGmIix6w7c8ZdVKxWq+3YwVPGWz+Mq0wRZXayEKY/YHb63b8uphfBzcFDmyq8frD9+UTc3wLyOhltRbtg==", "optional": true, "requires": { - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", "node-fetch": "^2.2.0", "stream-events": "^1.0.5", - "uuid": "^3.3.2" + "uuid": "^8.0.0" } }, "test-exclude": { @@ -9127,6 +7678,54 @@ "locate-path": "^3.0.0" } }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, "read-pkg-up": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", @@ -9142,6 +7741,12 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true } } }, @@ -9158,9 +7763,9 @@ "dev": true }, "thenify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", - "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "requires": { "any-promise": "^1.0.0" @@ -9182,32 +7787,12 @@ "dev": true }, "through2": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.0.tgz", - "integrity": "sha512-8B+sevlqP4OiCjonI1Zw03Sf8PuV1eRsYQgLad5eonILOdyeRsY27A/2Ze8IlvlMvq31OH+3fz/styI7Ya62yQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", "requires": { - "readable-stream": "2 || 3", - "xtend": "~4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.0.6.tgz", - "integrity": "sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", - "requires": { - "safe-buffer": "~5.1.0" - } - } + "inherits": "^2.0.4", + "readable-stream": "2 || 3" } }, "through2-filter": { @@ -9220,36 +7805,6 @@ "xtend": "~4.0.0" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -9344,36 +7899,6 @@ "through2": "^2.0.3" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -9387,21 +7912,14 @@ } }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", "dev": true, "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" } }, "tr46": { @@ -9413,12 +7931,6 @@ "punycode": "^2.1.0" } }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, "ts-node": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz", @@ -9446,10 +7958,16 @@ "color-convert": "^1.9.0" } }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", @@ -9465,15 +7983,6 @@ "requires": { "has-flag": "^3.0.0" } - }, - "v8flags": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz", - "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } } } }, @@ -9496,10 +8005,18 @@ } }, "tslib": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", - "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", - "dev": true + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } }, "tunnel-agent": { "version": "0.6.0", @@ -9517,9 +8034,9 @@ "dev": true }, "type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", - "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", "dev": true }, "type-check": { @@ -9558,76 +8075,56 @@ } }, "typedoc": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.15.0.tgz", - "integrity": "sha512-NOtfq5Tis4EFt+J2ozhVq9RCeUnfEYMFKoU6nCXCXUULJz1UQynOM+yH3TkfZCPLzigbqB0tQYGVlktUWweKlw==", + "version": "0.15.8", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.15.8.tgz", + "integrity": "sha512-a0zypcvfIFsS7Gqpf2MkC1+jNND3K1Om38pbDdy/gYWX01NuJZhC5+O0HkIp0oRIZOo7PWrA5+fC24zkANY28Q==", "dev": true, "requires": { "@types/minimatch": "3.0.3", "fs-extra": "^8.1.0", - "handlebars": "^4.1.2", - "highlight.js": "^9.15.8", + "handlebars": "^4.7.0", + "highlight.js": "^9.17.1", "lodash": "^4.17.15", - "marked": "^0.7.0", + "marked": "^0.8.0", "minimatch": "^3.0.0", "progress": "^2.0.3", "shelljs": "^0.8.3", - "typedoc-default-themes": "^0.6.0", - "typescript": "3.5.x" + "typedoc-default-themes": "^0.6.3", + "typescript": "3.7.x" }, "dependencies": { "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", + "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", "dev": true } } }, "typedoc-default-themes": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.6.0.tgz", - "integrity": "sha512-MdTROOojxod78CEv22rIA69o7crMPLnVZPefuDLt/WepXqJwgiSu8Xxq+H36x0Jj3YGc7lOglI2vPJ2GhoOybw==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.6.3.tgz", + "integrity": "sha512-rouf0TcIA4M2nOQFfC7Zp4NEwoYiEX4vX/ZtudJWU9IHA29MPC+PPgSXYLPESkUo7FuB//GxigO3mk9Qe1xp3Q==", "dev": true, "requires": { "backbone": "^1.4.0", "jquery": "^3.4.1", - "lunr": "^2.3.6", + "lunr": "^2.3.8", "underscore": "^1.9.1" } }, "typescript": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz", - "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.6.tgz", + "integrity": "sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==", "dev": true }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.0.tgz", + "integrity": "sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA==", "dev": true, - "optional": true, - "requires": { - "commander": "~2.20.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true, - "optional": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } + "optional": true }, "unc-path-regex": { "version": "0.1.2", @@ -9636,9 +8133,9 @@ "dev": true }, "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz", + "integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==", "dev": true }, "undertaker": { @@ -9738,19 +8235,13 @@ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true } } }, "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, "uri-js": { @@ -9780,20 +8271,21 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", + "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==", + "optional": true }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, "v8flags": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", - "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", "dev": true, "requires": { "homedir-polyfill": "^1.0.1" @@ -9827,14 +8319,17 @@ } }, "vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", "dev": true, "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" } }, "vinyl-fs": { @@ -9862,52 +8357,25 @@ "vinyl-sourcemap": "^1.1.0" }, "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" } }, "through2": { @@ -9919,20 +8387,6 @@ "readable-stream": "~2.3.6", "xtend": "~4.0.1" } - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } } } }, @@ -9949,49 +8403,15 @@ "now-and-later": "^2.0.0", "remove-bom-buffer": "^3.0.0", "vinyl": "^2.0.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } } }, "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", "dev": true, "requires": { - "browser-process-hrtime": "^0.1.2" + "browser-process-hrtime": "^1.0.0" } }, "w3c-xmlserializer": { @@ -10018,11 +8438,11 @@ "dev": true }, "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", + "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } @@ -10048,9 +8468,9 @@ "dev": true }, "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "dev": true, "requires": { "lodash.sortby": "^4.7.0", @@ -10089,9 +8509,9 @@ "dev": true }, "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, "wrap-ansi": { @@ -10119,24 +8539,22 @@ } }, "write-file-atomic": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", - "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", - "dev": true, + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "optional": true, "requires": { - "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, "ws": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.0.0.tgz", - "integrity": "sha512-cknCal4k0EAOrh1SHHPPWWh4qm93g1IuGGGwBjWkXmCG7LsDtL8w9w+YVfaF+KSVwiHQKDIMsSLBVftKf9d1pg==", - "dev": true, - "requires": { - "async-limiter": "^1.0.0" - } + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "dev": true }, "xdg-basedir": { "version": "4.0.0", @@ -10151,9 +8569,9 @@ "dev": true }, "xmlchars": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-1.3.1.tgz", - "integrity": "sha512-tGkGJkN8XqCod7OT+EvGYK5Z4SfDQGD30zAa58OcnAa0RRWgzUEK72tkXhsX1FZd+rgnhRxFtmO+ihkp8LHSkw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, "xmlhttprequest": { @@ -10163,9 +8581,9 @@ "dev": true }, "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "3.2.1", @@ -10179,22 +8597,21 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { "cliui": "^5.0.0", "find-up": "^3.0.0", "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" + "yargs-parser": "^13.1.2" }, "dependencies": { "ansi-regex": { @@ -10212,6 +8629,12 @@ "color-convert": "^1.9.0" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -10223,6 +8646,12 @@ "wrap-ansi": "^5.1.0" } }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -10238,38 +8667,12 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -10320,9 +8723,9 @@ "dev": true }, "yargs-parser": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.0.tgz", - "integrity": "sha512-Yq+32PrijHRri0vVKQEm+ys8mbqWjLiwQkMFNXEENutzLPP0bE4Lcd4iA3OQY5HF+GD3xXxf0MEHb8E4/SA3AA==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -10332,20 +8735,13 @@ } }, "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "version": "5.0.0-security.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", + "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", "dev": true, "requires": { - "camelcase": "^3.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - } + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" } }, "yn": { diff --git a/package.json b/package.json index 1725f3ec3b..4d6791b7a1 100644 --- a/package.json +++ b/package.json @@ -61,15 +61,15 @@ "node-forge": "^0.7.6" }, "optionalDependencies": { - "@google-cloud/firestore": "^3.0.0", - "@google-cloud/storage": "^4.1.2" + "@google-cloud/firestore": "^4.0.0", + "@google-cloud/storage": "^5.0.0" }, "devDependencies": { "@firebase/app": "^0.6.1", "@firebase/auth": "^0.13.3", "@firebase/auth-types": "^0.9.3", "@types/bcrypt": "^2.0.0", - "@types/chai": "^3.4.34", + "@types/chai": "^4.0.0", "@types/chai-as-promised": "0.0.29", "@types/firebase-token-generator": "^2.0.28", "@types/jsonwebtoken": "^7.2.8", @@ -80,7 +80,7 @@ "@types/request": "^2.47.0", "@types/request-promise": "^4.1.41", "@types/sinon": "^4.1.3", - "@types/sinon-chai": "^2.7.27", + "@types/sinon-chai": "^2.7.31", "@typescript-eslint/eslint-plugin": "^2.20.0", "@typescript-eslint/parser": "^2.20.0", "bcrypt": "^3.0.6", From 1b283e3e2a6cda89916e89683373007975b51b0d Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Thu, 9 Jul 2020 19:03:31 -0500 Subject: [PATCH 004/160] chore: Add ESLint rule for curly braces, apply fix (#939) * Add ESLint rule for curly braces, apply fix * Remove fix tag from package.json --- .eslintrc.js | 1 + src/auth/action-code-settings-builder.ts | 2 +- src/auth/auth-api-request.ts | 40 +-- src/auth/auth-config.ts | 6 +- src/auth/auth.ts | 14 +- src/auth/credential.ts | 6 +- src/auth/tenant-manager.ts | 8 +- src/auth/tenant.ts | 2 +- src/auth/token-generator.ts | 6 +- src/auth/token-verifier.ts | 2 +- src/auth/user-import-builder.ts | 6 +- src/auth/user-record.ts | 6 +- src/database/database.ts | 12 +- src/default-namespace.ts | 2 +- src/firebase-app.ts | 38 +- src/firebase-namespace.ts | 40 +-- src/firebase-service.ts | 2 +- src/firestore/firestore.ts | 12 +- src/instance-id/instance-id-request.ts | 4 +- src/instance-id/instance-id.ts | 8 +- src/machine-learning/machine-learning.ts | 16 +- src/messaging/messaging-api-request.ts | 2 +- src/messaging/messaging-errors.ts | 4 +- src/messaging/messaging-types.ts | 2 +- src/messaging/messaging.ts | 14 +- .../security-rules-api-client.ts | 2 +- src/storage/storage.ts | 10 +- src/utils/api-request.ts | 10 +- src/utils/error.ts | 8 +- src/utils/index.ts | 4 +- test/integration/app.spec.ts | 2 +- test/integration/auth.spec.ts | 72 ++-- test/integration/database.spec.ts | 2 +- test/integration/firestore.spec.ts | 6 +- test/integration/machine-learning.spec.ts | 40 +-- test/integration/messaging.spec.ts | 2 +- test/integration/setup.ts | 2 +- test/integration/storage.spec.ts | 4 +- .../typescript/src/example.test.ts | 6 +- test/resources/mocks.ts | 8 +- .../auth/action-code-settings-builder.spec.ts | 8 +- test/unit/auth/auth-api-request.spec.ts | 332 +++++++++--------- test/unit/auth/auth-config.spec.ts | 14 +- test/unit/auth/auth.spec.ts | 68 ++-- test/unit/auth/credential.spec.ts | 8 +- test/unit/auth/tenant-manager.spec.ts | 22 +- test/unit/auth/tenant.spec.ts | 4 +- test/unit/auth/token-generator.spec.ts | 14 +- test/unit/auth/token-verifier.spec.ts | 6 +- test/unit/auth/user-import-builder.spec.ts | 72 ++-- test/unit/auth/user-record.spec.ts | 14 +- test/unit/database/database.spec.ts | 16 +- test/unit/firebase-app.spec.ts | 28 +- test/unit/firebase-namespace.spec.ts | 16 +- test/unit/firebase.spec.ts | 2 +- test/unit/firestore/firestore.spec.ts | 6 +- .../instance-id/instance-id-request.spec.ts | 8 +- test/unit/instance-id/instance-id.spec.ts | 8 +- .../machine-learning-api-client.spec.ts | 24 +- .../machine-learning/machine-learning.spec.ts | 6 +- test/unit/messaging/batch-requests.spec.ts | 40 +-- test/unit/messaging/messaging.spec.ts | 166 ++++----- .../security-rules/security-rules.spec.ts | 6 +- test/unit/storage/storage.spec.ts | 8 +- test/unit/utils.ts | 4 +- test/unit/utils/api-request.spec.ts | 114 +++--- test/unit/utils/error.spec.ts | 4 +- test/unit/utils/index.spec.ts | 6 +- test/unit/utils/validator.spec.ts | 4 +- 69 files changed, 731 insertions(+), 730 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index c3d03a4f68..353fcd665a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,6 +36,7 @@ module.exports = { // Required checks 'indent': ['error', 2], + "object-curly-spacing": [2, "always"], '@typescript-eslint/explicit-function-return-type': [ 'error', { diff --git a/src/auth/action-code-settings-builder.ts b/src/auth/action-code-settings-builder.ts index 159e78af48..f2fc01d605 100644 --- a/src/auth/action-code-settings-builder.ts +++ b/src/auth/action-code-settings-builder.ts @@ -15,7 +15,7 @@ */ import * as validator from '../utils/validator'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; /** Defines the ActionCodeSettings interface. */ export interface ActionCodeSettings { diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index faa56a917b..359bc90c4d 100755 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -16,30 +16,30 @@ import * as validator from '../utils/validator'; -import {deepCopy, deepExtend} from '../utils/deep-copy'; +import { deepCopy, deepExtend } from '../utils/deep-copy'; import { UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, UidIdentifier, EmailIdentifier, PhoneIdentifier, ProviderIdentifier, } from './identifier'; -import {FirebaseApp} from '../firebase-app'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { FirebaseApp } from '../firebase-app'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError, } from '../utils/api-request'; -import {CreateRequest, UpdateRequest} from './user-record'; +import { CreateRequest, UpdateRequest } from './user-record'; import { UserImportBuilder, UserImportOptions, UserImportRecord, UserImportResult, AuthFactorInfo, convertMultiFactorInfoToServerFormat, } from './user-import-builder'; import * as utils from '../utils/index'; -import {ActionCodeSettings, ActionCodeSettingsBuilder} from './action-code-settings-builder'; +import { ActionCodeSettings, ActionCodeSettingsBuilder } from './action-code-settings-builder'; import { SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, OIDCConfigServerRequest, SAMLConfigServerRequest, AuthProviderConfig, OIDCUpdateAuthProviderRequest, SAMLUpdateAuthProviderRequest, } from './auth-config'; -import {Tenant, TenantOptions, TenantServerResponse} from './tenant'; +import { Tenant, TenantOptions, TenantServerResponse } from './tenant'; /** Firebase Auth request header. */ @@ -195,7 +195,7 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { public getUrl(api?: string, params?: object): Promise { return super.getUrl(api, params) .then((url) => { - return utils.formatString(url, {tenantId: this.tenantId}); + return utils.formatString(url, { tenantId: this.tenantId }); }); } } @@ -1039,7 +1039,7 @@ export abstract class AbstractAuthRequestHandler { */ public getAccountInfoByIdentifiers(identifiers: UserIdentifier[]): Promise { if (identifiers.length === 0) { - return Promise.resolve({users: []}); + return Promise.resolve({ users: [] }); } else if (identifiers.length > MAX_GET_ACCOUNTS_BATCH_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, @@ -1428,7 +1428,7 @@ export abstract class AbstractAuthRequestHandler { public getEmailActionLink( requestType: string, email: string, actionCodeSettings?: ActionCodeSettings): Promise { - let request = {requestType, email, returnOobLink: true}; + let request = { requestType, email, returnOobLink: true }; // ActionCodeSettings required for email link sign-in to determine the url where the sign-in will // be completed. if (typeof actionCodeSettings === 'undefined' && requestType === 'EMAIL_SIGNIN') { @@ -1464,7 +1464,7 @@ export abstract class AbstractAuthRequestHandler { if (!OIDCConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_OAUTH_IDP_CONFIG, {}, {providerId}); + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_OAUTH_IDP_CONFIG, {}, { providerId }); } /** @@ -1510,7 +1510,7 @@ export abstract class AbstractAuthRequestHandler { if (!OIDCConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_OAUTH_IDP_CONFIG, {}, {providerId}) + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_OAUTH_IDP_CONFIG, {}, { providerId }) .then(() => { // Return nothing. }); @@ -1532,7 +1532,7 @@ export abstract class AbstractAuthRequestHandler { return Promise.reject(e); } const providerId = options.providerId; - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), CREATE_OAUTH_IDP_CONFIG, request, {providerId}) + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), CREATE_OAUTH_IDP_CONFIG, request, { providerId }) .then((response: any) => { if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( @@ -1565,7 +1565,7 @@ export abstract class AbstractAuthRequestHandler { } const updateMask = utils.generateUpdateMask(request); return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), UPDATE_OAUTH_IDP_CONFIG, request, - {providerId, updateMask: updateMask.join(',')}) + { providerId, updateMask: updateMask.join(',') }) .then((response: any) => { if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( @@ -1586,7 +1586,7 @@ export abstract class AbstractAuthRequestHandler { if (!SAMLConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_INBOUND_SAML_CONFIG, {}, {providerId}); + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_INBOUND_SAML_CONFIG, {}, { providerId }); } /** @@ -1632,7 +1632,7 @@ export abstract class AbstractAuthRequestHandler { if (!SAMLConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_INBOUND_SAML_CONFIG, {}, {providerId}) + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_INBOUND_SAML_CONFIG, {}, { providerId }) .then(() => { // Return nothing. }); @@ -1655,7 +1655,7 @@ export abstract class AbstractAuthRequestHandler { } const providerId = options.providerId; return this.invokeRequestHandler( - this.getProjectConfigUrlBuilder(), CREATE_INBOUND_SAML_CONFIG, request, {providerId}) + this.getProjectConfigUrlBuilder(), CREATE_INBOUND_SAML_CONFIG, request, { providerId }) .then((response: any) => { if (!SAMLConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( @@ -1688,7 +1688,7 @@ export abstract class AbstractAuthRequestHandler { } const updateMask = utils.generateUpdateMask(request); return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), UPDATE_INBOUND_SAML_CONFIG, request, - {providerId, updateMask: updateMask.join(',')}) + { providerId, updateMask: updateMask.join(',') }) .then((response: any) => { if (!SAMLConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( @@ -1893,7 +1893,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.tenantMgmtResourceBuilder, GET_TENANT, {}, { tenantId }) .then((response: any) => { return response as TenantServerResponse; }); @@ -1943,7 +1943,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, {}, {tenantId}) + return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, DELETE_TENANT, {}, { tenantId }) .then(() => { // Return nothing. }); @@ -1984,7 +1984,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { const request = Tenant.buildServerRequest(tenantOptions, false); const updateMask = utils.generateUpdateMask(request); return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, UPDATE_TENANT, request, - {tenantId, updateMask: updateMask.join(',')}) + { 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 95527eb1a7..97fddbdaca 100755 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -15,8 +15,8 @@ */ import * as validator from '../utils/validator'; -import {deepCopy} from '../utils/deep-copy'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { deepCopy } from '../utils/deep-copy'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; /** The filter interface used for listing provider configurations. */ @@ -306,7 +306,7 @@ export class SAMLConfig implements SAMLAuthProviderConfig { }; if (options.x509Certificates) { for (const cert of (options.x509Certificates || [])) { - request.idpConfig!.idpCertificates!.push({x509Certificate: cert}); + request.idpConfig!.idpCertificates!.push({ x509Certificate: cert }); } } } diff --git a/src/auth/auth.ts b/src/auth/auth.ts index cf83e7b430..88e6f881a8 100755 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import {UserRecord, CreateRequest, UpdateRequest} from './user-record'; +import { UserRecord, CreateRequest, UpdateRequest } from './user-record'; import { UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, } from './identifier'; -import {FirebaseApp} from '../firebase-app'; -import {FirebaseTokenGenerator, cryptoSignerFromApp} from './token-generator'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseTokenGenerator, cryptoSignerFromApp } from './token-generator'; import { AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, } from './auth-api-request'; -import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo, FirebaseArrayIndexError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; +import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo, FirebaseArrayIndexError } from '../utils/error'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { UserImportOptions, UserImportRecord, UserImportResult, } from './user-import-builder'; @@ -32,12 +32,12 @@ import { import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier } from './token-verifier'; -import {ActionCodeSettings} from './action-code-settings-builder'; +import { ActionCodeSettings } from './action-code-settings-builder'; import { AuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults, UpdateAuthProviderRequest, SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from './auth-config'; -import {TenantManager} from './tenant-manager'; +import { TenantManager } from './tenant-manager'; /** diff --git a/src/auth/credential.ts b/src/auth/credential.ts index 43de4d3ed3..9770185ac6 100644 --- a/src/auth/credential.ts +++ b/src/auth/credential.ts @@ -19,9 +19,9 @@ import fs = require('fs'); import os = require('os'); import path = require('path'); -import {AppErrorCodes, FirebaseAppError} from '../utils/error'; -import {HttpClient, HttpRequestConfig, HttpError, HttpResponse} from '../utils/api-request'; -import {Agent} from 'http'; +import { AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { HttpClient, HttpRequestConfig, HttpError, HttpResponse } from '../utils/api-request'; +import { Agent } from 'http'; import * as util from '../utils/validator'; const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token'; diff --git a/src/auth/tenant-manager.ts b/src/auth/tenant-manager.ts index 73950a76bd..4f0b1c39cb 100644 --- a/src/auth/tenant-manager.ts +++ b/src/auth/tenant-manager.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import {AuthRequestHandler} from './auth-api-request'; -import {FirebaseApp} from '../firebase-app'; -import {TenantAwareAuth} from './auth'; +import { AuthRequestHandler } from './auth-api-request'; +import { FirebaseApp } from '../firebase-app'; +import { TenantAwareAuth } from './auth'; import { Tenant, TenantServerResponse, ListTenantsResult, TenantOptions, } from './tenant'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import * as validator from '../utils/validator'; /** diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 0b2ed1dca9..2d449035fe 100755 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -15,7 +15,7 @@ */ import * as validator from '../utils/validator'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { EmailSignInConfig, EmailSignInConfigServerRequest, EmailSignInProviderConfig, } from './auth-config'; diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 1cdebd49bd..929986b92e 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -15,8 +15,8 @@ */ import { FirebaseApp } from '../firebase-app'; -import {ServiceAccountCredential} from './credential'; -import {AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { ServiceAccountCredential } from './credential'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { AuthorizedHttpClient, HttpError, HttpRequestConfig, HttpClient } from '../utils/api-request'; import * as validator from '../utils/validator'; @@ -152,7 +152,7 @@ export class IAMSigner implements CryptoSigner { const request: HttpRequestConfig = { method: 'POST', url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:signBlob`, - data: {payload: buffer.toString('base64')}, + data: { payload: buffer.toString('base64') }, }; return this.httpClient.send(request); }).then((response: any) => { diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index cc0c4f4c02..1f8e3e825a 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo} from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; import * as util from '../utils/index'; import * as validator from '../utils/validator'; diff --git a/src/auth/user-import-builder.ts b/src/auth/user-import-builder.ts index 852f7740ce..1f1b784a03 100755 --- a/src/auth/user-import-builder.ts +++ b/src/auth/user-import-builder.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import {deepCopy, deepExtend} from '../utils/deep-copy'; +import { deepCopy, deepExtend } from '../utils/deep-copy'; import * as utils from '../utils'; import * as validator from '../utils/validator'; -import {AuthClientErrorCode, FirebaseAuthError, FirebaseArrayIndexError} from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError, FirebaseArrayIndexError } from '../utils/error'; /** Firebase Auth supported hashing algorithms for import operations. */ export type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | @@ -333,7 +333,7 @@ export class UserImportBuilder { const users = this.validatedUsers.map((user) => { return deepCopy(user); }); - return deepExtend({users}, deepCopy(this.validatedOptions)) as UploadAccountRequest; + return deepExtend({ users }, deepCopy(this.validatedOptions)) as UploadAccountRequest; } /** diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 7d0ebef81b..156f82b610 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import {deepCopy} from '../utils/deep-copy'; -import {isNonNullObject} from '../utils/validator'; +import { deepCopy } from '../utils/deep-copy'; +import { isNonNullObject } from '../utils/validator'; import * as utils from '../utils'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; /** * 'REDACTED', encoded as a base64 string. diff --git a/src/database/database.ts b/src/database/database.ts index 3da2a356ba..cb013e6469 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -1,10 +1,10 @@ -import {URL} from 'url'; +import { URL } from 'url'; import * as path from 'path'; -import {FirebaseApp} from '../firebase-app'; -import {FirebaseDatabaseError, AppErrorCodes, FirebaseAppError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {Database} from '@firebase/database'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseDatabaseError, AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; +import { Database } from '@firebase/database'; import * as validator from '../utils/validator'; import { AuthorizedHttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; @@ -160,7 +160,7 @@ class DatabaseRulesClient { const req: HttpRequestConfig = { method: 'GET', url: this.dbUrl, - data: {format: 'strict'}, + data: { format: 'strict' }, }; return this.httpClient.send(req) .then((resp) => { diff --git a/src/default-namespace.ts b/src/default-namespace.ts index e82e6b6b7a..a90d81089b 100644 --- a/src/default-namespace.ts +++ b/src/default-namespace.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {FirebaseNamespace} from './firebase-namespace'; +import { FirebaseNamespace } from './firebase-namespace'; const firebaseAdmin = new FirebaseNamespace(); diff --git a/src/firebase-app.ts b/src/firebase-app.ts index d9fce0055b..4baf2de08d 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -14,28 +14,28 @@ * limitations under the License. */ -import {Credential, GoogleOAuthAccessToken, getApplicationDefault} from './auth/credential'; +import { Credential, GoogleOAuthAccessToken, getApplicationDefault } from './auth/credential'; import * as validator from './utils/validator'; -import {deepCopy, deepExtend} from './utils/deep-copy'; -import {FirebaseServiceInterface} from './firebase-service'; -import {FirebaseNamespaceInternals} from './firebase-namespace'; -import {AppErrorCodes, FirebaseAppError} from './utils/error'; - -import {Auth} from './auth/auth'; -import {MachineLearning} from './machine-learning/machine-learning'; -import {Messaging} from './messaging/messaging'; -import {Storage} from './storage/storage'; -import {Database} from '@firebase/database'; -import {DatabaseService} from './database/database'; -import {Firestore} from '@google-cloud/firestore'; -import {FirestoreService} from './firestore/firestore'; -import {InstanceId} from './instance-id/instance-id'; - -import {ProjectManagement} from './project-management/project-management'; -import {SecurityRules} from './security-rules/security-rules'; +import { deepCopy, deepExtend } from './utils/deep-copy'; +import { FirebaseServiceInterface } from './firebase-service'; +import { FirebaseNamespaceInternals } from './firebase-namespace'; +import { AppErrorCodes, FirebaseAppError } from './utils/error'; + +import { Auth } from './auth/auth'; +import { MachineLearning } from './machine-learning/machine-learning'; +import { Messaging } from './messaging/messaging'; +import { Storage } from './storage/storage'; +import { Database } from '@firebase/database'; +import { DatabaseService } from './database/database'; +import { Firestore } from '@google-cloud/firestore'; +import { FirestoreService } from './firestore/firestore'; +import { InstanceId } from './instance-id/instance-id'; + +import { ProjectManagement } from './project-management/project-management'; +import { SecurityRules } from './security-rules/security-rules'; import { RemoteConfig } from './remote-config/remote-config'; -import {Agent} from 'http'; +import { Agent } from 'http'; /** * Type representing a callback which is called every time an app lifecycle event occurs. diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 2247387d18..a9caf80802 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -15,11 +15,11 @@ */ import fs = require('fs'); -import {Agent} from 'http'; -import {deepExtend} from './utils/deep-copy'; -import {AppErrorCodes, FirebaseAppError} from './utils/error'; -import {AppHook, FirebaseApp, FirebaseAppOptions} from './firebase-app'; -import {FirebaseServiceFactory, FirebaseServiceInterface} from './firebase-service'; +import { Agent } from 'http'; +import { deepExtend } from './utils/deep-copy'; +import { AppErrorCodes, FirebaseAppError } from './utils/error'; +import { AppHook, FirebaseApp, FirebaseAppOptions } from './firebase-app'; +import { FirebaseServiceFactory, FirebaseServiceInterface } from './firebase-service'; import { Credential, RefreshTokenCredential, @@ -27,14 +27,14 @@ import { getApplicationDefault, } from './auth/credential'; -import {Auth} from './auth/auth'; -import {MachineLearning} from './machine-learning/machine-learning'; -import {Messaging} from './messaging/messaging'; -import {Storage} from './storage/storage'; -import {Database} from '@firebase/database'; -import {Firestore} from '@google-cloud/firestore'; -import {InstanceId} from './instance-id/instance-id'; -import {ProjectManagement} from './project-management/project-management'; +import { Auth } from './auth/auth'; +import { MachineLearning } from './machine-learning/machine-learning'; +import { Messaging } from './messaging/messaging'; +import { Storage } from './storage/storage'; +import { Database } from '@firebase/database'; +import { Firestore } from '@google-cloud/firestore'; +import { InstanceId } from './instance-id/instance-id'; +import { ProjectManagement } from './project-management/project-management'; import { SecurityRules } from './security-rules/security-rules'; import { RemoteConfig } from './remote-config/remote-config'; @@ -331,7 +331,7 @@ export class FirebaseNamespace { return this.ensureApp(app).auth(); }; const auth = require('./auth/auth').Auth; - return Object.assign(fn, {Auth: auth}); + return Object.assign(fn, { Auth: auth }); } /** @@ -356,7 +356,7 @@ export class FirebaseNamespace { return this.ensureApp(app).messaging(); }; const messaging = require('./messaging/messaging').Messaging; - return Object.assign(fn, {Messaging: messaging}); + return Object.assign(fn, { Messaging: messaging }); } /** @@ -368,7 +368,7 @@ export class FirebaseNamespace { return this.ensureApp(app).storage(); }; const storage = require('./storage/storage').Storage; - return Object.assign(fn, {Storage: storage}); + return Object.assign(fn, { Storage: storage }); } /** @@ -413,7 +413,7 @@ export class FirebaseNamespace { }; const machineLearning = require('./machine-learning/machine-learning').MachineLearning; - return Object.assign(fn, {MachineLearning: machineLearning}); + return Object.assign(fn, { MachineLearning: machineLearning }); } /** @@ -425,7 +425,7 @@ export class FirebaseNamespace { return this.ensureApp(app).instanceId(); }; const instanceId = require('./instance-id/instance-id').InstanceId; - return Object.assign(fn, {InstanceId: instanceId}); + return Object.assign(fn, { InstanceId: instanceId }); } /** @@ -437,7 +437,7 @@ export class FirebaseNamespace { return this.ensureApp(app).projectManagement(); }; const projectManagement = require('./project-management/project-management').ProjectManagement; - return Object.assign(fn, {ProjectManagement: projectManagement}); + return Object.assign(fn, { ProjectManagement: projectManagement }); } /** @@ -449,7 +449,7 @@ export class FirebaseNamespace { return this.ensureApp(app).securityRules(); }; const securityRules = require('./security-rules/security-rules').SecurityRules; - return Object.assign(fn, {SecurityRules: securityRules}); + return Object.assign(fn, { SecurityRules: securityRules }); } /** diff --git a/src/firebase-service.ts b/src/firebase-service.ts index 8d8fd64387..15e3168ee3 100644 --- a/src/firebase-service.ts +++ b/src/firebase-service.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {FirebaseApp} from './firebase-app'; +import { FirebaseApp } from './firebase-app'; /** diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts index 89e7fe2771..28bf7e0272 100644 --- a/src/firestore/firestore.ts +++ b/src/firestore/firestore.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseFirestoreError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {ServiceAccountCredential, isApplicationDefault} from '../auth/credential'; -import {Firestore, Settings} from '@google-cloud/firestore'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseFirestoreError } from '../utils/error'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; +import { ServiceAccountCredential, isApplicationDefault } from '../auth/credential'; +import { Firestore, Settings } from '@google-cloud/firestore'; import * as validator from '../utils/validator'; import * as utils from '../utils/index'; @@ -90,7 +90,7 @@ export function getFirestoreOptions(app: FirebaseApp): Settings { // Try to use the Google application default credentials. // If an explicit project ID is not available, let Firestore client discover one from the // environment. This prevents the users from having to set GOOGLE_CLOUD_PROJECT in GCP runtimes. - return validator.isNonEmptyString(projectId) ? {projectId, firebaseVersion} : {firebaseVersion}; + return validator.isNonEmptyString(projectId) ? { projectId, firebaseVersion } : { firebaseVersion }; } throw new FirebaseFirestoreError({ diff --git a/src/instance-id/instance-id-request.ts b/src/instance-id/instance-id-request.ts index b683ff038b..bab069faf1 100644 --- a/src/instance-id/instance-id-request.ts +++ b/src/instance-id/instance-id-request.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; import { ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError, } from '../utils/api-request'; diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index 27e807528d..be63c8af89 100644 --- a/src/instance-id/instance-id.ts +++ b/src/instance-id/instance-id.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {FirebaseInstanceIdRequestHandler} from './instance-id-request'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; +import { FirebaseInstanceIdRequestHandler } from './instance-id-request'; import * as validator from '../utils/validator'; diff --git a/src/machine-learning/machine-learning.ts b/src/machine-learning/machine-learning.ts index 2879e5d460..9d2330aa92 100644 --- a/src/machine-learning/machine-learning.ts +++ b/src/machine-learning/machine-learning.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {MachineLearningApiClient, ModelResponse, OperationResponse, - ModelOptions, ModelUpdateOptions, ListModelsOptions} from './machine-learning-api-client'; -import {FirebaseError} from '../utils/error'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; +import { MachineLearningApiClient, ModelResponse, OperationResponse, + ModelOptions, ModelUpdateOptions, ListModelsOptions } from './machine-learning-api-client'; +import { FirebaseError } from '../utils/error'; import * as validator from '../utils/validator'; -import {FirebaseMachineLearningError} from './machine-learning-utils'; +import { FirebaseMachineLearningError } from './machine-learning-utils'; import { deepCopy } from '../utils/deep-copy'; import * as utils from '../utils'; @@ -166,7 +166,7 @@ export class MachineLearning implements FirebaseServiceInterface { if (resp.models) { models = resp.models.map((rs) => new Model(rs)); } - const result: ListModelsResult = {models}; + const result: ListModelsResult = { models }; if (resp.nextPageToken) { result.pageToken = resp.nextPageToken; } @@ -185,7 +185,7 @@ export class MachineLearning implements FirebaseServiceInterface { private setPublishStatus(modelId: string, publish: boolean): Promise { const updateMask = ['state.published']; - const options: ModelUpdateOptions = {state: {published: publish}}; + const options: ModelUpdateOptions = { state: { published: publish } }; return this.client.updateModel(modelId, options, updateMask) .then((operation) => handleOperation(operation)); } diff --git a/src/messaging/messaging-api-request.ts b/src/messaging/messaging-api-request.ts index 9af1ff8571..6a44ac67f6 100644 --- a/src/messaging/messaging-api-request.ts +++ b/src/messaging/messaging-api-request.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; +import { FirebaseApp } from '../firebase-app'; import { HttpMethod, AuthorizedHttpClient, HttpRequestConfig, HttpError, HttpResponse, } from '../utils/api-request'; diff --git a/src/messaging/messaging-errors.ts b/src/messaging/messaging-errors.ts index b317404087..ecd3155bae 100644 --- a/src/messaging/messaging-errors.ts +++ b/src/messaging/messaging-errors.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import {HttpError} from '../utils/api-request'; -import {FirebaseMessagingError, MessagingClientErrorCode} from '../utils/error'; +import { HttpError } from '../utils/api-request'; +import { FirebaseMessagingError, MessagingClientErrorCode } from '../utils/error'; import * as validator from '../utils/validator'; /** diff --git a/src/messaging/messaging-types.ts b/src/messaging/messaging-types.ts index 27891d27ed..795d0c1fb2 100644 --- a/src/messaging/messaging-types.ts +++ b/src/messaging/messaging-types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {renameProperties} from '../utils/index'; +import { renameProperties } from '../utils/index'; import { MessagingClientErrorCode, FirebaseMessagingError, FirebaseArrayIndexError, FirebaseError, } from '../utils/error'; diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index 6cabb1104b..e9eab2b43e 100644 --- a/src/messaging/messaging.ts +++ b/src/messaging/messaging.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {deepCopy, deepExtend} from '../utils/deep-copy'; -import {SubRequest} from './batch-request'; +import { FirebaseApp } from '../firebase-app'; +import { deepCopy, deepExtend } from '../utils/deep-copy'; +import { SubRequest } from './batch-request'; import { Message, validateMessage, MessagingDevicesResponse, MessagingDeviceGroupResponse, MessagingTopicManagementResponse, MessagingPayload, MessagingOptions, MessagingTopicResponse, MessagingConditionResponse, BatchResponse, MulticastMessage, DataMessagePayload, NotificationMessagePayload, } from './messaging-types'; -import {FirebaseMessagingRequestHandler} from './messaging-api-request'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; +import { FirebaseMessagingRequestHandler } from './messaging-api-request'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError, } from '../utils/error'; @@ -253,7 +253,7 @@ export class Messaging implements FirebaseServiceInterface { } return this.getUrlPath() .then((urlPath) => { - const request: {message: Message; validate_only?: boolean} = {message: copy}; + const request: {message: Message; validate_only?: boolean} = { message: copy }; if (dryRun) { request.validate_only = true; } @@ -306,7 +306,7 @@ export class Messaging implements FirebaseServiceInterface { .then((urlPath) => { const requests: SubRequest[] = copy.map((message) => { validateMessage(message); - const request: {message: Message; validate_only?: boolean} = {message}; + const request: {message: Message; validate_only?: boolean} = { message }; if (dryRun) { request.validate_only = true; } diff --git a/src/security-rules/security-rules-api-client.ts b/src/security-rules/security-rules-api-client.ts index 37989081fb..87e379513f 100644 --- a/src/security-rules/security-rules-api-client.ts +++ b/src/security-rules/security-rules-api-client.ts @@ -170,7 +170,7 @@ export class SecurityRulesApiClient { const request: HttpRequestConfig = { method: 'PATCH', url: `${url}/releases/${name}`, - data: {release}, + data: { release }, }; return this.sendRequest(request); }); diff --git a/src/storage/storage.ts b/src/storage/storage.ts index afd3b003de..af490eedd5 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {ServiceAccountCredential, isApplicationDefault} from '../auth/credential'; -import {Bucket, Storage as StorageClient} from '@google-cloud/storage'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseError } from '../utils/error'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; +import { ServiceAccountCredential, isApplicationDefault } from '../auth/credential'; +import { Bucket, Storage as StorageClient } from '@google-cloud/storage'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index 0597d8d9f3..03f52d0240 100644 --- a/src/utils/api-request.ts +++ b/src/utils/api-request.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {AppErrorCodes, FirebaseAppError} from './error'; +import { FirebaseApp } from '../firebase-app'; +import { AppErrorCodes, FirebaseAppError } from './error'; import * as validator from './validator'; import http = require('http'); import https = require('https'); import url = require('url'); -import {EventEmitter} from 'events'; -import {Readable} from 'stream'; +import { EventEmitter } from 'events'; +import { Readable } from 'stream'; import * as zlibmod from 'zlib'; /** Http method type definition. */ @@ -588,7 +588,7 @@ class AsyncHttpCall { response: LowLevelResponse, respStream: Readable, boundary: string): void { const dicer = require('dicer'); // eslint-disable-line @typescript-eslint/no-var-requires - const multipartParser = new dicer({boundary}); + const multipartParser = new dicer({ boundary }); const responseBuffer: Buffer[] = []; multipartParser.on('part', (part: any) => { const tempBuffers: Buffer[] = []; diff --git a/src/utils/error.ts b/src/utils/error.ts index 746cc3ca5d..8644511463 100755 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import {deepCopy} from '../utils/deep-copy'; +import { deepCopy } from '../utils/deep-copy'; /** * Defines error info type. This includes a code and message string. @@ -196,7 +196,7 @@ export class FirebaseAuthError extends PrefixedFirebaseError { export class FirebaseDatabaseError extends FirebaseError { constructor(info: ErrorInfo, message?: string) { // Override default message if custom message provided. - super({code: 'database/' + info.code, message: message || info.message}); + super({ code: 'database/' + info.code, message: message || info.message }); } } @@ -211,7 +211,7 @@ export class FirebaseDatabaseError extends FirebaseError { export class FirebaseFirestoreError extends FirebaseError { constructor(info: ErrorInfo, message?: string) { // Override default message if custom message provided. - super({code: 'firestore/' + info.code, message: message || info.message}); + super({ code: 'firestore/' + info.code, message: message || info.message }); } } @@ -226,7 +226,7 @@ export class FirebaseFirestoreError extends FirebaseError { export class FirebaseInstanceIdError extends FirebaseError { constructor(info: ErrorInfo, message?: string) { // Override default message if custom message provided. - super({code: 'instance-id/' + info.code, message: message || info.message}); + super({ code: 'instance-id/' + info.code, message: message || info.message }); } } diff --git a/src/utils/index.ts b/src/utils/index.ts index f6e2f41232..f3f75d6d80 100755 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import {FirebaseApp, FirebaseAppOptions} from '../firebase-app'; -import {ServiceAccountCredential, ComputeEngineCredential} from '../auth/credential'; +import { FirebaseApp, FirebaseAppOptions } from '../firebase-app'; +import { ServiceAccountCredential, ComputeEngineCredential } from '../auth/credential'; import * as validator from './validator'; diff --git a/test/integration/app.spec.ts b/test/integration/app.spec.ts index 532adb5798..3cdf356245 100644 --- a/test/integration/app.spec.ts +++ b/test/integration/app.spec.ts @@ -15,7 +15,7 @@ */ import * as admin from '../../lib/index'; -import {expect} from 'chai'; +import { expect } from 'chai'; import { defaultApp, nullApp, nonNullApp, databaseUrl, projectId, storageBucket, } from './setup'; diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index ac7d8bde08..e4617357fb 100755 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -21,7 +21,7 @@ import * as crypto from 'crypto'; import * as bcrypt from 'bcrypt'; import firebase from '@firebase/app'; import '@firebase/auth'; -import {clone} from 'lodash'; +import { clone } from 'lodash'; import { generateRandomString, projectId, apiKey, noServiceAccountApp, cmdArgs, } from './setup'; @@ -288,14 +288,14 @@ describe('admin.auth', () => { { uid: 'uid_that_doesnt_exist' }, { uid: 'uid3' }, ]); - expect(users.notFound).to.have.deep.members([{uid: 'uid_that_doesnt_exist'}]); + expect(users.notFound).to.have.deep.members([{ uid: 'uid_that_doesnt_exist' }]); const foundUsers = mapUserRecordsToUidEmailPhones(users.users); expect(foundUsers).to.have.deep.members([testUser1, testUser3]); }); it('returns nothing when queried for only non-existing users', async () => { - const notFoundIds = [{uid: 'non-existing user'}]; + const notFoundIds = [{ uid: 'non-existing user' }]; const users = await admin.auth().getUsers(notFoundIds); expect(users.users).to.be.empty; @@ -412,7 +412,7 @@ describe('admin.auth', () => { let currentUser: User; // Sign in with an email and password account. return clientAuth().signInWithEmailAndPassword(mockUserData.email, mockUserData.password) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; currentUser = user!; // Get user's ID token. @@ -449,7 +449,7 @@ describe('admin.auth', () => { return clientAuth().signInWithEmailAndPassword( mockUserData.email, mockUserData.password); }) - .then(({user}) => { + .then(({ user }) => { // Get new session's ID token. expect(user).to.exist; return user!.getIdToken(); @@ -474,7 +474,7 @@ describe('admin.auth', () => { return clientAuth().signInWithEmailAndPassword( userRecord.email!, mockUserData.password); }) - .then(({user}) => { + .then(({ user }) => { // Get the user's ID token. expect(user).to.exist; return user!.getIdToken(); @@ -614,7 +614,7 @@ describe('admin.auth', () => { .then((customToken) => { return clientAuth().signInWithCustomToken(customToken); }) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) @@ -634,7 +634,7 @@ describe('admin.auth', () => { .then((customToken) => { return clientAuth().signInWithCustomToken(customToken); }) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) @@ -680,7 +680,7 @@ describe('admin.auth', () => { it('generatePasswordResetLink() should return a password reset link', () => { // Ensure old password set on created user. - return admin.auth().updateUser(uid, {password: 'password'}) + return admin.auth().updateUser(uid, { password: 'password' }) .then(() => { return admin.auth().generatePasswordResetLink(email, actionCodeSettings); }) @@ -702,7 +702,7 @@ describe('admin.auth', () => { it('generateEmailVerificationLink() should return a verification link', () => { // Ensure the user's email is unverified. - return admin.auth().updateUser(uid, {password: 'password', emailVerified: false}) + return admin.auth().updateUser(uid, { password: 'password', emailVerified: false }) .then((userRecord) => { expect(userRecord.emailVerified).to.be.false; return admin.auth().generateEmailVerificationLink(email, actionCodeSettings); @@ -972,7 +972,7 @@ describe('admin.auth', () => { clientAuth().tenantId = createdTenantId; const customToken = await tenantAwareAuth.createCustomToken('uid1'); - const {user} = await clientAuth().signInWithCustomToken(customToken); + const { user } = await clientAuth().signInWithCustomToken(customToken); expect(user).to.not.be.null; const idToken = await user!.getIdToken(); const token = await tenantAwareAuth.verifyIdToken(idToken); @@ -1041,7 +1041,7 @@ describe('admin.auth', () => { }) .then((config) => { const modifiedConfig = deepExtend( - {providerId: authProviderConfig.providerId}, modifiedConfigOptions); + { providerId: authProviderConfig.providerId }, modifiedConfigOptions); assertDeepEqualUnordered(modifiedConfig, config); return tenantAwareAuth.deleteProviderConfig(authProviderConfig.providerId); }) @@ -1099,7 +1099,7 @@ describe('admin.auth', () => { }) .then((config) => { const modifiedConfig = deepExtend( - {providerId: authProviderConfig.providerId}, modifiedConfigOptions); + { providerId: authProviderConfig.providerId }, modifiedConfigOptions); assertDeepEqualUnordered(modifiedConfig, config); return tenantAwareAuth.deleteProviderConfig(authProviderConfig.providerId); }) @@ -1242,7 +1242,7 @@ describe('admin.auth', () => { it('listProviderConfig() successfully returns the list of SAML providers', () => { const configs: AuthProviderConfig[] = []; const listProviders: any = (type: 'saml' | 'oidc', maxResults?: number, pageToken?: string) => { - return admin.auth().listProviderConfigs({type, maxResults, pageToken}) + return admin.auth().listProviderConfigs({ type, maxResults, pageToken }) .then((result) => { result.providerConfigs.forEach((config: AuthProviderConfig) => { configs.push(config); @@ -1284,7 +1284,7 @@ describe('admin.auth', () => { return admin.auth().updateProviderConfig(authProviderConfig1.providerId, modifiedConfigOptions) .then((config) => { const modifiedConfig = deepExtend( - {providerId: authProviderConfig1.providerId}, modifiedConfigOptions); + { providerId: authProviderConfig1.providerId }, modifiedConfigOptions); assertDeepEqualUnordered(modifiedConfig, config); }); }); @@ -1312,7 +1312,7 @@ describe('admin.auth', () => { return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges) .then((config) => { const modifiedConfig = deepExtend( - {providerId: authProviderConfig1.providerId}, modifiedConfigOptions); + { providerId: authProviderConfig1.providerId }, modifiedConfigOptions); assertDeepEqualUnordered(modifiedConfig, config); }); }); @@ -1374,7 +1374,7 @@ describe('admin.auth', () => { it('listProviderConfig() successfully returns the list of OIDC providers', () => { const configs: AuthProviderConfig[] = []; const listProviders: any = (type: 'saml' | 'oidc', maxResults?: number, pageToken?: string) => { - return admin.auth().listProviderConfigs({type, maxResults, pageToken}) + return admin.auth().listProviderConfigs({ type, maxResults, pageToken }) .then((result) => { result.providerConfigs.forEach((config: AuthProviderConfig) => { configs.push(config); @@ -1412,7 +1412,7 @@ describe('admin.auth', () => { return admin.auth().updateProviderConfig(authProviderConfig1.providerId, modifiedConfigOptions) .then((config) => { const modifiedConfig = deepExtend( - {providerId: authProviderConfig1.providerId}, modifiedConfigOptions); + { providerId: authProviderConfig1.providerId }, modifiedConfigOptions); assertDeepEqualUnordered(modifiedConfig, config); }); }); @@ -1432,7 +1432,7 @@ describe('admin.auth', () => { return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges) .then((config) => { const modifiedConfig = deepExtend( - {providerId: authProviderConfig1.providerId}, modifiedConfigOptions); + { providerId: authProviderConfig1.providerId }, modifiedConfigOptions); assertDeepEqualUnordered(modifiedConfig, config); }); }); @@ -1458,7 +1458,7 @@ describe('admin.auth', () => { const uid1 = await admin.auth().createUser({}).then((ur) => ur.uid); const uid2 = await admin.auth().createUser({}).then((ur) => ur.uid); const uid3 = await admin.auth().createUser({}).then((ur) => ur.uid); - const ids = [{uid: uid1}, {uid: uid2}, {uid: uid3}]; + const ids = [{ uid: uid1 }, { uid: uid2 }, { uid: uid3 }]; return deleteUsersWithDelay([uid1, uid2, uid3]) .then((deleteUsersResult) => { @@ -1477,7 +1477,7 @@ describe('admin.auth', () => { it('deletes users that exist even when non-existing users also specified', async () => { const uid1 = await admin.auth().createUser({}).then((ur) => ur.uid); const uid2 = 'uid-that-doesnt-exist'; - const ids = [{uid: uid1}, {uid: uid2}]; + const ids = [{ uid: uid1 }, { uid: uid2 }]; return deleteUsersWithDelay([uid1, uid2]) .then((deleteUsersResult) => { @@ -1521,9 +1521,9 @@ describe('admin.auth', () => { const uid3 = sessionCookieUids[2]; it('creates a valid Firebase session cookie', () => { - return admin.auth().createCustomToken(uid, {admin: true, groupId: '1234'}) + return admin.auth().createCustomToken(uid, { admin: true, groupId: '1234' }) .then((customToken) => clientAuth().signInWithCustomToken(customToken)) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) @@ -1539,7 +1539,7 @@ describe('admin.auth', () => { delete payloadClaims.iat; expectedIat = Math.floor(new Date().getTime() / 1000); // One day long session cookie. - return admin.auth().createSessionCookie(currentIdToken, {expiresIn}); + return admin.auth().createSessionCookie(currentIdToken, { expiresIn }); }) .then((sessionCookie) => admin.auth().verifySessionCookie(sessionCookie)) .then((decodedIdToken) => { @@ -1559,13 +1559,13 @@ describe('admin.auth', () => { let currentSessionCookie: string; return admin.auth().createCustomToken(uid2) .then((customToken) => clientAuth().signInWithCustomToken(customToken)) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) .then((idToken) => { // One day long session cookie. - return admin.auth().createSessionCookie(idToken, {expiresIn}); + return admin.auth().createSessionCookie(idToken, { expiresIn }); }) .then((sessionCookie) => { currentSessionCookie = sessionCookie; @@ -1584,9 +1584,9 @@ describe('admin.auth', () => { }); it('fails when called with a revoked ID token', () => { - return admin.auth().createCustomToken(uid3, {admin: true, groupId: '1234'}) + return admin.auth().createCustomToken(uid3, { admin: true, groupId: '1234' }) .then((customToken) => clientAuth().signInWithCustomToken(customToken)) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) @@ -1597,7 +1597,7 @@ describe('admin.auth', () => { ), 1000)); }) .then(() => { - return admin.auth().createSessionCookie(currentIdToken, {expiresIn}) + return admin.auth().createSessionCookie(currentIdToken, { expiresIn }) .should.eventually.be.rejected.and.have.property('code', 'auth/id-token-expired'); }); }); @@ -1614,7 +1614,7 @@ describe('admin.auth', () => { it('fails when called with a Firebase ID token', () => { return admin.auth().createCustomToken(uid) .then((customToken) => clientAuth().signInWithCustomToken(customToken)) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) @@ -1817,7 +1817,7 @@ describe('admin.auth', () => { photoURL, phoneNumber: '+15554446666', disabled: false, - customClaims: {admin: true}, + customClaims: { admin: true }, metadata: { lastSignInTime: now, creationTime: now, @@ -1922,10 +1922,10 @@ describe('admin.auth', () => { it('fails when invalid users are provided', () => { const users = [ - {uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1error'}, - {uid: generateRandomString(20).toLowerCase(), email: 'invalid'}, - {uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1invalid'}, - {uid: generateRandomString(20).toLowerCase(), emailVerified: 'invalid'} as any, + { uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1error' }, + { uid: generateRandomString(20).toLowerCase(), email: 'invalid' }, + { uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1invalid' }, + { uid: generateRandomString(20).toLowerCase(), emailVerified: 'invalid' } as any, ]; return admin.auth().importUsers(users) .then((result) => { @@ -1969,7 +1969,7 @@ function testImportAndSignInUser( // Sign in with an email and password to the imported account. return clientAuth().signInWithEmailAndPassword(users[0].email!, rawPassword); }) - .then(({user}) => { + .then(({ user }) => { // Confirm successful sign-in. expect(user).to.exist; expect(user!.email).to.equal(users[0].email); diff --git a/test/integration/database.spec.ts b/test/integration/database.spec.ts index b64e099813..e8d0a746a4 100644 --- a/test/integration/database.spec.ts +++ b/test/integration/database.spec.ts @@ -17,7 +17,7 @@ import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {defaultApp, nullApp, nonNullApp, cmdArgs, databaseUrl} from './setup'; +import { defaultApp, nullApp, nonNullApp, cmdArgs, databaseUrl } from './setup'; // eslint-disable-next-line @typescript-eslint/no-var-requires const chalk = require('chalk'); diff --git a/test/integration/firestore.spec.ts b/test/integration/firestore.spec.ts index d3bbf51493..3a6dc89fff 100644 --- a/test/integration/firestore.spec.ts +++ b/test/integration/firestore.spec.ts @@ -17,7 +17,7 @@ import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {clone} from 'lodash'; +import { clone } from 'lodash'; chai.should(); chai.use(chaiAsPromised); @@ -118,7 +118,7 @@ describe('admin.firestore', () => { const target = admin.firestore().collection('cities').doc(); return source.set(mountainView) .then(() => { - return target.set({name: 'Palo Alto', sisterCity: source}); + return target.set({ name: 'Palo Alto', sisterCity: source }); }) .then(() => { return target.get(); @@ -141,7 +141,7 @@ describe('admin.firestore', () => { admin.firestore.setLogFunction((log) => { logs.push(log); }); - return source.set({name: 'San Francisco'}) + return source.set({ name: 'San Francisco' }) .then(() => { return source.delete(); }) diff --git a/test/integration/machine-learning.spec.ts b/test/integration/machine-learning.spec.ts index 9cef0d31db..da2c0a60fd 100644 --- a/test/integration/machine-learning.spec.ts +++ b/test/integration/machine-learning.spec.ts @@ -18,7 +18,7 @@ import path = require('path'); import * as chai from 'chai'; import * as admin from '../../lib/index'; -import {Bucket} from '@google-cloud/storage'; +import { Bucket } from '@google-cloud/storage'; const expect = chai.expect; @@ -61,7 +61,7 @@ describe('admin.machineLearning', () => { function uploadModelToGcs(localFileName: string, gcsFileName: string): Promise { const bucket: Bucket = admin.storage().bucket(); const tfliteFileName = path.join(__dirname, `../resources/${localFileName}`); - return bucket.upload(tfliteFileName, {destination: gcsFileName}) + return bucket.upload(tfliteFileName, { destination: gcsFileName }) .then(() => { return `gs://${bucket.name}/${gcsFileName}`; }); @@ -75,7 +75,7 @@ describe('admin.machineLearning', () => { it('creates a new Model without ModelFormat', () => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: 'node-integration-test-create-1', - tags: ['tag123', 'tag345']}; + tags: ['tag123', 'tag345'] }; return admin.machineLearning().createModel(modelOptions) .then((model) => { scheduleForDelete(model); @@ -87,7 +87,7 @@ describe('admin.machineLearning', () => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: 'node-integration-test-create-2', tags: ['tag234', 'tag456'], - tfliteModel: {gcsTfliteUri: 'this will be replaced below'}, + tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, }; return uploadModelToGcs('model1.tflite', 'valid_model.tflite') .then((fileName: string) => { @@ -105,7 +105,7 @@ describe('admin.machineLearning', () => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: 'node-integration-test-create-3', tags: ['tag234', 'tag456'], - tfliteModel: {gcsTfliteUri: 'this will be replaced below'}, + tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, }; return uploadModelToGcs('invalid_model.tflite', 'invalid_model.tflite') .then((fileName: string) => { @@ -150,7 +150,7 @@ describe('admin.machineLearning', () => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: 'Invalid Name#*^!', }; - return createTemporaryModel({displayName: 'node-integration-invalid-arg'}) + return createTemporaryModel({ displayName: 'node-integration-invalid-arg' }) .then((model) => admin.machineLearning().updateModel(model.modelId, modelOptions) .should.eventually.be.rejected.and.have.property( 'code', 'machine-learning/invalid-argument')); @@ -158,7 +158,7 @@ describe('admin.machineLearning', () => { it('updates the displayName', () => { const DISPLAY_NAME = 'node-integration-test-update-1b'; - return createTemporaryModel({displayName: 'node-integration-test-update-1a'}) + return createTemporaryModel({ displayName: 'node-integration-test-update-1a' }) .then((model) => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: DISPLAY_NAME, @@ -195,7 +195,7 @@ describe('admin.machineLearning', () => { uploadModelToGcs('model1.tflite', 'valid_model.tflite')]) .then(([model, fileName]) => { const modelOptions: admin.machineLearning.ModelOptions = { - tfliteModel: {gcsTfliteUri: fileName}, + tfliteModel: { gcsTfliteUri: fileName }, }; return admin.machineLearning().updateModel(model.modelId, modelOptions) .then((updatedModel) => { @@ -207,7 +207,7 @@ describe('admin.machineLearning', () => { it('can update more than 1 field', () => { const DISPLAY_NAME = 'node-integration-test-update-3b'; const TAGS = ['node-integration-tag-1', 'node-integration-tag-2']; - return createTemporaryModel({displayName: 'node-integration-test-update-3a'}) + return createTemporaryModel({ displayName: 'node-integration-test-update-3a' }) .then((model) => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: DISPLAY_NAME, @@ -239,7 +239,7 @@ describe('admin.machineLearning', () => { it('publishes the model successfully', () => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: 'node-integration-test-publish-1', - tfliteModel: {gcsTfliteUri: 'this will be replaced below'}, + tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, }; return uploadModelToGcs('model1.tflite', 'valid_model.tflite') .then((fileName: string) => { @@ -274,7 +274,7 @@ describe('admin.machineLearning', () => { it('unpublishes the model successfully', () => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: 'node-integration-test-unpublish1', - tfliteModel: {gcsTfliteUri: 'this will be replaced below'}, + tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, }; return uploadModelToGcs('model1.tflite', 'valid_model.tflite') .then((fileName: string) => { @@ -357,7 +357,7 @@ describe('admin.machineLearning', () => { }); it('resolves with a list of models', () => { - return admin.machineLearning().listModels({pageSize: 100}) + return admin.machineLearning().listModels({ pageSize: 100 }) .then((modelList) => { expect(modelList.models.length).to.be.at.least(2); expect(modelList.models).to.deep.include(model1); @@ -367,7 +367,7 @@ describe('admin.machineLearning', () => { }); it('respects page size', () => { - return admin.machineLearning().listModels({pageSize: 2}) + return admin.machineLearning().listModels({ pageSize: 2 }) .then((modelList) => { expect(modelList.models.length).to.equal(2); expect(modelList.pageToken).not.to.be.empty; @@ -375,7 +375,7 @@ describe('admin.machineLearning', () => { }); it('filters by exact displayName', () => { - return admin.machineLearning().listModels({filter: 'displayName=node-integration-list1'}) + return admin.machineLearning().listModels({ filter: 'displayName=node-integration-list1' }) .then((modelList) => { expect(modelList.models.length).to.equal(1); expect(modelList.models[0]).to.deep.equal(model1); @@ -384,7 +384,7 @@ describe('admin.machineLearning', () => { }); it('filters by displayName prefix', () => { - return admin.machineLearning().listModels({filter: 'displayName:node-integration-list*', pageSize: 100}) + return admin.machineLearning().listModels({ filter: 'displayName:node-integration-list*', pageSize: 100 }) .then((modelList) => { expect(modelList.models.length).to.be.at.least(3); expect(modelList.models).to.deep.include(model1); @@ -395,7 +395,7 @@ describe('admin.machineLearning', () => { }); it('filters by tag', () => { - return admin.machineLearning().listModels({filter: 'tags:node-integration-tag-1', pageSize: 100}) + return admin.machineLearning().listModels({ filter: 'tags:node-integration-tag-1', pageSize: 100 }) .then((modelList) => { expect(modelList.models.length).to.be.at.least(3); expect(modelList.models).to.deep.include(model1); @@ -406,14 +406,14 @@ describe('admin.machineLearning', () => { }); it('handles pageTokens properly', () => { - return admin.machineLearning().listModels({filter: 'displayName:node-integration-list*', pageSize: 2}) + return admin.machineLearning().listModels({ filter: 'displayName:node-integration-list*', pageSize: 2 }) .then((modelList) => { expect(modelList.models.length).to.equal(2); expect(modelList.pageToken).not.to.be.empty; return admin.machineLearning().listModels({ filter: 'displayName:node-integration-list*', pageSize: 2, - pageToken: modelList.pageToken}) + pageToken: modelList.pageToken }) .then((modelList2) => { expect(modelList2.models.length).to.be.at.least(1); expect(modelList2.pageToken).to.be.empty; @@ -422,7 +422,7 @@ describe('admin.machineLearning', () => { }); it('successfully returns an empty list of models', () => { - return admin.machineLearning().listModels({filter: 'displayName=non-existing-model'}) + return admin.machineLearning().listModels({ filter: 'displayName=non-existing-model' }) .then((modelList) => { expect(modelList.models.length).to.equal(0); expect(modelList.pageToken).to.be.empty; @@ -430,7 +430,7 @@ describe('admin.machineLearning', () => { }); it('rejects with invalid argument if the filter is invalid', () => { - return admin.machineLearning().listModels({filter: 'invalidFilterItem=foo'}) + return admin.machineLearning().listModels({ filter: 'invalidFilterItem=foo' }) .should.eventually.be.rejected.and.have.property( 'code', 'machine-learning/invalid-argument'); }); diff --git a/test/integration/messaging.spec.ts b/test/integration/messaging.spec.ts index 7f0d7e24f9..e67a81602b 100644 --- a/test/integration/messaging.spec.ts +++ b/test/integration/messaging.spec.ts @@ -125,7 +125,7 @@ describe('admin.messaging', () => { it('sendAll(500)', () => { const messages: admin.messaging.Message[] = []; for (let i = 0; i < 500; i++) { - messages.push({topic: `foo-bar-${i % 10}`}); + messages.push({ topic: `foo-bar-${i % 10}` }); } return admin.messaging().sendAll(messages, true) .then((response) => { diff --git a/test/integration/setup.ts b/test/integration/setup.ts index a3558ae6b2..4d58cede9c 100644 --- a/test/integration/setup.ts +++ b/test/integration/setup.ts @@ -18,7 +18,7 @@ import * as admin from '../../lib/index'; import fs = require('fs'); import minimist = require('minimist'); import path = require('path'); -import {random} from 'lodash'; +import { random } from 'lodash'; import { Credential, GoogleOAuthAccessToken } from '../../src/auth/credential'; // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/test/integration/storage.spec.ts b/test/integration/storage.spec.ts index 55e2414580..25b7a39e43 100644 --- a/test/integration/storage.spec.ts +++ b/test/integration/storage.spec.ts @@ -17,9 +17,9 @@ import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {Bucket, File} from '@google-cloud/storage'; +import { Bucket, File } from '@google-cloud/storage'; -import {projectId} from './setup'; +import { projectId } from './setup'; chai.should(); chai.use(chaiAsPromised); diff --git a/test/integration/typescript/src/example.test.ts b/test/integration/typescript/src/example.test.ts index 2b1159f696..8b730ea3f6 100644 --- a/test/integration/typescript/src/example.test.ts +++ b/test/integration/typescript/src/example.test.ts @@ -15,9 +15,9 @@ */ import initApp from './example'; -import {expect} from 'chai'; -import {Bucket} from '@google-cloud/storage'; -import {Firestore} from '@google-cloud/firestore'; +import { expect } from 'chai'; +import { Bucket } from '@google-cloud/storage'; +import { Firestore } from '@google-cloud/firestore'; import * as admin from 'firebase-admin'; diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index 7c73e8fe54..cb2be41a8f 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -24,10 +24,10 @@ import stream = require('stream'); import * as _ from 'lodash'; import * as jwt from 'jsonwebtoken'; -import {FirebaseNamespace} from '../../src/firebase-namespace'; -import {FirebaseServiceInterface} from '../../src/firebase-service'; -import {FirebaseApp, FirebaseAppOptions} from '../../src/firebase-app'; -import {Credential, GoogleOAuthAccessToken, ServiceAccountCredential} from '../../src/auth/credential'; +import { FirebaseNamespace } from '../../src/firebase-namespace'; +import { FirebaseServiceInterface } from '../../src/firebase-service'; +import { FirebaseApp, FirebaseAppOptions } from '../../src/firebase-app'; +import { Credential, GoogleOAuthAccessToken, ServiceAccountCredential } from '../../src/auth/credential'; const ALGORITHM = 'RS256'; const ONE_HOUR_IN_SECONDS = 60 * 60; diff --git a/test/unit/auth/action-code-settings-builder.spec.ts b/test/unit/auth/action-code-settings-builder.spec.ts index 78eace7a04..ceadde3e9b 100644 --- a/test/unit/auth/action-code-settings-builder.spec.ts +++ b/test/unit/auth/action-code-settings-builder.spec.ts @@ -19,8 +19,8 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {ActionCodeSettingsBuilder} from '../../../src/auth/action-code-settings-builder'; -import {AuthClientErrorCode} from '../../../src/utils/error'; +import { ActionCodeSettingsBuilder } from '../../../src/auth/action-code-settings-builder'; +import { AuthClientErrorCode } from '../../../src/utils/error'; chai.should(); @@ -139,7 +139,7 @@ describe('ActionCodeSettingsBuilder', () => { return new ActionCodeSettingsBuilder({ url: 'https://www.example.com/path/file?a=1&b=2', handleCodeInApp: true, - iOS: {bundleId}, + iOS: { bundleId }, } as any); }).to.throw('"ActionCodeSettings.iOS.bundleId" must be a valid non-empty string.'); }); @@ -175,7 +175,7 @@ describe('ActionCodeSettingsBuilder', () => { return new ActionCodeSettingsBuilder({ url: 'https://www.example.com/path/file?a=1&b=2', handleCodeInApp: true, - android: {packageName}, + android: { packageName }, } as any); }).to.throw('"ActionCodeSettings.android.packageName" must be a valid non-empty string.'); }); diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 15f90407d7..f1173b5d0b 100755 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -25,9 +25,9 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {deepCopy, deepExtend} from '../../../src/utils/deep-copy'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {HttpClient, HttpRequestConfig} from '../../../src/utils/api-request'; +import { deepCopy, deepExtend } from '../../../src/utils/deep-copy'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; import * as validator from '../../../src/utils/validator'; import { AuthRequestHandler, FIREBASE_AUTH_GET_ACCOUNT_INFO, FIREBASE_AUTH_GET_ACCOUNTS_INFO, @@ -36,15 +36,15 @@ import { RESERVED_CLAIMS, FIREBASE_AUTH_UPLOAD_ACCOUNT, FIREBASE_AUTH_CREATE_SESSION_COOKIE, EMAIL_ACTION_REQUEST_TYPES, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, } from '../../../src/auth/auth-api-request'; -import {UserImportBuilder, UserImportRecord} from '../../../src/auth/user-import-builder'; -import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; -import {ActionCodeSettingsBuilder} from '../../../src/auth/action-code-settings-builder'; +import { UserImportBuilder, UserImportRecord } from '../../../src/auth/user-import-builder'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { ActionCodeSettingsBuilder } from '../../../src/auth/action-code-settings-builder'; import { OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest, SAMLUpdateAuthProviderRequest, SAMLConfigServerResponse, } from '../../../src/auth/auth-config'; -import {UserIdentifier} from '../../../src/auth/identifier'; -import {TenantOptions} from '../../../src/auth/tenant'; +import { UserIdentifier } from '../../../src/auth/identifier'; +import { TenantOptions } from '../../../src/auth/tenant'; import { UpdateRequest, UpdateMultiFactorInfoRequest } from '../../../src/auth/user-record'; chai.should(); @@ -109,7 +109,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_CREATE_SESSION_COOKIE.getRequestValidator(); it('should succeed with valid parameters passed', () => { - const validRequest = {idToken: 'ID_TOKEN', validDuration: 60 * 60}; + const validRequest = { idToken: 'ID_TOKEN', validDuration: 60 * 60 }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -118,7 +118,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { }); it('should succeed with duration set at minimum allowed', () => { const validDuration = 60 * 5; - const validRequest = {idToken: 'ID_TOKEN', validDuration}; + const validRequest = { idToken: 'ID_TOKEN', validDuration }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -127,7 +127,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { }); it('should succeed with duration set at maximum allowed', () => { const validDuration = 60 * 60 * 24 * 14; - const validRequest = {idToken: 'ID_TOKEN', validDuration}; + const validRequest = { idToken: 'ID_TOKEN', validDuration }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -135,14 +135,14 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { expect(isNumber).to.have.been.calledOnce.and.calledWith(validDuration); }); it('should fail when idToken not passed', () => { - const invalidRequest = {validDuration: 60 * 60}; + const invalidRequest = { validDuration: 60 * 60 }; expect(() => { return requestValidator(invalidRequest); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith(undefined); }); it('should fail when validDuration not passed', () => { - const invalidRequest = {idToken: 'ID_TOKEN'}; + const invalidRequest = { idToken: 'ID_TOKEN' }; expect(() => { return requestValidator(invalidRequest); }).to.throw(); @@ -151,13 +151,13 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { describe('called with invalid parameters', () => { it('should fail with invalid idToken', () => { expect(() => { - return requestValidator({idToken: '', validDuration: 60 * 60}); + return requestValidator({ idToken: '', validDuration: 60 * 60 }); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith(''); }); it('should fail with invalid validDuration', () => { expect(() => { - return requestValidator({idToken: 'ID_TOKEN', validDuration: 'invalid'}); + return requestValidator({ idToken: 'ID_TOKEN', validDuration: 'invalid' }); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith('ID_TOKEN'); expect(isNumber).to.have.been.calledOnce.and.calledWith('invalid'); @@ -166,7 +166,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { // Duration less 5 minutes. const outOfBoundDuration = 60 * 5 - 1; expect(() => { - return requestValidator({idToken: 'ID_TOKEN', validDuration: outOfBoundDuration}); + return requestValidator({ idToken: 'ID_TOKEN', validDuration: outOfBoundDuration }); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith('ID_TOKEN'); expect(isNumber).to.have.been.calledOnce.and.calledWith(outOfBoundDuration); @@ -175,7 +175,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { // Duration greater than 14 days. const outOfBoundDuration = 60 * 60 * 24 * 14 + 1; expect(() => { - return requestValidator({idToken: 'ID_TOKEN', validDuration: outOfBoundDuration}); + return requestValidator({ idToken: 'ID_TOKEN', validDuration: outOfBoundDuration }); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith('ID_TOKEN'); expect(isNumber).to.have.been.calledOnce.and.calledWith(outOfBoundDuration); @@ -185,7 +185,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_CREATE_SESSION_COOKIE.getResponseValidator(); it('should succeed with sessionCookie returned', () => { - const validResponse = {sessionCookie: 'SESSION_COOKIE'}; + const validResponse = { sessionCookie: 'SESSION_COOKIE' }; expect(() => { return responseValidator(validResponse); }).not.to.throw(); @@ -257,7 +257,7 @@ describe('FIREBASE_AUTH_DOWNLOAD_ACCOUNT', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_DOWNLOAD_ACCOUNT.getRequestValidator(); it('should succeed with valid maxResults passed', () => { - const validRequest = {maxResults: 500}; + const validRequest = { maxResults: 500 }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -286,31 +286,31 @@ describe('FIREBASE_AUTH_DOWNLOAD_ACCOUNT', () => { describe('called with invalid parameters', () => { it('should fail with invalid maxResults', () => { expect(() => { - return requestValidator({maxResults: ''}); + return requestValidator({ maxResults: '' }); }).to.throw(); expect(isNumber).to.have.been.calledOnce.and.calledWith(''); }); it('should fail with zero maxResults', () => { expect(() => { - return requestValidator({maxResults: 0}); + return requestValidator({ maxResults: 0 }); }).to.throw(); expect(isNumber).to.have.been.calledOnce.and.calledWith(0); }); it('should fail with negative maxResults', () => { expect(() => { - return requestValidator({maxResults: -500}); + return requestValidator({ maxResults: -500 }); }).to.throw(); expect(isNumber).to.have.been.calledOnce.and.calledWith(-500); }); it('should fail with maxResults exceeding allowed limit', () => { expect(() => { - return requestValidator({maxResults: 1001}); + return requestValidator({ maxResults: 1001 }); }).to.throw(); expect(isNumber).to.have.been.calledOnce.and.calledWith(1001); }); it('should fail with invalid nextPageToken', () => { expect(() => { - return requestValidator({maxResults: 1000, nextPageToken: ['PAGE_TOKEN']}); + return requestValidator({ maxResults: 1000, nextPageToken: ['PAGE_TOKEN'] }); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith(['PAGE_TOKEN']); }); @@ -328,31 +328,31 @@ describe('FIREBASE_AUTH_GET_ACCOUNT_INFO', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_GET_ACCOUNT_INFO.getRequestValidator(); it('should succeed with localId passed', () => { - const validRequest = {localId: ['1234']}; + const validRequest = { localId: ['1234'] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should succeed with email passed', () => { - const validRequest = {email: ['user@example.com']}; + const validRequest = { email: ['user@example.com'] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should succeed with phoneNumber passed', () => { - const validRequest = {phoneNumber: ['+11234567890']}; + const validRequest = { phoneNumber: ['+11234567890'] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should succeed with federatedUserId passed', () => { - const validRequest = {federatedUserId: [{providerId: 'google.com', rawId: 'google_uid'}]}; + const validRequest = { federatedUserId: [{ providerId: 'google.com', rawId: 'google_uid' }] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should fail when neither localId, email or phoneNumber are passed', () => { - const invalidRequest = {bla: ['1234']}; + const invalidRequest = { bla: ['1234'] }; expect(() => { return requestValidator(invalidRequest); }).to.throw(); @@ -361,7 +361,7 @@ describe('FIREBASE_AUTH_GET_ACCOUNT_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_GET_ACCOUNT_INFO.getResponseValidator(); it('should succeed with users returned', () => { - const validResponse: object = {users: []}; + const validResponse: object = { users: [] }; expect(() => { return responseValidator(validResponse); }).not.to.throw(); @@ -385,31 +385,31 @@ describe('FIREBASE_AUTH_GET_ACCOUNTS_INFO', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_GET_ACCOUNTS_INFO.getRequestValidator(); it('should succeed with localId passed', () => { - const validRequest = {localId: ['1234']}; + const validRequest = { localId: ['1234'] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should succeed with email passed', () => { - const validRequest = {email: ['user@example.com']}; + const validRequest = { email: ['user@example.com'] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should succeed with phoneNumber passed', () => { - const validRequest = {phoneNumber: ['+11234567890']}; + const validRequest = { phoneNumber: ['+11234567890'] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should succeed with federatedUserId passed', () => { - const validRequest = {federatedUserId: [{providerId: 'google.com', rawId: 'google_uid'}]}; + const validRequest = { federatedUserId: [{ providerId: 'google.com', rawId: 'google_uid' }] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should fail when neither localId, email or phoneNumber are passed', () => { - const invalidRequest = {bla: ['1234']}; + const invalidRequest = { bla: ['1234'] }; expect(() => { return requestValidator(invalidRequest); }).to.throw(); @@ -420,9 +420,9 @@ describe('FIREBASE_AUTH_GET_ACCOUNTS_INFO', () => { email: ['user1@example.com', 'user2@example.com'], phoneNumber: ['+15555550001', '+15555550002'], federatedUserId: [ - {providerId: 'google.com', rawId: 'google_uid1'}, - {providerId: 'google.com', rawId: 'google_uid2'} - ]}; + { providerId: 'google.com', rawId: 'google_uid1' }, + { providerId: 'google.com', rawId: 'google_uid2' } + ] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -431,7 +431,7 @@ describe('FIREBASE_AUTH_GET_ACCOUNTS_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_GET_ACCOUNTS_INFO.getResponseValidator(); it('should succeed with users returned', () => { - const validResponse: object = {users: []}; + const validResponse: object = { users: [] }; expect(() => { return responseValidator(validResponse); }).not.to.throw(); @@ -463,13 +463,13 @@ describe('FIREBASE_AUTH_DELETE_ACCOUNT', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_DELETE_ACCOUNT.getRequestValidator(); it('should succeed with localId passed', () => { - const validRequest = {localId: '1234'}; + const validRequest = { localId: '1234' }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should fail when localId not passed', () => { - const invalidRequest = {bla: '1234'}; + const invalidRequest = { bla: '1234' }; expect(() => { return requestValidator(invalidRequest); }).to.throw(); @@ -512,7 +512,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_SET_ACCOUNT_INFO.getRequestValidator(); it('should succeed with valid localId passed', () => { - const validRequest = {localId: '1234'}; + const validRequest = { localId: '1234' }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -528,7 +528,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { photoUrl: 'http://www.example.com/1234/photo.png', disableUser: false, phoneNumber: '+11234567890', - customAttributes: JSON.stringify({admin: true, groupId: '123'}), + customAttributes: JSON.stringify({ admin: true, groupId: '123' }), validSince: 1476136676, // Pass an unsupported parameter which should be ignored. ignoreMe: 'bla', @@ -546,9 +546,9 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { }); it('should succeed with valid localId and customAttributes with 1000 char payload', () => { // Test with 1000 characters. - const atLimitClaims = JSON.stringify({key: createRandomString(990)}); + const atLimitClaims = JSON.stringify({ key: createRandomString(990) }); expect(() => { - return requestValidator({localId: '1234', customAttributes: atLimitClaims}); + return requestValidator({ localId: '1234', customAttributes: atLimitClaims }); }).not.to.throw(); }); it('should fail when localId not passed', () => { @@ -561,59 +561,59 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { describe('called with invalid parameters', () => { it('should fail with invalid localId', () => { expect(() => { - return requestValidator({localId: ''}); + return requestValidator({ localId: '' }); }).to.throw(); expect(isUidSpy).to.have.been.calledOnce.and.calledWith(''); }); it('should fail with invalid displayName', () => { expect(() => { - return requestValidator({localId: '1234', displayName: ['John Doe']}); + return requestValidator({ localId: '1234', displayName: ['John Doe'] }); }).to.throw(); }); it('should fail with invalid email', () => { expect(() => { - return requestValidator({localId: '1234', email: 'invalid'}); + return requestValidator({ localId: '1234', email: 'invalid' }); }).to.throw(); expect(isEmailSpy).to.have.been.calledOnce.and.calledWith('invalid'); }); it('should fail with invalid password', () => { expect(() => { - return requestValidator({localId: '1234', password: 'short'}); + return requestValidator({ localId: '1234', password: 'short' }); }).to.throw(); expect(isPasswordSpy).to.have.been.calledOnce.and.calledWith('short'); }); it('should fail with invalid emailVerified flag', () => { expect(() => { - return requestValidator({localId: '1234', emailVerified: 'yes'}); + return requestValidator({ localId: '1234', emailVerified: 'yes' }); }).to.throw(); }); it('should fail with invalid photoUrl', () => { expect(() => { - return requestValidator({localId: '1234', photoUrl: 'invalid url'}); + return requestValidator({ localId: '1234', photoUrl: 'invalid url' }); }).to.throw(); expect(isUrlSpy).to.have.been.calledOnce.and.calledWith('invalid url'); }); it('should fail with invalid disableUser flag', () => { expect(() => { - return requestValidator({localId: '1234', disableUser: 'no'}); + return requestValidator({ localId: '1234', disableUser: 'no' }); }).to.throw(); }); it('should fail with invalid phoneNumber', () => { expect(() => { - return requestValidator({localId: '1234', phoneNumber: 'invalid'}); + return requestValidator({ localId: '1234', phoneNumber: 'invalid' }); }).to.throw(); expect(isPhoneNumberSpy).to.have.been.calledOnce.and.calledWith('invalid'); }); it('should fail with invalid JSON customAttributes', () => { expect(() => { - return requestValidator({localId: '1234', customAttributes: 'invalid'}); + return requestValidator({ localId: '1234', customAttributes: 'invalid' }); }).to.throw(); }); it('should fail with customAttributes exceeding maximum allowed payload', () => { // Test with 1001 characters. - const largeClaims = JSON.stringify({key: createRandomString(991)}); + const largeClaims = JSON.stringify({ key: createRandomString(991) }); expect(() => { - return requestValidator({localId: '1234', customAttributes: largeClaims}); + return requestValidator({ localId: '1234', customAttributes: largeClaims }); }).to.throw(`Developer claims payload should not exceed 1000 characters.`); }); RESERVED_CLAIMS.forEach((invalidClaim) => { @@ -622,7 +622,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { // Instantiate custom attributes with invalid claims. const claims: {[key: string]: any} = {}; claims[invalidClaim] = 'bla'; - return requestValidator({localId: '1234', customAttributes: JSON.stringify(claims)}); + return requestValidator({ localId: '1234', customAttributes: JSON.stringify(claims) }); }).to.throw(`Developer claim "${invalidClaim}" is reserved and cannot be specified.`); }); }); @@ -632,12 +632,12 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { sub: 'sub', auth_time: 'time', // eslint-disable-line @typescript-eslint/camelcase }; - return requestValidator({localId: '1234', customAttributes: JSON.stringify(claims)}); + return requestValidator({ localId: '1234', customAttributes: JSON.stringify(claims) }); }).to.throw(`Developer claims "auth_time", "sub" are reserved and cannot be specified.`); }); it('should fail with invalid validSince', () => { expect(() => { - return requestValidator({localId: '1234', validSince: 'invalid'}); + return requestValidator({ localId: '1234', validSince: 'invalid' }); }).to.throw('The tokensValidAfterTime must be a valid UTC number in seconds.'); expect(isNumberSpy).to.have.been.calledOnce.and.calledWith('invalid'); }); @@ -646,7 +646,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_SET_ACCOUNT_INFO.getResponseValidator(); it('should succeed with localId returned', () => { - const validResponse = {localId: '1234'}; + const validResponse = { localId: '1234' }; expect(() => { return responseValidator(validResponse); }).not.to.throw(); @@ -744,57 +744,57 @@ describe('FIREBASE_AUTH_SIGN_UP_NEW_USER', () => { describe('called with invalid parameters', () => { it('should fail with invalid localId', () => { expect(() => { - return requestValidator({localId: ''}); + return requestValidator({ localId: '' }); }).to.throw(); expect(isUidSpy).to.have.been.calledOnce.and.calledWith(''); }); it('should fail with invalid displayName', () => { expect(() => { - return requestValidator({displayName: ['John Doe']}); + return requestValidator({ displayName: ['John Doe'] }); }).to.throw(); }); it('should fail with invalid email', () => { expect(() => { - return requestValidator({email: 'invalid'}); + return requestValidator({ email: 'invalid' }); }).to.throw(); expect(isEmailSpy).to.have.been.calledOnce.and.calledWith('invalid'); }); it('should fail with invalid password', () => { expect(() => { - return requestValidator({password: 'short'}); + return requestValidator({ password: 'short' }); }).to.throw(); expect(isPasswordSpy).to.have.been.calledOnce.and.calledWith('short'); }); it('should fail with invalid emailVerified flag', () => { expect(() => { - return requestValidator({emailVerified: 'yes'}); + return requestValidator({ emailVerified: 'yes' }); }).to.throw(); }); it('should fail with invalid photoUrl', () => { expect(() => { - return requestValidator({photoUrl: 'invalid url'}); + return requestValidator({ photoUrl: 'invalid url' }); }).to.throw(); expect(isUrlSpy).to.have.been.calledOnce.and.calledWith('invalid url'); }); it('should fail with invalid disabled flag', () => { expect(() => { - return requestValidator({disabled: 'no'}); + return requestValidator({ disabled: 'no' }); }).to.throw(); }); it('should fail with invalid phoneNumber', () => { expect(() => { - return requestValidator({phoneNumber: 'invalid'}); + return requestValidator({ phoneNumber: 'invalid' }); }).to.throw(); expect(isPhoneNumberSpy).to.have.been.calledOnce.and.calledWith('invalid'); }); it('should fail with customAttributes', () => { expect(() => { - return requestValidator({customAttributes: JSON.stringify({admin: true})}); + return requestValidator({ customAttributes: JSON.stringify({ admin: true }) }); }).to.throw(); }); it('should fail with validSince', () => { expect(() => { - return requestValidator({validSince: 1476136676}); + return requestValidator({ validSince: 1476136676 }); }).to.throw(); }); }); @@ -802,7 +802,7 @@ describe('FIREBASE_AUTH_SIGN_UP_NEW_USER', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_SIGN_UP_NEW_USER.getResponseValidator(); it('should succeed with localId returned', () => { - const validResponse = {localId: '1234'}; + const validResponse = { localId: '1234' }; expect(() => { return responseValidator(validResponse); }).not.to.throw(); @@ -897,7 +897,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedResult = utils.responseFrom({ sessionCookie: 'SESSION_COOKIE', }); - const data = {idToken: 'ID_TOKEN', validDuration: durationInMs / 1000}; + const data = { idToken: 'ID_TOKEN', validDuration: durationInMs / 1000 }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -913,7 +913,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { sessionCookie: 'SESSION_COOKIE', }); const durationAtLimitInMs = 14 * 24 * 60 * 60 * 1000; - const data = {idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000}; + const data = { idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000 }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -929,7 +929,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { sessionCookie: 'SESSION_COOKIE', }); const durationAtLimitInMs = 5 * 60 * 1000; - const data = {idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000}; + const data = { idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000 }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1001,7 +1001,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, }); const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); - const data = {idToken: 'invalid-token', validDuration: durationInMs / 1000}; + const data = { idToken: 'invalid-token', validDuration: durationInMs / 1000 }; const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); stubs.push(stub); @@ -1022,10 +1022,10 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be fulfilled given a valid email', () => { const expectedResult = utils.responseFrom({ users : [ - {email: 'user@example.com'}, + { email: 'user@example.com' }, ], }); - const data = {email: ['user@example.com']}; + const data = { email: ['user@example.com'] }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1047,7 +1047,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { kind: 'identitytoolkit#GetAccountInfoResponse', }); const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const data = {email: ['user@example.com']}; + const data = { email: ['user@example.com'] }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1068,10 +1068,10 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be fulfilled given a valid localId', () => { const expectedResult = utils.responseFrom({ users : [ - {localId: 'uid'}, + { localId: 'uid' }, ], }); - const data = {localId: ['uid']}; + const data = { localId: ['uid'] }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1087,7 +1087,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { kind: 'identitytoolkit#GetAccountInfoResponse', }); const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const data = {localId: ['uid']}; + const data = { localId: ['uid'] }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1107,7 +1107,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, }); const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const data = {localId: ['uid']}; + const data = { localId: ['uid'] }; const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); stubs.push(stub); @@ -1199,7 +1199,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should throw when given more than 100 identifiers', () => { const identifiers: UserIdentifier[] = []; for (let i = 0; i < 101; i++) { - identifiers.push({uid: 'id' + i}); + identifiers.push({ uid: 'id' + i }); } const requestHandler = handler.init(mockApp); @@ -1212,7 +1212,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.getAccountInfoByIdentifiers([]) .then((getUsersResult) => { - expect(getUsersResult).to.deep.equal({users: []}); + expect(getUsersResult).to.deep.equal({ users: [] }); }); }); @@ -1222,7 +1222,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { stubs.push(stub); const requestHandler = handler.init(mockApp); - const notFoundIds = [{uid: 'id that doesnt exist'}]; + const notFoundIds = [{ uid: 'id that doesnt exist' }]; return requestHandler.getAccountInfoByIdentifiers(notFoundIds) .then((getUsersResult) => { expect(getUsersResult).to.deep.equal({ users: [] }); @@ -1231,39 +1231,39 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should throw when given an invalid uid', () => { const requestHandler = handler.init(mockApp); - expect(() => requestHandler.getAccountInfoByIdentifiers([{uid: 'too long ' + ('.' as any).repeat(128)}])) + expect(() => requestHandler.getAccountInfoByIdentifiers([{ uid: 'too long ' + ('.' as any).repeat(128) }])) .to.throw(FirebaseAuthError) .with.property('code', 'auth/invalid-uid'); }); it('should throw when given an invalid email', () => { const requestHandler = handler.init(mockApp); - expect(() => requestHandler.getAccountInfoByIdentifiers([{email: 'invalid email addr'}])) + expect(() => requestHandler.getAccountInfoByIdentifiers([{ email: 'invalid email addr' }])) .to.throw(FirebaseAuthError) .with.property('code', 'auth/invalid-email'); }); it('should throw when given an invalid phone number', () => { const requestHandler = handler.init(mockApp); - expect(() => requestHandler.getAccountInfoByIdentifiers([{phoneNumber: 'invalid phone number'}])) + expect(() => requestHandler.getAccountInfoByIdentifiers([{ phoneNumber: 'invalid phone number' }])) .to.throw(FirebaseAuthError) .with.property('code', 'auth/invalid-phone-number'); }); it('should throw when given an invalid provider', () => { const requestHandler = handler.init(mockApp); - expect(() => requestHandler.getAccountInfoByIdentifiers([{providerUid: '', providerId: ''}])) + expect(() => requestHandler.getAccountInfoByIdentifiers([{ providerUid: '', providerId: '' }])) .to.throw(FirebaseAuthError) .with.property('code', 'auth/invalid-provider-id'); }); it('should throw when given a single bad identifier', () => { const identifiers: UserIdentifier[] = [ - {uid: 'valid_id1'}, - {uid: 'valid_id2'}, - {uid: 'invalid id; too long. ' + ('.' as any).repeat(128)}, - {uid: 'valid_id4'}, - {uid: 'valid_id5'}, + { uid: 'valid_id1' }, + { uid: 'valid_id2' }, + { uid: 'invalid id; too long. ' + ('.' as any).repeat(128) }, + { uid: 'valid_id4' }, + { uid: 'valid_id5' }, ]; const requestHandler = handler.init(mockApp); @@ -1355,7 +1355,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, ], }, - customClaims: {admin: true}, + customClaims: { admin: true }, // Tenant ID accepted on user batch upload. tenantId, }, @@ -1365,7 +1365,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { passwordHash: Buffer.from('userpass'), passwordSalt: Buffer.from('NaCl'), }, - {uid: '5678', phoneNumber: '+16505550101'}, + { uid: '5678', phoneNumber: '+16505550101' }, ]; const options = { hash: { @@ -1489,8 +1489,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should resolve with expected result on underlying API partial succcess', () => { const expectedResult = utils.responseFrom({ error: [ - {index: 0, message: 'Some error occurred'}, - {index: 1, message: 'Another error occurred'}, + { index: 0, message: 'Some error occurred' }, + { index: 1, message: 'Another error occurred' }, ], }); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); @@ -1509,15 +1509,15 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should resolve without underlying API call when users are processed client side', () => { // These users should fail to upload due to invalid phone number and email fields. const testUsers = [ - {uid: '1234', phoneNumber: 'invalid'}, - {uid: '5678', email: 'invalid'}, + { uid: '1234', phoneNumber: 'invalid' }, + { uid: '5678', email: 'invalid' }, ] as any; const expectedResult = { successCount: 0, failureCount: 2, errors: [ - {index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, - {index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)}, + { index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, + { index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL) }, ], }; const stub = sinon.stub(HttpClient.prototype, 'send'); @@ -1533,33 +1533,33 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should validate underlying users and resolve with expected errors', () => { const testUsers = [ - {uid: 'user1', displayName: false}, - {uid: 123}, - {uid: 'user2', email: 'invalid'}, - {uid: 'user3', phoneNumber: 'invalid'}, - {uid: 'user4', emailVerified: 'invalid'}, - {uid: 'user5', photoURL: 'invalid'}, - {uid: 'user6', disabled: 'invalid'}, - {uid: 'user7', metadata: {creationTime: 'invalid'}}, - {uid: 'user8', metadata: {lastSignInTime: 'invalid'}}, - {uid: 'user9', customClaims: {admin: true, aud: 'bla'}}, - {uid: 'user10', email: 'user10@example.com', passwordHash: 'invalid'}, - {uid: 'user11', email: 'user11@example.com', passwordSalt: 'invalid'}, - {uid: 'user12', providerData: [{providerId: 'google.com'}]}, + { uid: 'user1', displayName: false }, + { uid: 123 }, + { uid: 'user2', email: 'invalid' }, + { uid: 'user3', phoneNumber: 'invalid' }, + { uid: 'user4', emailVerified: 'invalid' }, + { uid: 'user5', photoURL: 'invalid' }, + { uid: 'user6', disabled: 'invalid' }, + { uid: 'user7', metadata: { creationTime: 'invalid' } }, + { uid: 'user8', metadata: { lastSignInTime: 'invalid' } }, + { uid: 'user9', customClaims: { admin: true, aud: 'bla' } }, + { uid: 'user10', email: 'user10@example.com', passwordHash: 'invalid' }, + { uid: 'user11', email: 'user11@example.com', passwordSalt: 'invalid' }, + { uid: 'user12', providerData: [{ providerId: 'google.com' }] }, { uid: 'user13', - providerData: [{providerId: 'google.com', uid: 'RAW_ID', displayName: false}], + providerData: [{ providerId: 'google.com', uid: 'RAW_ID', displayName: false }], }, { uid: 'user14', - providerData: [{providerId: 'google.com', uid: 'RAW_ID', email: 'invalid'}], + providerData: [{ providerId: 'google.com', uid: 'RAW_ID', email: 'invalid' }], }, { uid: 'user15', - providerData: [{providerId: 'google.com', uid: 'RAW_ID', photoURL: 'invalid'}], + providerData: [{ providerId: 'google.com', uid: 'RAW_ID', photoURL: 'invalid' }], }, - {uid: 'user16', providerData: [{}]}, - {email: 'user17@example.com'}, + { uid: 'user16', providerData: [{}] }, + { email: 'user17@example.com' }, { uid: 'user18', email: 'user18@example.com', @@ -1644,15 +1644,15 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { successCount: 0, failureCount: testUsers.length, errors: [ - {index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME)}, - {index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)}, - {index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)}, - {index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, - {index: 4, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED)}, - {index: 5, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL)}, - {index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD)}, - {index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_CREATION_TIME)}, - {index: 8, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME)}, + { index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME) }, + { index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID) }, + { index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL) }, + { index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, + { index: 4, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED) }, + { index: 5, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL) }, + { index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD) }, + { index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_CREATION_TIME) }, + { index: 8, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME) }, { index: 9, error: new FirebaseAuthError( @@ -1660,8 +1660,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { `Developer claim "aud" is reserved and cannot be specified.`, ), }, - {index: 10, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH)}, - {index: 11, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT)}, + { index: 10, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH) }, + { index: 11, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT) }, { index: 12, error: new FirebaseAuthError( @@ -1690,8 +1690,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { `The provider "photoURL" for "google.com" must be a valid URL string.`, ), }, - {index: 16, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)}, - {index: 17, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)}, + { index: 16, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID) }, + { index: 17, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID) }, { index: 18, error: new FirebaseAuthError( @@ -1776,8 +1776,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const maxResults = 500; const expectedResult = utils.responseFrom({ users : [ - {localId: 'uid1'}, - {localId: 'uid2'}, + { localId: 'uid1' }, + { localId: 'uid2' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }); @@ -1808,7 +1808,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.downloadAccount(maxResults, nextPageToken) .then((result) => { - expect(result).to.deep.equal({users: []}); + expect(result).to.deep.equal({ users: [] }); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1887,7 +1887,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedResult = utils.responseFrom({ kind: 'identitytoolkit#DeleteAccountResponse', }); - const data = {localId: 'uid'}; + const data = { localId: 'uid' }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1905,7 +1905,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, }); const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const data = {localId: 'uid'}; + const data = { localId: 'uid' }; const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); stubs.push(stub); @@ -1953,7 +1953,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be fulfilled given valid uids', async () => { const expectedResult = utils.responseFrom({}); - const data = {localIds: ['uid1', 'uid2', 'uid3'], force: true}; + const data = { localIds: ['uid1', 'uid2', 'uid3'], force: true }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -2080,7 +2080,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { expect(returnedUid).to.be.equal(uid); // Confirm expected rpc request parameters sent. expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, {localId: uid})); + callParams(path, method, { localId: uid })); }); }); @@ -2161,7 +2161,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); // Send update request to delete enrolled factors. - return requestHandler.updateExistingAccount(uid, {multiFactor: {enrolledFactors: null}}) + return requestHandler.updateExistingAccount(uid, { multiFactor: { enrolledFactors: null } }) .then((returnedUid: string) => { // uid should be returned. expect(returnedUid).to.be.equal(uid); @@ -2183,7 +2183,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); // Send update request to delete enrolled factors. - return requestHandler.updateExistingAccount(uid, {multiFactor: {enrolledFactors: []}}) + return requestHandler.updateExistingAccount(uid, { multiFactor: { enrolledFactors: [] } }) .then((returnedUid: string) => { // uid should be returned. expect(returnedUid).to.be.equal(uid); @@ -2355,7 +2355,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const path = handler.path('v1', '/accounts:update', 'project_id'); const method = 'POST'; const uid = '12345678'; - const claims = {admin: true, groupId: '1234'}; + const claims = { admin: true, groupId: '1234' }; const expectedValidData = { localId: uid, customAttributes: JSON.stringify(claims), @@ -2440,7 +2440,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { `Developer claim "aud" is reserved and cannot be specified.`, ); const requestHandler = handler.init(mockApp); - const blacklistedClaims = {admin: true, aud: 'bla'}; + const blacklistedClaims = { admin: true, aud: 'bla' }; // Send request with blacklisted claims. return requestHandler.setCustomUserClaims(uid, blacklistedClaims) .then(() => { @@ -2513,7 +2513,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid uid', () => { const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); - const invalidUid: any = {localId: uid}; + const invalidUid: any = { localId: uid }; const requestHandler = handler.init(mockApp); return requestHandler.revokeRefreshTokens(invalidUid as any) @@ -2623,7 +2623,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); // Send empty create new account request with only a uid provided. - return requestHandler.createNewAccount({uid}) + return requestHandler.createNewAccount({ uid }) .then((returnedUid: string) => { // uid should be returned. expect(returnedUid).to.be.equal(uid); @@ -2679,7 +2679,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); // Send create new account request with no enrolled factors. - const request: any = {uid, multiFactor: {enrolledFactors: null}}; + const request: any = { uid, multiFactor: { enrolledFactors: null } }; return requestHandler.createNewAccount(request) .then((returnedUid: string) => { // uid should be returned. @@ -3139,7 +3139,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, expectedActionCodeSettingsRequest); // Simulate response missing link. const stub = sinon.stub(HttpClient.prototype, 'send') - .resolves(utils.responseFrom({email})); + .resolves(utils.responseFrom({ email })); stubs.push(stub); const requestHandler = handler.init(mockApp); @@ -3245,8 +3245,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const maxResults = 50; const expectedResult = utils.responseFrom({ oauthIdpConfigs : [ - {name: 'projects/project1/oauthIdpConfigs/oidc.provider1'}, - {name: 'projects/project1/oauthIdpConfigs/oidc.provider2'}, + { name: 'projects/project1/oauthIdpConfigs/oidc.provider1' }, + { name: 'projects/project1/oauthIdpConfigs/oidc.provider2' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }); @@ -3279,7 +3279,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) .then((result) => { - expect(result).to.deep.equal({oauthIdpConfigs: []}); + expect(result).to.deep.equal({ oauthIdpConfigs: [] }); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, data)); }); @@ -3737,8 +3737,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const maxResults = 50; const expectedResult = utils.responseFrom({ inboundSamlConfigs : [ - {name: 'projects/project1/inboundSamlConfigs/saml.provider1'}, - {name: 'projects/project1/inboundSamlConfigs/saml.provider2'}, + { name: 'projects/project1/inboundSamlConfigs/saml.provider1' }, + { name: 'projects/project1/inboundSamlConfigs/saml.provider2' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }); @@ -3770,7 +3770,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) .then((result) => { - expect(result).to.deep.equal({inboundSamlConfigs: []}); + expect(result).to.deep.equal({ inboundSamlConfigs: [] }); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); }); }); @@ -3922,8 +3922,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -4029,8 +4029,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -4051,8 +4051,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { ssoUrl: 'https://example.com/login2', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -4280,8 +4280,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const maxResults = 500; const expectedResult = utils.responseFrom({ tenants : [ - {name: 'projects/project_id/tenants/tenant-id1'}, - {name: 'projects/project_id/tenants/tenant-id2'}, + { name: 'projects/project_id/tenants/tenant-id1' }, + { name: 'projects/project_id/tenants/tenant-id2' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }); @@ -4313,7 +4313,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp) as AuthRequestHandler; return requestHandler.listTenants(maxResults, nextPageToken) .then((result) => { - expect(result).to.deep.equal({tenants: []}); + expect(result).to.deep.equal({ tenants: [] }); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -4516,7 +4516,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { ); // Resource name should have /tenants/tenant-id in path. This should throw an error. const stub = sinon.stub(HttpClient.prototype, 'send') - .resolves(utils.responseFrom({name: 'projects/project_id'})); + .resolves(utils.responseFrom({ name: 'projects/project_id' })); stubs.push(stub); const requestHandler = handler.init(mockApp) as AuthRequestHandler; @@ -4595,7 +4595,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { allowPasswordSignup: true, }; const partialTenantOptions = { - emailSignInConfig: {enabled: true}, + emailSignInConfig: { enabled: true }, }; stubs.push(stub); @@ -4688,7 +4688,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { ); // Resource name should have /tenants/tenant-id in path. This should throw an error. const stub = sinon.stub(HttpClient.prototype, 'send') - .resolves(utils.responseFrom({name: 'projects/project_id'})); + .resolves(utils.responseFrom({ name: 'projects/project_id' })); stubs.push(stub); const requestHandler = handler.init(mockApp) as AuthRequestHandler; diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts index d5777d787d..d74a6c9666 100755 --- a/test/unit/auth/auth-config.spec.ts +++ b/test/unit/auth/auth-config.spec.ts @@ -19,7 +19,7 @@ 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 { deepCopy } from '../../../src/utils/deep-copy'; import { OIDCConfig, SAMLConfig, SAMLConfigServerRequest, SAMLConfigServerResponse, OIDCConfigServerRequest, @@ -159,8 +159,8 @@ describe('SAMLConfig', () => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -177,8 +177,8 @@ describe('SAMLConfig', () => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -347,7 +347,7 @@ describe('SAMLConfig', () => { .to.throw('"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".'); }); - const nonAuthConfigOptions = [null, undefined, {}, {other: 'value'}]; + const nonAuthConfigOptions = [null, undefined, {}, { other: 'value' }]; nonAuthConfigOptions.forEach((nonAuthConfig) => { it('should return null when no AuthConfig is provided: ' + JSON.stringify(nonAuthConfig), () => { expect(SAMLConfig.buildServerRequest(nonAuthConfig as any)) @@ -645,7 +645,7 @@ describe('OIDCConfig', () => { .to.throw('"OIDCAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "oidc.".'); }); - const nonAuthConfigOptions = [null, undefined, {}, {other: 'value'}]; + const nonAuthConfigOptions = [null, undefined, {}, { other: 'value' }]; nonAuthConfigOptions.forEach((nonAuthConfig) => { it('should return null when no AuthConfig is provided: ' + JSON.stringify(nonAuthConfig), () => { expect(OIDCConfig.buildServerRequest(nonAuthConfig as any)).to.be.null; diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index b869580c1f..0d211a718d 100755 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -26,13 +26,13 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {Auth, TenantAwareAuth, BaseAuth, DecodedIdToken} from '../../../src/auth/auth'; -import {UserRecord, UpdateRequest} from '../../../src/auth/user-record'; -import {FirebaseApp} from '../../../src/firebase-app'; +import { Auth, TenantAwareAuth, BaseAuth, DecodedIdToken } from '../../../src/auth/auth'; +import { UserRecord, UpdateRequest } from '../../../src/auth/user-record'; +import { FirebaseApp } from '../../../src/firebase-app'; import { AuthRequestHandler, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, } from '../../../src/auth/auth-api-request'; -import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; import * as validator from '../../../src/utils/validator'; import { FirebaseTokenVerifier } from '../../../src/auth/token-verifier'; @@ -40,7 +40,7 @@ import { AuthProviderConfigFilter, OIDCConfig, SAMLConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from '../../../src/auth/auth-config'; -import {deepCopy} from '../../../src/utils/deep-copy'; +import { deepCopy } from '../../../src/utils/deep-copy'; import { TenantManager } from '../../../src/auth/tenant-manager'; import { ServiceAccountCredential } from '../../../src/auth/credential'; import { HttpClient } from '../../../src/utils/api-request'; @@ -220,8 +220,8 @@ function getSAMLConfigServerResponse(providerId: string): SAMLConfigServerRespon ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -353,7 +353,7 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('createCustomToken()', () => { it('should return a jwt', async () => { const token = await auth.createCustomToken('uid1'); - const decodedToken = jwt.decode(token, {complete: true}); + const decodedToken = jwt.decode(token, { complete: true }); expect(decodedToken).to.have.property('header').that.has.property('typ', 'JWT'); }); @@ -1171,7 +1171,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByIdentifiers') .resolves({}); stubs.push(stub); - const notFoundIds = [{uid: 'id that doesnt exist'}]; + const notFoundIds = [{ uid: 'id that doesnt exist' }]; return auth.getUsers(notFoundIds) .then((getUsersResult) => { expect(getUsersResult.users).to.deep.equal([]); @@ -1203,7 +1203,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }]; const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByIdentifiers') - .resolves({users: mockUsers}); + .resolves({ users: mockUsers }); stubs.push(stub); const users = await auth.getUsers([ @@ -1215,13 +1215,13 @@ AUTH_CONFIGS.forEach((testConfig) => { ]); expect(users.users).to.have.deep.members(mockUsers.map((u) => new UserRecord(u))); - expect(users.notFound).to.have.deep.members([{uid: 'this-user-doesnt-exist'}]); + expect(users.notFound).to.have.deep.members([{ uid: 'this-user-doesnt-exist' }]); }); }); describe('deleteUser()', () => { const uid = 'abcdefghijklmnopqrstuvwxyz'; - const expectedDeleteAccountResult = {kind: 'identitytoolkit#DeleteAccountResponse'}; + const expectedDeleteAccountResult = { kind: 'identitytoolkit#DeleteAccountResponse' }; const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); // Stubs used to simulate underlying api calls. @@ -1732,17 +1732,17 @@ AUTH_CONFIGS.forEach((testConfig) => { const maxResult = 500; const downloadAccountResponse: any = { users: [ - {localId: 'UID1'}, - {localId: 'UID2'}, - {localId: 'UID3'}, + { localId: 'UID1' }, + { localId: 'UID2' }, + { localId: 'UID3' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }; const expectedResult: any = { users: [ - new UserRecord({localId: 'UID1'}), - new UserRecord({localId: 'UID2'}), - new UserRecord({localId: 'UID3'}), + new UserRecord({ localId: 'UID1' }), + new UserRecord({ localId: 'UID2' }), + new UserRecord({ localId: 'UID3' }), ], pageToken: 'NEXT_PAGE_TOKEN', }; @@ -1949,8 +1949,8 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('importUsers()', () => { const users = [ - {uid: '1234', email: 'user@example.com', passwordHash: Buffer.from('password')}, - {uid: '5678', phoneNumber: 'invalid'}, + { uid: '1234', email: 'user@example.com', passwordHash: Buffer.from('password') }, + { uid: '5678', phoneNumber: 'invalid' }, ]; const options = { hash: { @@ -2034,7 +2034,7 @@ AUTH_CONFIGS.forEach((testConfig) => { .throws(expectedOptionsError); stubs.push(uploadAccountStub); expect(() => { - return auth.importUsers(users, {hash: {algorithm: 'invalid' as any}}); + return auth.importUsers(users, { hash: { algorithm: 'invalid' as any } }); }).to.throw(expectedOptionsError); }); @@ -2072,7 +2072,7 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('createSessionCookie()', () => { const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; const idToken = 'ID_TOKEN'; - const options = {expiresIn: 60 * 60 * 24 * 1000}; + const options = { expiresIn: 60 * 60 * 24 * 1000 }; const sessionCookie = 'SESSION_COOKIE'; const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); @@ -2138,7 +2138,7 @@ AUTH_CONFIGS.forEach((testConfig) => { stubs.push(sinon.stub(testConfig.Auth.prototype, 'verifyIdToken') .returns(Promise.resolve(decodedIdToken))); // 1 minute duration. - const invalidOptions = {expiresIn: 60 * 1000}; + const invalidOptions = { expiresIn: 60 * 1000 }; return auth.createSessionCookie(idToken, invalidOptions) .should.eventually.be.rejected.and.have.property( 'code', 'auth/invalid-session-cookie-duration'); @@ -2234,9 +2234,9 @@ AUTH_CONFIGS.forEach((testConfig) => { }); const emailActionFlows: EmailActionTest[] = [ - {api: 'generatePasswordResetLink', requestType: 'PASSWORD_RESET', requiresSettings: false}, - {api: 'generateEmailVerificationLink', requestType: 'VERIFY_EMAIL', requiresSettings: false}, - {api: 'generateSignInWithEmailLink', requestType: 'EMAIL_SIGNIN', requiresSettings: true}, + { api: 'generatePasswordResetLink', requestType: 'PASSWORD_RESET', requiresSettings: false }, + { api: 'generateEmailVerificationLink', requestType: 'VERIFY_EMAIL', requiresSettings: false }, + { api: 'generateSignInWithEmailLink', requestType: 'EMAIL_SIGNIN', requiresSettings: true }, ]; emailActionFlows.forEach((emailActionFlow) => { describe(`${emailActionFlow.api}()`, () => { @@ -2448,8 +2448,8 @@ AUTH_CONFIGS.forEach((testConfig) => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -2584,7 +2584,7 @@ AUTH_CONFIGS.forEach((testConfig) => { .stub(testConfig.RequestHandler.prototype, 'listOAuthIdpConfigs') .resolves(listConfigsResponse); stubs.push(listConfigsStub); - return (auth as Auth).listProviderConfigs({type: 'oidc'}) + return (auth as Auth).listProviderConfigs({ type: 'oidc' }) .then((response) => { expect(response).to.deep.equal(expectedResult); // Confirm underlying API called with expected parameters. @@ -2679,7 +2679,7 @@ AUTH_CONFIGS.forEach((testConfig) => { .stub(testConfig.RequestHandler.prototype, 'listInboundSamlConfigs') .resolves(listConfigsResponse); stubs.push(listConfigsStub); - return (auth as Auth).listProviderConfigs({type: 'saml'}) + return (auth as Auth).listProviderConfigs({ type: 'saml' }) .then((response) => { expect(response).to.deep.equal(expectedResult); // Confirm underlying API called with expected parameters. @@ -2966,8 +2966,8 @@ AUTH_CONFIGS.forEach((testConfig) => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -3133,8 +3133,8 @@ AUTH_CONFIGS.forEach((testConfig) => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { diff --git a/test/unit/auth/credential.spec.ts b/test/unit/auth/credential.spec.ts index 881dcb7525..dc66ae317b 100644 --- a/test/unit/auth/credential.spec.ts +++ b/test/unit/auth/credential.spec.ts @@ -35,7 +35,7 @@ import { ComputeEngineCredential, getApplicationDefault, isApplicationDefault, Credential, } from '../../../src/auth/credential'; import { HttpClient } from '../../../src/utils/api-request'; -import {Agent} from 'https'; +import { Agent } from 'https'; import { FirebaseAppError } from '../../../src/utils/error'; chai.should(); @@ -325,7 +325,7 @@ describe('Credential', () => { expect(httpStub).to.have.been.calledOnce.and.calledWith({ method: 'GET', url: 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', - headers: {'Metadata-Flavor': 'Google'}, + headers: { 'Metadata-Flavor': 'Google' }, httpAgent: undefined, }); }); @@ -342,7 +342,7 @@ describe('Credential', () => { expect(httpStub).to.have.been.calledOnce.and.calledWith({ method: 'GET', url: 'http://metadata.google.internal/computeMetadata/v1/project/project-id', - headers: {'Metadata-Flavor': 'Google'}, + headers: { 'Metadata-Flavor': 'Google' }, httpAgent: undefined, }); }); @@ -364,7 +364,7 @@ describe('Credential', () => { expect(httpStub).to.have.been.calledOnce.and.calledWith({ method: 'GET', url: 'http://metadata.google.internal/computeMetadata/v1/project/project-id', - headers: {'Metadata-Flavor': 'Google'}, + headers: { 'Metadata-Flavor': 'Google' }, httpAgent: undefined, }); }); diff --git a/test/unit/auth/tenant-manager.spec.ts b/test/unit/auth/tenant-manager.spec.ts index 82f89599d9..6a578323d9 100644 --- a/test/unit/auth/tenant-manager.spec.ts +++ b/test/unit/auth/tenant-manager.spec.ts @@ -23,11 +23,11 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {AuthRequestHandler} from '../../../src/auth/auth-api-request'; -import {Tenant, TenantOptions, TenantServerResponse, ListTenantsResult} from '../../../src/auth/tenant'; -import {TenantManager} from '../../../src/auth/tenant-manager'; -import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; +import { Tenant, TenantOptions, TenantServerResponse, ListTenantsResult } from '../../../src/auth/tenant'; +import { TenantManager } from '../../../src/auth/tenant-manager'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; chai.should(); chai.use(sinonChai); @@ -175,15 +175,15 @@ describe('TenantManager', () => { const maxResult = 500; const listTenantsResponse: any = { tenants : [ - {name: 'projects/project-id/tenants/tenant-id1'}, - {name: 'projects/project-id/tenants/tenant-id2'}, + { name: 'projects/project-id/tenants/tenant-id1' }, + { name: 'projects/project-id/tenants/tenant-id2' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }; const expectedResult: ListTenantsResult = { tenants: [ - new Tenant({name: 'projects/project-id/tenants/tenant-id1'}), - new Tenant({name: 'projects/project-id/tenants/tenant-id2'}), + new Tenant({ name: 'projects/project-id/tenants/tenant-id1' }), + new Tenant({ name: 'projects/project-id/tenants/tenant-id2' }), ], pageToken: 'NEXT_PAGE_TOKEN', }; @@ -412,7 +412,7 @@ describe('TenantManager', () => { it('should be rejected given TenantOptions with invalid type property', () => { // Create tenant using invalid type. This should throw an argument error. - return tenantManager.createTenant({type: 'invalid'} as any) + return tenantManager.createTenant({ type: 'invalid' } as any) .then(() => { throw new Error('Unexpected success'); }) @@ -521,7 +521,7 @@ describe('TenantManager', () => { it('should be rejected given TenantOptions with invalid update property', () => { // Updating the tenantId of an existing tenant will throw an error as tenantId is // an immutable property. - return tenantManager.updateTenant(tenantId, {tenantId: 'unmodifiable'} as any) + return tenantManager.updateTenant(tenantId, { tenantId: 'unmodifiable' } as any) .then(() => { throw new Error('Unexpected success'); }) diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 1fb1e24dfa..21440a1a8f 100755 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -19,8 +19,8 @@ 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 {EmailSignInConfig, EmailSignInProviderConfig} from '../../../src/auth/auth-config'; +import { deepCopy } from '../../../src/utils/deep-copy'; +import { EmailSignInConfig, EmailSignInProviderConfig } from '../../../src/auth/auth-config'; import { Tenant, TenantOptions, TenantServerResponse, } from '../../../src/auth/tenant'; diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index a257026193..cc733270b2 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -127,13 +127,13 @@ describe('CryptoSigner', () => { }); describe('explicit service account ID', () => { - const response = {signedBlob: Buffer.from('testsignature').toString('base64')}; + const response = { signedBlob: Buffer.from('testsignature').toString('base64') }; const input = Buffer.from('input'); const signRequest = { method: 'POST', url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-service-account:signBlob`, - headers: {Authorization: `Bearer ${mockAccessToken}`}, - data: {payload: input.toString('base64')}, + headers: { Authorization: `Bearer ${mockAccessToken}` }, + data: { payload: input.toString('base64') }, }; let stub: sinon.SinonStub; @@ -179,17 +179,17 @@ describe('CryptoSigner', () => { describe('auto discovered service account', () => { const input = Buffer.from('input'); - const response = {signedBlob: Buffer.from('testsignature').toString('base64')}; + const response = { signedBlob: Buffer.from('testsignature').toString('base64') }; const metadataRequest = { method: 'GET', url: `http://metadata/computeMetadata/v1/instance/service-accounts/default/email`, - headers: {'Metadata-Flavor': 'Google'}, + headers: { 'Metadata-Flavor': 'Google' }, }; const signRequest = { method: 'POST', url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/discovered-service-account:signBlob`, - headers: {Authorization: `Bearer ${mockAccessToken}`}, - data: {payload: input.toString('base64')}, + headers: { Authorization: `Bearer ${mockAccessToken}` }, + data: { payload: input.toString('base64') }, }; let stub: sinon.SinonStub; diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index bf95f96380..d4f4722af9 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -28,10 +28,10 @@ import * as chaiAsPromised from 'chai-as-promised'; import LegacyFirebaseTokenGenerator = require('firebase-token-generator'); import * as mocks from '../../resources/mocks'; -import {FirebaseTokenGenerator, ServiceAccountSigner} from '../../../src/auth/token-generator'; +import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator'; import * as verifier from '../../../src/auth/token-verifier'; -import {ServiceAccountCredential} from '../../../src/auth/credential'; +import { ServiceAccountCredential } from '../../../src/auth/credential'; import { AuthClientErrorCode } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; @@ -336,7 +336,7 @@ describe('FirebaseTokenVerifier', () => { it('should be rejected given a Firebase JWT token with no kid', () => { const mockIdToken = mocks.generateIdToken({ - header: {foo: 'bar'}, + header: { foo: 'bar' }, }); return tokenVerifier.verifyJWT(mockIdToken) .should.eventually.be.rejectedWith('Firebase ID token has no "kid" claim'); diff --git a/test/unit/auth/user-import-builder.spec.ts b/test/unit/auth/user-import-builder.spec.ts index b94e22f1f4..8923d42b8c 100755 --- a/test/unit/auth/user-import-builder.spec.ts +++ b/test/unit/auth/user-import-builder.spec.ts @@ -18,13 +18,13 @@ 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 { deepCopy } from '../../../src/utils/deep-copy'; import { UserImportBuilder, ValidatorFunction, UserImportResult, UserImportRecord, UploadAccountRequest, } from '../../../src/auth/user-import-builder'; -import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; -import {toWebSafeBase64} from '../../../src/utils'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { toWebSafeBase64 } from '../../../src/utils'; chai.should(); @@ -69,7 +69,7 @@ describe('UserImportBuilder', () => { providerId: 'google.com', }, ], - customClaims: {admin: true}, + customClaims: { admin: true }, tenantId: 'TENANT-ID', }, { @@ -78,7 +78,7 @@ describe('UserImportBuilder', () => { passwordHash: Buffer.from('userpass'), passwordSalt: Buffer.from('NaCl'), }, - {uid: '5678', phoneNumber: '+16505550101'}, + { uid: '5678', phoneNumber: '+16505550101' }, { uid: '3456', email: 'janedoe@example.com', @@ -122,7 +122,7 @@ describe('UserImportBuilder', () => { providerId: 'google.com', }, ], - customAttributes: JSON.stringify({admin: true}), + customAttributes: JSON.stringify({ admin: true }), tenantId: 'TENANT-ID', }, { @@ -200,8 +200,8 @@ describe('UserImportBuilder', () => { it('should not throw when no hash options are provided and no hashing is needed', () => { const noHashUsers = [ - {uid: '1234', email: 'user@example.com'}, - {uid: '5678', phoneNumber: '+16505550101'}, + { uid: '1234', email: 'user@example.com' }, + { uid: '5678', phoneNumber: '+16505550101' }, ]; expect(() => { return new UserImportBuilder(noHashUsers, undefined, userRequestValidator); @@ -594,7 +594,7 @@ describe('UserImportBuilder', () => { } as any, ); testUsers.push( - {uid: 'INVALID2', email: 'other@domain.com', passwordHash: 'not a buffer'} as any, + { uid: 'INVALID2', email: 'other@domain.com', passwordHash: 'not a buffer' } as any, ); const expectedRequest = { hashAlgorithm: algorithm, @@ -610,13 +610,13 @@ describe('UserImportBuilder', () => { it('should return expected request with no hash options when not required', () => { const noHashUsers = [ - {uid: '1234', email: 'user@example.com'}, - {uid: '5678', phoneNumber: '+16505550101'}, + { uid: '1234', email: 'user@example.com' }, + { uid: '5678', phoneNumber: '+16505550101' }, ]; const expectedRequest = { users: [ - {localId: '1234', email: 'user@example.com'}, - {localId: '5678', phoneNumber: '+16505550101'}, + { localId: '1234', email: 'user@example.com' }, + { localId: '5678', phoneNumber: '+16505550101' }, ], }; const userImportBuilder = @@ -626,13 +626,13 @@ describe('UserImportBuilder', () => { it('should return expected request with no multi-factor fields when not available', () => { const noMultiFactorUsers: any[] = [ - {uid: '1234', email: 'user@example.com', multiFactor: null}, - {uid: '5678', phoneNumber: '+16505550101', multiFactor: {enrolledFactors: []}}, + { uid: '1234', email: 'user@example.com', multiFactor: null }, + { uid: '5678', phoneNumber: '+16505550101', multiFactor: { enrolledFactors: [] } }, ]; const expectedRequest = { users: [ - {localId: '1234', email: 'user@example.com'}, - {localId: '5678', phoneNumber: '+16505550101'}, + { localId: '1234', email: 'user@example.com' }, + { localId: '5678', phoneNumber: '+16505550101' }, ], }; const userImportBuilder = @@ -656,11 +656,11 @@ describe('UserImportBuilder', () => { ], }, }, - {uid: '5678', phoneNumber: '+16505550102'}, + { uid: '5678', phoneNumber: '+16505550102' }, ]; const expectedRequest: UploadAccountRequest = { users: [ - {localId: '5678', phoneNumber: '+16505550102'}, + { localId: '5678', phoneNumber: '+16505550102' }, ], }; const userImportBuilder = @@ -683,11 +683,11 @@ describe('UserImportBuilder', () => { ], }, }, - {uid: '5678', phoneNumber: '+16505550102'}, + { uid: '5678', phoneNumber: '+16505550102' }, ]; const expectedRequest: UploadAccountRequest = { users: [ - {localId: '5678', phoneNumber: '+16505550102'}, + { localId: '5678', phoneNumber: '+16505550102' }, ], }; const userImportBuilder = @@ -719,7 +719,7 @@ describe('UserImportBuilder', () => { it('should return the expected response for import with server side errors', () => { const failingServerResponse = [ - {index: 1, message: 'Some error occurred!'}, + { index: 1, message: 'Some error occurred!' }, ]; const serverErrorUserImportResponse = { successCount: 3, @@ -747,7 +747,7 @@ describe('UserImportBuilder', () => { successCount: 3, failureCount: 1, errors: [ - {index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, + { index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, ], }; // userRequestValidatorWithError will throw on the 3rd user (index = 2). @@ -760,8 +760,8 @@ describe('UserImportBuilder', () => { it('should return the expected response for import with mixed client/server errors', () => { // Server errors will occur on USER3 and USER6 passed to backend. const failingServerResponse = [ - {index: 1, message: 'Some error occurred in USER3!'}, - {index: 3, message: 'Another error occurred in USER6!'}, + { index: 1, message: 'Some error occurred in USER3!' }, + { index: 3, message: 'Another error occurred in USER6!' }, ]; const userRequestValidatorWithMultipleErrors: ValidatorFunction = (request) => { // Simulate a validation error is thrown for specific users. @@ -777,13 +777,13 @@ describe('UserImportBuilder', () => { // Seventh, eighth and nineth user will throw a client side error due to invalid type provided. // Tenth user will throw a client side error due to an unsupported second factor. const testUsers = [ - {uid: 'USER1'}, - {uid: 'USER2', email: 'invalid', passwordHash: Buffer.from('userpass')}, - {uid: 'USER3'}, - {uid: 'USER4', email: 'user@example.com', phoneNumber: 'invalid'}, - {uid: 'USER5', email: 'johndoe@example.com', passwordHash: Buffer.from('password')}, - {uid: 'USER6', phoneNumber: '+16505550101'}, - {uid: 'USER7', email: 'other@domain.com', passwordHash: 'not a buffer' as any}, + { uid: 'USER1' }, + { uid: 'USER2', email: 'invalid', passwordHash: Buffer.from('userpass') }, + { uid: 'USER3' }, + { uid: 'USER4', email: 'user@example.com', phoneNumber: 'invalid' }, + { uid: 'USER5', email: 'johndoe@example.com', passwordHash: Buffer.from('password') }, + { uid: 'USER6', phoneNumber: '+16505550101' }, + { uid: 'USER7', email: 'other@domain.com', passwordHash: 'not a buffer' as any }, { uid: 'USER8', email: 'other@domain.com', @@ -822,7 +822,7 @@ describe('UserImportBuilder', () => { failureCount: 8, errors: [ // Client side detected error. - {index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)}, + { index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL) }, // Server side detected error. { index: 2, @@ -832,7 +832,7 @@ describe('UserImportBuilder', () => { ), }, // Client side detected error. - {index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, + { index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, // Server side detected error. { index: 5, @@ -842,8 +842,8 @@ describe('UserImportBuilder', () => { ), }, // Client side errors. - {index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH)}, - {index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT)}, + { index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH) }, + { index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT) }, { index: 8, error: new FirebaseAuthError( diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index af1ae9773b..33a641bc4a 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -18,7 +18,7 @@ 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 { deepCopy } from '../../../src/utils/deep-copy'; import { UserInfo, UserMetadata, UserRecord, GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactor, PhoneMultiFactorInfo, MultiFactorInfo, MultiFactorInfoResponse, @@ -515,19 +515,19 @@ describe('UserInfo', () => { it('should succeed when rawId and providerId are both provided', () => { expect(() => { - return new UserInfo({providerId: 'google.com', rawId: '1234567890'}); + return new UserInfo({ providerId: 'google.com', rawId: '1234567890' }); }).not.to.throw(Error); }); it('should throw when only rawId is provided', () => { expect(() => { - return new UserInfo({rawId: '1234567890'} as any); + return new UserInfo({ rawId: '1234567890' } as any); }).to.throw(Error); }); it('should throw when only providerId is provided', () => { expect(() => { - return new UserInfo({providerId: 'google.com'} as any); + return new UserInfo({ providerId: 'google.com' } as any); }).to.throw(Error); }); }); @@ -631,7 +631,7 @@ describe('UserMetadata', () => { describe('constructor', () => { it('should initialize as expected when a valid creationTime is provided', () => { expect(() => { - return new UserMetadata({createdAt: '1476136676000'} as any); + return new UserMetadata({ createdAt: '1476136676000' } as any); }).not.to.throw(Error); }); @@ -701,7 +701,7 @@ describe('UserRecord', () => { it('should succeed when only localId is provided', () => { expect(() => { - return new UserRecord({localId: '123456789'}); + return new UserRecord({ localId: '123456789' }); }).not.to.throw(Error); }); }); @@ -849,7 +849,7 @@ describe('UserRecord', () => { it('should throw when modifying readonly customClaims property', () => { expect(() => { - (userRecord as any).customClaims = {admin: false}; + (userRecord as any).customClaims = { admin: false }; }).to.throw(Error); }); diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 54d59432a3..78d73f21d6 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -17,13 +17,13 @@ 'use strict'; import * as _ from 'lodash'; -import {expect} from 'chai'; +import { expect } from 'chai'; import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {DatabaseService} from '../../../src/database/database'; -import {Database} from '@firebase/database'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { DatabaseService } from '../../../src/database/database'; +import { Database } from '@firebase/database'; import * as utils from '../utils'; import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; @@ -165,7 +165,7 @@ describe('Database', () => { }; if (strict) { - params.data = {format: 'strict'}; + params.data = { format: 'strict' }; } return params; @@ -228,7 +228,7 @@ describe('Database', () => { it('should throw if the server responds with a well-formed error', () => { const db: Database = database.getDatabase(); - stubErrorResponse({error: 'test error'}); + stubErrorResponse({ error: 'test error' }); return db.getRules().should.eventually.be.rejectedWith( 'Error while accessing security rules: test error'); }); @@ -282,7 +282,7 @@ describe('Database', () => { it('should throw if the server responds with a well-formed error', () => { const db: Database = database.getDatabase(); - stubErrorResponse({error: 'test error'}); + stubErrorResponse({ error: 'test error' }); return db.getRulesJSON().should.eventually.be.rejectedWith( 'Error while accessing security rules: test error'); }); @@ -386,7 +386,7 @@ describe('Database', () => { it('should throw if the server responds with a well-formed error', () => { const db: Database = database.getDatabase(); - stubErrorResponse({error: 'test error'}); + stubErrorResponse({ error: 'test error' }); return db.setRules(rules).should.eventually.be.rejectedWith( 'Error while accessing security rules: test error'); }); diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index ef63754993..dc89b6383c 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -25,19 +25,19 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from './utils'; import * as mocks from '../resources/mocks'; -import {GoogleOAuthAccessToken, ServiceAccountCredential} from '../../src/auth/credential'; -import {FirebaseServiceInterface} from '../../src/firebase-service'; -import {FirebaseApp, FirebaseAccessToken} from '../../src/firebase-app'; -import {FirebaseNamespace, FirebaseNamespaceInternals, FIREBASE_CONFIG_VAR} from '../../src/firebase-namespace'; - -import {Auth} from '../../src/auth/auth'; -import {Messaging} from '../../src/messaging/messaging'; -import {MachineLearning} from '../../src/machine-learning/machine-learning'; -import {Storage} from '../../src/storage/storage'; -import {Firestore} from '@google-cloud/firestore'; -import {Database} from '@firebase/database'; -import {InstanceId} from '../../src/instance-id/instance-id'; -import {ProjectManagement} from '../../src/project-management/project-management'; +import { GoogleOAuthAccessToken, ServiceAccountCredential } from '../../src/auth/credential'; +import { FirebaseServiceInterface } from '../../src/firebase-service'; +import { FirebaseApp, FirebaseAccessToken } from '../../src/firebase-app'; +import { FirebaseNamespace, FirebaseNamespaceInternals, FIREBASE_CONFIG_VAR } from '../../src/firebase-namespace'; + +import { Auth } from '../../src/auth/auth'; +import { Messaging } from '../../src/messaging/messaging'; +import { MachineLearning } from '../../src/machine-learning/machine-learning'; +import { Storage } from '../../src/storage/storage'; +import { Firestore } from '@google-cloud/firestore'; +import { Database } from '@firebase/database'; +import { InstanceId } from '../../src/instance-id/instance-id'; +import { ProjectManagement } from '../../src/project-management/project-management'; import { SecurityRules } from '../../src/security-rules/security-rules'; import { FirebaseAppError, AppErrorCodes } from '../../src/utils/error'; import { RemoteConfig } from '../../src/remote-config/remote-config'; @@ -728,7 +728,7 @@ describe('FirebaseApp', () => { getAccessToken: () => Promise.resolve(oracle), }; - const app = utils.createAppWithOptions({credential}); + const app = utils.createAppWithOptions({ credential }); return app.INTERNAL.getToken().then((token) => { expect(token.accessToken).to.equal(oracle.access_token); diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 97478a330c..2c3a7b2dc1 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -24,9 +24,9 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../resources/mocks'; -import {FirebaseNamespace} from '../../src/firebase-namespace'; -import {FirebaseApp} from '../../src/firebase-app'; -import {Auth} from '../../src/auth/auth'; +import { FirebaseNamespace } from '../../src/firebase-namespace'; +import { FirebaseApp } from '../../src/firebase-app'; +import { Auth } from '../../src/auth/auth'; import { enableLogging, Database, @@ -36,9 +36,9 @@ import { Reference, ServerValue, } from '@firebase/database'; -import {Messaging} from '../../src/messaging/messaging'; -import {MachineLearning} from '../../src/machine-learning/machine-learning'; -import {Storage} from '../../src/storage/storage'; +import { Messaging } from '../../src/messaging/messaging'; +import { MachineLearning } from '../../src/machine-learning/machine-learning'; +import { Storage } from '../../src/storage/storage'; import { Firestore, FieldPath, @@ -48,8 +48,8 @@ import { v1beta1, setLogFunction, } from '@google-cloud/firestore'; -import {InstanceId} from '../../src/instance-id/instance-id'; -import {ProjectManagement} from '../../src/project-management/project-management'; +import { InstanceId } from '../../src/instance-id/instance-id'; +import { ProjectManagement } from '../../src/project-management/project-management'; import { SecurityRules } from '../../src/security-rules/security-rules'; import { RemoteConfig } from '../../src/remote-config/remote-config'; diff --git a/test/unit/firebase.spec.ts b/test/unit/firebase.spec.ts index eca361b7e4..b697afa541 100644 --- a/test/unit/firebase.spec.ts +++ b/test/unit/firebase.spec.ts @@ -27,7 +27,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../resources/mocks'; import * as firebaseAdmin from '../../src/index'; -import {RefreshTokenCredential, ServiceAccountCredential, isApplicationDefault} from '../../src/auth/credential'; +import { RefreshTokenCredential, ServiceAccountCredential, isApplicationDefault } from '../../src/auth/credential'; chai.should(); chai.use(chaiAsPromised); diff --git a/test/unit/firestore/firestore.spec.ts b/test/unit/firestore/firestore.spec.ts index 482211ffa0..236050dfbe 100644 --- a/test/unit/firestore/firestore.spec.ts +++ b/test/unit/firestore/firestore.spec.ts @@ -17,12 +17,12 @@ 'use strict'; import * as _ from 'lodash'; -import {expect} from 'chai'; +import { expect } from 'chai'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; +import { FirebaseApp } from '../../../src/firebase-app'; import { ComputeEngineCredential, RefreshTokenCredential } from '../../../src/auth/credential'; -import {FirestoreService, getFirestoreOptions} from '../../../src/firestore/firestore'; +import { FirestoreService, getFirestoreOptions } from '../../../src/firestore/firestore'; describe('Firestore', () => { let mockApp: FirebaseApp; diff --git a/test/unit/instance-id/instance-id-request.spec.ts b/test/unit/instance-id/instance-id-request.spec.ts index 7515f9cd7e..ae8580d3c2 100644 --- a/test/unit/instance-id/instance-id-request.spec.ts +++ b/test/unit/instance-id/instance-id-request.spec.ts @@ -25,9 +25,9 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {HttpClient} from '../../../src/utils/api-request'; -import {FirebaseInstanceIdRequestHandler} from '../../../src/instance-id/instance-id-request'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { HttpClient } from '../../../src/utils/api-request'; +import { FirebaseInstanceIdRequestHandler } from '../../../src/instance-id/instance-id-request'; chai.should(); chai.use(sinonChai); @@ -129,7 +129,7 @@ describe('FirebaseInstanceIdRequestHandler', () => { }); it('should throw for unexpected HTTP errors', () => { - const expectedResult = {error: 'test error'}; + const expectedResult = { error: 'test error' }; const stub = sinon.stub(HttpClient.prototype, 'send') .rejects(utils.errorFrom(expectedResult, 511)); stubs.push(stub); diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index db4a9dcb10..9de49086b4 100644 --- a/test/unit/instance-id/instance-id.spec.ts +++ b/test/unit/instance-id/instance-id.spec.ts @@ -25,10 +25,10 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {InstanceId} from '../../../src/instance-id/instance-id'; -import {FirebaseInstanceIdRequestHandler} from '../../../src/instance-id/instance-id-request'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../../../src/utils/error'; +import { InstanceId } from '../../../src/instance-id/instance-id'; +import { FirebaseInstanceIdRequestHandler } from '../../../src/instance-id/instance-id-request'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../../../src/utils/error'; chai.should(); chai.use(sinonChai); diff --git a/test/unit/machine-learning/machine-learning-api-client.spec.ts b/test/unit/machine-learning/machine-learning-api-client.spec.ts index b308164a8b..3b98c2b21d 100644 --- a/test/unit/machine-learning/machine-learning-api-client.spec.ts +++ b/test/unit/machine-learning/machine-learning-api-client.spec.ts @@ -43,7 +43,7 @@ describe('MachineLearningApiClient', () => { modelHash: 'modelHash123', displayName: 'model_1', tags: ['tag_1', 'tag_2'], - state: {published: true}, + state: { published: true }, tfliteModel: { gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', sizeBytes: 16900988, @@ -57,7 +57,7 @@ describe('MachineLearningApiClient', () => { modelHash: 'modelHash234', displayName: 'model_2', tags: ['tag_2', 'tag_3'], - state: {published: true}, + state: { published: true }, tfliteModel: { gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model2.tflite', sizeBytes: 2220022, @@ -124,10 +124,10 @@ describe('MachineLearningApiClient', () => { }); describe('createModel', () => { - const NAME_ONLY_CONTENT: ModelContent = {displayName: 'name1'}; + const NAME_ONLY_CONTENT: ModelContent = { displayName: 'name1' }; - const invalidContent: any[] = [null, undefined, {}, { tags: []}]; + const invalidContent: any[] = [null, undefined, {}, { tags: [] }]; invalidContent.forEach((content) => { it(`should reject when called with: ${JSON.stringify(content)}`, () => { return apiClient.createModel(content) @@ -210,7 +210,7 @@ describe('MachineLearningApiClient', () => { }); describe('updateModel', () => { - const NAME_ONLY_CONTENT: ModelContent = {displayName: 'name1'}; + const NAME_ONLY_CONTENT: ModelContent = { displayName: 'name1' }; const NAME_ONLY_MASK = ['displayName']; const invalidContent: any[] = [null, undefined]; @@ -395,7 +395,7 @@ describe('MachineLearningApiClient', () => { const invalidListFilters: any[] = [null, 0, '', true, {}, []]; invalidListFilters.forEach((invalidFilter) => { it(`should reject when called with invalid pageToken: ${JSON.stringify(invalidFilter)}`, () => { - return apiClient.listModels({filter: invalidFilter}) + return apiClient.listModels({ filter: invalidFilter }) .should.eventually.be.rejected.and.have.property( 'message', 'Invalid list filter.'); }); @@ -404,7 +404,7 @@ describe('MachineLearningApiClient', () => { const invalidPageSizes: any[] = [null, '', '10', true, {}, []]; invalidPageSizes.forEach((invalidPageSize) => { it(`should reject when called with invalid page size: ${JSON.stringify(invalidPageSize)}`, () => { - return apiClient.listModels({pageSize: invalidPageSize}) + return apiClient.listModels({ pageSize: invalidPageSize }) .should.eventually.be.rejected.and.have.property( 'message', 'Invalid page size.'); }); @@ -413,7 +413,7 @@ describe('MachineLearningApiClient', () => { const outOfRangePageSizes: number[] = [-1, 0, 101]; outOfRangePageSizes.forEach((invalidPageSize) => { it(`should reject when called with invalid page size: ${invalidPageSize}`, () => { - return apiClient.listModels({pageSize: invalidPageSize}) + return apiClient.listModels({ pageSize: invalidPageSize }) .should.eventually.be.rejected.and.have.property( 'message', 'Page size must be between 1 and 100.'); }); @@ -422,7 +422,7 @@ describe('MachineLearningApiClient', () => { const invalidPageTokens: any[] = [null, 0, '', true, {}, []]; invalidPageTokens.forEach((invalidToken) => { it(`should reject when called with invalid pageToken: ${JSON.stringify(invalidToken)}`, () => { - return apiClient.listModels({pageToken: invalidToken}) + return apiClient.listModels({ pageToken: invalidToken }) .should.eventually.be.rejected.and.have.property( 'message', 'Next page token must be a non-empty string.'); }); @@ -446,9 +446,9 @@ describe('MachineLearningApiClient', () => { }); const validOptions: ListModelsOptions[] = [ - {pageSize: 5}, - {pageToken: 'next'}, - {filter: 'displayName=name1'}, + { pageSize: 5 }, + { pageToken: 'next' }, + { filter: 'displayName=name1' }, { filter: 'displayName=name1', pageSize: 5, diff --git a/test/unit/machine-learning/machine-learning.spec.ts b/test/unit/machine-learning/machine-learning.spec.ts index 1a49b2cab1..b5c824955d 100644 --- a/test/unit/machine-learning/machine-learning.spec.ts +++ b/test/unit/machine-learning/machine-learning.spec.ts @@ -62,7 +62,7 @@ describe('MachineLearning', () => { modelHash: 'modelHash123', displayName: 'model_1', tags: ['tag_1', 'tag_2'], - state: {published: true}, + state: { published: true }, tfliteModel: { gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', sizeBytes: 16900988, @@ -97,7 +97,7 @@ describe('MachineLearning', () => { modelHash: 'modelHash234', displayName: 'model_2', tags: ['tag_2', 'tag_3'], - state: {published: false}, + state: { published: false }, tfliteModel: { gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model2.tflite', sizeBytes: 22200222, @@ -209,7 +209,7 @@ describe('MachineLearning', () => { displayName: 'foo', tfliteModel: { gcsTfliteUri: 'gs://some-bucket/model.tflite', - }}); + } }); }).to.throw(expectedError); }); diff --git a/test/unit/messaging/batch-requests.spec.ts b/test/unit/messaging/batch-requests.spec.ts index bcfee5ef74..c7c96a75f1 100644 --- a/test/unit/messaging/batch-requests.spec.ts +++ b/test/unit/messaging/batch-requests.spec.ts @@ -65,7 +65,7 @@ function createMultipartResponse(success: object[], failures: object[] = []): Ht }); return { status: 200, - headers: {'Content-Type': 'multipart/mixed; boundary=boundary'}, + headers: { 'Content-Type': 'multipart/mixed; boundary=boundary' }, multipart, text: '', data: null, @@ -93,7 +93,7 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}}, + { url: 'https://example.com', body: { foo: 1 } }, ]; const batch = new BatchRequestClient(httpClient, batchUrl); @@ -110,9 +110,9 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject, responseObject, responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}}, - {url: 'https://example.com', body: {foo: 2}}, - {url: 'https://example.com', body: {foo: 3}}, + { url: 'https://example.com', body: { foo: 1 } }, + { url: 'https://example.com', body: { foo: 2 } }, + { url: 'https://example.com', body: { foo: 3 } }, ]; const batch = new BatchRequestClient(httpClient, batchUrl); @@ -131,9 +131,9 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject, responseObject], [responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}}, - {url: 'https://example.com', body: {foo: 2}}, - {url: 'https://example.com', body: {foo: 3}}, + { url: 'https://example.com', body: { foo: 1 } }, + { url: 'https://example.com', body: { foo: 2 } }, + { url: 'https://example.com', body: { foo: 3 } }, ]; const batch = new BatchRequestClient(httpClient, batchUrl); @@ -150,12 +150,12 @@ describe('BatchRequestClient', () => { it('should reject on top-level HTTP error responses', async () => { const stub = sinon.stub(httpClient, 'send').rejects( - utils.errorFrom({error: 'test'})); + utils.errorFrom({ error: 'test' })); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}}, - {url: 'https://example.com', body: {foo: 2}}, - {url: 'https://example.com', body: {foo: 3}}, + { url: 'https://example.com', body: { foo: 1 } }, + { url: 'https://example.com', body: { foo: 2 } }, + { url: 'https://example.com', body: { foo: 3 } }, ]; const batch = new BatchRequestClient(httpClient, batchUrl); @@ -174,10 +174,10 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}}, - {url: 'https://example.com', body: {foo: 2}}, + { url: 'https://example.com', body: { foo: 1 } }, + { url: 'https://example.com', body: { foo: 2 } }, ]; - const commonHeaders = {'X-Custom-Header': 'value'}; + const commonHeaders = { 'X-Custom-Header': 'value' }; const batch = new BatchRequestClient(httpClient, batchUrl, commonHeaders); const responses: HttpResponse[] = await batch.send(requests); @@ -200,8 +200,8 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}, headers: {'X-Custom-Header': 'value'}}, - {url: 'https://example.com', body: {foo: 1}, headers: {'X-Custom-Header': 'value'}}, + { url: 'https://example.com', body: { foo: 1 }, headers: { 'X-Custom-Header': 'value' } }, + { url: 'https://example.com', body: { foo: 1 }, headers: { 'X-Custom-Header': 'value' } }, ]; const batch = new BatchRequestClient(httpClient, batchUrl); @@ -223,10 +223,10 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}, headers: {'X-Custom-Header': 'overwrite'}}, - {url: 'https://example.com', body: {foo: 1}, headers: {'X-Custom-Header': 'overwrite'}}, + { url: 'https://example.com', body: { foo: 1 }, headers: { 'X-Custom-Header': 'overwrite' } }, + { url: 'https://example.com', body: { foo: 1 }, headers: { 'X-Custom-Header': 'overwrite' } }, ]; - const commonHeaders = {'X-Custom-Header': 'value'}; + const commonHeaders = { 'X-Custom-Header': 'value' }; const batch = new BatchRequestClient(httpClient, batchUrl, commonHeaders); const responses: HttpResponse[] = await batch.send(requests); diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index 9bd41b374e..69453a216e 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -26,7 +26,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; +import { FirebaseApp } from '../../../src/firebase-app'; import { Message, MessagingOptions, MessagingPayload, MessagingDevicesResponse, MessagingDeviceGroupResponse, MessagingTopicManagementResponse, BatchResponse, SendResponse, MulticastMessage, @@ -86,7 +86,7 @@ function mockBatchRequest(ids: string[]): nock.Scope { function mockBatchRequestWithErrors(ids: string[], errors: object[] = []): nock.Scope { const mockPayload = createMultipartPayloadWithErrors(ids.map((id) => { - return {name: id}; + return { name: id }; }), errors); return nock(`https://${FCM_SEND_HOST}:443`) .post('/batch') @@ -371,7 +371,7 @@ describe('Messaging', () => { it('should reject given app without project ID', () => { const appWithoutProjectId = mocks.mockCredentialApp(); const messagingWithoutProjectId = new Messaging(appWithoutProjectId); - messagingWithoutProjectId.send({topic: 'test'}) + messagingWithoutProjectId.send({ topic: 'test' }) .should.eventually.be.rejectedWith( 'Failed to determine project ID for Messaging. Initialize the SDK with service ' + 'account credentials or set project ID as an app option. Alternatively set the ' @@ -409,7 +409,7 @@ describe('Messaging', () => { }); const noTarget = [ - {}, {token: null}, {token: ''}, {topic: null}, {topic: ''}, {condition: null}, {condition: ''}, + {}, { token: null }, { token: '' }, { topic: null }, { topic: '' }, { condition: null }, { condition: '' }, ]; noTarget.forEach((message) => { it(`should throw given message without target: ${ JSON.stringify(message) }`, () => { @@ -420,10 +420,10 @@ describe('Messaging', () => { }); const multipleTargets = [ - {token: 'a', topic: 'b'}, - {token: 'a', condition: 'b'}, - {condition: 'a', topic: 'b'}, - {token: 'a', topic: 'b', condition: 'c'}, + { token: 'a', topic: 'b' }, + { token: 'a', condition: 'b' }, + { condition: 'a', topic: 'b' }, + { token: 'a', topic: 'b', condition: 'c' }, ]; multipleTargets.forEach((message) => { it(`should throw given message without target: ${ JSON.stringify(message)}`, () => { @@ -437,7 +437,7 @@ describe('Messaging', () => { invalidDryRun.forEach((dryRun) => { it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { expect(() => { - messaging.send({token: 'a'}, dryRun as any); + messaging.send({ token: 'a' }, dryRun as any); }).to.throw('dryRun must be a boolean'); }); }); @@ -446,14 +446,14 @@ describe('Messaging', () => { invalidTopics.forEach((topic) => { it(`should throw given invalid topic name: ${JSON.stringify(topic)}`, () => { expect(() => { - messaging.send({topic}); + messaging.send({ topic }); }).to.throw('Malformed topic name'); }); }); const targetMessages = [ - {token: 'mock-token'}, {topic: 'mock-topic'}, - {topic: '/topics/mock-topic'}, {condition: '"foo" in topics'}, + { token: 'mock-token' }, { topic: 'mock-topic' }, + { topic: '/topics/mock-topic' }, { condition: '"foo" in topics' }, ]; targetMessages.forEach((message) => { it(`should be fulfilled with a message ID given a valid message: ${JSON.stringify(message)}`, () => { @@ -482,7 +482,7 @@ describe('Messaging', () => { }; mockedRequests.push(mockSendError(400, 'json', resp)); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/invalid-argument'); }); @@ -502,7 +502,7 @@ describe('Messaging', () => { }; mockedRequests.push(mockSendError(404, 'json', resp)); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/registration-token-not-registered'); }); @@ -523,7 +523,7 @@ describe('Messaging', () => { }; mockedRequests.push(mockSendError(404, 'json', resp)); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/third-party-auth-error'); }); @@ -538,16 +538,16 @@ describe('Messaging', () => { }; mockedRequests.push(mockSendError(404, 'json', resp)); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/registration-token-not-registered'); }); it('should fail when the backend server returns an unknown error', () => { - const resp = {error: 'test error message'}; + const resp = { error: 'test error message' }; mockedRequests.push(mockSendError(400, 'json', resp)); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejected.and.have.property('code', 'messaging/unknown-error'); }); @@ -555,13 +555,13 @@ describe('Messaging', () => { // Error code will be determined based on the status code. mockedRequests.push(mockSendError(400, 'text', 'foo bar')); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); }); describe('sendAll()', () => { - const validMessage: Message = {token: 'a'}; + const validMessage: Message = { token: 'a' }; function checkSendResponseSuccess(response: SendResponse, messageId: string): void { expect(response.success).to.be.true; @@ -610,7 +610,7 @@ describe('Messaging', () => { invalidDryRun.forEach((dryRun) => { it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { expect(() => { - messaging.sendAll([{token: 'a'}], dryRun as any); + messaging.sendAll([{ token: 'a' }], dryRun as any); }).to.throw('dryRun must be a boolean'); }); }); @@ -795,7 +795,7 @@ describe('Messaging', () => { }); it('should fail when the backend server returns an unknown error', () => { - const resp = {error: 'test error message'}; + const resp = { error: 'test error message' }; mockedRequests.push(mockBatchError(400, 'json', resp)); return messaging.sendAll( [validMessage], @@ -822,9 +822,9 @@ describe('Messaging', () => { successCount: 3, failureCount: 0, responses: [ - {success: true, messageId: 'projects/projec_id/messages/1'}, - {success: true, messageId: 'projects/projec_id/messages/2'}, - {success: true, messageId: 'projects/projec_id/messages/3'}, + { success: true, messageId: 'projects/projec_id/messages/1' }, + { success: true, messageId: 'projects/projec_id/messages/2' }, + { success: true, messageId: 'projects/projec_id/messages/3' }, ], }; @@ -845,7 +845,7 @@ describe('Messaging', () => { messaging.sendMulticast({} as any); }).to.throw('tokens must be a non-empty array'); expect(() => { - messaging.sendMulticast({tokens: []}); + messaging.sendMulticast({ tokens: [] }); }).to.throw('tokens must be a non-empty array'); }); @@ -855,7 +855,7 @@ describe('Messaging', () => { tokens.push(`token${i}`); } expect(() => { - messaging.sendMulticast({tokens}); + messaging.sendMulticast({ tokens }); }).to.throw('tokens list must not contain more than 500 items'); }); @@ -863,7 +863,7 @@ describe('Messaging', () => { invalidDryRun.forEach((dryRun) => { it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { expect(() => { - messaging.sendMulticast({tokens: ['a']}, dryRun as any); + messaging.sendMulticast({ tokens: ['a'] }, dryRun as any); }).to.throw('dryRun must be a boolean'); }); }); @@ -871,7 +871,7 @@ describe('Messaging', () => { it('should create multiple messages using the empty multicast payload', () => { stub = sinon.stub(messaging, 'sendAll').resolves(mockResponse); const tokens = ['a', 'b', 'c']; - return messaging.sendMulticast({tokens}) + return messaging.sendMulticast({ tokens }) .then((response: BatchResponse) => { expect(response).to.deep.equal(mockResponse); expect(stub).to.have.been.calledOnce; @@ -894,12 +894,12 @@ describe('Messaging', () => { const tokens = ['a', 'b', 'c']; const multicast: MulticastMessage = { tokens, - android: {ttl: 100}, - apns: {payload: {aps: {badge: 42}}}, - data: {key: 'value'}, - notification: {title: 'test title'}, - webpush: {data: {webKey: 'webValue'}}, - fcmOptions: {analyticsLabel: 'label'}, + android: { ttl: 100 }, + apns: { payload: { aps: { badge: 42 } } }, + data: { key: 'value' }, + notification: { title: 'test title' }, + webpush: { data: { webKey: 'webValue' } }, + fcmOptions: { analyticsLabel: 'label' }, }; return messaging.sendMulticast(multicast) .then((response: BatchResponse) => { @@ -923,7 +923,7 @@ describe('Messaging', () => { it('should pass dryRun argument through', () => { stub = sinon.stub(messaging, 'sendAll').resolves(mockResponse); const tokens = ['a', 'b', 'c']; - return messaging.sendMulticast({tokens}, true) + return messaging.sendMulticast({ tokens }, true) .then((response: BatchResponse) => { expect(response).to.deep.equal(mockResponse); expect(stub).to.have.been.calledOnce; @@ -940,11 +940,11 @@ describe('Messaging', () => { mockedRequests.push(mockBatchRequest(messageIds)); return messaging.sendMulticast({ tokens: ['a', 'b', 'c'], - android: {ttl: 100}, - apns: {payload: {aps: {badge: 42}}}, - data: {key: 'value'}, - notification: {title: 'test title'}, - webpush: {data: {webKey: 'webValue'}}, + android: { ttl: 100 }, + apns: { payload: { aps: { badge: 42 } } }, + data: { key: 'value' }, + notification: { title: 'test title' }, + webpush: { data: { webKey: 'webValue' } }, }).then((response: BatchResponse) => { expect(response.successCount).to.equal(3); expect(response.failureCount).to.equal(0); @@ -965,11 +965,11 @@ describe('Messaging', () => { mockedRequests.push(mockBatchRequest(messageIds)); return messaging.sendMulticast({ tokens: ['a', 'b', 'c'], - android: {ttl: 100}, - apns: {payload: {aps: {badge: 42}}}, - data: {key: 'value'}, - notification: {title: 'test title'}, - webpush: {data: {webKey: 'webValue'}}, + android: { ttl: 100 }, + apns: { payload: { aps: { badge: 42 } } }, + data: { key: 'value' }, + notification: { title: 'test title' }, + webpush: { data: { webKey: 'webValue' } }, }, true).then((response: BatchResponse) => { expect(response.successCount).to.equal(3); expect(response.failureCount).to.equal(0); @@ -994,7 +994,7 @@ describe('Messaging', () => { }, ]; mockedRequests.push(mockBatchRequestWithErrors(messageIds, errors)); - return messaging.sendMulticast({tokens: ['a', 'b']}) + return messaging.sendMulticast({ tokens: ['a', 'b'] }) .then((response: BatchResponse) => { expect(response.successCount).to.equal(2); expect(response.failureCount).to.equal(1); @@ -1027,7 +1027,7 @@ describe('Messaging', () => { }, ]; mockedRequests.push(mockBatchRequestWithErrors(messageIds, errors)); - return messaging.sendMulticast({tokens: ['a', 'b']}) + return messaging.sendMulticast({ tokens: ['a', 'b'] }) .then((response: BatchResponse) => { expect(response.successCount).to.equal(1); expect(response.failureCount).to.equal(1); @@ -1049,7 +1049,7 @@ describe('Messaging', () => { }; mockedRequests.push(mockBatchError(400, 'json', resp)); return messaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/invalid-argument'); }); @@ -1069,7 +1069,7 @@ describe('Messaging', () => { }; mockedRequests.push(mockBatchError(404, 'json', resp)); return messaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/registration-token-not-registered'); }); @@ -1083,16 +1083,16 @@ describe('Messaging', () => { }; mockedRequests.push(mockBatchError(404, 'json', resp)); return messaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/registration-token-not-registered'); }); it('should fail when the backend server returns an unknown error', () => { - const resp = {error: 'test error message'}; + const resp = { error: 'test error message' }; mockedRequests.push(mockBatchError(400, 'json', resp)); return messaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejected.and.have.property('code', 'messaging/unknown-error'); }); @@ -1100,13 +1100,13 @@ describe('Messaging', () => { // Error code will be determined based on the status code. mockedRequests.push(mockBatchError(400, 'text', 'foo bar')); return messaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); it('should be rejected given an app which returns null access tokens', () => { return nullAccessTokenMessaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); @@ -2590,49 +2590,49 @@ describe('Messaging', () => { invalidObjects.forEach((arg) => { it(`should throw given invalid android config: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({android: arg, topic: 'test'}); + messaging.send({ android: arg, topic: 'test' }); }).to.throw('android must be a non-null object'); }); it(`should throw given invalid android notification: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({android: {notification: arg}, topic: 'test'}); + messaging.send({ android: { notification: arg }, topic: 'test' }); }).to.throw('android.notification must be a non-null object'); }); it(`should throw given invalid apns config: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({apns: arg, topic: 'test'}); + messaging.send({ apns: arg, topic: 'test' }); }).to.throw('apns must be a non-null object'); }); it(`should throw given invalid webpush config: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({webpush: arg, topic: 'test'}); + messaging.send({ webpush: arg, topic: 'test' }); }).to.throw('webpush must be a non-null object'); }); it(`should throw given invalid data: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({data: arg, topic: 'test'}); + messaging.send({ data: arg, topic: 'test' }); }).to.throw('data must be a non-null object'); }); it(`should throw given invalid fcmOptions: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({fcmOptions: arg, topic: 'test'}); + messaging.send({ fcmOptions: arg, topic: 'test' }); }).to.throw('fcmOptions must be a non-null object'); }); it(`should throw given invalid AndroidFcmOptions: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({android: {fcmOptions: arg}, topic: 'test'}); + messaging.send({ android: { fcmOptions: arg }, topic: 'test' }); }).to.throw('fcmOptions must be a non-null object'); }); it(`should throw given invalid ApnsFcmOptions: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({apns: {fcmOptions: arg}, topic: 'test'}); + messaging.send({ apns: { fcmOptions: arg }, topic: 'test' }); }).to.throw('fcmOptions must be a non-null object'); }); }); @@ -2640,17 +2640,17 @@ describe('Messaging', () => { invalidImages.forEach((imageUrl) => { it(`should throw given invalid URL string for imageUrl`, () => { expect(() => { - messaging.send({apns: {fcmOptions: {imageUrl}}, topic: 'test'}); + messaging.send({ apns: { fcmOptions: { imageUrl } }, topic: 'test' }); }).to.throw('imageUrl must be a valid URL string'); }); }); const invalidDataMessages: any[] = [ - {label: 'data', message: {data: {k1: true}}}, - {label: 'android.data', message: {android: {data: {k1: true}}}}, - {label: 'webpush.data', message: {webpush: {data: {k1: true}}}}, - {label: 'webpush.headers', message: {webpush: {headers: {k1: true}}}}, - {label: 'apns.headers', message: {apns: {headers: {k1: true}}}}, + { label: 'data', message: { data: { k1: true } } }, + { label: 'android.data', message: { android: { data: { k1: true } } } }, + { label: 'webpush.data', message: { webpush: { data: { k1: true } } } }, + { label: 'webpush.headers', message: { webpush: { headers: { k1: true } } } }, + { label: 'apns.headers', message: { apns: { headers: { k1: true } } } }, ]; invalidDataMessages.forEach((config) => { it(`should throw given data with non-string value: ${config.label}`, () => { @@ -2666,14 +2666,14 @@ describe('Messaging', () => { invalidApnsPayloads.forEach((payload) => { it(`should throw given APNS payload with invalid object: ${JSON.stringify(payload)}`, () => { expect(() => { - messaging.send({apns: {payload}, token: 'token'}); + messaging.send({ apns: { payload }, token: 'token' }); }).to.throw('apns.payload must be a non-null object'); }); }); invalidApnsPayloads.forEach((aps) => { it(`should throw given APNS payload with invalid aps object: ${JSON.stringify(aps)}`, () => { expect(() => { - messaging.send({apns: {payload: {aps}}, token: 'token'}); + messaging.send({ apns: { payload: { aps } }, token: 'token' }); }).to.throw('apns.payload.aps must be a non-null object'); }); }); @@ -2682,7 +2682,7 @@ describe('Messaging', () => { messaging.send({ apns: { payload: { - aps: {'mutableContent': true, 'mutable-content': 1}, + aps: { 'mutableContent': true, 'mutable-content': 1 }, }, }, token: 'token', @@ -2694,7 +2694,7 @@ describe('Messaging', () => { invalidApnsAlerts.forEach((alert) => { it(`should throw given APNS payload with invalid aps alert: ${JSON.stringify(alert)}`, () => { expect(() => { - messaging.send({apns: {payload: {aps: {alert}}}, token: 'token'}); + messaging.send({ apns: { payload: { aps: { alert } } }, token: 'token' }); }).to.throw('apns.payload.aps.alert must be a string or a non-null object'); }); }); @@ -2703,7 +2703,7 @@ describe('Messaging', () => { invalidApnsSounds.forEach((sound) => { it(`should throw given APNS payload with invalid aps sound: ${JSON.stringify(sound)}`, () => { expect(() => { - messaging.send({apns: {payload: {aps: {sound}}}, token: 'token'}); + messaging.send({ apns: { payload: { aps: { sound } } }, token: 'token' }); }).to.throw('apns.payload.aps.sound must be a non-empty string or a non-null object'); }); }); @@ -2714,7 +2714,7 @@ describe('Messaging', () => { apns: { payload: { aps: { - sound: {name}, + sound: { name }, }, }, }, @@ -3283,7 +3283,7 @@ describe('Messaging', () => { threadId: 'thread.id', }, customKey1: 'custom.value', - customKey2: {nested: 'value'}, + customKey2: { nested: 'value' }, }, fcmOptions: { analyticsLabel: 'test.analytics', @@ -3320,7 +3320,7 @@ describe('Messaging', () => { 'thread-id': 'thread.id', }, customKey1: 'custom.value', - customKey2: {nested: 'value'}, + customKey2: { nested: 'value' }, }, fcmOptions: { analyticsLabel: 'test.analytics', @@ -3444,7 +3444,7 @@ describe('Messaging', () => { expectedReq: { apns: { payload: { - aps: {'content-available': 1}, + aps: { 'content-available': 1 }, }, }, }, @@ -3498,7 +3498,7 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - const resp = utils.responseFrom({message: 'test'}); + const resp = utils.responseFrom({ message: 'test' }); httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(resp); const req = config.req; req.token = 'mock-token'; @@ -3509,7 +3509,7 @@ describe('Messaging', () => { expectedReq.token = 'mock-token'; expect(httpsRequestStub).to.have.been.calledOnce.and.calledWith({ method: 'POST', - data: {message: expectedReq}, + data: { message: expectedReq }, timeout: 10000, url: 'https://fcm.googleapis.com/v1/projects/project_id/messages:send', headers: expectedHeaders, @@ -3521,14 +3521,14 @@ describe('Messaging', () => { it('should not throw when the message is addressed to the prefixed topic name', () => { return mockApp.INTERNAL.getToken() .then(() => { - const resp = utils.responseFrom({message: 'test'}); + const resp = utils.responseFrom({ message: 'test' }); httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(resp); - return messaging.send({topic: '/topics/mock-topic'}); + return messaging.send({ topic: '/topics/mock-topic' }); }) .then(() => { expect(httpsRequestStub).to.have.been.calledOnce; const requestData = httpsRequestStub.args[0][0].data; - const expectedReq = {topic: 'mock-topic'}; + const expectedReq = { topic: 'mock-topic' }; expect(requestData.message).to.deep.equal(expectedReq); }); }); diff --git a/test/unit/security-rules/security-rules.spec.ts b/test/unit/security-rules/security-rules.spec.ts index 45e1f98a87..bc9ba713fa 100644 --- a/test/unit/security-rules/security-rules.spec.ts +++ b/test/unit/security-rules/security-rules.spec.ts @@ -57,7 +57,7 @@ describe('SecurityRules', () => { 'invalid-argument', 'ruleset must be a non-empty name or a RulesetMetadata object.', ); - const INVALID_RULESETS: any[] = [null, undefined, '', 1, true, {}, [], {name: ''}]; + const INVALID_RULESETS: any[] = [null, undefined, '', 1, true, {}, [], { name: '' }]; const INVALID_BUCKET_ERROR = new FirebaseSecurityRulesError( 'invalid-argument', @@ -397,7 +397,7 @@ describe('SecurityRules', () => { }); stubs.push(stub); - return securityRules.releaseFirestoreRuleset({name: 'foo', createTime: 'time'}) + return securityRules.releaseFirestoreRuleset({ name: 'foo', createTime: 'time' }) .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith('cloud.firestore', 'foo'); }); @@ -520,7 +520,7 @@ describe('SecurityRules', () => { }); stubs.push(stub); - return securityRules.releaseStorageRuleset({name: 'foo', createTime: 'time'}) + return securityRules.releaseStorageRuleset({ name: 'foo', createTime: 'time' }) .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith( 'firebase.storage/bucketName.appspot.com', 'foo'); diff --git a/test/unit/storage/storage.spec.ts b/test/unit/storage/storage.spec.ts index 55fc8ff6df..0dba8db5a0 100644 --- a/test/unit/storage/storage.spec.ts +++ b/test/unit/storage/storage.spec.ts @@ -17,11 +17,11 @@ 'use strict'; import * as _ from 'lodash'; -import {expect} from 'chai'; +import { expect } from 'chai'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {Storage} from '../../../src/storage/storage'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { Storage } from '../../../src/storage/storage'; describe('Storage', () => { let mockApp: FirebaseApp; @@ -90,7 +90,7 @@ describe('Storage', () => { const expectedError = 'Bucket name not specified or invalid. Specify a valid bucket name via ' + 'the storageBucket option when initializing the app, or specify the bucket name ' + 'explicitly when calling the getBucket() method.'; - const invalidNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1}, _.noop]; + const invalidNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidNames.forEach((invalidName) => { it(`should throw given invalid bucket name: ${ JSON.stringify(invalidName) }`, () => { expect(() => { diff --git a/test/unit/utils.ts b/test/unit/utils.ts index 12bc260b65..fa655dd4e8 100644 --- a/test/unit/utils.ts +++ b/test/unit/utils.ts @@ -19,8 +19,8 @@ import * as sinon from 'sinon'; import * as mocks from '../resources/mocks'; -import {FirebaseNamespace} from '../../src/firebase-namespace'; -import {FirebaseApp, FirebaseAppOptions, FirebaseAppInternals, FirebaseAccessToken} from '../../src/firebase-app'; +import { FirebaseNamespace } from '../../src/firebase-namespace'; +import { FirebaseApp, FirebaseAppOptions, FirebaseAppInternals, FirebaseAccessToken } from '../../src/firebase-app'; import { HttpError, HttpResponse } from '../../src/utils/api-request'; /** diff --git a/test/unit/utils/api-request.spec.ts b/test/unit/utils/api-request.spec.ts index c2798de455..cab4c4f9ac 100644 --- a/test/unit/utils/api-request.spec.ts +++ b/test/unit/utils/api-request.spec.ts @@ -25,13 +25,13 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; +import { FirebaseApp } from '../../../src/firebase-app'; import { ApiSettings, HttpClient, HttpError, AuthorizedHttpClient, ApiCallbackFunction, HttpRequestConfig, HttpResponse, parseHttpResponse, RetryConfig, defaultRetryConfig, } from '../../../src/utils/api-request'; import { deepCopy } from '../../../src/utils/deep-copy'; -import {Agent} from 'http'; +import { Agent } from 'http'; import * as zlib from 'zlib'; chai.should(); @@ -141,7 +141,7 @@ describe('HttpClient', () => { invalidNumbers.forEach((maxRetries: any) => { it(`should throw when maxRetries is: ${maxRetries}`, () => { expect(() => { - new HttpClient({maxRetries} as any); + new HttpClient({ maxRetries } as any); }).to.throw('maxRetries must be a non-negative integer'); }); }); @@ -150,7 +150,7 @@ describe('HttpClient', () => { if (typeof backOffFactor !== 'undefined') { it(`should throw when backOffFactor is: ${backOffFactor}`, () => { expect(() => { - new HttpClient({maxRetries: 1, backOffFactor} as any); + new HttpClient({ maxRetries: 1, backOffFactor } as any); }).to.throw('backOffFactor must be a non-negative number'); }); } @@ -159,7 +159,7 @@ describe('HttpClient', () => { invalidNumbers.forEach((maxDelayInMillis: any) => { it(`should throw when maxDelayInMillis is: ${maxDelayInMillis}`, () => { expect(() => { - new HttpClient({maxRetries: 1, maxDelayInMillis} as any); + new HttpClient({ maxRetries: 1, maxDelayInMillis } as any); }).to.throw('maxDelayInMillis must be a non-negative integer'); }); }); @@ -167,7 +167,7 @@ describe('HttpClient', () => { invalidArrays.forEach((ioErrorCodes: any) => { it(`should throw when ioErrorCodes is: ${ioErrorCodes}`, () => { expect(() => { - new HttpClient({maxRetries: 1, maxDelayInMillis: 10000, ioErrorCodes} as any); + new HttpClient({ maxRetries: 1, maxDelayInMillis: 10000, ioErrorCodes } as any); }).to.throw('ioErrorCodes must be an array'); }); }); @@ -175,13 +175,13 @@ describe('HttpClient', () => { invalidArrays.forEach((statusCodes: any) => { it(`should throw when statusCodes is: ${statusCodes}`, () => { expect(() => { - new HttpClient({maxRetries: 1, maxDelayInMillis: 10000, statusCodes} as any); + new HttpClient({ maxRetries: 1, maxDelayInMillis: 10000, statusCodes } as any); }).to.throw('statusCodes must be an array'); }); }); it('should be fulfilled for a 2xx response with a json payload', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -337,7 +337,7 @@ describe('HttpClient', () => { }); it('should use the specified HTTP agent', () => { - const respData = {success: true}; + const respData = { success: true }; const scope = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -369,8 +369,8 @@ describe('HttpClient', () => { }); it('should make a POST request with the provided headers and data', () => { - const reqData = {request: 'data'}; - const respData = {success: true}; + const reqData = { request: 'data' }; + const respData = { success: true }; const scope = nock('https://' + mockHost, { reqheaders: { 'Authorization': 'Bearer token', @@ -402,8 +402,8 @@ describe('HttpClient', () => { }); it('should use the specified content-type header for the body', () => { - const reqData = {request: 'data'}; - const respData = {success: true}; + const reqData = { request: 'data' }; + const respData = { success: true }; const scope = nock('https://' + mockHost, { reqheaders: { 'Content-Type': (header) => { @@ -432,7 +432,7 @@ describe('HttpClient', () => { }); it('should not mutate the arguments', () => { - const reqData = {request: 'data'}; + const reqData = { request: 'data' }; const scope = nock('https://' + mockHost, { reqheaders: { 'Authorization': 'Bearer token', @@ -442,7 +442,7 @@ describe('HttpClient', () => { 'My-Custom-Header': 'CustomValue', }, }).post(mockPath, reqData) - .reply(200, {success: true}, { + .reply(200, { success: true }, { 'content-type': 'application/json', }); mockedRequests.push(scope); @@ -464,8 +464,8 @@ describe('HttpClient', () => { }); it('should make a GET request with the provided headers and data', () => { - const reqData = {key1: 'value1', key2: 'value2'}; - const respData = {success: true}; + const reqData = { key1: 'value1', key2: 'value2' }; + const respData = { success: true }; const scope = nock('https://' + mockHost, { reqheaders: { 'Authorization': 'Bearer token', @@ -495,9 +495,9 @@ describe('HttpClient', () => { }); it('should merge query parameters in URL with data', () => { - const reqData = {key1: 'value1', key2: 'value2'}; - const mergedData = {...reqData, key3: 'value3'}; - const respData = {success: true}; + const reqData = { key1: 'value1', key2: 'value2' }; + const mergedData = { ...reqData, key3: 'value3' }; + const respData = { success: true }; const scope = nock('https://' + mockHost) .get(mockPath) .query(mergedData) @@ -519,9 +519,9 @@ describe('HttpClient', () => { }); it('should urlEncode query parameters in URL', () => { - const reqData = {key1: 'value 1!', key2: 'value 2!'}; - const mergedData = {...reqData, key3: 'value 3!'}; - const respData = {success: true}; + const reqData = { key1: 'value 1!', key2: 'value 2!' }; + const mergedData = { ...reqData, key3: 'value 3!' }; + const respData = { success: true }; const scope = nock('https://' + mockHost) .get(mockPath) .query(mergedData) @@ -543,7 +543,7 @@ describe('HttpClient', () => { }); it('should default to https when protocol not specified', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -576,8 +576,8 @@ describe('HttpClient', () => { }); it('should make a HEAD request with the provided headers and data', () => { - const reqData = {key1: 'value1', key2: 'value2'}; - const respData = {success: true}; + const reqData = { key1: 'value1', key2: 'value2' }; + const respData = { success: true }; const scope = nock('https://' + mockHost, { reqheaders: { 'Authorization': 'Bearer token', @@ -618,7 +618,7 @@ describe('HttpClient', () => { }); it('should fail with an HttpError for a 4xx response', () => { - const data = {error: 'data'}; + const data = { error: 'data' }; mockedRequests.push(mockRequestWithHttpError(400, 'application/json', data)); const client = new HttpClient(); return client.send({ @@ -635,7 +635,7 @@ describe('HttpClient', () => { }); it('should fail with an HttpError for a 5xx response', () => { - const data = {error: 'data'}; + const data = { error: 'data' }; mockedRequests.push(mockRequestWithHttpError(500, 'application/json', data)); const client = new HttpClient(); return client.send({ @@ -676,7 +676,7 @@ describe('HttpClient', () => { }); it('should fail with a FirebaseAppError for a network error', () => { - mockedRequests.push(mockRequestWithError({message: 'test error', code: 'AWFUL_ERROR'})); + mockedRequests.push(mockRequestWithError({ message: 'test error', code: 'AWFUL_ERROR' })); const client = new HttpClient(); const err = 'Error while making request: test error. Error code: AWFUL_ERROR'; return client.send({ @@ -686,7 +686,7 @@ describe('HttpClient', () => { }); it('should timeout when the response is repeatedly delayed', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost) .get(mockPath) .times(5) @@ -707,7 +707,7 @@ describe('HttpClient', () => { }); it('should timeout when multiple socket timeouts encountered', () => { - const respData = {foo: 'bar timeout'}; + const respData = { foo: 'bar timeout' }; const scope = nock('https://' + mockHost) .get(mockPath) .times(5) @@ -729,7 +729,7 @@ describe('HttpClient', () => { it('should be rejected, after 4 retries, on multiple network errors', () => { for (let i = 0; i < 5; i++) { - mockedRequests.push(mockRequestWithError({message: `connection reset ${i + 1}`, code: 'ECONNRESET'})); + mockedRequests.push(mockRequestWithError({ message: `connection reset ${i + 1}`, code: 'ECONNRESET' })); } const client = new HttpClient(testRetryConfig()); @@ -767,8 +767,8 @@ describe('HttpClient', () => { }); it('should succeed, after 1 retry, on a single network error', () => { - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ECONNRESET'})); - const respData = {foo: 'bar'}; + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -786,7 +786,7 @@ describe('HttpClient', () => { }); it('should not retry when RetryConfig is explicitly null', () => { - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ECONNRESET'})); + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); const client = new HttpClient(null); const err = 'Error while making request: connection reset 1'; return client.send({ @@ -796,7 +796,7 @@ describe('HttpClient', () => { }); it('should not retry when maxRetries is set to 0', () => { - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ECONNRESET'})); + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); const client = new HttpClient({ maxRetries: 0, ioErrorCodes: ['ECONNRESET'], @@ -810,7 +810,7 @@ describe('HttpClient', () => { }); it('should not retry when error codes are not configured', () => { - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ECONNRESET'})); + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); const client = new HttpClient({ maxRetries: 1, maxDelayInMillis: 10000, @@ -823,8 +823,8 @@ describe('HttpClient', () => { }); it('should succeed after a retry on a configured I/O error', () => { - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ETESTCODE'})); - const respData = {foo: 'bar'}; + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ETESTCODE' })); + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -852,7 +852,7 @@ describe('HttpClient', () => { 'content-type': 'application/json', }); mockedRequests.push(scope1); - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope2 = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -871,8 +871,8 @@ describe('HttpClient', () => { it('should not retry more than maxRetries', () => { // simulate 2 low-level errors - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ECONNRESET'})); - mockedRequests.push(mockRequestWithError({message: 'connection reset 2', code: 'ECONNRESET'})); + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); + mockedRequests.push(mockRequestWithError({ message: 'connection reset 2', code: 'ECONNRESET' })); // followed by 3 HTTP errors const scope = nock('https://' + mockHost) @@ -1020,7 +1020,7 @@ describe('HttpClient', () => { 'retry-after': '30', }); mockedRequests.push(scope1); - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope2 = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -1056,7 +1056,7 @@ describe('HttpClient', () => { 'retry-after': timestamp.toUTCString(), }); mockedRequests.push(scope1); - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope2 = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -1090,7 +1090,7 @@ describe('HttpClient', () => { 'retry-after': timestamp.toUTCString(), }); mockedRequests.push(scope1); - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope2 = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -1122,7 +1122,7 @@ describe('HttpClient', () => { 'retry-after': 'invalid', }); mockedRequests.push(scope1); - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope2 = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -1158,7 +1158,7 @@ describe('HttpClient', () => { }); it('should use the port 80 for http URLs', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('http://' + mockHost + ':80') .get('/') .reply(200, respData, { @@ -1175,7 +1175,7 @@ describe('HttpClient', () => { }); it('should use the port specified in the URL', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost + ':8080') .get('/') .reply(200, respData, { @@ -1223,7 +1223,7 @@ describe('AuthorizedHttpClient', () => { }); it('should be fulfilled for a 2xx response with a json payload', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost, requestHeaders) .get(mockPath) .reply(200, respData, { @@ -1265,7 +1265,7 @@ describe('AuthorizedHttpClient', () => { }); it('should use the HTTP agent set in request', () => { - const respData = {success: true}; + const respData = { success: true }; const scope = nock('https://' + mockHost, requestHeaders) .get(mockPath) .reply(200, respData, { @@ -1287,7 +1287,7 @@ describe('AuthorizedHttpClient', () => { }); it('should use the HTTP agent set in AppOptions', () => { - const respData = {success: true}; + const respData = { success: true }; const scope = nock('https://' + mockHost, requestHeaders) .get(mockPath) .reply(200, respData, { @@ -1308,8 +1308,8 @@ describe('AuthorizedHttpClient', () => { }); it('should make a POST request with the provided headers and data', () => { - const reqData = {request: 'data'}; - const respData = {success: true}; + const reqData = { request: 'data' }; + const respData = { success: true }; const options = { reqheaders: { 'Content-Type': (header: string) => { @@ -1341,7 +1341,7 @@ describe('AuthorizedHttpClient', () => { }); it('should not mutate the arguments', () => { - const reqData = {request: 'data'}; + const reqData = { request: 'data' }; const options = { reqheaders: { 'Content-Type': (header: string) => { @@ -1353,7 +1353,7 @@ describe('AuthorizedHttpClient', () => { Object.assign(options.reqheaders, requestHeaders.reqheaders); const scope = nock('https://' + mockHost, options) .post(mockPath, reqData) - .reply(200, {success: true}, { + .reply(200, { success: true }, { 'content-type': 'application/json', }); mockedRequests.push(scope); @@ -1464,7 +1464,7 @@ describe('parseHttpResponse()', () => { expect(response.headers).to.have.property('content-type', 'application/json'); expect(response.headers).to.have.property('date', 'Thu, 07 Feb 2019 19:20:34 GMT'); expect(response.isJson()).to.be.true; - expect(response.data).to.deep.equal({foo: 1}); + expect(response.data).to.deep.equal({ foo: 1 }); expect(response.text).to.equal('{"foo": 1}'); }); @@ -1482,7 +1482,7 @@ describe('parseHttpResponse()', () => { expect(response.headers).to.have.property('content-type', 'application/json'); expect(response.headers).to.have.property('date', 'Thu, 07 Feb 2019 19:20:34 GMT'); expect(response.isJson()).to.be.true; - expect(response.data).to.deep.equal({foo: 1}); + expect(response.data).to.deep.equal({ foo: 1 }); expect(response.text).to.equal('{"foo": 1}'); }); diff --git a/test/unit/utils/error.spec.ts b/test/unit/utils/error.spec.ts index 41e977f725..3b9e59269d 100755 --- a/test/unit/utils/error.spec.ts +++ b/test/unit/utils/error.spec.ts @@ -33,7 +33,7 @@ const expect = chai.expect; describe('FirebaseError', () => { const code = 'code'; const message = 'message'; - const errorInfo = {code, message}; + const errorInfo = { code, message }; it('should initialize successfully with error info specified', () => { const error = new FirebaseError(errorInfo); expect(error.code).to.be.equal(code); @@ -49,7 +49,7 @@ describe('FirebaseError', () => { it('toJSON() should resolve with the expected object', () => { const error = new FirebaseError(errorInfo); - expect(error.toJSON()).to.deep.equal({code, message}); + expect(error.toJSON()).to.deep.equal({ code, message }); }); }); diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 2065e2fb49..1cd683de48 100755 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -15,7 +15,7 @@ */ import * as _ from 'lodash'; -import {expect} from 'chai'; +import { expect } from 'chai'; import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; @@ -23,8 +23,8 @@ import { addReadonlyGetter, getExplicitProjectId, findProjectId, toWebSafeBase64, formatString, generateUpdateMask, } from '../../../src/utils/index'; -import {isNonEmptyString} from '../../../src/utils/validator'; -import {FirebaseApp, FirebaseAppOptions} from '../../../src/firebase-app'; +import { isNonEmptyString } from '../../../src/utils/validator'; +import { FirebaseApp, FirebaseAppOptions } from '../../../src/firebase-app'; import { ComputeEngineCredential } from '../../../src/auth/credential'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; diff --git a/test/unit/utils/validator.spec.ts b/test/unit/utils/validator.spec.ts index c8ab764f93..bcf2a67d3a 100755 --- a/test/unit/utils/validator.spec.ts +++ b/test/unit/utils/validator.spec.ts @@ -307,7 +307,7 @@ describe('isUid()', () => { }); it('should return false with an invalid type', () => { - expect(isUid({uid: createRandomString(1)})).to.be.false; + expect(isUid({ uid: createRandomString(1) })).to.be.false; }); it('should return false with an empty string', () => { @@ -355,7 +355,7 @@ describe('isEmail()', () => { }); it('should return false with a non string', () => { - expect(isEmail({email: 'user@example.com'})).to.be.false; + expect(isEmail({ email: 'user@example.com' })).to.be.false; }); it('show return true with a valid email string', () => { From 70c233ce614202ad59408d0a2ba72124bad9ecf4 Mon Sep 17 00:00:00 2001 From: Sebastian Kreft Date: Fri, 10 Jul 2020 13:03:16 -0400 Subject: [PATCH 005/160] chore: update node-forge to ^0.9.1 (#941) As commnented on https://github.com/firebase/firebase-admin-node/pull/934#discussion_r452226525 Given that node-forge is a 0.x version, npm considers it to be a breaking change, and hence not compatible with the current requirement. node-forge is also transitively being used by firestore (direct dependency is google-p12-pem), and requiring version ^0.9.0, which means two copies of the library are currently needed. Switching to just one dependency would save 1.7Mib. --- package-lock.json | 14 +++----------- package.json | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 05a01eafa1..6677e4e5d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3441,14 +3441,6 @@ "optional": true, "requires": { "node-forge": "^0.9.0" - }, - "dependencies": { - "node-forge": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", - "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==", - "optional": true - } } }, "graceful-fs": { @@ -5621,9 +5613,9 @@ "optional": true }, "node-forge": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", - "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", + "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" }, "node-pre-gyp": { "version": "0.14.0", diff --git a/package.json b/package.json index 4d6791b7a1..1c6f0953b2 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@types/node": "^10.10.0", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", - "node-forge": "^0.7.6" + "node-forge": "^0.9.1" }, "optionalDependencies": { "@google-cloud/firestore": "^4.0.0", From 6cce54fec0730158809ac86f3315941033677030 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 10 Jul 2020 12:06:22 -0700 Subject: [PATCH 006/160] fix: Upgraded Chai, Sinon and other test dependencies (#938) * change: Upgraded Firestore dependency to v4 * Upgrading Chai and associated dependencies * Upgraded Sinon; Fixed affected unit and integration tests * Fixed a series of TS compilation issues in tests * Extracted test helper function * fix: Updated some error checks to use deep.include --- package-lock.json | 271 +++++++----------- package.json | 14 +- test/integration/machine-learning.spec.ts | 24 +- test/unit/auth/auth-api-request.spec.ts | 181 ++++++------ test/unit/auth/auth.spec.ts | 14 +- test/unit/auth/user-import-builder.spec.ts | 34 ++- test/unit/instance-id/instance-id.spec.ts | 2 +- .../machine-learning-api-client.spec.ts | 48 ++-- .../machine-learning/machine-learning.spec.ts | 16 +- .../project-management/android-app.spec.ts | 6 +- test/unit/project-management/ios-app.spec.ts | 4 +- .../project-management.spec.ts | 10 +- .../remote-config-api-client.spec.ts | 14 +- test/unit/remote-config/remote-config.spec.ts | 2 +- .../security-rules-api-client.spec.ts | 50 ++-- .../security-rules/security-rules.spec.ts | 98 +++---- 16 files changed, 360 insertions(+), 428 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6677e4e5d0..faacfd5dbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -282,9 +282,9 @@ } }, "@google-cloud/firestore": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.0.0.tgz", - "integrity": "sha512-TU+MPOjvbouJM+xyQFJpSFYZoL1/nQhBR+9sqMiEwXkwa1CZKskSz0JspNE906cMBYgsyl6x1jH87QjGGwkV2w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.1.0.tgz", + "integrity": "sha512-odcAxfpgDUHK5air9bBtx6pewrgg0+LGHJ6Kkc7qJJMzjoyVyckQ3POiAxTZyKshr5+7gycIWKlieY0ewcb+pQ==", "optional": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -358,9 +358,9 @@ } }, "@grpc/grpc-js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.1.tgz", - "integrity": "sha512-mhZRszS0SKwnWPJaNyrECePZ9U7vaHFGqrzxQbWinWR3WznBIU+nmh2L5J3elF+lp5DEUIzARXkifbs6LQVAHA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.2.tgz", + "integrity": "sha512-k2u86Bkm/3xrjUaSWeIyzXScBt/cC8uE7BznR0cpueQi11R33W6qfJdMrkrsmSHirp5likR55JSXUrcWG6ybHA==", "optional": true, "requires": { "semver": "^6.2.0" @@ -447,34 +447,36 @@ "dev": true, "requires": { "type-detect": "4.0.8" - }, - "dependencies": { - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - } + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" } }, "@sinonjs/formatio": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", - "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", "dev": true, "requires": { - "samsam": "1.3.0" + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" } }, "@sinonjs/samsam": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", - "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.0.3.tgz", + "integrity": "sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==", "dev": true, "requires": { - "@sinonjs/commons": "^1.3.0", - "array-from": "^2.1.1", - "lodash": "^4.17.15" + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" } }, "@sinonjs/text-encoding": { @@ -514,13 +516,12 @@ "dev": true }, "@types/chai-as-promised": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-0.0.29.tgz", - "integrity": "sha1-Q9UokqqZjhhaPePiR37bhXO+HXc=", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.3.tgz", + "integrity": "sha512-FQnh1ohPXJELpKhzjuDkPLR2BZCAqed+a6xV4MI/T3XzHfd2FlarfUGUdZYgqYe8oxkYn0fchHEeHfHqdZ96sg==", "dev": true, "requires": { - "@types/chai": "*", - "@types/promises-a-plus": "*" + "@types/chai": "*" } }, "@types/color-name": { @@ -600,12 +601,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.26.tgz", "integrity": "sha512-myMwkO2Cr82kirHY8uknNRHEVtn0wV3DTQfkrjx17jmkstDRZ24gNUdl8AHXVyVclTYI/bNjgTPTAWvWLqXqkw==" }, - "@types/promises-a-plus": { - "version": "0.0.27", - "resolved": "https://registry.npmjs.org/@types/promises-a-plus/-/promises-a-plus-0.0.27.tgz", - "integrity": "sha1-xkZRE0YUyEuPXXEUzokB02pgl4A=", - "dev": true - }, "@types/request": { "version": "2.48.5", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz", @@ -629,21 +624,30 @@ } }, "@types/sinon": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz", - "integrity": "sha512-Tt7w/ylBS/OEAlSCwzB0Db1KbxnkycP/1UkQpbvKFYoUuRn4uYsC3xh5TRPrOjTy0i8TIkSz1JdNL4GPVdf3KQ==", - "dev": true + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.4.tgz", + "integrity": "sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } }, "@types/sinon-chai": { - "version": "2.7.37", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-2.7.37.tgz", - "integrity": "sha512-blMkVSMl8FrrodQfYzKAxs+T1ET0ylTbmYkyytMAJokYdbLw7jMgI8/YliM9y5z93yIB1ZYjwmXFBWJDZRM2RA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.4.tgz", + "integrity": "sha512-xq5KOWNg70PRC7dnR2VOxgYQ6paumW+4pTZP+6uTSdhpYsAUEeeT5bw6rRHHQrZ4KyR+M5ojOR+lje6TGSpUxA==", "dev": true, "requires": { "@types/chai": "*", "@types/sinon": "*" } }, + "@types/sinonjs__fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz", + "integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==", + "dev": true + }, "@types/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -959,12 +963,6 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", - "dev": true - }, "array-initial": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", @@ -1448,20 +1446,23 @@ "dev": true }, "chai": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", "dev": true, "requires": { - "assertion-error": "^1.0.1", - "deep-eql": "^0.1.3", - "type-detect": "^1.0.0" + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" } }, "chai-as-promised": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-6.0.0.tgz", - "integrity": "sha1-GgKkM6byTa+sY7nJb6FoTbGqjaY=", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", "dev": true, "requires": { "check-error": "^1.0.2" @@ -1983,20 +1984,12 @@ "dev": true }, "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "dev": true, "requires": { - "type-detect": "0.1.1" - }, - "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - } + "type-detect": "^4.0.0" } }, "deep-equal": { @@ -5082,12 +5075,6 @@ "lodash._reinterpolate": "^3.0.0" } }, - "lolex": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", - "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", - "dev": true - }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -5486,9 +5473,9 @@ } }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, "nested-error-stacks": { @@ -5510,37 +5497,16 @@ "dev": true }, "nise": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", - "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", + "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", "dev": true, "requires": { - "@sinonjs/formatio": "^3.2.1", + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", - "lolex": "^5.0.1", "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "@sinonjs/formatio": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", - "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - }, - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - } } }, "nock": { @@ -5560,20 +5526,6 @@ "semver": "^5.5.0" }, "dependencies": { - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -5583,26 +5535,11 @@ "ms": "^2.1.1" } }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true } } }, @@ -6984,12 +6921,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "samsam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", - "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", - "dev": true - }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -7086,41 +7017,47 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "sinon": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", - "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", - "dev": true, - "requires": { - "@sinonjs/formatio": "^2.0.0", - "diff": "^3.1.0", - "lodash.get": "^4.4.2", - "lolex": "^2.2.0", - "nise": "^1.2.0", - "supports-color": "^5.1.0", - "type-detect": "^4.0.5" + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.2.tgz", + "integrity": "sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.2", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.0.3", + "diff": "^4.0.2", + "nise": "^4.0.1", + "supports-color": "^7.1.0" }, "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true } } }, "sinon-chai": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.14.0.tgz", - "integrity": "sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.5.0.tgz", + "integrity": "sha512-IifbusYiQBpUxxFJkR3wTU68xzBN0+bxCScEaKMjBvAQERg6FnTTc1F17rseLb1tjmkJ23730AXpFI0c47FgAg==", "dev": true }, "slice-ansi": { @@ -8041,9 +7978,9 @@ } }, "type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, "type-fest": { diff --git a/package.json b/package.json index 1c6f0953b2..a068621221 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@firebase/auth-types": "^0.9.3", "@types/bcrypt": "^2.0.0", "@types/chai": "^4.0.0", - "@types/chai-as-promised": "0.0.29", + "@types/chai-as-promised": "^7.1.0", "@types/firebase-token-generator": "^2.0.28", "@types/jsonwebtoken": "^7.2.8", "@types/lodash": "^4.14.104", @@ -79,13 +79,13 @@ "@types/nock": "^9.1.0", "@types/request": "^2.47.0", "@types/request-promise": "^4.1.41", - "@types/sinon": "^4.1.3", - "@types/sinon-chai": "^2.7.31", + "@types/sinon": "^9.0.0", + "@types/sinon-chai": "^3.0.0", "@typescript-eslint/eslint-plugin": "^2.20.0", "@typescript-eslint/parser": "^2.20.0", "bcrypt": "^3.0.6", - "chai": "^3.5.0", - "chai-as-promised": "^6.0.0", + "chai": "^4.2.0", + "chai-as-promised": "^7.0.0", "chalk": "^1.1.3", "child-process-promise": "^2.2.1", "del": "^2.2.1", @@ -107,8 +107,8 @@ "request": "^2.75.0", "request-promise": "^4.1.1", "run-sequence": "^1.1.5", - "sinon": "^4.5.0", - "sinon-chai": "^2.14.0", + "sinon": "^9.0.0", + "sinon-chai": "^3.0.0", "ts-node": "^3.3.0", "typedoc": "^0.15.0", "typescript": "^3.7.3", diff --git a/test/integration/machine-learning.spec.ts b/test/integration/machine-learning.spec.ts index da2c0a60fd..a679c31111 100644 --- a/test/integration/machine-learning.spec.ts +++ b/test/integration/machine-learning.spec.ts @@ -246,7 +246,7 @@ describe('admin.machineLearning', () => { modelOptions.tfliteModel!.gcsTfliteUri = fileName; return createTemporaryModel(modelOptions) .then((createdModel) => { - expect(createdModel.validationError).to.be.empty; + expect(createdModel.validationError).to.be.undefined; expect(createdModel.published).to.be.false; return admin.machineLearning().publishModel(createdModel.modelId) .then((publishedModel) => { @@ -281,7 +281,7 @@ describe('admin.machineLearning', () => { modelOptions.tfliteModel!.gcsTfliteUri = fileName; return createTemporaryModel(modelOptions) .then((createdModel) => { - expect(createdModel.validationError).to.be.empty; + expect(createdModel.validationError).to.be.undefined; expect(createdModel.published).to.be.false; return admin.machineLearning().publishModel(createdModel.modelId) .then((publishedModel) => { @@ -362,7 +362,7 @@ describe('admin.machineLearning', () => { expect(modelList.models.length).to.be.at.least(2); expect(modelList.models).to.deep.include(model1); expect(modelList.models).to.deep.include(model2); - expect(modelList.pageToken).to.be.empty; + expect(modelList.pageToken).to.be.undefined; }); }); @@ -370,7 +370,7 @@ describe('admin.machineLearning', () => { return admin.machineLearning().listModels({ pageSize: 2 }) .then((modelList) => { expect(modelList.models.length).to.equal(2); - expect(modelList.pageToken).not.to.be.empty; + expect(modelList.pageToken).not.to.be.undefined; }); }); @@ -379,7 +379,7 @@ describe('admin.machineLearning', () => { .then((modelList) => { expect(modelList.models.length).to.equal(1); expect(modelList.models[0]).to.deep.equal(model1); - expect(modelList.pageToken).to.be.empty; + expect(modelList.pageToken).to.be.undefined; }); }); @@ -390,7 +390,7 @@ describe('admin.machineLearning', () => { expect(modelList.models).to.deep.include(model1); expect(modelList.models).to.deep.include(model2); expect(modelList.models).to.deep.include(model3); - expect(modelList.pageToken).to.be.empty; + expect(modelList.pageToken).to.be.undefined; }); }); @@ -401,7 +401,7 @@ describe('admin.machineLearning', () => { expect(modelList.models).to.deep.include(model1); expect(modelList.models).to.deep.include(model2); expect(modelList.models).to.deep.include(model3); - expect(modelList.pageToken).to.be.empty; + expect(modelList.pageToken).to.be.undefined; }); }); @@ -416,7 +416,7 @@ describe('admin.machineLearning', () => { pageToken: modelList.pageToken }) .then((modelList2) => { expect(modelList2.models.length).to.be.at.least(1); - expect(modelList2.pageToken).to.be.empty; + expect(modelList2.pageToken).to.be.undefined; }); }); }); @@ -425,7 +425,7 @@ describe('admin.machineLearning', () => { return admin.machineLearning().listModels({ filter: 'displayName=non-existing-model' }) .then((modelList) => { expect(modelList.models.length).to.equal(0); - expect(modelList.pageToken).to.be.empty; + expect(modelList.pageToken).to.be.undefined; }); }); @@ -490,10 +490,10 @@ describe('admin.machineLearning', () => { function verifyTfliteModel(model: admin.machineLearning.Model, expectedGcsTfliteUri: string): void { expect(model.tfliteModel!.gcsTfliteUri).to.equal(expectedGcsTfliteUri); if (expectedGcsTfliteUri.endsWith('invalid_model.tflite')) { - expect(model.modelHash).to.be.empty; + expect(model.modelHash).to.be.undefined; expect(model.validationError).to.equal('Invalid flatbuffer format'); } else { - expect(model.modelHash).to.not.be.empty; - expect(model.validationError).to.be.empty; + expect(model.modelHash).to.not.be.undefined; + expect(model.validationError).to.be.undefined; } } diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index f1173b5d0b..98e8781d7d 100755 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -46,6 +46,7 @@ import { import { UserIdentifier } from '../../../src/auth/identifier'; import { TenantOptions } from '../../../src/auth/tenant'; import { UpdateRequest, UpdateMultiFactorInfoRequest } from '../../../src/auth/user-record'; +import { expectUserImportResult } from './user-import-builder.spec'; chai.should(); chai.use(sinonChai); @@ -950,7 +951,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected given an invalid duration', () => { @@ -963,7 +964,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected given a duration less than minimum allowed', () => { @@ -977,7 +978,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected given a duration greater than maximum allowed', () => { @@ -991,7 +992,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected when the backend returns an error', () => { @@ -1010,7 +1011,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1056,7 +1057,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1096,7 +1097,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1117,7 +1118,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1167,7 +1168,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.not.been.called; }); @@ -1189,7 +1190,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1500,7 +1501,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const userImportBuilder = new UserImportBuilder(users, options); return requestHandler.uploadAccount(users, options) .then((result) => { - expect(result).to.deep.equal(userImportBuilder.buildResponse(expectedResult.data.error)); + expectUserImportResult( + result, userImportBuilder.buildResponse(expectedResult.data.error)); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, userImportBuilder.buildRequest())); }); @@ -1526,7 +1528,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.uploadAccount(testUsers) .then((result) => { - expect(result).to.deep.equal(expectedResult); + expectUserImportResult(result, expectedResult); expect(stub).to.have.not.been.called; }); }); @@ -1736,7 +1738,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.uploadAccount(testUsers, validOptions) .then((result) => { - expect(result).to.deep.equal(expectedResult); + expectUserImportResult(result, expectedResult); expect(stub).to.have.not.been.called; }); }); @@ -1761,12 +1763,11 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, userImportBuilder.buildRequest())); }); }); - }); describe('downloadAccount', () => { @@ -1839,7 +1840,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected given an invalid next page token', () => { @@ -1852,7 +1853,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected when the backend returns an error', () => { @@ -1874,7 +1875,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1914,7 +1915,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -2204,7 +2205,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid email error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2289,7 +2290,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Expected error should be thrown. - expect(error).to.deep.equal(invalidSecondFactorTest.error); + expect(error).to.deep.include(invalidSecondFactorTest.error); }); }); }); @@ -2309,7 +2310,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2323,7 +2324,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid phone number error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2344,7 +2345,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -2412,7 +2413,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid uid error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2429,7 +2430,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2447,7 +2448,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Forbidden claims error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2467,7 +2468,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -2521,7 +2522,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid uid error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2547,7 +2548,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); }); }); @@ -2662,7 +2663,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Expected invalid email error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2773,7 +2774,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Expected error should be thrown. - expect(error).to.deep.equal(invalidSecondFactorTest.error); + expect(error).to.deep.include(invalidSecondFactorTest.error); }); }); }); @@ -2793,7 +2794,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Expected invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2807,7 +2808,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Expected invalid phone number error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2829,7 +2830,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -2853,7 +2854,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -2876,7 +2877,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -2945,7 +2946,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2959,7 +2960,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2980,7 +2981,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -3090,7 +3091,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid email error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3107,7 +3108,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3124,7 +3125,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3147,7 +3148,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); }); }); @@ -3173,7 +3174,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); }); }); @@ -3211,7 +3212,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -3231,7 +3232,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, {})); }); @@ -3314,7 +3315,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3328,7 +3329,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3351,7 +3352,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, data)); }); @@ -3388,7 +3389,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -3408,7 +3409,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, {})); }); @@ -3462,7 +3463,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3479,7 +3480,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, expectedRequest)); }); @@ -3500,7 +3501,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, expectedRequest)); }); @@ -3607,7 +3608,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -3625,7 +3626,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3643,7 +3644,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, expectedHttpMethod, expectedRequest)); }); @@ -3665,7 +3666,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, expectedHttpMethod, expectedRequest)); }); @@ -3704,7 +3705,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -3724,7 +3725,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); }); }); @@ -3803,7 +3804,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3817,7 +3818,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3840,7 +3841,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); }); }); @@ -3875,7 +3876,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -3895,7 +3896,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); }); }); @@ -3963,7 +3964,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3980,7 +3981,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, expectedRequest)); }); @@ -4001,7 +4002,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, expectedRequest)); }); @@ -4151,7 +4152,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -4169,7 +4170,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -4187,7 +4188,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, expectedHttpMethod, expectedRequest)); }); @@ -4209,7 +4210,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, expectedHttpMethod, expectedRequest)); }); @@ -4232,7 +4233,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp) as AuthRequestHandler; return requestHandler.getTenant(tenantId) .then((result) => { - expect(result).to.deep.equal(expectedResult.data); + expect(result).to.deep.include(expectedResult.data); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, {})); }); }); @@ -4247,7 +4248,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -4267,7 +4268,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, {})); }); }); @@ -4346,7 +4347,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -4360,7 +4361,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -4383,7 +4384,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -4417,7 +4418,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -4437,7 +4438,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, {})); }); }); @@ -4487,7 +4488,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -4504,7 +4505,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, postMethod, expectedRequest)); }); }); @@ -4524,7 +4525,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, postMethod, expectedRequest)); }); }); @@ -4548,7 +4549,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, postMethod, expectedRequest)); }); }); @@ -4638,7 +4639,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -4656,7 +4657,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -4674,7 +4675,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, patchMethod, expectedRequest)); }); @@ -4696,7 +4697,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, patchMethod, expectedRequest)); }); @@ -4722,7 +4723,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, patchMethod, expectedRequest)); }); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 0d211a718d..e0168443d6 100755 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -378,14 +378,16 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be fulfilled given an app which returns null access tokens', () => { - getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').resolves(null); + getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken') + .resolves(null as any); // createCustomToken() does not rely on an access token and therefore works in this scenario. return auth.createCustomToken(mocks.uid, mocks.developerClaims) .should.eventually.be.fulfilled; }); it('should be fulfilled given an app which returns invalid access tokens', () => { - getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').resolves('malformed'); + getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken') + .resolves('malformed' as any); // createCustomToken() does not rely on an access token and therefore works in this scenario. return auth.createCustomToken(mocks.uid, mocks.developerClaims) .should.eventually.be.fulfilled; @@ -637,7 +639,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm expected error returned. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -654,7 +656,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm expected error returned. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); } @@ -881,7 +883,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm expected error returned. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -898,7 +900,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm expected error returned. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); } diff --git a/test/unit/auth/user-import-builder.spec.ts b/test/unit/auth/user-import-builder.spec.ts index 8923d42b8c..ab1eab477e 100755 --- a/test/unit/auth/user-import-builder.spec.ts +++ b/test/unit/auth/user-import-builder.spec.ts @@ -33,6 +33,17 @@ chai.use(chaiAsPromised); const expect = chai.expect; +export function expectUserImportResult(result: UserImportResult, expected: UserImportResult): void { + expect(result.successCount).to.equal(expected.successCount); + expect(result.failureCount).to.equal(expected.failureCount); + expect(result.errors.length).to.equal(expected.errors.length); + result.errors.forEach((err, idx) => { + const want = expected.errors[idx]; + expect(err.index).to.equal(want.index); + expect(err.error).to.deep.include(want.error); + }); +} + describe('UserImportBuilder', () => { const now = new Date('2019-10-25T04:30:52.000Z'); const nowString = now.toUTCString(); @@ -713,8 +724,9 @@ describe('UserImportBuilder', () => { }; const userImportBuilder = new UserImportBuilder(users, validOptions as any, userRequestValidator); - expect(userImportBuilder.buildResponse(successfulServerResponse)) - .to.deep.equal(successfulUserImportResponse); + expectUserImportResult( + userImportBuilder.buildResponse(successfulServerResponse), + successfulUserImportResponse); }); it('should return the expected response for import with server side errors', () => { @@ -737,13 +749,14 @@ describe('UserImportBuilder', () => { }; const userImportBuilder = new UserImportBuilder(users, validOptions as any, userRequestValidator); - expect(userImportBuilder.buildResponse(failingServerResponse)) - .to.deep.equal(serverErrorUserImportResponse); + expectUserImportResult( + userImportBuilder.buildResponse(failingServerResponse), + serverErrorUserImportResponse); }); it('should return the expected response for import with client side errors', () => { const successfulServerResponse: any = []; - const clientErrorUserImportResponse: UserImportResult = { + const clientErrorUserImportResponse = { successCount: 3, failureCount: 1, errors: [ @@ -753,8 +766,9 @@ describe('UserImportBuilder', () => { // userRequestValidatorWithError will throw on the 3rd user (index = 2). const userImportBuilder = new UserImportBuilder(users, validOptions as any, userRequestValidatorWithError); - expect(userImportBuilder.buildResponse(successfulServerResponse)) - .to.deep.equal(clientErrorUserImportResponse); + expectUserImportResult( + userImportBuilder.buildResponse(successfulServerResponse), + clientErrorUserImportResponse); }); it('should return the expected response for import with mixed client/server errors', () => { @@ -861,9 +875,9 @@ describe('UserImportBuilder', () => { }; const userImportBuilder = new UserImportBuilder( testUsers, validOptions as any, userRequestValidatorWithMultipleErrors); - expect(userImportBuilder.buildResponse(failingServerResponse)) - .to.deep.equal(mixedErrorUserImportResponse); + expectUserImportResult( + userImportBuilder.buildResponse(failingServerResponse), + mixedErrorUserImportResponse); }); }); - }); diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index 9de49086b4..f5d078f074 100644 --- a/test/unit/instance-id/instance-id.spec.ts +++ b/test/unit/instance-id/instance-id.spec.ts @@ -161,7 +161,7 @@ describe('InstanceId', () => { it('should resolve without errors on success', () => { const stub = sinon.stub(FirebaseInstanceIdRequestHandler.prototype, 'deleteInstanceId') - .returns(Promise.resolve(null)); + .resolves(); stubs.push(stub); return iid.deleteInstanceId(testInstanceId) .then(() => { diff --git a/test/unit/machine-learning/machine-learning-api-client.spec.ts b/test/unit/machine-learning/machine-learning-api-client.spec.ts index 3b98c2b21d..69f5f1d877 100644 --- a/test/unit/machine-learning/machine-learning-api-client.spec.ts +++ b/test/unit/machine-learning/machine-learning-api-client.spec.ts @@ -148,7 +148,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); return apiClient.createModel(NAME_ONLY_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should resolve with the created resource on success', () => { @@ -159,7 +159,7 @@ describe('MachineLearningApiClient', () => { return apiClient.createModel(NAME_ONLY_CONTENT) .then((resp) => { expect(resp.done).to.be.true; - expect(resp.name).to.be.empty; + expect(resp.name).to.be.undefined; expect(resp.response).to.deep.equal(MODEL_RESPONSE); }); }); @@ -172,7 +172,7 @@ describe('MachineLearningApiClient', () => { return apiClient.createModel(NAME_ONLY_CONTENT) .then((resp) => { expect(resp.done).to.be.true; - expect(resp.name).to.be.empty; + expect(resp.name).to.be.undefined; expect(resp.error).to.deep.equal(STATUS_ERROR_RESPONSE); }); }); @@ -184,7 +184,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); return apiClient.createModel(NAME_ONLY_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error for non-json response', () => { @@ -195,7 +195,7 @@ describe('MachineLearningApiClient', () => { const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.createModel(NAME_ONLY_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with when failed with a FirebaseAppError', () => { @@ -205,7 +205,7 @@ describe('MachineLearningApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.createModel(NAME_ONLY_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -240,7 +240,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should resolve with the updated resource on success', () => { @@ -251,7 +251,7 @@ describe('MachineLearningApiClient', () => { return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) .then((resp) => { expect(resp.done).to.be.true; - expect(resp.name).to.be.empty; + expect(resp.name).to.be.undefined; expect(resp.response).to.deep.equal(MODEL_RESPONSE); expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'PATCH', @@ -270,7 +270,7 @@ describe('MachineLearningApiClient', () => { return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) .then((resp) => { expect(resp.done).to.be.true; - expect(resp.name).to.be.empty; + expect(resp.name).to.be.undefined; expect(resp.error).to.deep.equal(STATUS_ERROR_RESPONSE); }); }); @@ -282,7 +282,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error for non-json response', () => { @@ -293,7 +293,7 @@ describe('MachineLearningApiClient', () => { const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with when failed with a FirebaseAppError', () => { @@ -303,7 +303,7 @@ describe('MachineLearningApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -351,7 +351,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject unknown-error when error code is not present', () => { @@ -361,7 +361,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject unknown-error for non-json response', () => { @@ -372,7 +372,7 @@ describe('MachineLearningApiClient', () => { const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject when failed with a FirebaseAppError', () => { @@ -382,7 +382,7 @@ describe('MachineLearningApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -484,7 +484,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -494,7 +494,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -505,7 +505,7 @@ describe('MachineLearningApiClient', () => { const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -515,7 +515,7 @@ describe('MachineLearningApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -562,7 +562,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error when error code is not present', () => { @@ -572,7 +572,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error for non-json response', () => { @@ -583,7 +583,7 @@ describe('MachineLearningApiClient', () => { const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject when failed with a FirebaseAppError', () => { @@ -593,7 +593,7 @@ describe('MachineLearningApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); }); diff --git a/test/unit/machine-learning/machine-learning.spec.ts b/test/unit/machine-learning/machine-learning.spec.ts index b5c824955d..ece3057ba6 100644 --- a/test/unit/machine-learning/machine-learning.spec.ts +++ b/test/unit/machine-learning/machine-learning.spec.ts @@ -235,7 +235,7 @@ describe('MachineLearning', () => { expect(model.tags).to.deep.equal(['tag_1', 'tag_2']); expect(model.createTime).to.equal(CREATE_TIME_UTC); expect(model.updateTime).to.equal(UPDATE_TIME_UTC); - expect(model.validationError).to.be.empty; + expect(model.validationError).to.be.undefined; expect(model.published).to.be.true; expect(model.etag).to.equal('etag123'); expect(model.modelHash).to.equal('modelHash123'); @@ -260,7 +260,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'getModel') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.getModel(MODEL_ID) .should.eventually.be.rejected.and.have.property( @@ -362,7 +362,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'listModels') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.listModels() .should.eventually.be.rejected.and.have.property( @@ -397,7 +397,7 @@ describe('MachineLearning', () => { it('should resolve on success', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'deleteModel') - .resolves({}); + .resolves(); stubs.push(stub); return machineLearning.deleteModel(MODEL_ID); @@ -430,7 +430,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'createModel') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.createModel(MODEL_OPTIONS_WITH_GCS) .should.eventually.be.rejected.and.have.property( @@ -547,7 +547,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'updateModel') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_WITH_GCS) .should.eventually.be.rejected.and.have.property( @@ -651,7 +651,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'updateModel') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.publishModel(MODEL_ID) .should.eventually.be.rejected.and.have.property( @@ -755,7 +755,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'updateModel') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.unpublishModel(MODEL_ID) .should.eventually.be.rejected.and.have.property( diff --git a/test/unit/project-management/android-app.spec.ts b/test/unit/project-management/android-app.spec.ts index d38c2600b7..dbb725a9df 100644 --- a/test/unit/project-management/android-app.spec.ts +++ b/test/unit/project-management/android-app.spec.ts @@ -109,7 +109,7 @@ describe('AndroidApp', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'getResource') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return androidApp.getMetadata() .should.eventually.be.rejected @@ -200,7 +200,7 @@ describe('AndroidApp', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'getAndroidShaCertificates') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return androidApp.getShaCertificates() .should.eventually.be.rejected @@ -336,7 +336,7 @@ describe('AndroidApp', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'getConfig') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return androidApp.getConfig() .should.eventually.be.rejected diff --git a/test/unit/project-management/ios-app.spec.ts b/test/unit/project-management/ios-app.spec.ts index ec300ea554..3b6748b4c1 100644 --- a/test/unit/project-management/ios-app.spec.ts +++ b/test/unit/project-management/ios-app.spec.ts @@ -108,7 +108,7 @@ describe('IosApp', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'getResource') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return iosApp.getMetadata() .should.eventually.be.rejected @@ -183,7 +183,7 @@ describe('IosApp', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'getConfig') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return iosApp.getConfig() .should.eventually.be.rejected diff --git a/test/unit/project-management/project-management.spec.ts b/test/unit/project-management/project-management.spec.ts index bcd02f6d71..1674be4645 100644 --- a/test/unit/project-management/project-management.spec.ts +++ b/test/unit/project-management/project-management.spec.ts @@ -126,7 +126,7 @@ describe('ProjectManagement', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'listAndroidApps') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return projectManagement.listAndroidApps() .should.eventually.be.rejected @@ -210,7 +210,7 @@ describe('ProjectManagement', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'listIosApps') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return projectManagement.listIosApps() .should.eventually.be.rejected @@ -307,7 +307,7 @@ describe('ProjectManagement', () => { it('should throw when initial API response is null', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'createAndroidApp') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return projectManagement.createAndroidApp(PACKAGE_NAME) .should.eventually.be.rejected @@ -355,7 +355,7 @@ describe('ProjectManagement', () => { it('should throw when initial API response is null', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'createIosApp') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return projectManagement.createIosApp(BUNDLE_ID) .should.eventually.be.rejected @@ -425,7 +425,7 @@ describe('ProjectManagement', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'listAppMetadata') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return projectManagement.listAppMetadata() .should.eventually.be.rejected diff --git a/test/unit/remote-config/remote-config-api-client.spec.ts b/test/unit/remote-config/remote-config-api-client.spec.ts index 9ff46c788b..9543b3b957 100644 --- a/test/unit/remote-config/remote-config-api-client.spec.ts +++ b/test/unit/remote-config/remote-config-api-client.spec.ts @@ -344,7 +344,7 @@ describe('RemoteConfigApiClient', () => { stubs.push(stub); const expected = new FirebaseRemoteConfigError('failed-precondition', message); return apiClient.validateTemplate(REMOTE_CONFIG_TEMPLATE) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); }); @@ -439,7 +439,7 @@ describe('RemoteConfigApiClient', () => { stubs.push(stub); const expected = new FirebaseRemoteConfigError('failed-precondition', message); return apiClient.publishTemplate(REMOTE_CONFIG_TEMPLATE) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); }); @@ -672,7 +672,7 @@ describe('RemoteConfigApiClient', () => { const expected = new FirebaseRemoteConfigError('invalid-argument', 'ETag header is not present in the server response.'); return rcOperation() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); } @@ -684,7 +684,7 @@ describe('RemoteConfigApiClient', () => { stubs.push(stub); const expected = new FirebaseRemoteConfigError('not-found', 'Requested entity not found'); return rcOperation() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error when error code is not present', () => { @@ -694,7 +694,7 @@ describe('RemoteConfigApiClient', () => { stubs.push(stub); const expected = new FirebaseRemoteConfigError('unknown-error', 'Unknown server error: {}'); return rcOperation() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error for non-json response', () => { @@ -705,7 +705,7 @@ describe('RemoteConfigApiClient', () => { const expected = new FirebaseRemoteConfigError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return rcOperation() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject when rejected with a FirebaseAppError', () => { @@ -715,7 +715,7 @@ describe('RemoteConfigApiClient', () => { .rejects(expected); stubs.push(stub); return rcOperation() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); } diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index 0d05656507..2490085bed 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -369,7 +369,7 @@ describe('RemoteConfig', () => { it('should resolve with an empty versions list if the no results are availble for requested list options', () => { const stub = sinon .stub(RemoteConfigApiClient.prototype, 'listVersions') - .resolves({}); + .resolves({} as any); stubs.push(stub); return remoteConfig.listVersions({ pageSize: 2, diff --git a/test/unit/security-rules/security-rules-api-client.spec.ts b/test/unit/security-rules/security-rules-api-client.spec.ts index ffa9230b7f..6ffa7d9c3d 100644 --- a/test/unit/security-rules/security-rules-api-client.spec.ts +++ b/test/unit/security-rules/security-rules-api-client.spec.ts @@ -123,7 +123,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -133,7 +133,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -144,7 +144,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -154,7 +154,7 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -237,7 +237,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when the rulesets limit reached', () => { @@ -254,7 +254,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('resource-exhausted', resourceExhaustedError.error.message); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -264,7 +264,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -275,7 +275,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -285,7 +285,7 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -390,7 +390,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -400,7 +400,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -411,7 +411,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -421,7 +421,7 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -454,7 +454,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -464,7 +464,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -475,7 +475,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -485,7 +485,7 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -524,7 +524,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -534,7 +534,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -545,7 +545,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -555,7 +555,7 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -602,7 +602,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -612,7 +612,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -623,7 +623,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -633,7 +633,7 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); }); diff --git a/test/unit/security-rules/security-rules.spec.ts b/test/unit/security-rules/security-rules.spec.ts index bc9ba713fa..8946d1d685 100644 --- a/test/unit/security-rules/security-rules.spec.ts +++ b/test/unit/security-rules/security-rules.spec.ts @@ -31,15 +31,7 @@ const expect = chai.expect; describe('SecurityRules', () => { const EXPECTED_ERROR = new FirebaseSecurityRulesError('internal-error', 'message'); - const FIRESTORE_RULESET_RESPONSE: { - // This type is effectively a RulesetResponse, but with non-readonly fields - // to allow easier use from within the tests. An improvement would be to - // alter this into a helper that creates customized RulesetResponses based - // on the needs of the test, as that would ensure type-safety. - name: string; - createTime: string; - source: object | null; - } = { + const FIRESTORE_RULESET_RESPONSE = { name: 'projects/test-project/rulesets/foo', createTime: '2019-03-08T23:45:23.288047Z', source: { @@ -51,6 +43,10 @@ describe('SecurityRules', () => { ], }, }; + const FIRESTORE_RULESET_RELEASE = { + name: 'projects/test-project/releases/firestore.release', + rulesetName: 'projects/test-project/rulesets/foo', + }; const CREATE_TIME_UTC = 'Fri, 08 Mar 2019 23:45:23 GMT'; const INVALID_RULESET_ERROR = new FirebaseSecurityRulesError( @@ -99,9 +95,7 @@ describe('SecurityRules', () => { .resolves(FIRESTORE_RULESET_RESPONSE); const updateRelease = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(createRuleset, updateRelease); return [createRuleset, updateRelease]; } @@ -161,13 +155,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.getRuleset('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should reject when API response is invalid', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'getRuleset') - .resolves(null); + .resolves(null as any); stubs.push(stub); return securityRules.getRuleset('foo') .should.eventually.be.rejected.and.have.property( @@ -200,7 +194,7 @@ describe('SecurityRules', () => { it('should reject when API response does not contain a source', () => { const response = deepCopy(FIRESTORE_RULESET_RESPONSE); - response.source = null; + response.source = null as any; const stub = sinon .stub(SecurityRulesApiClient.prototype, 'getRuleset') .resolves(response); @@ -236,13 +230,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.getFirestoreRuleset() - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should reject when getRelease response is invalid', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'getRelease') - .resolves({}); + .resolves({} as any); stubs.push(stub); return securityRules.getFirestoreRuleset() @@ -253,9 +247,7 @@ describe('SecurityRules', () => { it('should resolve with Ruleset on success', () => { const getRelease = sinon .stub(SecurityRulesApiClient.prototype, 'getRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); const getRuleset = sinon .stub(SecurityRulesApiClient.prototype, 'getRuleset') .resolves(FIRESTORE_RULESET_RESPONSE); @@ -281,7 +273,7 @@ describe('SecurityRules', () => { INVALID_BUCKET_NAMES.forEach((bucketName) => { it(`should reject when called with: ${JSON.stringify(bucketName)}`, () => { return securityRules.getStorageRuleset(bucketName) - .should.eventually.be.rejected.and.deep.equal(INVALID_BUCKET_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_BUCKET_ERROR); }); }); @@ -291,13 +283,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.getStorageRuleset() - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should reject when getRelease response is invalid', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'getRelease') - .resolves({}); + .resolves({} as any); stubs.push(stub); return securityRules.getStorageRuleset() @@ -308,9 +300,7 @@ describe('SecurityRules', () => { it('should resolve with Ruleset for the default bucket on success', () => { const getRelease = sinon .stub(SecurityRulesApiClient.prototype, 'getRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); const getRuleset = sinon .stub(SecurityRulesApiClient.prototype, 'getRuleset') .resolves(FIRESTORE_RULESET_RESPONSE); @@ -334,9 +324,7 @@ describe('SecurityRules', () => { it('should resolve with Ruleset for the specified bucket on success', () => { const getRelease = sinon .stub(SecurityRulesApiClient.prototype, 'getRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); const getRuleset = sinon .stub(SecurityRulesApiClient.prototype, 'getRuleset') .resolves(FIRESTORE_RULESET_RESPONSE); @@ -362,7 +350,7 @@ describe('SecurityRules', () => { INVALID_RULESETS.forEach((invalidRuleset) => { it(`should reject when called with: ${JSON.stringify(invalidRuleset)}`, () => { return securityRules.releaseFirestoreRuleset(invalidRuleset) - .should.eventually.be.rejected.and.deep.equal(INVALID_RULESET_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_RULESET_ERROR); }); }); @@ -372,15 +360,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.releaseFirestoreRuleset('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should resolve on success when the ruleset specified by name', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(stub); return securityRules.releaseFirestoreRuleset('foo') @@ -392,9 +378,7 @@ describe('SecurityRules', () => { it('should resolve on success when the ruleset specified as an object', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(stub); return securityRules.releaseFirestoreRuleset({ name: 'foo', createTime: 'time' }) @@ -413,7 +397,7 @@ describe('SecurityRules', () => { INVALID_SOURCES.forEach((invalidSource) => { it(`should reject when called with: ${JSON.stringify(invalidSource)}`, () => { return securityRules.releaseFirestoreRulesetFromSource(invalidSource) - .should.eventually.be.rejected.and.deep.equal(INVALID_SOURCE_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_SOURCE_ERROR); }); }); @@ -423,7 +407,7 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.releaseFirestoreRulesetFromSource('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); const sources: {[key: string]: string | Buffer} = { @@ -462,14 +446,14 @@ describe('SecurityRules', () => { INVALID_RULESETS.forEach((invalidRuleset) => { it(`should reject when called with: ${JSON.stringify(invalidRuleset)}`, () => { return securityRules.releaseStorageRuleset(invalidRuleset) - .should.eventually.be.rejected.and.deep.equal(INVALID_RULESET_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_RULESET_ERROR); }); }); INVALID_BUCKET_NAMES.forEach((bucketName) => { it(`should reject when called with: ${JSON.stringify(bucketName)}`, () => { return securityRules.releaseStorageRuleset('foo', bucketName) - .should.eventually.be.rejected.and.deep.equal(INVALID_BUCKET_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_BUCKET_ERROR); }); }); @@ -479,15 +463,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.releaseStorageRuleset('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should resolve on success when the ruleset specified by name', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(stub); return securityRules.releaseStorageRuleset('foo') @@ -500,9 +482,7 @@ describe('SecurityRules', () => { it('should resolve on success when a custom bucket name is specified', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(stub); return securityRules.releaseStorageRuleset('foo', 'other.appspot.com') @@ -515,9 +495,7 @@ describe('SecurityRules', () => { it('should resolve on success when the ruleset specified as an object', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(stub); return securityRules.releaseStorageRuleset({ name: 'foo', createTime: 'time' }) @@ -544,14 +522,14 @@ describe('SecurityRules', () => { INVALID_SOURCES.forEach((invalidSource) => { it(`should reject when called with source: ${JSON.stringify(invalidSource)}`, () => { return securityRules.releaseStorageRulesetFromSource(invalidSource) - .should.eventually.be.rejected.and.deep.equal(INVALID_SOURCE_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_SOURCE_ERROR); }); }); INVALID_BUCKET_NAMES.forEach((invalidBucket) => { it(`should reject when called with bucket: ${JSON.stringify(invalidBucket)}`, () => { return securityRules.releaseStorageRulesetFromSource(RULES_FILE.content, invalidBucket) - .should.eventually.be.rejected.and.deep.equal(INVALID_BUCKET_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_BUCKET_ERROR); }); }); @@ -561,7 +539,7 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.releaseStorageRulesetFromSource('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); const sources: {[key: string]: string | Buffer} = { @@ -654,13 +632,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.createRuleset(RULES_FILE) - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should reject when API response is invalid', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'createRuleset') - .resolves(null); + .resolves(null as any); stubs.push(stub); return securityRules.createRuleset(RULES_FILE) .should.eventually.be.rejected.and.have.property( @@ -726,13 +704,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.deleteRuleset('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should resolve on success', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'deleteRuleset') - .resolves({}); + .resolves(); stubs.push(stub); return securityRules.deleteRuleset('foo'); @@ -760,13 +738,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.listRulesetMetadata() - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should reject when API response is invalid', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'listRulesets') - .resolves(null); + .resolves(null as any); stubs.push(stub); return securityRules.listRulesetMetadata() .should.eventually.be.rejected.and.have.property( From 82ced1e59d8946cfbfab9565a64f7c6e6d180ecf Mon Sep 17 00:00:00 2001 From: Ryunosuke Sato Date: Tue, 14 Jul 2020 03:00:55 +0900 Subject: [PATCH 007/160] $ chmod -x test/**/*.ts src/**/*.ts (#942) Remove unnecessary executive permissions. --- src/auth/auth-api-request.ts | 0 src/auth/auth-config.ts | 0 src/auth/auth.ts | 0 src/auth/tenant.ts | 0 src/auth/user-import-builder.ts | 0 src/index.d.ts | 0 src/utils/error.ts | 0 src/utils/index.ts | 0 src/utils/validator.ts | 0 test/integration/auth.spec.ts | 0 test/unit/auth/auth-api-request.spec.ts | 0 test/unit/auth/auth-config.spec.ts | 0 test/unit/auth/auth.spec.ts | 0 test/unit/auth/tenant.spec.ts | 0 test/unit/auth/user-import-builder.spec.ts | 0 test/unit/index.spec.ts | 0 test/unit/utils/error.spec.ts | 0 test/unit/utils/index.spec.ts | 0 test/unit/utils/validator.spec.ts | 0 19 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 src/auth/auth-api-request.ts mode change 100755 => 100644 src/auth/auth-config.ts mode change 100755 => 100644 src/auth/auth.ts mode change 100755 => 100644 src/auth/tenant.ts mode change 100755 => 100644 src/auth/user-import-builder.ts mode change 100755 => 100644 src/index.d.ts mode change 100755 => 100644 src/utils/error.ts mode change 100755 => 100644 src/utils/index.ts mode change 100755 => 100644 src/utils/validator.ts mode change 100755 => 100644 test/integration/auth.spec.ts mode change 100755 => 100644 test/unit/auth/auth-api-request.spec.ts mode change 100755 => 100644 test/unit/auth/auth-config.spec.ts mode change 100755 => 100644 test/unit/auth/auth.spec.ts mode change 100755 => 100644 test/unit/auth/tenant.spec.ts mode change 100755 => 100644 test/unit/auth/user-import-builder.spec.ts mode change 100755 => 100644 test/unit/index.spec.ts mode change 100755 => 100644 test/unit/utils/error.spec.ts mode change 100755 => 100644 test/unit/utils/index.spec.ts mode change 100755 => 100644 test/unit/utils/validator.spec.ts diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts old mode 100755 new mode 100644 diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts old mode 100755 new mode 100644 diff --git a/src/auth/auth.ts b/src/auth/auth.ts old mode 100755 new mode 100644 diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts old mode 100755 new mode 100644 diff --git a/src/auth/user-import-builder.ts b/src/auth/user-import-builder.ts old mode 100755 new mode 100644 diff --git a/src/index.d.ts b/src/index.d.ts old mode 100755 new mode 100644 diff --git a/src/utils/error.ts b/src/utils/error.ts old mode 100755 new mode 100644 diff --git a/src/utils/index.ts b/src/utils/index.ts old mode 100755 new mode 100644 diff --git a/src/utils/validator.ts b/src/utils/validator.ts old mode 100755 new mode 100644 diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts old mode 100755 new mode 100644 diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts old mode 100755 new mode 100644 diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts old mode 100755 new mode 100644 diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts old mode 100755 new mode 100644 diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts old mode 100755 new mode 100644 diff --git a/test/unit/auth/user-import-builder.spec.ts b/test/unit/auth/user-import-builder.spec.ts old mode 100755 new mode 100644 diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts old mode 100755 new mode 100644 diff --git a/test/unit/utils/error.spec.ts b/test/unit/utils/error.spec.ts old mode 100755 new mode 100644 diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts old mode 100755 new mode 100644 diff --git a/test/unit/utils/validator.spec.ts b/test/unit/utils/validator.spec.ts old mode 100755 new mode 100644 From b01822fb6aa1ea447c37148cd7d1012ad2b0fdd9 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 14 Jul 2020 11:57:44 -0700 Subject: [PATCH 008/160] [chore] Release 9.0.0 (#946) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a068621221..0cda64390a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "8.13.0", + "version": "9.0.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From eca00eebfdc1692a9977cf744eaaaa0181d6b642 Mon Sep 17 00:00:00 2001 From: matamatanot <39780486+matamatanot@users.noreply.github.com> Date: Fri, 17 Jul 2020 07:54:04 +0900 Subject: [PATCH 009/160] fix fragment (#944) --- src/auth.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth.d.ts b/src/auth.d.ts index f17a84b675..f0aa19df6e 100644 --- a/src/auth.d.ts +++ b/src/auth.d.ts @@ -397,7 +397,7 @@ export namespace admin.auth { /** * Interface representing a decoded Firebase ID token, returned from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#verifyIdToken `verifyIdToken()`} method. + * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#verifyidtoken `verifyIdToken()`} method. * * Firebase ID tokens are OpenID Connect spec-compliant JSON Web Tokens (JWTs). * See the From 92b0d37a6de9f19f6a625dbc4bc1a055b162622a Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Fri, 17 Jul 2020 13:43:56 -0400 Subject: [PATCH 010/160] chore: Remove instances of XXX_SDK_VERSION_XXX by reading version from package.json (#952) * Remove instances of XXX_SDK_VERSION_XXX by reading version from package.json * fix: Lowercase module * fix: Use function to lazily get SDK version, simplify unit test --- gulpfile.js | 3 --- src/auth/auth-api-request.ts | 2 +- src/database/database.ts | 4 ++-- src/firebase-namespace.ts | 3 ++- src/machine-learning/machine-learning-api-client.ts | 2 +- src/messaging/messaging-api-request.ts | 5 +++-- .../project-management-api-request.ts | 3 ++- src/remote-config/remote-config-api-client.ts | 2 +- src/security-rules/security-rules-api-client.ts | 2 +- src/utils/index.ts | 10 ++++++++++ test/unit/auth/auth-api-request.spec.ts | 3 ++- test/unit/firebase-namespace.spec.ts | 3 ++- .../machine-learning-api-client.spec.ts | 3 ++- test/unit/messaging/messaging.spec.ts | 3 ++- .../project-management-api-request.spec.ts | 3 ++- .../remote-config/remote-config-api-client.spec.ts | 3 ++- .../security-rules/security-rules-api-client.spec.ts | 3 ++- test/unit/utils/index.spec.ts | 8 ++++++++ 18 files changed, 45 insertions(+), 20 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 959cf46c05..d058227482 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -76,9 +76,6 @@ gulp.task('compile', function() { // Compile Typescript into .js and .d.ts files .pipe(buildProject()) - // Replace SDK version - .pipe(replace(/\/g, pkg.version)) - // Add header .pipe(header(banner)) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 359bc90c4d..cf0df17b92 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -44,7 +44,7 @@ import { Tenant, TenantOptions, TenantServerResponse } from './tenant'; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { - 'X-Client-Version': 'Node/Admin/', + 'X-Client-Version': `Node/Admin/${utils.getSdkVersion()}`, }; /** Firebase Auth request timeout duration in milliseconds. */ const FIREBASE_AUTH_TIMEOUT = 25000; diff --git a/src/database/database.ts b/src/database/database.ts index cb013e6469..686120909d 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -8,6 +8,7 @@ import { Database } from '@firebase/database'; import * as validator from '../utils/validator'; import { AuthorizedHttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; +import { getSdkVersion } from '../utils/index'; /** @@ -78,8 +79,7 @@ export class DatabaseService implements FirebaseServiceInterface { let db: Database = this.INTERNAL.databases[dbUrl]; if (typeof db === 'undefined') { const rtdb = require('@firebase/database'); // eslint-disable-line @typescript-eslint/no-var-requires - const { version } = require('../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires - db = rtdb.initStandalone(this.appInternal, dbUrl, version).instance; + db = rtdb.initStandalone(this.appInternal, dbUrl, getSdkVersion()).instance; const rulesClient = new DatabaseRulesClient(this.app, dbUrl); db.getRules = () => { diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index a9caf80802..916132ebbf 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -39,6 +39,7 @@ import { SecurityRules } from './security-rules/security-rules'; import { RemoteConfig } from './remote-config/remote-config'; import * as validator from './utils/validator'; +import { getSdkVersion } from './utils/index'; const DEFAULT_APP_NAME = '[DEFAULT]'; @@ -309,7 +310,7 @@ export class FirebaseNamespace { /* tslint:enable:variable-name */ public credential = firebaseCredential; - public SDK_VERSION = ''; + public SDK_VERSION = getSdkVersion(); public INTERNAL: FirebaseNamespaceInternals; /* tslint:disable */ diff --git a/src/machine-learning/machine-learning-api-client.ts b/src/machine-learning/machine-learning-api-client.ts index 56b47118c7..4e7a1a7383 100644 --- a/src/machine-learning/machine-learning-api-client.ts +++ b/src/machine-learning/machine-learning-api-client.ts @@ -23,7 +23,7 @@ import { FirebaseApp } from '../firebase-app'; const ML_V1BETA2_API = 'https://firebaseml.googleapis.com/v1beta2'; const FIREBASE_VERSION_HEADER = { - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, }; export interface StatusErrorResponse { diff --git a/src/messaging/messaging-api-request.ts b/src/messaging/messaging-api-request.ts index 6a44ac67f6..65b9944a04 100644 --- a/src/messaging/messaging-api-request.ts +++ b/src/messaging/messaging-api-request.ts @@ -21,16 +21,17 @@ import { import { createFirebaseError, getErrorCode } from './messaging-errors'; import { SubRequest, BatchRequestClient } from './batch-request'; import { SendResponse, BatchResponse } from './messaging-types'; +import { getSdkVersion } from '../utils/index'; // FCM backend constants const FIREBASE_MESSAGING_TIMEOUT = 10000; const FIREBASE_MESSAGING_BATCH_URL = 'https://fcm.googleapis.com/batch'; const FIREBASE_MESSAGING_HTTP_METHOD: HttpMethod = 'POST'; const FIREBASE_MESSAGING_HEADERS = { - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, }; const LEGACY_FIREBASE_MESSAGING_HEADERS = { - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, 'access_token_auth': 'true', }; diff --git a/src/project-management/project-management-api-request.ts b/src/project-management/project-management-api-request.ts index c7528f9157..95dcb76a56 100644 --- a/src/project-management/project-management-api-request.ts +++ b/src/project-management/project-management-api-request.ts @@ -21,6 +21,7 @@ import { import { FirebaseProjectManagementError, ProjectManagementErrorCode } from '../utils/error'; import * as validator from '../utils/validator'; import { ShaCertificate } from './android-app'; +import { getSdkVersion } from '../utils/index'; /** Project management backend host and port. */ const PROJECT_MANAGEMENT_HOST_AND_PORT = 'firebase.googleapis.com:443'; @@ -30,7 +31,7 @@ const PROJECT_MANAGEMENT_PATH = '/v1/'; const PROJECT_MANAGEMENT_BETA_PATH = '/v1beta1/'; /** Project management request header. */ const PROJECT_MANAGEMENT_HEADERS = { - 'X-Client-Version': 'Node/Admin/', + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, }; /** Project management request timeout duration in milliseconds. */ const PROJECT_MANAGEMENT_TIMEOUT_MILLIS = 10000; diff --git a/src/remote-config/remote-config-api-client.ts b/src/remote-config/remote-config-api-client.ts index ed1d4d759f..26bd1b8931 100644 --- a/src/remote-config/remote-config-api-client.ts +++ b/src/remote-config/remote-config-api-client.ts @@ -25,7 +25,7 @@ import { deepCopy } from '../utils/deep-copy'; // Remote Config backend constants const FIREBASE_REMOTE_CONFIG_V1_API = 'https://firebaseremoteconfig.googleapis.com/v1'; const FIREBASE_REMOTE_CONFIG_HEADERS = { - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, // There is a known issue in which the ETag is not properly returned in cases where the request // does not specify a compression type. Currently, it is required to include the header // `Accept-Encoding: gzip` or equivalent in all requests. diff --git a/src/security-rules/security-rules-api-client.ts b/src/security-rules/security-rules-api-client.ts index 87e379513f..5048113825 100644 --- a/src/security-rules/security-rules-api-client.ts +++ b/src/security-rules/security-rules-api-client.ts @@ -23,7 +23,7 @@ import { FirebaseApp } from '../firebase-app'; const RULES_V1_API = 'https://firebaserules.googleapis.com/v1'; const FIREBASE_VERSION_HEADER = { - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, }; export interface Release { diff --git a/src/utils/index.ts b/src/utils/index.ts index f3f75d6d80..8c3a540dc0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -19,6 +19,16 @@ import { ServiceAccountCredential, ComputeEngineCredential } from '../auth/crede import * as validator from './validator'; +let sdkVersion: string; + +export function getSdkVersion(): string { + if (!sdkVersion) { + const { version } = require('../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires + sdkVersion = version; + } + return sdkVersion; +} + /** * Renames properties on an object given a mapping from old to new property names. * diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 98e8781d7d..3599aac026 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -47,6 +47,7 @@ import { UserIdentifier } from '../../../src/auth/identifier'; import { TenantOptions } from '../../../src/auth/tenant'; import { UpdateRequest, UpdateMultiFactorInfoRequest } from '../../../src/auth/user-record'; import { expectUserImportResult } from './user-import-builder.spec'; +import { getSdkVersion } from '../../../src/utils/index'; chai.should(); chai.use(sinonChai); @@ -849,7 +850,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { let getTokenStub: sinon.SinonStub; const mockAccessToken: string = utils.generateRandomAccessToken(); const expectedHeaders: {[key: string]: string} = { - 'X-Client-Version': 'Node/Admin/', + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, 'Authorization': 'Bearer ' + mockAccessToken, }; const callParams = (path: string, method: any, data: any): HttpRequestConfig => { diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 2c3a7b2dc1..60b26ff91d 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -52,6 +52,7 @@ import { InstanceId } from '../../src/instance-id/instance-id'; import { ProjectManagement } from '../../src/project-management/project-management'; import { SecurityRules } from '../../src/security-rules/security-rules'; import { RemoteConfig } from '../../src/remote-config/remote-config'; +import { getSdkVersion } from '../../src/utils/index'; chai.should(); chai.use(sinonChai); @@ -73,7 +74,7 @@ describe('FirebaseNamespace', () => { describe('#SDK_VERSION', () => { it('should return the SDK version', () => { - expect(firebaseNamespace.SDK_VERSION).to.equal(''); + expect(firebaseNamespace.SDK_VERSION).to.equal(getSdkVersion()); }); }); diff --git a/test/unit/machine-learning/machine-learning-api-client.spec.ts b/test/unit/machine-learning/machine-learning-api-client.spec.ts index 69f5f1d877..aa2dc536d6 100644 --- a/test/unit/machine-learning/machine-learning-api-client.spec.ts +++ b/test/unit/machine-learning/machine-learning-api-client.spec.ts @@ -27,6 +27,7 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { FirebaseAppError } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; +import { getSdkVersion } from '../../../src/utils/index'; const expect = chai.expect; @@ -86,7 +87,7 @@ describe('MachineLearningApiClient', () => { }; const EXPECTED_HEADERS = { 'Authorization': 'Bearer mock-token', - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, }; 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 ' diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index 69453a216e..b99542278d 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -35,6 +35,7 @@ import { Messaging, BLACKLISTED_OPTIONS_KEYS, BLACKLISTED_DATA_PAYLOAD_KEYS, } from '../../../src/messaging/messaging'; import { HttpClient } from '../../../src/utils/api-request'; +import { getSdkVersion } from '../../../src/utils/index'; chai.should(); chai.use(sinonChai); @@ -321,7 +322,7 @@ describe('Messaging', () => { const mockAccessToken: string = utils.generateRandomAccessToken(); const expectedHeaders = { 'Authorization': 'Bearer ' + mockAccessToken, - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, 'access_token_auth': 'true', }; const emptyResponse = utils.responseFrom({}); diff --git a/test/unit/project-management/project-management-api-request.spec.ts b/test/unit/project-management/project-management-api-request.spec.ts index f2978b4a34..dcffb29b76 100644 --- a/test/unit/project-management/project-management-api-request.spec.ts +++ b/test/unit/project-management/project-management-api-request.spec.ts @@ -26,6 +26,7 @@ import { ProjectManagementRequestHandler } from '../../../src/project-management import { HttpClient } from '../../../src/utils/api-request'; import * as mocks from '../../resources/mocks'; import * as utils from '../utils'; +import { getSdkVersion } from '../../../src/utils/index'; import { ShaCertificate } from '../../../src/project-management/android-app'; import { AppPlatform } from '../../../src/project-management/app-metadata'; @@ -72,7 +73,7 @@ describe('ProjectManagementRequestHandler', () => { beforeEach(() => { mockApp = mocks.app(); expectedHeaders = { - 'X-Client-Version': 'Node/Admin/', + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, 'Authorization': 'Bearer ' + mockAccessToken, }; requestHandler = new ProjectManagementRequestHandler(mockApp); diff --git a/test/unit/remote-config/remote-config-api-client.spec.ts b/test/unit/remote-config/remote-config-api-client.spec.ts index 9543b3b957..0561d0d40b 100644 --- a/test/unit/remote-config/remote-config-api-client.spec.ts +++ b/test/unit/remote-config/remote-config-api-client.spec.ts @@ -33,6 +33,7 @@ import * as mocks from '../../resources/mocks'; import { FirebaseAppError } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; import { deepCopy } from '../../../src/utils/deep-copy'; +import { getSdkVersion } from '../../../src/utils/index'; const expect = chai.expect; @@ -53,7 +54,7 @@ describe('RemoteConfigApiClient', () => { const EXPECTED_HEADERS = { 'Authorization': 'Bearer mock-token', - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, 'Accept-Encoding': 'gzip', }; diff --git a/test/unit/security-rules/security-rules-api-client.spec.ts b/test/unit/security-rules/security-rules-api-client.spec.ts index 6ffa7d9c3d..81b4472dc2 100644 --- a/test/unit/security-rules/security-rules-api-client.spec.ts +++ b/test/unit/security-rules/security-rules-api-client.spec.ts @@ -26,6 +26,7 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { FirebaseAppError } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; +import { getSdkVersion } from '../../../src/utils/index'; const expect = chai.expect; @@ -42,7 +43,7 @@ describe('SecurityRulesApiClient', () => { }; const EXPECTED_HEADERS = { 'Authorization': 'Bearer mock-token', - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, }; 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 ' diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 1cd683de48..acb76f074a 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -29,11 +29,19 @@ import { ComputeEngineCredential } from '../../../src/auth/credential'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; import { FirebaseAppError } from '../../../src/utils/error'; +import { getSdkVersion } from '../../../src/utils/index'; interface Obj { [key: string]: any; } +describe('SDK_VERSION', () => { + it('utils index should retrieve the SDK_VERSION from package.json', () => { + const { version } = require('../../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires + expect(getSdkVersion()).to.equal(version); + }); +}); + describe('addReadonlyGetter()', () => { it('should add a new property to the provided object', () => { const obj: Obj = {}; From c70144fd5a75d96a3d6d26fbfede4c87dbfb3b8e Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 20 Jul 2020 14:27:19 -0700 Subject: [PATCH 011/160] chore: Enable keyword-spacing ESLint rule (#957) --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index 353fcd665a..1fc00bbd78 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,6 +36,7 @@ module.exports = { // Required checks 'indent': ['error', 2], + 'keyword-spacing': ['error'], "object-curly-spacing": [2, "always"], '@typescript-eslint/explicit-function-return-type': [ 'error', From 54a58348ab7d8ee5e1ef4fd90cb68110a3e41391 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Mon, 20 Jul 2020 18:07:11 -0400 Subject: [PATCH 012/160] Add null to customUserClaims (#958) --- src/auth/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 88e6f881a8..20949ed297 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -424,7 +424,7 @@ export class BaseAuth { * @return {Promise} A promise that resolves when the operation completes * successfully. */ - public setCustomUserClaims(uid: string, customUserClaims: object): Promise { + public setCustomUserClaims(uid: string, customUserClaims: object | null): Promise { return this.authRequestHandler.setCustomUserClaims(uid, customUserClaims) .then(() => { // Return nothing on success. From fdd968fa9c4c4bea46072c2e55c6eaf089a554d0 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Mon, 27 Jul 2020 13:30:42 -0400 Subject: [PATCH 013/160] fix: Use object instead of Object in typings (#961) * fix: Use object instead of Object in typings (#961) --- src/auth.d.ts | 4 ++-- src/database.d.ts | 18 +++++++++--------- src/index.d.ts | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/auth.d.ts b/src/auth.d.ts index f0aa19df6e..a5dbad182e 100644 --- a/src/auth.d.ts +++ b/src/auth.d.ts @@ -1324,7 +1324,7 @@ export namespace admin.auth { * @return A promise fulfilled with a custom token for the * provided `uid` and payload. */ - createCustomToken(uid: string, developerClaims?: Object): Promise; + createCustomToken(uid: string, developerClaims?: object): Promise; /** * Creates a new user. @@ -1513,7 +1513,7 @@ export namespace admin.auth { * @return A promise that resolves when the operation completes * successfully. */ - setCustomUserClaims(uid: string, customUserClaims: Object | null): Promise; + setCustomUserClaims(uid: string, customUserClaims: object | null): Promise; /** * Revokes all refresh tokens for an existing user. diff --git a/src/database.d.ts b/src/database.d.ts index 1bdf425ea1..3046e25f21 100644 --- a/src/database.d.ts +++ b/src/database.d.ts @@ -604,7 +604,7 @@ export namespace admin.database { * }); * ``` * - * @param values Object containing multiple values. + * @param values object containing multiple values. * @param onComplete An optional callback function that will * be called when synchronization to the server has completed. The * callback will be passed a single parameter: null for success, or an Error @@ -612,7 +612,7 @@ export namespace admin.database { * @return Resolves when synchronization to the * Database is complete. */ - update(values: Object, onComplete?: (a: Error | null) => any): Promise; + update(values: object, onComplete?: (a: Error | null) => any): Promise; } type EventType = 'value' | 'child_added' | 'child_changed' | 'child_moved' | 'child_removed'; @@ -865,7 +865,7 @@ export namespace admin.database { off( eventType?: admin.database.EventType, callback?: (a: admin.database.DataSnapshot, b?: string | null) => any, - context?: Object | null + context?: object | null ): void; /** @@ -987,8 +987,8 @@ export namespace admin.database { on( eventType: admin.database.EventType, callback: (a: admin.database.DataSnapshot, b?: string | null) => any, - cancelCallbackOrContext?: ((a: Error) => any) | Object | null, - context?: Object | null + cancelCallbackOrContext?: ((a: Error) => any) | object | null, + context?: object | null ): (a: admin.database.DataSnapshot | null, b?: string) => any; /** @@ -1025,8 +1025,8 @@ export namespace admin.database { once( eventType: admin.database.EventType, successCallback?: (a: admin.database.DataSnapshot, b?: string | null ) => any, - failureCallbackOrContext?: ((a: Error) => void) | Object | null, - context?: Object | null + failureCallbackOrContext?: ((a: Error) => void) | object | null, + context?: object | null ): Promise; /** @@ -1618,12 +1618,12 @@ export namespace admin.database { * adaNameRef.update({ first: 'Ada', last: 'Lovelace' }); * ``` * - * @param values Object containing multiple values. + * @param values object containing multiple values. * @param onComplete Callback called when write to server is * complete. * @return Resolves when update on server is complete. */ - update(values: Object, onComplete?: (a: Error | null) => any): Promise; + update(values: object, onComplete?: (a: Error | null) => any): Promise; } /** diff --git a/src/index.d.ts b/src/index.d.ts index 3df285bed9..e39fc8086a 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -149,7 +149,7 @@ declare namespace admin { * [Authenticate with limited privileges](/docs/database/admin/start#authenticate-with-limited-privileges) * for detailed documentation and code samples. */ - databaseAuthVariableOverride?: Object | null; + databaseAuthVariableOverride?: object | null; /** * The URL of the Realtime Database from which to read and write data. @@ -737,7 +737,7 @@ declare namespace admin.credential { * @return A credential authenticated via the * provided service account that can be used to initialize an app. */ - function refreshToken(refreshTokenPathOrObject: string | Object, httpAgent?: Agent): admin.credential.Credential; + function refreshToken(refreshTokenPathOrObject: string | object, httpAgent?: Agent): admin.credential.Credential; } declare namespace admin.database { From 5e6ebfa57649c425961351fbf772df4fead4f2f8 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Tue, 28 Jul 2020 16:10:40 -0400 Subject: [PATCH 014/160] fix(auth): Fix several typing inconsistencies (#966) * Perform several typing fixes --- src/auth/auth-config.ts | 2 +- src/auth/auth.ts | 1 + src/auth/user-record.ts | 6 +++--- test/unit/auth/auth.spec.ts | 2 ++ test/unit/auth/user-record.spec.ts | 6 +++--- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 97fddbdaca..ed0c563e94 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -166,7 +166,7 @@ export interface EmailSignInConfigServerRequest { * to a format that is understood by the Auth server. */ export class EmailSignInConfig implements EmailSignInProviderConfig { - public readonly enabled?: boolean; + public readonly enabled: boolean; public readonly passwordRequired?: boolean; /** diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 20949ed297..d9d4df207d 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -107,6 +107,7 @@ export interface DecodedIdToken { picture?: string; sub: string; tenant?: string; + uid: string; [key: string]: any; } diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 156f82b610..257a21b220 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -148,7 +148,7 @@ export enum MultiFactorId { */ export abstract class MultiFactorInfo { public readonly uid: string; - public readonly displayName: string | null; + public readonly displayName: string; public readonly factorId: MultiFactorId; public readonly enrollmentTime: string; @@ -213,7 +213,7 @@ export abstract class MultiFactorInfo { } utils.addReadonlyGetter(this, 'uid', response.mfaEnrollmentId); utils.addReadonlyGetter(this, 'factorId', factorId); - utils.addReadonlyGetter(this, 'displayName', response.displayName || null); + utils.addReadonlyGetter(this, 'displayName', response.displayName); // Encoded using [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. // For example, "2017-01-15T01:30:15.01Z". // This can be parsed directly via Date constructor. @@ -265,7 +265,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { /** Class representing multi-factor related properties of a user. */ export class MultiFactor { - public readonly enrolledFactors: ReadonlyArray; + public enrolledFactors: MultiFactorInfo[]; /** * Initializes the MultiFactor object using the server side or JWT format response. diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index e0168443d6..c5ae2ddbbb 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -161,6 +161,7 @@ function getDecodedIdToken(uid: string, authTime: Date, tenantId?: string): Deco sign_in_provider: 'custom', // eslint-disable-line @typescript-eslint/camelcase tenant: tenantId, }, + uid, }; } @@ -186,6 +187,7 @@ function getDecodedSessionCookie(uid: string, authTime: Date, tenantId?: string) sign_in_provider: 'custom', // eslint-disable-line @typescript-eslint/camelcase tenant: tenantId, }, + uid, }; } diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index 33a641bc4a..7fe6e058af 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -173,7 +173,7 @@ function getUserJSON(tenantId?: string): object { }, { uid: 'enrollmentId2', - displayName: null, + displayName: undefined, enrollmentTime: now.toUTCString(), phoneNumber: '+16505556789', factorId: 'phone', @@ -294,7 +294,7 @@ describe('PhoneMultiFactorInfo', () => { describe('getters', () => { it('should set missing optional fields to null', () => { expect(phoneMultiFactorInfoMissingFields.uid).to.equal(serverResponse.mfaEnrollmentId); - expect(phoneMultiFactorInfoMissingFields.displayName).to.be.null; + expect(phoneMultiFactorInfoMissingFields.displayName).to.be.undefined; expect(phoneMultiFactorInfoMissingFields.phoneNumber).to.equal(serverResponse.phoneInfo); expect(phoneMultiFactorInfoMissingFields.enrollmentTime).to.be.null; expect(phoneMultiFactorInfoMissingFields.factorId).to.equal('phone'); @@ -365,7 +365,7 @@ describe('PhoneMultiFactorInfo', () => { it('should return expected JSON object with missing fields set to null', () => { expect(phoneMultiFactorInfoMissingFields.toJSON()).to.deep.equal({ uid: 'enrollmentId1', - displayName: null, + displayName: undefined, enrollmentTime: null, phoneNumber: '+16505551234', factorId: 'phone', From 02f82df05f8f6e85adb5e2067593128f68221639 Mon Sep 17 00:00:00 2001 From: Andika Tanuwijaya Date: Wed, 29 Jul 2020 12:14:58 +0700 Subject: [PATCH 015/160] Fix updateUser's typing jsdoc (delete => update) (#964) * fix wording of updateUser's jsdoc (delete => update) * add missing fullstop --- src/auth.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth.d.ts b/src/auth.d.ts index a5dbad182e..421388c435 100644 --- a/src/auth.d.ts +++ b/src/auth.d.ts @@ -1459,7 +1459,7 @@ export namespace admin.auth { * See [Update a user](/docs/auth/admin/manage-users#update_a_user) for code * samples and detailed documentation. * - * @param uid The `uid` corresponding to the user to delete. + * @param uid The `uid` corresponding to the user to update. * @param properties The properties to update on * the provided user. * From 300f7abe0a70f483ff0e15e6708e26c0dfd03a02 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Wed, 29 Jul 2020 16:03:49 -0400 Subject: [PATCH 016/160] Modify gulpfile to allow for autogenerated types per-service (#967) * Add auto-generate mode for Gulpfile --- gulpfile.js | 76 +++++++++++++++++++++++++++++++-------- package-lock.json | 90 +++++++++++++++++++++++++++++++++-------------- package.json | 1 + 3 files changed, 127 insertions(+), 40 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index d058227482..9e1bb329d6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -30,6 +30,7 @@ var ts = require('gulp-typescript'); var del = require('del'); var header = require('gulp-header'); var replace = require('gulp-replace'); +var filter = require('gulp-filter'); /****************/ @@ -50,12 +51,35 @@ var paths = { ], build: 'lib/', + + curatedTypings: ['src/*.d.ts'], }; -// Create a separate project for buildProject that overrides the rootDir +const TEMPORARY_TYPING_EXCLUDES = [ + '!lib/default-namespace.d.ts', + '!lib/firebase-namespace.d.ts', + '!lib/firebase-app.d.ts', + '!lib/firebase-service.d.ts', + '!lib/auth/*.d.ts', + '!lib/database/*.d.ts', + '!lib/firestore/*.d.ts', + '!lib/instance-id/*.d.ts', + '!lib/machine-learning/*.d.ts', + '!lib/messaging/*.d.ts', + '!lib/project-management/*.d.ts', + '!lib/remote-config/*.d.ts', + '!lib/security-rules/*.d.ts', + '!lib/storage/*.d.ts', + '!lib/utils/*.d.ts' +]; + +// Create a separate project for buildProject that overrides the rootDir. // This ensures that the generated production files are in their own root -// rather than including both src and test in the lib dir. -var buildProject = ts.createProject('tsconfig.json', {rootDir: 'src'}); +// rather than including both src and test in the lib dir. Declaration +// is used by TypeScript to determine if auto-generated typings should be +// emitted. +const declaration = process.env.TYPE_GENERATION_MODE === 'auto'; +var buildProject = ts.createProject('tsconfig.json', { rootDir: 'src', declaration }); var buildTest = ts.createProject('tsconfig.json'); @@ -71,16 +95,36 @@ gulp.task('cleanup', function() { ]); }); +// Task used to compile the TypeScript project. If automatic typings +// are set to be generated (determined by TYPE_GENERATION_MODE), declarations +// for files terminating in -internal.d.ts are removed because we do not +// want to expose internally used types to developers. As auto-generated +// typings are a work-in-progress, we remove the *.d.ts files for modules +// which we do not intend to auto-generate typings for yet. gulp.task('compile', function() { - return gulp.src(paths.src) + let workflow = gulp.src(paths.src) // Compile Typescript into .js and .d.ts files .pipe(buildProject()) // Add header - .pipe(header(banner)) - - // Write to build directory - .pipe(gulp.dest(paths.build)) + .pipe(header(banner)); + + // Exclude typings that are unintended (currently excludes all auto-generated + // typings, but as services are refactored to auto-generate typings this will + // change). Moreover, all *-internal.d.ts typings should not be exposed to + // developers as it denotes internally used types. + if (declaration) { + const configuration = [ + 'lib/**/*.js', + 'lib/**/*.d.ts', + '!lib/**/*-internal.d.ts', + ].concat(TEMPORARY_TYPING_EXCLUDES); + + workflow = workflow.pipe(filter(configuration)); + } + + // Write to build directory + return workflow.pipe(gulp.dest(paths.build)) }); /** @@ -104,19 +148,23 @@ gulp.task('copyDatabase', function() { }); gulp.task('copyTypings', function() { - return gulp.src('src/*.d.ts') + let workflow = gulp.src('src/*.d.ts') // Add header - .pipe(header(banner)) - - // Write to build directory - .pipe(gulp.dest(paths.build)) + .pipe(header(banner)); + + if (declaration) { + workflow = workflow.pipe(filter(paths.curatedTypings)); + } + + // Write to build directory + return workflow.pipe(gulp.dest(paths.build)) }); gulp.task('compile_all', gulp.series('compile', 'copyDatabase', 'copyTypings', 'compile_test')); // Regenerates js every time a source file changes gulp.task('watch', function() { - gulp.watch(paths.src.concat(paths.test), {ignoreInitial: false}, gulp.series('compile_all')); + gulp.watch(paths.src.concat(paths.test), { ignoreInitial: false }, gulp.series('compile_all')); }); // Build task diff --git a/package-lock.json b/package-lock.json index faacfd5dbf..623482b748 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "8.13.0", + "version": "9.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -572,8 +572,7 @@ "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" }, "@types/minimist": { "version": "1.2.0", @@ -795,7 +794,6 @@ "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" } @@ -841,8 +839,7 @@ "ansi-wrap": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", - "dev": true + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" }, "any-promise": { "version": "1.3.0", @@ -912,8 +909,7 @@ "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" }, "arr-filter": { "version": "1.1.2", @@ -942,8 +938,7 @@ "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" }, "array-differ": { "version": "1.0.0", @@ -1047,8 +1042,7 @@ "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "optional": true + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" }, "asn1": { "version": "0.2.4", @@ -1074,8 +1068,7 @@ "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, "astral-regex": { "version": "1.0.0", @@ -1163,8 +1156,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -1289,7 +1281,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1724,8 +1715,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "2.0.0", @@ -2709,7 +2699,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -2719,7 +2708,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -3532,6 +3520,16 @@ } } }, + "gulp-filter": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-6.0.0.tgz", + "integrity": "sha512-veQFW93kf6jBdWdF/RxMEIlDK2mkjHyPftM381DID2C9ImTVngwYpyyThxm4/EpgcNOT37BLefzMOjEKbyYg0Q==", + "requires": { + "multimatch": "^4.0.0", + "plugin-error": "^1.0.1", + "streamfilter": "^3.0.0" + } + }, "gulp-header": { "version": "1.8.12", "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz", @@ -4348,7 +4346,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "requires": { "isobject": "^3.0.1" } @@ -4438,8 +4435,7 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "isstream": { "version": "0.1.2", @@ -5248,7 +5244,6 @@ "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" } @@ -5387,6 +5382,30 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "multimatch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz", + "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==", + "requires": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==" + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + } + } + }, "multipipe": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", @@ -6334,7 +6353,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", - "dev": true, "requires": { "ansi-colors": "^1.0.1", "arr-diff": "^4.0.0", @@ -7393,6 +7411,26 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "streamfilter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-3.0.0.tgz", + "integrity": "sha512-kvKNfXCmUyC8lAXSSHCIXBUlo/lhsLcCU/OmzACZYpRUdtKIH68xYhm/+HI15jFJYtNJGYtCgn2wmIiExY1VwA==", + "requires": { + "readable-stream": "^3.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "streamsearch": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", diff --git a/package.json b/package.json index 0cda64390a..81008d942b 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@firebase/database": "^0.6.0", "@types/node": "^10.10.0", "dicer": "^0.3.0", + "gulp-filter": "^6.0.0", "jsonwebtoken": "^8.5.1", "node-forge": "^0.9.1" }, From 031edc2c3eb3b676b95063ce33bfc5673d7dc2bc Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Thu, 30 Jul 2020 15:50:23 -0400 Subject: [PATCH 017/160] Allow instance-id to auto-generate typings, separate internal vs external APIs (#969) * Allow instance-id to auto-generate typings, separate internal vs external APIs (#969) --- gulpfile.js | 3 +- src/firebase-app.ts | 3 +- src/firebase-namespace.ts | 2 +- src/instance-id/index.ts | 41 +++++++++++ src/instance-id/instance-id-internal.ts | 85 +++++++++++++++++++++++ src/instance-id/instance-id.ts | 84 +++++++--------------- test/unit/firebase-namespace.spec.ts | 3 +- test/unit/instance-id/instance-id.spec.ts | 19 ++--- 8 files changed, 167 insertions(+), 73 deletions(-) create mode 100644 src/instance-id/index.ts create mode 100644 src/instance-id/instance-id-internal.ts diff --git a/gulpfile.js b/gulpfile.js index 9e1bb329d6..c9ead95fb7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -52,7 +52,7 @@ var paths = { build: 'lib/', - curatedTypings: ['src/*.d.ts'], + curatedTypings: ['src/*.d.ts', '!src/instance-id.d.ts'], }; const TEMPORARY_TYPING_EXCLUDES = [ @@ -63,7 +63,6 @@ const TEMPORARY_TYPING_EXCLUDES = [ '!lib/auth/*.d.ts', '!lib/database/*.d.ts', '!lib/firestore/*.d.ts', - '!lib/instance-id/*.d.ts', '!lib/machine-learning/*.d.ts', '!lib/messaging/*.d.ts', '!lib/project-management/*.d.ts', diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 4baf2de08d..3c36c675b1 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -30,6 +30,7 @@ import { DatabaseService } from './database/database'; import { Firestore } from '@google-cloud/firestore'; import { FirestoreService } from './firestore/firestore'; import { InstanceId } from './instance-id/instance-id'; +import { InstanceIdImpl } from './instance-id/instance-id-internal'; import { ProjectManagement } from './project-management/project-management'; import { SecurityRules } from './security-rules/security-rules'; @@ -352,7 +353,7 @@ export class FirebaseApp { */ public instanceId(): InstanceId { return this.ensureService_('iid', () => { - const iidService: typeof InstanceId = require('./instance-id/instance-id').InstanceId; + const iidService: typeof InstanceIdImpl = require('./instance-id/instance-id-internal').InstanceIdImpl; return new iidService(this); }); } diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 916132ebbf..15a8b31740 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -425,7 +425,7 @@ export class FirebaseNamespace { const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { return this.ensureApp(app).instanceId(); }; - const instanceId = require('./instance-id/instance-id').InstanceId; + const instanceId = require('./instance-id/instance-id-internal').InstanceIdImpl; return Object.assign(fn, { InstanceId: instanceId }); } diff --git a/src/instance-id/index.ts b/src/instance-id/index.ts new file mode 100644 index 0000000000..5daa3741cd --- /dev/null +++ b/src/instance-id/index.ts @@ -0,0 +1,41 @@ +/*! + * Copyright 2020 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 { FirebaseApp } from '../firebase-app'; +import * as instanceIdApi from './instance-id'; +import * as firebaseAdmin from '../index'; + +export function instanceId(app?: FirebaseApp): instanceIdApi.InstanceId { + if (typeof(app) === 'undefined') { + app = firebaseAdmin.app(); + } + return app.instanceId(); +} + +/** + * We must define a namespace to make the typings work correctly. Otherwise + * `admin.instanceId()` cannot be called like a function. Temporarily, + * admin.instanceId is used as the namespace name because we cannot barrel + * re-export the contents from instance-id, and we want it to + * match the namespacing in the re-export inside src/index.d.ts + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace admin.instanceId { + // See https://github.com/microsoft/TypeScript/issues/4336 + /* eslint-disable @typescript-eslint/no-unused-vars */ + // See https://github.com/typescript-eslint/typescript-eslint/issues/363 + export import InstanceId = instanceIdApi.InstanceId; +} diff --git a/src/instance-id/instance-id-internal.ts b/src/instance-id/instance-id-internal.ts new file mode 100644 index 0000000000..ff2926f5f3 --- /dev/null +++ b/src/instance-id/instance-id-internal.ts @@ -0,0 +1,85 @@ +/*! + * Copyright 2020 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 { FirebaseApp } from '../firebase-app'; +import { InstanceId } from './instance-id'; +import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; +import { FirebaseInstanceIdRequestHandler } from './instance-id-request'; + +import * as validator from '../utils/validator'; + +/** + * Internals of an InstanceId service instance. + */ +class InstanceIdInternals implements FirebaseServiceInternalsInterface { + /** + * Deletes the service and its associated resources. + * + * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. + */ + public delete(): Promise { + // There are no resources to clean up + return Promise.resolve(undefined); + } +} + +export class InstanceIdImpl implements FirebaseServiceInterface, InstanceId { + public INTERNAL: InstanceIdInternals = new InstanceIdInternals(); + + private app_: FirebaseApp; + private requestHandler: FirebaseInstanceIdRequestHandler; + + /** + * @param {FirebaseApp} app The app for this InstanceId service. + * @constructor + */ + constructor(app: FirebaseApp) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseInstanceIdError( + InstanceIdClientErrorCode.INVALID_ARGUMENT, + 'First argument passed to admin.instanceId() must be a valid Firebase app instance.', + ); + } + + this.app_ = app; + this.requestHandler = new FirebaseInstanceIdRequestHandler(app); + } + + /** + * Deletes the specified instance ID from Firebase. This can be used to delete an instance ID + * and associated user data from a Firebase project, pursuant to the General Data Protection + * Regulation (GDPR). + * + * @param {string} instanceId The instance ID to be deleted + * @return {Promise} A promise that resolves when the instance ID is successfully deleted. + */ + public deleteInstanceId(instanceId: string): Promise { + return this.requestHandler.deleteInstanceId(instanceId) + .then(() => { + // Return nothing on success + }); + } + + /** + * Returns the app associated with this InstanceId instance. + * + * @return {FirebaseApp} The app associated with this InstanceId instance. + */ + get app(): FirebaseApp { + return this.app_; + } +} diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index be63c8af89..88d9213283 100644 --- a/src/instance-id/instance-id.ts +++ b/src/instance-id/instance-id.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2017 Google Inc. + * Copyright 2020 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,70 +15,36 @@ */ import { FirebaseApp } from '../firebase-app'; -import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { FirebaseInstanceIdRequestHandler } from './instance-id-request'; - -import * as validator from '../utils/validator'; /** - * Internals of an InstanceId service instance. + * Gets the {@link InstanceId `InstanceId`} service for the + * current app. + * + * @example + * ```javascript + * var instanceId = app.instanceId(); + * // The above is shorthand for: + * // var instanceId = admin.instanceId(app); + * ``` + * + * @return The `InstanceId` service for the + * current app. */ -class InstanceIdInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up - return Promise.resolve(undefined); - } -} - -export class InstanceId implements FirebaseServiceInterface { - public INTERNAL: InstanceIdInternals = new InstanceIdInternals(); - - private app_: FirebaseApp; - private requestHandler: FirebaseInstanceIdRequestHandler; +export interface InstanceId { + app: FirebaseApp; /** - * @param {FirebaseApp} app The app for this InstanceId service. - * @constructor - */ - constructor(app: FirebaseApp) { - if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseInstanceIdError( - InstanceIdClientErrorCode.INVALID_ARGUMENT, - 'First argument passed to admin.instanceId() must be a valid Firebase app instance.', - ); - } - - this.app_ = app; - this.requestHandler = new FirebaseInstanceIdRequestHandler(app); - } - - /** - * Deletes the specified instance ID from Firebase. This can be used to delete an instance ID - * and associated user data from a Firebase project, pursuant to the General Data Protection - * Regulation (GDPR). + * Deletes the specified instance ID and the associated data from Firebase. * - * @param {string} instanceId The instance ID to be deleted - * @return {Promise} A promise that resolves when the instance ID is successfully deleted. - */ - public deleteInstanceId(instanceId: string): Promise { - return this.requestHandler.deleteInstanceId(instanceId) - .then(() => { - // Return nothing on success - }); - } - - /** - * Returns the app associated with this InstanceId instance. + * Note that Google Analytics for Firebase uses its own form of Instance ID to + * keep track of analytics data. Therefore deleting a Firebase Instance ID does + * not delete Analytics data. See + * [Delete an Instance ID](/support/privacy/manage-iids#delete_an_instance_id) + * for more information. + * + * @param instanceId The instance ID to be deleted. * - * @return {FirebaseApp} The app associated with this InstanceId instance. + * @return A promise fulfilled when the instance ID is deleted. */ - get app(): FirebaseApp { - return this.app_; - } + deleteInstanceId(instanceId: string): Promise; } diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 60b26ff91d..9ad7c58b58 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -49,6 +49,7 @@ import { setLogFunction, } from '@google-cloud/firestore'; import { InstanceId } from '../../src/instance-id/instance-id'; +import { InstanceIdImpl } from '../../src/instance-id/instance-id-internal'; import { ProjectManagement } from '../../src/project-management/project-management'; import { SecurityRules } from '../../src/security-rules/security-rules'; import { RemoteConfig } from '../../src/remote-config/remote-config'; @@ -623,7 +624,7 @@ describe('FirebaseNamespace', () => { }); it('should return a reference to InstanceId type', () => { - expect(firebaseNamespace.instanceId.InstanceId).to.be.deep.equal(InstanceId); + expect(firebaseNamespace.instanceId.InstanceId).to.be.deep.equal(InstanceIdImpl); }); }); diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index f5d078f074..c4a28dd9be 100644 --- a/test/unit/instance-id/instance-id.spec.ts +++ b/test/unit/instance-id/instance-id.spec.ts @@ -26,6 +26,7 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { InstanceId } from '../../../src/instance-id/instance-id'; +import { InstanceIdImpl } from '../../../src/instance-id/instance-id-internal'; import { FirebaseInstanceIdRequestHandler } from '../../../src/instance-id/instance-id-request'; import { FirebaseApp } from '../../../src/firebase-app'; import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../../../src/utils/error'; @@ -57,14 +58,14 @@ describe('InstanceId', () => { mockApp = mocks.app(); getTokenStub = utils.stubGetAccessToken(undefined, mockApp); mockCredentialApp = mocks.mockCredentialApp(); - iid = new InstanceId(mockApp); + iid = new InstanceIdImpl(mockApp); googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; gcloudProject = process.env.GCLOUD_PROJECT; - nullAccessTokenClient = new InstanceId(mocks.appReturningNullAccessToken()); - malformedAccessTokenClient = new InstanceId(mocks.appReturningMalformedAccessToken()); - rejectedPromiseAccessTokenClient = new InstanceId(mocks.appRejectedWhileFetchingAccessToken()); + nullAccessTokenClient = new InstanceIdImpl(mocks.appReturningNullAccessToken()); + malformedAccessTokenClient = new InstanceIdImpl(mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenClient = new InstanceIdImpl(mocks.appRejectedWhileFetchingAccessToken()); }); afterEach(() => { @@ -80,7 +81,7 @@ describe('InstanceId', () => { invalidApps.forEach((invalidApp) => { it('should throw given invalid app: ' + JSON.stringify(invalidApp), () => { expect(() => { - const iidAny: any = InstanceId; + const iidAny: any = InstanceIdImpl; return new iidAny(invalidApp); }).to.throw('First argument passed to admin.instanceId() must be a valid Firebase app instance.'); }); @@ -88,7 +89,7 @@ describe('InstanceId', () => { it('should throw given no app', () => { expect(() => { - const iidAny: any = InstanceId; + const iidAny: any = InstanceIdImpl; return new iidAny(); }).to.throw('First argument passed to admin.instanceId() must be a valid Firebase app instance.'); }); @@ -97,14 +98,14 @@ describe('InstanceId', () => { // Project ID not set in the environment. delete process.env.GOOGLE_CLOUD_PROJECT; delete process.env.GCLOUD_PROJECT; - const instanceId = new InstanceId(mockCredentialApp); + const instanceId = new InstanceIdImpl(mockCredentialApp); return instanceId.deleteInstanceId('iid') .should.eventually.rejectedWith(noProjectIdError); }); it('should not throw given a valid app', () => { expect(() => { - return new InstanceId(mockApp); + return new InstanceIdImpl(mockApp); }).not.to.throw(); }); }); @@ -118,7 +119,7 @@ describe('InstanceId', () => { it('is read-only', () => { expect(() => { (iid as any).app = mockApp; - }).to.throw('Cannot set property app of # which has only a getter'); + }).to.throw('Cannot set property app of # which has only a getter'); }); }); From f3d9da5d727e84c9c8dcf0e9484608e0c2b615c4 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Fri, 31 Jul 2020 18:23:45 -0400 Subject: [PATCH 018/160] fix(auth): Make displayName optional for AuthProviderConfig typings (#970) * fix(auth): Make displayName optional for AuthProviderConfig typings #970 --- src/auth.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth.d.ts b/src/auth.d.ts index 421388c435..39fa97c21b 100644 --- a/src/auth.d.ts +++ b/src/auth.d.ts @@ -1072,7 +1072,7 @@ export namespace admin.auth { * The user-friendly display name to the current configuration. This name is * also used as the provider label in the Cloud Console. */ - displayName: string; + displayName?: string; /** * Whether the provider configuration is enabled or disabled. A user From 646d90ff26bd4df6e53bf2f8a2d362ed7e4a537a Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Tue, 4 Aug 2020 11:54:07 -0400 Subject: [PATCH 019/160] chore(rc): Move manual typings remote-config typings to separate folder (#975) * Move remote-config typings to another file Co-authored-by: Lahiru Maramba --- src/index.d.ts | 358 ++--------------------------------------- src/remote-config.d.ts | 350 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 363 insertions(+), 345 deletions(-) create mode 100644 src/remote-config.d.ts diff --git a/src/index.d.ts b/src/index.d.ts index e39fc8086a..6b4c8826f5 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -23,6 +23,7 @@ import * as _database from './database'; import * as _messaging from './messaging'; import * as _instanceId from './instance-id'; import * as _projectManagement from './project-management'; +import * as _remoteConfig from './remote-config'; import * as _securityRules from './security-rules'; /* eslint-disable @typescript-eslint/ban-types */ @@ -847,351 +848,18 @@ declare namespace admin.projectManagement { } declare namespace admin.remoteConfig { - - /** - * Colors that are associated with conditions for display purposes. - */ - type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | - 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL'; - - /** - * Interface representing a Remote Config template. - */ - interface RemoteConfigTemplate { - /** - * A list of conditions in descending order by priority. - */ - conditions: RemoteConfigCondition[]; - - /** - * Map of parameter keys to their optional default values and optional conditional values. - */ - parameters: { [key: string]: RemoteConfigParameter }; - - /** - * Map of parameter group names to their parameter group objects. - * A group's name is mutable but must be unique among groups in the Remote Config template. - * The name is limited to 256 characters and intended to be human-readable. Any Unicode - * characters are allowed. - */ - parameterGroups: { [key: string]: RemoteConfigParameterGroup }; - - /** - * ETag of the current Remote Config template (readonly). - */ - readonly etag: string; - - /** - * Version information for the current Remote Config template. - */ - version?: Version; - } - - /** - * Interface representing a Remote Config parameter. - * At minimum, a `defaultValue` or a `conditionalValues` entry must be present for the - * parameter to have any effect. - */ - interface RemoteConfigParameter { - - /** - * The value to set the parameter to, when none of the named conditions evaluate to `true`. - */ - defaultValue?: RemoteConfigParameterValue; - - /** - * A `(condition name, value)` map. The condition name of the highest priority - * (the one listed first in the Remote Config template's conditions list) determines the value of - * this parameter. - */ - conditionalValues?: { [key: string]: RemoteConfigParameterValue }; - - /** - * A description for this parameter. Should not be over 100 characters and may contain any - * Unicode characters. - */ - description?: string; - } - - /** - * Interface representing a Remote Config parameter group. - * Grouping parameters is only for management purposes and does not affect client-side - * fetching of parameter values. - */ - export interface RemoteConfigParameterGroup { - /** - * A description for the group. Its length must be less than or equal to 256 characters. - * A description may contain any Unicode characters. - */ - description?: string; - - /** - * Map of parameter keys to their optional default values and optional conditional values for - * parameters that belong to this group. A parameter only appears once per - * Remote Config template. An ungrouped parameter appears at the top level, whereas a - * parameter organized within a group appears within its group's map of parameters. - */ - parameters: { [key: string]: RemoteConfigParameter }; - } - - /** - * Interface representing a Remote Config condition. - * A condition targets a specific group of users. A list of these conditions make up - * part of a Remote Config template. - */ - interface RemoteConfigCondition { - - /** - * A non-empty and unique name of this condition. - */ - name: string; - - /** - * The logic of this condition. - * See the documentation on - * {@link https://firebase.google.com/docs/remote-config/condition-reference condition expressions} - * for the expected syntax of this field. - */ - expression: string; - - /** - * The color associated with this condition for display purposes in the Firebase Console. - * Not specifying this value results in the console picking an arbitrary color to associate - * with the condition. - */ - tagColor?: TagColor; - } - - /** - * Interface representing an explicit parameter value. - */ - interface ExplicitParameterValue { - /** - * The `string` value that the parameter is set to. - */ - value: string; - } - - /** - * Interface representing an in-app-default value. - */ - interface InAppDefaultValue { - /** - * If `true`, the parameter is omitted from the parameter values returned to a client. - */ - useInAppDefault: boolean; - } - - /** - * Type representing a Remote Config parameter value. - * A `RemoteConfigParameterValue` could be either an `ExplicitParameterValue` or - * an `InAppDefaultValue`. - */ - type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; - - /** - * Interface representing a Remote Config template version. - * Output only, except for the version description. Contains metadata about a particular - * version of the Remote Config template. All fields are set at the time the specified Remote - * Config template is published. A version's description field may be specified in - * `publishTemplate` calls. - */ - export interface Version { - /** - * The version number of a Remote Config template. - */ - versionNumber?: string; - - /** - * The timestamp of when this version of the Remote Config template was written to the - * Remote Config backend. - */ - updateTime?: string; - - /** - * The origin of the template update action. - */ - updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | - 'REST_API' | 'ADMIN_SDK_NODE'); - - /** - * The type of the template update action. - */ - updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | - 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); - - /** - * Aggregation of all metadata fields about the account that performed the update. - */ - updateUser?: RemoteConfigUser; - - /** - * The user-provided description of the corresponding Remote Config template. - */ - description?: string; - - /** - * The version number of the Remote Config template that has become the current version - * due to a rollback. Only present if this version is the result of a rollback. - */ - rollbackSource?: string; - - /** - * Indicates whether this Remote Config template was published before version history was - * supported. - */ - isLegacy?: boolean; - } - - /** Interface representing a list of Remote Config template versions. */ - export interface ListVersionsResult { - /** - * A list of version metadata objects, sorted in reverse chronological order. - */ - versions: Version[]; - - /** - * Token to retrieve the next page of results, or empty if there are no more results - * in the list. - */ - nextPageToken?: string; - } - - /** Interface representing options for Remote Config list versions operation. */ - export interface ListVersionsOptions { - /** - * The maximum number of items to return per page. - */ - pageSize?: number; - - /** - * The `nextPageToken` value returned from a previous list versions request, if any. - */ - pageToken?: string; - - /** - * Specifies the newest version number to include in the results. - * If specified, must be greater than zero. Defaults to the newest version. - */ - endVersionNumber?: string | number; - - /** - * Specifies the earliest update time to include in the results. Any entries updated before this - * time are omitted. - */ - startTime?: Date | string; - - /** - * Specifies the latest update time to include in the results. Any entries updated on or after - * this time are omitted. - */ - endTime?: Date | string; - } - - /** Interface representing a Remote Config user.*/ - export interface RemoteConfigUser { - /** - * Email address. Output only. - */ - email: string; - - /** - * Display name. Output only. - */ - name?: string; - - /** - * Image URL. Output only. - */ - imageUrl?: string; - } - - /** - * The Firebase `RemoteConfig` service interface. - * - * Do not call this constructor directly. Instead, use - * [`admin.remoteConfig()`](admin.remoteConfig#remoteConfig). - */ - interface RemoteConfig { - app: admin.app.App; - - /** - * Gets the current active version of the {@link admin.remoteConfig.RemoteConfigTemplate - * `RemoteConfigTemplate`} of the project. - * - * @return A promise that fulfills with a `RemoteConfigTemplate`. - */ - getTemplate(): Promise; - - /** - * Gets the requested version of the {@link admin.remoteConfig.RemoteConfigTemplate - * `RemoteConfigTemplate`} of the project. - * - * @param versionNumber Version number of the Remote Config template to look up. - * - * @return A promise that fulfills with a `RemoteConfigTemplate`. - */ - getTemplateAtVersion(versionNumber: number | string): Promise; - - /** - * Validates a {@link admin.remoteConfig.RemoteConfigTemplate `RemoteConfigTemplate`}. - * - * @param template The Remote Config template to be validated. - * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. - */ - validateTemplate(template: RemoteConfigTemplate): Promise; - - /** - * Publishes a Remote Config template. - * - * @param template The Remote Config template to be published. - * @param options Optional options object when publishing a Remote Config template: - * - {boolean} `force` Setting this to `true` forces the Remote Config template to - * be updated and circumvent the ETag. This approach is not recommended - * because it risks causing the loss of updates to your Remote Config - * template if multiple clients are updating the Remote Config template. - * See {@link https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates - * ETag usage and forced updates}. - * - * @return A Promise that fulfills with the published `RemoteConfigTemplate`. - */ - publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise; - - /** - * Rolls back a project's published Remote Config template to the specified version. - * A rollback is equivalent to getting a previously published Remote Config - * template and re-publishing it using a force update. - * - * @param versionNumber The version number of the Remote Config template to roll back to. - * The specified version number must be lower than the current version number, and not have - * been deleted due to staleness. Only the last 300 versions are stored. - * All versions that correspond to non-active Remote Config templates (that is, all except the - * template that is being fetched by clients) are also deleted if they are more than 90 days old. - * @return A promise that fulfills with the published `RemoteConfigTemplate`. - */ - rollback(versionNumber: string | number): Promise; - - /** - * Gets a list of Remote Config template versions that have been published, sorted in reverse - * chronological order. Only the last 300 versions are stored. - * All versions that correspond to non-active Remote Config templates (that is, all except the - * template that is being fetched by clients) are also deleted if they are more than 90 days old. - * - * @param options Optional {@link admin.remoteConfig.ListVersionsOptions `ListVersionsOptions`} - * object for getting a list of template versions. - * @return A promise that fulfills with a `ListVersionsResult`. - */ - listVersions(options?: ListVersionsOptions): Promise; - - /** - * Creates and returns a new Remote Config template from a JSON string. - * - * @param json The JSON string to populate a Remote Config template. - * - * @return A new template instance. - */ - createTemplateFromJSON(json: string): RemoteConfigTemplate; - } + export import TagColor = _remoteConfig.admin.remoteConfig.TagColor; + export import RemoteConfigTemplate = _remoteConfig.admin.remoteConfig.RemoteConfigTemplate; + export import RemoteConfigParameter = _remoteConfig.admin.remoteConfig.RemoteConfigParameter; + export import RemoteConfigParameterGroup = _remoteConfig.admin.remoteConfig.RemoteConfigParameterGroup; + export import RemoteConfigCondition = _remoteConfig.admin.remoteConfig.RemoteConfigCondition; + export import ExplicitParameterValue = _remoteConfig.admin.remoteConfig.ExplicitParameterValue; + export import InAppDefaultValue = _remoteConfig.admin.remoteConfig.InAppDefaultValue; + export import RemoteConfigParameterValue = _remoteConfig.admin.remoteConfig.RemoteConfigParameterValue; + export import Version = _remoteConfig.admin.remoteConfig.Version; + export import ListVersionsResult = _remoteConfig.admin.remoteConfig.ListVersionsResult; + export import RemoteConfigUser = _remoteConfig.admin.remoteConfig.RemoteConfigUser; + export import RemoteConfig = _remoteConfig.admin.remoteConfig.RemoteConfig; } declare namespace admin.securityRules { diff --git a/src/remote-config.d.ts b/src/remote-config.d.ts new file mode 100644 index 0000000000..c3897a2dd2 --- /dev/null +++ b/src/remote-config.d.ts @@ -0,0 +1,350 @@ +import * as _admin from './index.d'; + +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +export namespace admin.remoteConfig { + + /** + * Colors that are associated with conditions for display purposes. + */ + type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | + 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL'; + + /** + * Interface representing a Remote Config template. + */ + interface RemoteConfigTemplate { + /** + * A list of conditions in descending order by priority. + */ + conditions: RemoteConfigCondition[]; + + /** + * Map of parameter keys to their optional default values and optional conditional values. + */ + parameters: { [key: string]: RemoteConfigParameter }; + + /** + * Map of parameter group names to their parameter group objects. + * A group's name is mutable but must be unique among groups in the Remote Config template. + * The name is limited to 256 characters and intended to be human-readable. Any Unicode + * characters are allowed. + */ + parameterGroups: { [key: string]: RemoteConfigParameterGroup }; + + /** + * ETag of the current Remote Config template (readonly). + */ + readonly etag: string; + + /** + * Version information for the current Remote Config template. + */ + version?: Version; + } + + /** + * Interface representing a Remote Config parameter. + * At minimum, a `defaultValue` or a `conditionalValues` entry must be present for the + * parameter to have any effect. + */ + interface RemoteConfigParameter { + + /** + * The value to set the parameter to, when none of the named conditions evaluate to `true`. + */ + defaultValue?: RemoteConfigParameterValue; + + /** + * A `(condition name, value)` map. The condition name of the highest priority + * (the one listed first in the Remote Config template's conditions list) determines the value of + * this parameter. + */ + conditionalValues?: { [key: string]: RemoteConfigParameterValue }; + + /** + * A description for this parameter. Should not be over 100 characters and may contain any + * Unicode characters. + */ + description?: string; + } + + /** + * Interface representing a Remote Config parameter group. + * Grouping parameters is only for management purposes and does not affect client-side + * fetching of parameter values. + */ + export interface RemoteConfigParameterGroup { + /** + * A description for the group. Its length must be less than or equal to 256 characters. + * A description may contain any Unicode characters. + */ + description?: string; + + /** + * Map of parameter keys to their optional default values and optional conditional values for + * parameters that belong to this group. A parameter only appears once per + * Remote Config template. An ungrouped parameter appears at the top level, whereas a + * parameter organized within a group appears within its group's map of parameters. + */ + parameters: { [key: string]: RemoteConfigParameter }; + } + + /** + * Interface representing a Remote Config condition. + * A condition targets a specific group of users. A list of these conditions make up + * part of a Remote Config template. + */ + interface RemoteConfigCondition { + + /** + * A non-empty and unique name of this condition. + */ + name: string; + + /** + * The logic of this condition. + * See the documentation on + * {@link https://firebase.google.com/docs/remote-config/condition-reference condition expressions} + * for the expected syntax of this field. + */ + expression: string; + + /** + * The color associated with this condition for display purposes in the Firebase Console. + * Not specifying this value results in the console picking an arbitrary color to associate + * with the condition. + */ + tagColor?: TagColor; + } + + /** + * Interface representing an explicit parameter value. + */ + interface ExplicitParameterValue { + /** + * The `string` value that the parameter is set to. + */ + value: string; + } + + /** + * Interface representing an in-app-default value. + */ + interface InAppDefaultValue { + /** + * If `true`, the parameter is omitted from the parameter values returned to a client. + */ + useInAppDefault: boolean; + } + + /** + * Type representing a Remote Config parameter value. + * A `RemoteConfigParameterValue` could be either an `ExplicitParameterValue` or + * an `InAppDefaultValue`. + */ + type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; + + /** + * Interface representing a Remote Config template version. + * Output only, except for the version description. Contains metadata about a particular + * version of the Remote Config template. All fields are set at the time the specified Remote + * Config template is published. A version's description field may be specified in + * `publishTemplate` calls. + */ + export interface Version { + /** + * The version number of a Remote Config template. + */ + versionNumber?: string; + + /** + * The timestamp of when this version of the Remote Config template was written to the + * Remote Config backend. + */ + updateTime?: string; + + /** + * The origin of the template update action. + */ + updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | + 'REST_API' | 'ADMIN_SDK_NODE'); + + /** + * The type of the template update action. + */ + updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | + 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); + + /** + * Aggregation of all metadata fields about the account that performed the update. + */ + updateUser?: RemoteConfigUser; + + /** + * The user-provided description of the corresponding Remote Config template. + */ + description?: string; + + /** + * The version number of the Remote Config template that has become the current version + * due to a rollback. Only present if this version is the result of a rollback. + */ + rollbackSource?: string; + + /** + * Indicates whether this Remote Config template was published before version history was + * supported. + */ + isLegacy?: boolean; + } + + /** Interface representing a list of Remote Config template versions. */ + export interface ListVersionsResult { + /** + * A list of version metadata objects, sorted in reverse chronological order. + */ + versions: Version[]; + + /** + * Token to retrieve the next page of results, or empty if there are no more results + * in the list. + */ + nextPageToken?: string; + } + + /** Interface representing options for Remote Config list versions operation. */ + export interface ListVersionsOptions { + /** + * The maximum number of items to return per page. + */ + pageSize?: number; + + /** + * The `nextPageToken` value returned from a previous list versions request, if any. + */ + pageToken?: string; + + /** + * Specifies the newest version number to include in the results. + * If specified, must be greater than zero. Defaults to the newest version. + */ + endVersionNumber?: string | number; + + /** + * Specifies the earliest update time to include in the results. Any entries updated before this + * time are omitted. + */ + startTime?: Date | string; + + /** + * Specifies the latest update time to include in the results. Any entries updated on or after + * this time are omitted. + */ + endTime?: Date | string; + } + + /** Interface representing a Remote Config user.*/ + export interface RemoteConfigUser { + /** + * Email address. Output only. + */ + email: string; + + /** + * Display name. Output only. + */ + name?: string; + + /** + * Image URL. Output only. + */ + imageUrl?: string; + } + + /** + * The Firebase `RemoteConfig` service interface. + * + * Do not call this constructor directly. Instead, use + * [`admin.remoteConfig()`](admin.remoteConfig#remoteConfig). + */ + interface RemoteConfig { + app: _admin.app.App; + + /** + * Gets the current active version of the {@link admin.remoteConfig.RemoteConfigTemplate + * `RemoteConfigTemplate`} of the project. + * + * @return A promise that fulfills with a `RemoteConfigTemplate`. + */ + getTemplate(): Promise; + + /** + * Gets the requested version of the {@link admin.remoteConfig.RemoteConfigTemplate + * `RemoteConfigTemplate`} of the project. + * + * @param versionNumber Version number of the Remote Config template to look up. + * + * @return A promise that fulfills with a `RemoteConfigTemplate`. + */ + getTemplateAtVersion(versionNumber: number | string): Promise; + + /** + * Validates a {@link admin.remoteConfig.RemoteConfigTemplate `RemoteConfigTemplate`}. + * + * @param template The Remote Config template to be validated. + * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. + */ + validateTemplate(template: RemoteConfigTemplate): Promise; + + /** + * Publishes a Remote Config template. + * + * @param template The Remote Config template to be published. + * @param options Optional options object when publishing a Remote Config template: + * - {boolean} `force` Setting this to `true` forces the Remote Config template to + * be updated and circumvent the ETag. This approach is not recommended + * because it risks causing the loss of updates to your Remote Config + * template if multiple clients are updating the Remote Config template. + * See {@link https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates + * ETag usage and forced updates}. + * + * @return A Promise that fulfills with the published `RemoteConfigTemplate`. + */ + publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise; + + /** + * Rolls back a project's published Remote Config template to the specified version. + * A rollback is equivalent to getting a previously published Remote Config + * template and re-publishing it using a force update. + * + * @param versionNumber The version number of the Remote Config template to roll back to. + * The specified version number must be lower than the current version number, and not have + * been deleted due to staleness. Only the last 300 versions are stored. + * All versions that correspond to non-active Remote Config templates (that is, all except the + * template that is being fetched by clients) are also deleted if they are more than 90 days old. + * @return A promise that fulfills with the published `RemoteConfigTemplate`. + */ + rollback(versionNumber: string | number): Promise; + + /** + * Gets a list of Remote Config template versions that have been published, sorted in reverse + * chronological order. Only the last 300 versions are stored. + * All versions that correspond to non-active Remote Config templates (that is, all except the + * template that is being fetched by clients) are also deleted if they are more than 90 days old. + * + * @param options Optional {@link admin.remoteConfig.ListVersionsOptions `ListVersionsOptions`} + * object for getting a list of template versions. + * @return A promise that fulfills with a `ListVersionsResult`. + */ + listVersions(options?: ListVersionsOptions): Promise; + + /** + * Creates and returns a new Remote Config template from a JSON string. + * + * @param json The JSON string to populate a Remote Config template. + * + * @return A new template instance. + */ + createTemplateFromJSON(json: string): RemoteConfigTemplate; + } +} From aef6bc4e62b93b087306642de98f6d02f7fae883 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Thu, 6 Aug 2020 11:15:57 -0400 Subject: [PATCH 020/160] Allow project-management to auto-generate typings, separate internal vs external APIs (#971) * Allow project-management to auto-generate typings, separate internal vs external APIs (#971) --- gulpfile.js | 7 +- src/project-management/android-app.ts | 66 +++++++++++- src/project-management/app-metadata.ts | 101 +++++++++++++++++- src/project-management/index.ts | 54 ++++++++++ src/project-management/ios-app.ts | 23 +++- ...roject-management-api-request-internal.ts} | 0 src/project-management/project-management.ts | 63 +++++++++-- .../project-management/android-app.spec.ts | 2 +- test/unit/project-management/ios-app.spec.ts | 2 +- .../project-management-api-request.spec.ts | 2 +- .../project-management.spec.ts | 2 +- 11 files changed, 296 insertions(+), 26 deletions(-) create mode 100644 src/project-management/index.ts rename src/project-management/{project-management-api-request.ts => project-management-api-request-internal.ts} (100%) diff --git a/gulpfile.js b/gulpfile.js index c9ead95fb7..1dc9445125 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -52,7 +52,11 @@ var paths = { build: 'lib/', - curatedTypings: ['src/*.d.ts', '!src/instance-id.d.ts'], + curatedTypings: [ + 'src/*.d.ts', + '!src/instance-id.d.ts', + '!src/project-management.d.ts' + ], }; const TEMPORARY_TYPING_EXCLUDES = [ @@ -65,7 +69,6 @@ const TEMPORARY_TYPING_EXCLUDES = [ '!lib/firestore/*.d.ts', '!lib/machine-learning/*.d.ts', '!lib/messaging/*.d.ts', - '!lib/project-management/*.d.ts', '!lib/remote-config/*.d.ts', '!lib/security-rules/*.d.ts', '!lib/storage/*.d.ts', diff --git a/src/project-management/android-app.ts b/src/project-management/android-app.ts index 0cbd8951fc..793d9137f3 100644 --- a/src/project-management/android-app.ts +++ b/src/project-management/android-app.ts @@ -16,7 +16,7 @@ import { FirebaseProjectManagementError } from '../utils/error'; import * as validator from '../utils/validator'; -import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request'; +import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; import { AndroidAppMetadata, AppPlatform } from './app-metadata'; export class AndroidApp { @@ -33,6 +33,11 @@ export class AndroidApp { this.resourceName = `projects/-/androidApps/${appId}`; } + /** + * Retrieves metadata about this Android app. + * + * @return A promise that resolves to the retrieved metadata about this Android app. + */ public getMetadata(): Promise { return this.requestHandler.getResource(this.resourceName) .then((responseData: any) => { @@ -61,10 +66,23 @@ export class AndroidApp { }); } + /** + * Sets the optional user-assigned display name of the app. + * + * @param newDisplayName The new display name to set. + * + * @return A promise that resolves when the display name has been set. + */ public setDisplayName(newDisplayName: string): Promise { return this.requestHandler.setDisplayName(this.resourceName, newDisplayName); } + /** + * Gets the list of SHA certificates associated with this Android app in Firebase. + * + * @return The list of SHA-1 and SHA-256 certificates associated with this Android app in + * Firebase. + */ public getShaCertificates(): Promise { return this.requestHandler.getAndroidShaCertificates(this.resourceName) .then((responseData: any) => { @@ -98,10 +116,26 @@ export class AndroidApp { }); } + /** + * Adds the given SHA certificate to this Android app. + * + * @param certificateToAdd The SHA certificate to add. + * + * @return A promise that resolves when the given certificate + * has been added to the Android app. + */ public addShaCertificate(certificateToAdd: ShaCertificate): Promise { return this.requestHandler.addAndroidShaCertificate(this.resourceName, certificateToAdd); } + /** + * Deletes the specified SHA certificate from this Android app. + * + * @param certificateToDelete The SHA certificate to delete. + * + * @return A promise that resolves when the specified + * certificate has been removed from the Android app. + */ public deleteShaCertificate(certificateToDelete: ShaCertificate): Promise { if (!certificateToDelete.resourceName) { throw new FirebaseProjectManagementError( @@ -113,8 +147,12 @@ export class AndroidApp { } /** - * @return {Promise} A promise that resolves to a UTF-8 JSON string, typically intended to - * be written to a JSON file. + * Gets the configuration artifact associated with this app. + * + * @return A promise that resolves to the Android app's + * Firebase config file, in UTF-8 string format. This string is typically + * intended to be written to a JSON file that gets shipped with your Android + * app. */ public getConfig(): Promise { return this.requestHandler.getConfig(this.resourceName) @@ -135,7 +173,21 @@ export class AndroidApp { } } +/** + * A SHA-1 or SHA-256 certificate. + * + * Do not call this constructor directly. Instead, use + * [`projectManagement.shaCertificate()`](admin.projectManagement.ProjectManagement#shaCertificate). + */ export class ShaCertificate { + /** + * The SHA certificate type. + * + * @example + * ```javascript + * var certType = shaCertificate.certType; + * ``` + */ public readonly certType: ('sha1' | 'sha256'); /** @@ -143,8 +195,16 @@ export class ShaCertificate { * automatically determined from the hash itself. * * @param shaHash The sha256 or sha1 hash for this certificate. + * @example + * ```javascript + * var shaHash = shaCertificate.shaHash; + * ``` * @param resourceName The Firebase resource name for this certificate. This does not need to be * set when creating a new certificate. + * @example + * ```javascript + * var resourceName = shaCertificate.resourceName; + * ``` */ constructor(public readonly shaHash: string, public readonly resourceName?: string) { if (/^[a-fA-F0-9]{40}$/.test(shaHash)) { diff --git a/src/project-management/app-metadata.ts b/src/project-management/app-metadata.ts index 7f55551d53..42be9c6111 100644 --- a/src/project-management/app-metadata.ts +++ b/src/project-management/app-metadata.ts @@ -14,26 +14,117 @@ * limitations under the License. */ +/** + * Platforms with which a Firebase App can be associated. + */ export enum AppPlatform { + + /** + * Unknown state. This is only used for distinguishing unset values. + */ PLATFORM_UNKNOWN = 'PLATFORM_UNKNOWN', + + /** + * The Firebase App is associated with iOS. + */ IOS = 'IOS', + + /** + * The Firebase App is associated with Android. + */ ANDROID = 'ANDROID', } +/** + * Metadata about a Firebase app. + */ export interface AppMetadata { + + /** + * The globally unique, Firebase-assigned identifier of the app. + * + * @example + * ```javascript + * var appId = appMetadata.appId; + * ``` + */ appId: string; + + /** + * The optional user-assigned display name of the app. + * + * @example + * ```javascript + * var displayName = appMetadata.displayName; + * ``` + */ displayName?: string; + + /** + * The development platform of the app. Supporting Android and iOS app platforms. + * + * @example + * ```javascript + * var platform = AppPlatform.ANDROID; + * ``` + */ platform: AppPlatform; + + /** + * The globally unique, user-assigned ID of the parent project for the app. + * + * @example + * ```javascript + * var projectId = appMetadata.projectId; + * ``` + */ projectId: string; - resourceName: string; -} -export interface AndroidAppMetadata extends AppMetadata { - platform: AppPlatform.ANDROID; - packageName: string; + /** + * The fully-qualified resource name that identifies this app. + * + * This is useful when manually constructing requests for Firebase's public API. + * + * @example + * ```javascript + * var resourceName = androidAppMetadata.resourceName; + * ``` + */ + resourceName: string; } +/** + * Metadata about a Firebase iOS App. + */ export interface IosAppMetadata extends AppMetadata { platform: AppPlatform.IOS; + + /** + * The canonical bundle ID of the iOS App as it would appear in the iOS App Store. + * + * @example + * ```javascript + * var bundleId = iosAppMetadata.bundleId; + *``` + */ bundleId: string; } + +/** + * Metadata about a Firebase Android App. + */ +export interface AndroidAppMetadata extends AppMetadata { + + platform: AppPlatform.ANDROID; + + /** + * The canonical package name of the Android App, as would appear in the Google Play Developer + * Console. + * + * @example + * ```javascript + * var packageName = androidAppMetadata.packageName; + * ``` + */ + packageName: string; +} diff --git a/src/project-management/index.ts b/src/project-management/index.ts new file mode 100644 index 0000000000..7f9bb1a3c5 --- /dev/null +++ b/src/project-management/index.ts @@ -0,0 +1,54 @@ +/*! + * Copyright 2020 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 { FirebaseApp } from '../firebase-app'; +import * as androidAppApi from './android-app'; +import * as appMetadataApi from './app-metadata'; +import * as iosAppApi from './ios-app'; +import * as projectManagementApi from './project-management'; +import * as firebaseAdmin from '../index'; + +export function projectManagement(app?: FirebaseApp): projectManagementApi.ProjectManagement { + if (typeof(app) === 'undefined') { + app = firebaseAdmin.app(); + } + return app.projectManagement(); +} + +/** + * We must define a namespace to make the typings work correctly. Otherwise + * `admin.projectManagement()` cannot be called like a function. Temporarily, + * admin.projectManagement is used as the namespace name because we cannot barrel + * re-export the contents from instance-id, and we want it to + * match the namespacing in the re-export inside src/index.d.ts + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace admin.projectManagement { + // See https://github.com/microsoft/TypeScript/issues/4336 + /* eslint-disable @typescript-eslint/no-unused-vars */ + // See https://github.com/typescript-eslint/typescript-eslint/issues/363 + export import AndroidAppMetadata = appMetadataApi.AndroidAppMetadata + export import AppMetadata = appMetadataApi.AppMetadata + export import AppPlatform = appMetadataApi.AppPlatform + export import IosAppMetadata = appMetadataApi.IosAppMetadata + + // Allows for exposing classes as interfaces in typings + /* eslint-disable @typescript-eslint/no-empty-interface */ + export interface AndroidApp extends androidAppApi.AndroidApp {} + export interface IosApp extends iosAppApi.IosApp {} + export interface ProjectManagement extends projectManagementApi.ProjectManagement {} + export interface ShaCertificate extends androidAppApi.ShaCertificate {} +} diff --git a/src/project-management/ios-app.ts b/src/project-management/ios-app.ts index 18acb47ae1..0f5955c1a1 100644 --- a/src/project-management/ios-app.ts +++ b/src/project-management/ios-app.ts @@ -16,7 +16,7 @@ import { FirebaseProjectManagementError } from '../utils/error'; import * as validator from '../utils/validator'; -import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request'; +import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; import { IosAppMetadata, AppPlatform } from './app-metadata'; export class IosApp { @@ -33,6 +33,12 @@ export class IosApp { this.resourceName = `projects/-/iosApps/${appId}`; } + /** + * Retrieves metadata about this iOS app. + * + * @return {!Promise} A promise that + * resolves to the retrieved metadata about this iOS app. + */ public getMetadata(): Promise { return this.requestHandler.getResource(this.resourceName) .then((responseData: any) => { @@ -61,13 +67,24 @@ export class IosApp { }); } + /** + * Sets the optional user-assigned display name of the app. + * + * @param newDisplayName The new display name to set. + * + * @return A promise that resolves when the display name has + * been set. + */ public setDisplayName(newDisplayName: string): Promise { return this.requestHandler.setDisplayName(this.resourceName, newDisplayName); } /** - * @return {Promise} A promise that resolves to a UTF-8 XML string, typically intended to - * be written to a plist file. + * Gets the configuration artifact associated with this app. + * + * @return A promise that resolves to the iOS app's Firebase + * config file, in UTF-8 string format. This string is typically intended to + * be written to a plist file that gets shipped with your iOS app. */ public getConfig(): Promise { return this.requestHandler.getConfig(this.resourceName) diff --git a/src/project-management/project-management-api-request.ts b/src/project-management/project-management-api-request-internal.ts similarity index 100% rename from src/project-management/project-management-api-request.ts rename to src/project-management/project-management-api-request-internal.ts diff --git a/src/project-management/project-management.ts b/src/project-management/project-management.ts index dfe6be3a3f..daade63172 100644 --- a/src/project-management/project-management.ts +++ b/src/project-management/project-management.ts @@ -21,7 +21,7 @@ import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { AndroidApp, ShaCertificate } from './android-app'; import { IosApp } from './ios-app'; -import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request'; +import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; import { AppMetadata, AppPlatform } from './app-metadata'; /** @@ -40,10 +40,12 @@ class ProjectManagementInternals implements FirebaseServiceInternalsInterface { } /** - * ProjectManagement service bound to the provided app. + * The Firebase ProjectManagement service interface. + * + * Do not call this constructor directly. Instead, use + * [`admin.projectManagement()`](admin.projectManagement#projectManagement). */ export class ProjectManagement implements FirebaseServiceInterface { - public readonly INTERNAL: ProjectManagementInternals = new ProjectManagementInternals(); private readonly requestHandler: ProjectManagementRequestHandler; @@ -66,6 +68,8 @@ export class ProjectManagement implements FirebaseServiceInterface { /** * Lists up to 100 Firebase Android apps associated with this Firebase project. + * + * @return The list of Android apps. */ public listAndroidApps(): Promise { return this.listPlatformApps('android', 'listAndroidApps()'); @@ -73,34 +77,63 @@ export class ProjectManagement implements FirebaseServiceInterface { /** * Lists up to 100 Firebase iOS apps associated with this Firebase project. + * + * @return The list of iOS apps. */ public listIosApps(): Promise { return this.listPlatformApps('ios', 'listIosApps()'); } /** - * Returns an AndroidApp object for the given appId. No RPC is made. + * Creates an `AndroidApp` object, referencing the specified Android app within + * this Firebase project. + * + * This method does not perform an RPC. + * + * @param appId The `appId` of the Android app to reference. + * + * @return An `AndroidApp` object that references the specified Firebase Android app. */ public androidApp(appId: string): AndroidApp { return new AndroidApp(appId, this.requestHandler); } /** - * Returns an IosApp object for the given appId. No RPC is made. + * Creates an `iOSApp` object, referencing the specified iOS app within + * this Firebase project. + * + * This method does not perform an RPC. + * + * @param appId The `appId` of the iOS app to reference. + * + * @return An `iOSApp` object that references the specified Firebase iOS app. */ public iosApp(appId: string): IosApp { return new IosApp(appId, this.requestHandler); } /** - * Returns a ShaCertificate object for the given shaHash. No RPC is made. + * Creates a `ShaCertificate` object. + * + * This method does not perform an RPC. + * + * @param shaHash The SHA-1 or SHA-256 hash for this certificate. + * + * @return A `ShaCertificate` object contains the specified SHA hash. */ public shaCertificate(shaHash: string): ShaCertificate { return new ShaCertificate(shaHash); } /** - * Creates a new Firebase Android app, associated with this Firebase project. + * Creates a new Firebase Android app associated with this Firebase project. + * + * @param packageName The canonical package name of the Android App, + * as would appear in the Google Play Developer Console. + * @param displayName An optional user-assigned display name for this + * new app. + * + * @return A promise that resolves to the newly created Android app. */ public createAndroidApp(packageName: string, displayName?: string): Promise { return this.getResourceName() @@ -122,7 +155,13 @@ export class ProjectManagement implements FirebaseServiceInterface { } /** - * Creates a new Firebase iOS app, associated with this Firebase project. + * Creates a new Firebase iOS app associated with this Firebase project. + * + * @param bundleId The iOS app bundle ID to use for this new app. + * @param displayName An optional user-assigned display name for this + * new app. + * + * @return A promise that resolves to the newly created iOS app. */ public createIosApp(bundleId: string, displayName?: string): Promise { return this.getResourceName() @@ -145,6 +184,8 @@ export class ProjectManagement implements FirebaseServiceInterface { /** * Lists up to 100 Firebase apps associated with this Firebase project. + * + * @return A promise that resolves to the metadata list of the apps. */ public listAppMetadata(): Promise { return this.getResourceName() @@ -160,7 +201,11 @@ export class ProjectManagement implements FirebaseServiceInterface { } /** - * Update display name of the project + * Update the display name of this Firebase project. + * + * @param newDisplayName The new display name to be updated. + * + * @return A promise that resolves when the project display name has been updated. */ public setDisplayName(newDisplayName: string): Promise { return this.getResourceName() diff --git a/test/unit/project-management/android-app.spec.ts b/test/unit/project-management/android-app.spec.ts index dbb725a9df..ff17f0d3e1 100644 --- a/test/unit/project-management/android-app.spec.ts +++ b/test/unit/project-management/android-app.spec.ts @@ -21,7 +21,7 @@ import * as _ from 'lodash'; import * as sinon from 'sinon'; import { FirebaseApp } from '../../../src/firebase-app'; import { AndroidApp, ShaCertificate } from '../../../src/project-management/android-app'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request'; +import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; diff --git a/test/unit/project-management/ios-app.spec.ts b/test/unit/project-management/ios-app.spec.ts index 3b6748b4c1..3445076cf7 100644 --- a/test/unit/project-management/ios-app.spec.ts +++ b/test/unit/project-management/ios-app.spec.ts @@ -21,7 +21,7 @@ import * as _ from 'lodash'; import * as sinon from 'sinon'; import { FirebaseApp } from '../../../src/firebase-app'; import { IosApp } from '../../../src/project-management/ios-app'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request'; +import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; diff --git a/test/unit/project-management/project-management-api-request.spec.ts b/test/unit/project-management/project-management-api-request.spec.ts index dcffb29b76..44a9c46cc5 100644 --- a/test/unit/project-management/project-management-api-request.spec.ts +++ b/test/unit/project-management/project-management-api-request.spec.ts @@ -22,7 +22,7 @@ import * as _ from 'lodash'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import { FirebaseApp } from '../../../src/firebase-app'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request'; +import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; import { HttpClient } from '../../../src/utils/api-request'; import * as mocks from '../../resources/mocks'; import * as utils from '../utils'; diff --git a/test/unit/project-management/project-management.spec.ts b/test/unit/project-management/project-management.spec.ts index 1674be4645..62f819f264 100644 --- a/test/unit/project-management/project-management.spec.ts +++ b/test/unit/project-management/project-management.spec.ts @@ -22,7 +22,7 @@ import * as sinon from 'sinon'; import { FirebaseApp } from '../../../src/firebase-app'; import { AndroidApp } from '../../../src/project-management/android-app'; import { ProjectManagement } from '../../../src/project-management/project-management'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request'; +import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; import { IosApp } from '../../../src/project-management/ios-app'; From 69c268fc1230242f06793459c5841f82d8c0ef7c Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Thu, 6 Aug 2020 15:02:15 -0400 Subject: [PATCH 021/160] chore: Make instance-id use new modularization pattern (#977) --- src/firebase-app.ts | 3 +- src/firebase-namespace.ts | 2 +- src/instance-id/index.ts | 4 +- src/instance-id/instance-id-internal.ts | 85 ------------------- ...est.ts => instance-id-request-internal.ts} | 0 src/instance-id/instance-id.ts | 59 ++++++++++++- test/unit/firebase-namespace.spec.ts | 3 +- .../instance-id/instance-id-request.spec.ts | 2 +- test/unit/instance-id/instance-id.spec.ts | 21 +++-- 9 files changed, 73 insertions(+), 106 deletions(-) delete mode 100644 src/instance-id/instance-id-internal.ts rename src/instance-id/{instance-id-request.ts => instance-id-request-internal.ts} (100%) diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 3c36c675b1..4baf2de08d 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -30,7 +30,6 @@ import { DatabaseService } from './database/database'; import { Firestore } from '@google-cloud/firestore'; import { FirestoreService } from './firestore/firestore'; import { InstanceId } from './instance-id/instance-id'; -import { InstanceIdImpl } from './instance-id/instance-id-internal'; import { ProjectManagement } from './project-management/project-management'; import { SecurityRules } from './security-rules/security-rules'; @@ -353,7 +352,7 @@ export class FirebaseApp { */ public instanceId(): InstanceId { return this.ensureService_('iid', () => { - const iidService: typeof InstanceIdImpl = require('./instance-id/instance-id-internal').InstanceIdImpl; + const iidService: typeof InstanceId = require('./instance-id/instance-id').InstanceId; return new iidService(this); }); } diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 15a8b31740..916132ebbf 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -425,7 +425,7 @@ export class FirebaseNamespace { const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { return this.ensureApp(app).instanceId(); }; - const instanceId = require('./instance-id/instance-id-internal').InstanceIdImpl; + const instanceId = require('./instance-id/instance-id').InstanceId; return Object.assign(fn, { InstanceId: instanceId }); } diff --git a/src/instance-id/index.ts b/src/instance-id/index.ts index 5daa3741cd..2f6f48fc2b 100644 --- a/src/instance-id/index.ts +++ b/src/instance-id/index.ts @@ -37,5 +37,7 @@ export namespace admin.instanceId { // See https://github.com/microsoft/TypeScript/issues/4336 /* eslint-disable @typescript-eslint/no-unused-vars */ // See https://github.com/typescript-eslint/typescript-eslint/issues/363 - export import InstanceId = instanceIdApi.InstanceId; + // Allows for exposing classes as interfaces in typings + /* eslint-disable @typescript-eslint/no-empty-interface */ + export interface InstanceId extends instanceIdApi.InstanceId {} } diff --git a/src/instance-id/instance-id-internal.ts b/src/instance-id/instance-id-internal.ts deleted file mode 100644 index ff2926f5f3..0000000000 --- a/src/instance-id/instance-id-internal.ts +++ /dev/null @@ -1,85 +0,0 @@ -/*! - * Copyright 2020 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 { FirebaseApp } from '../firebase-app'; -import { InstanceId } from './instance-id'; -import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { FirebaseInstanceIdRequestHandler } from './instance-id-request'; - -import * as validator from '../utils/validator'; - -/** - * Internals of an InstanceId service instance. - */ -class InstanceIdInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up - return Promise.resolve(undefined); - } -} - -export class InstanceIdImpl implements FirebaseServiceInterface, InstanceId { - public INTERNAL: InstanceIdInternals = new InstanceIdInternals(); - - private app_: FirebaseApp; - private requestHandler: FirebaseInstanceIdRequestHandler; - - /** - * @param {FirebaseApp} app The app for this InstanceId service. - * @constructor - */ - constructor(app: FirebaseApp) { - if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseInstanceIdError( - InstanceIdClientErrorCode.INVALID_ARGUMENT, - 'First argument passed to admin.instanceId() must be a valid Firebase app instance.', - ); - } - - this.app_ = app; - this.requestHandler = new FirebaseInstanceIdRequestHandler(app); - } - - /** - * Deletes the specified instance ID from Firebase. This can be used to delete an instance ID - * and associated user data from a Firebase project, pursuant to the General Data Protection - * Regulation (GDPR). - * - * @param {string} instanceId The instance ID to be deleted - * @return {Promise} A promise that resolves when the instance ID is successfully deleted. - */ - public deleteInstanceId(instanceId: string): Promise { - return this.requestHandler.deleteInstanceId(instanceId) - .then(() => { - // Return nothing on success - }); - } - - /** - * Returns the app associated with this InstanceId instance. - * - * @return {FirebaseApp} The app associated with this InstanceId instance. - */ - get app(): FirebaseApp { - return this.app_; - } -} diff --git a/src/instance-id/instance-id-request.ts b/src/instance-id/instance-id-request-internal.ts similarity index 100% rename from src/instance-id/instance-id-request.ts rename to src/instance-id/instance-id-request-internal.ts diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index 88d9213283..b17434e9a3 100644 --- a/src/instance-id/instance-id.ts +++ b/src/instance-id/instance-id.ts @@ -15,6 +15,26 @@ */ import { FirebaseApp } from '../firebase-app'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; +import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; +import { FirebaseInstanceIdRequestHandler } from './instance-id-request-internal'; + +import * as validator from '../utils/validator'; + +/** + * Internals of an InstanceId service instance. + */ +class InstanceIdInternals implements FirebaseServiceInternalsInterface { + /** + * Deletes the service and its associated resources. + * + * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. + */ + public delete(): Promise { + // There are no resources to clean up + return Promise.resolve(undefined); + } +} /** * Gets the {@link InstanceId `InstanceId`} service for the @@ -30,8 +50,27 @@ import { FirebaseApp } from '../firebase-app'; * @return The `InstanceId` service for the * current app. */ -export interface InstanceId { - app: FirebaseApp; +export class InstanceId implements FirebaseServiceInterface { + public INTERNAL: InstanceIdInternals = new InstanceIdInternals(); + + private app_: FirebaseApp; + private requestHandler: FirebaseInstanceIdRequestHandler; + + /** + * @param {FirebaseApp} app The app for this InstanceId service. + * @constructor + */ + constructor(app: FirebaseApp) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseInstanceIdError( + InstanceIdClientErrorCode.INVALID_ARGUMENT, + 'First argument passed to admin.instanceId() must be a valid Firebase app instance.', + ); + } + + this.app_ = app; + this.requestHandler = new FirebaseInstanceIdRequestHandler(app); + } /** * Deletes the specified instance ID and the associated data from Firebase. @@ -46,5 +85,19 @@ export interface InstanceId { * * @return A promise fulfilled when the instance ID is deleted. */ - deleteInstanceId(instanceId: string): Promise; + public deleteInstanceId(instanceId: string): Promise { + return this.requestHandler.deleteInstanceId(instanceId) + .then(() => { + // Return nothing on success + }); + } + + /** + * Returns the app associated with this InstanceId instance. + * + * @return {FirebaseApp} The app associated with this InstanceId instance. + */ + get app(): FirebaseApp { + return this.app_; + } } diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 9ad7c58b58..60b26ff91d 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -49,7 +49,6 @@ import { setLogFunction, } from '@google-cloud/firestore'; import { InstanceId } from '../../src/instance-id/instance-id'; -import { InstanceIdImpl } from '../../src/instance-id/instance-id-internal'; import { ProjectManagement } from '../../src/project-management/project-management'; import { SecurityRules } from '../../src/security-rules/security-rules'; import { RemoteConfig } from '../../src/remote-config/remote-config'; @@ -624,7 +623,7 @@ describe('FirebaseNamespace', () => { }); it('should return a reference to InstanceId type', () => { - expect(firebaseNamespace.instanceId.InstanceId).to.be.deep.equal(InstanceIdImpl); + expect(firebaseNamespace.instanceId.InstanceId).to.be.deep.equal(InstanceId); }); }); diff --git a/test/unit/instance-id/instance-id-request.spec.ts b/test/unit/instance-id/instance-id-request.spec.ts index ae8580d3c2..f8abeb6bc8 100644 --- a/test/unit/instance-id/instance-id-request.spec.ts +++ b/test/unit/instance-id/instance-id-request.spec.ts @@ -27,7 +27,7 @@ import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; import { HttpClient } from '../../../src/utils/api-request'; -import { FirebaseInstanceIdRequestHandler } from '../../../src/instance-id/instance-id-request'; +import { FirebaseInstanceIdRequestHandler } from '../../../src/instance-id/instance-id-request-internal'; chai.should(); chai.use(sinonChai); diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index c4a28dd9be..28e37cb906 100644 --- a/test/unit/instance-id/instance-id.spec.ts +++ b/test/unit/instance-id/instance-id.spec.ts @@ -26,8 +26,7 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { InstanceId } from '../../../src/instance-id/instance-id'; -import { InstanceIdImpl } from '../../../src/instance-id/instance-id-internal'; -import { FirebaseInstanceIdRequestHandler } from '../../../src/instance-id/instance-id-request'; +import { FirebaseInstanceIdRequestHandler } from '../../../src/instance-id/instance-id-request-internal'; import { FirebaseApp } from '../../../src/firebase-app'; import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../../../src/utils/error'; @@ -58,14 +57,14 @@ describe('InstanceId', () => { mockApp = mocks.app(); getTokenStub = utils.stubGetAccessToken(undefined, mockApp); mockCredentialApp = mocks.mockCredentialApp(); - iid = new InstanceIdImpl(mockApp); + iid = new InstanceId(mockApp); googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; gcloudProject = process.env.GCLOUD_PROJECT; - nullAccessTokenClient = new InstanceIdImpl(mocks.appReturningNullAccessToken()); - malformedAccessTokenClient = new InstanceIdImpl(mocks.appReturningMalformedAccessToken()); - rejectedPromiseAccessTokenClient = new InstanceIdImpl(mocks.appRejectedWhileFetchingAccessToken()); + nullAccessTokenClient = new InstanceId(mocks.appReturningNullAccessToken()); + malformedAccessTokenClient = new InstanceId(mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenClient = new InstanceId(mocks.appRejectedWhileFetchingAccessToken()); }); afterEach(() => { @@ -81,7 +80,7 @@ describe('InstanceId', () => { invalidApps.forEach((invalidApp) => { it('should throw given invalid app: ' + JSON.stringify(invalidApp), () => { expect(() => { - const iidAny: any = InstanceIdImpl; + const iidAny: any = InstanceId; return new iidAny(invalidApp); }).to.throw('First argument passed to admin.instanceId() must be a valid Firebase app instance.'); }); @@ -89,7 +88,7 @@ describe('InstanceId', () => { it('should throw given no app', () => { expect(() => { - const iidAny: any = InstanceIdImpl; + const iidAny: any = InstanceId; return new iidAny(); }).to.throw('First argument passed to admin.instanceId() must be a valid Firebase app instance.'); }); @@ -98,14 +97,14 @@ describe('InstanceId', () => { // Project ID not set in the environment. delete process.env.GOOGLE_CLOUD_PROJECT; delete process.env.GCLOUD_PROJECT; - const instanceId = new InstanceIdImpl(mockCredentialApp); + const instanceId = new InstanceId(mockCredentialApp); return instanceId.deleteInstanceId('iid') .should.eventually.rejectedWith(noProjectIdError); }); it('should not throw given a valid app', () => { expect(() => { - return new InstanceIdImpl(mockApp); + return new InstanceId(mockApp); }).not.to.throw(); }); }); @@ -119,7 +118,7 @@ describe('InstanceId', () => { it('is read-only', () => { expect(() => { (iid as any).app = mockApp; - }).to.throw('Cannot set property app of # which has only a getter'); + }).to.throw('Cannot set property app of # which has only a getter'); }); }); From 42fac27f51a4e8266461e93537efcd071371ce29 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Thu, 6 Aug 2020 15:07:26 -0400 Subject: [PATCH 022/160] chore: Adopt no-unused-vars-experimental for eslint to prevent incorrect linting errors (#981) --- .eslintrc.js | 10 +++++++++- src/database.d.ts | 2 -- src/instance-id.d.ts | 1 - src/machine-learning/machine-learning.ts | 3 +-- src/remote-config.d.ts | 1 - test/integration/database.spec.ts | 2 -- test/resources/mocks.ts | 2 +- tsconfig.eslint.json | 12 ++++++++++++ 8 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 tsconfig.eslint.json diff --git a/.eslintrc.js b/.eslintrc.js index 1fc00bbd78..37125d1535 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,6 +33,7 @@ module.exports = { // Disabled checks '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/no-use-before-define': 0, + '@typescript-eslint/no-unused-vars': 0, // Required checks 'indent': ['error', 2], @@ -46,5 +47,12 @@ module.exports = { 'allowHigherOrderFunctions': true } ], - } + '@typescript-eslint/no-unused-vars-experimental': 2, + }, + // Required by the @typescript-eslint/no-unused-vars-experimental rule. + // We use a separate tsconfig file for linting to reduce the time complexity of the operation. + // See github.com/typescript-eslint/typescript-eslint/issues/1320 + parserOptions: { + project: './tsconfig.eslint.json', + }, }; diff --git a/src/database.d.ts b/src/database.d.ts index 3046e25f21..40ca71ea73 100644 --- a/src/database.d.ts +++ b/src/database.d.ts @@ -1631,11 +1631,9 @@ export namespace admin.database { */ interface ThenableReference extends admin.database.Reference, Promise { } - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ function enableLogging(logger?: boolean | ((message: string) => any), persistent?: boolean): any; } -/* eslint-disable @typescript-eslint/no-unused-vars */ export namespace admin.database.ServerValue { /** diff --git a/src/instance-id.d.ts b/src/instance-id.d.ts index 616d0d193e..34269be5b0 100644 --- a/src/instance-id.d.ts +++ b/src/instance-id.d.ts @@ -1,6 +1,5 @@ import * as _admin from './index.d'; -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ export namespace admin.instanceId { /** * Gets the {@link InstanceId `InstanceId`} service for the diff --git a/src/machine-learning/machine-learning.ts b/src/machine-learning/machine-learning.ts index 9d2330aa92..3b09d07c23 100644 --- a/src/machine-learning/machine-learning.ts +++ b/src/machine-learning/machine-learning.ts @@ -285,8 +285,7 @@ export class Model { return false; } - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - public waitForUnlocked(maxTimeSeconds?: number): Promise { + public waitForUnlocked(_maxTimeSeconds?: number): Promise { // Backend does not currently return locked models. // This will likely change in future. return Promise.resolve(); diff --git a/src/remote-config.d.ts b/src/remote-config.d.ts index c3897a2dd2..2da1d28336 100644 --- a/src/remote-config.d.ts +++ b/src/remote-config.d.ts @@ -1,6 +1,5 @@ import * as _admin from './index.d'; -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ export namespace admin.remoteConfig { /** diff --git a/test/integration/database.spec.ts b/test/integration/database.spec.ts index e8d0a746a4..708c3a153a 100644 --- a/test/integration/database.spec.ts +++ b/test/integration/database.spec.ts @@ -173,8 +173,6 @@ describe('admin.database', () => { // Check for type compilation. This method is not invoked by any tests. But it // will trigger a TS compilation failure if the RTDB typings were not loaded // correctly. (Marked as export to avoid compilation warning.) -// -// eslint-disable-next-line @typescript-eslint/no-unused-vars export function addValueEventListener( db: admin.database.Database, callback: (s: admin.database.DataSnapshot | null) => any): void { diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index cb2be41a8f..28884b30d2 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -221,7 +221,7 @@ export function generateSessionCookie(overrides?: object, expiresIn?: number): s export function firebaseServiceFactory( firebaseApp: FirebaseApp, - extendApp?: (props: object) => void, // eslint-disable-line @typescript-eslint/no-unused-vars + _extendApp?: (props: object) => void, ): FirebaseServiceInterface { const result = { app: firebaseApp, diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000000..fdb5ad2719 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,12 @@ +// Used for @typescript-eslint/no-unused-vars-experimental to glob the entire +// source. An alternative is createDefaultProgram: true but it has exponential +// complexity. See: github.com/typescript-eslint/typescript-eslint/issues/967 +{ + "compilerOptions": { + "strict": true, + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts", + ], +} From 1984f1fd592e3cddbc3c5b27655628dd878aaebc Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Thu, 6 Aug 2020 16:56:26 -0400 Subject: [PATCH 023/160] Allow security-rules to auto-generate typings, separate internal vs external APIs (#974) --- gulpfile.js | 2 +- src/security-rules/index.ts | 47 +++++++++++ ... => security-rules-api-client-internal.ts} | 2 +- ...es-utils.ts => security-rules-internal.ts} | 0 src/security-rules/security-rules.ts | 79 +++++++++++++------ .../security-rules-api-client.spec.ts | 4 +- .../security-rules/security-rules.spec.ts | 4 +- 7 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 src/security-rules/index.ts rename src/security-rules/{security-rules-api-client.ts => security-rules-api-client-internal.ts} (99%) rename src/security-rules/{security-rules-utils.ts => security-rules-internal.ts} (100%) diff --git a/gulpfile.js b/gulpfile.js index 1dc9445125..ab23375bd5 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -55,6 +55,7 @@ var paths = { curatedTypings: [ 'src/*.d.ts', '!src/instance-id.d.ts', + '!src/security-rules.d.ts', '!src/project-management.d.ts' ], }; @@ -70,7 +71,6 @@ const TEMPORARY_TYPING_EXCLUDES = [ '!lib/machine-learning/*.d.ts', '!lib/messaging/*.d.ts', '!lib/remote-config/*.d.ts', - '!lib/security-rules/*.d.ts', '!lib/storage/*.d.ts', '!lib/utils/*.d.ts' ]; diff --git a/src/security-rules/index.ts b/src/security-rules/index.ts new file mode 100644 index 0000000000..ecdae9d8f5 --- /dev/null +++ b/src/security-rules/index.ts @@ -0,0 +1,47 @@ +/*! + * Copyright 2020 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 { FirebaseApp } from '../firebase-app'; +import * as securityRulesApi from './security-rules'; +import * as firebaseAdmin from '../index'; + +export function securityRules(app?: FirebaseApp): securityRulesApi.SecurityRules { + if (typeof(app) === 'undefined') { + app = firebaseAdmin.app(); + } + return app.securityRules(); +} + +/** + * We must define a namespace to make the typings work correctly. Otherwise + * `admin.securityRules()` cannot be called like a function. Temporarily, + * admin.securityRules is used as the namespace name because we cannot barrel + * re-export the contents from security-rules, and we want it to + * match the namespacing in the re-export inside src/index.d.ts + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace admin.securityRules { + // See https://github.com/microsoft/TypeScript/issues/4336 + /* eslint-disable @typescript-eslint/no-unused-vars */ + // See https://github.com/typescript-eslint/typescript-eslint/issues/363 + export import RulesFile = securityRulesApi.RulesFile; + export import RulesetMetadata = securityRulesApi.RulesetMetadata; + export import RulesetMetadataList = securityRulesApi.RulesetMetadataList; + + /* eslint-disable @typescript-eslint/no-empty-interface */ + export interface Ruleset extends securityRulesApi.Ruleset {} + export interface SecurityRules extends securityRulesApi.SecurityRules {} +} diff --git a/src/security-rules/security-rules-api-client.ts b/src/security-rules/security-rules-api-client-internal.ts similarity index 99% rename from src/security-rules/security-rules-api-client.ts rename to src/security-rules/security-rules-api-client-internal.ts index 5048113825..50fdbc9cb8 100644 --- a/src/security-rules/security-rules-api-client.ts +++ b/src/security-rules/security-rules-api-client-internal.ts @@ -16,7 +16,7 @@ import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient } from '../utils/api-request'; import { PrefixedFirebaseError } from '../utils/error'; -import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './security-rules-utils'; +import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './security-rules-internal'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { FirebaseApp } from '../firebase-app'; diff --git a/src/security-rules/security-rules-utils.ts b/src/security-rules/security-rules-internal.ts similarity index 100% rename from src/security-rules/security-rules-utils.ts rename to src/security-rules/security-rules-internal.ts diff --git a/src/security-rules/security-rules.ts b/src/security-rules/security-rules.ts index 21da0eabab..89eda3825f 100644 --- a/src/security-rules/security-rules.ts +++ b/src/security-rules/security-rules.ts @@ -19,11 +19,14 @@ import { FirebaseApp } from '../firebase-app'; import * as validator from '../utils/validator'; import { SecurityRulesApiClient, RulesetResponse, RulesetContent, ListRulesetsResponse, -} from './security-rules-api-client'; -import { FirebaseSecurityRulesError } from './security-rules-utils'; +} from './security-rules-api-client-internal'; +import { FirebaseSecurityRulesError } from './security-rules-internal'; /** - * A source file containing some Firebase security rules. + * A source file containing some Firebase security rules. The content includes raw + * source code including text formatting, indentation and comments. Use the + * [`securityRules.createRulesFileFromSource()`](admin.securityRules.SecurityRules#createRulesFileFromSource) + * method to create new instances of this type. */ export interface RulesFile { readonly name: string; @@ -31,10 +34,18 @@ export interface RulesFile { } /** - * Additional metadata associated with a Ruleset. + * Required metadata associated with a ruleset. */ export interface RulesetMetadata { + /** + * Name of the `Ruleset` as a short string. This can be directly passed into APIs + * like [`securityRules.getRuleset()`](admin.securityRules.SecurityRules#getRuleset) + * and [`securityRules.deleteRuleset()`](admin.securityRules.SecurityRules#deleteRuleset). + */ readonly name: string; + /** + * Creation time of the `Ruleset` as a UTC timestamp string. + */ readonly createTime: string; } @@ -42,7 +53,13 @@ export interface RulesetMetadata { * A page of ruleset metadata. */ export interface RulesetMetadataList { + /** + * A batch of ruleset metadata. + */ readonly rulesets: RulesetMetadata[]; + /** + * The next page token if available. This is needed to retrieve the next batch. + */ readonly nextPageToken?: string; } @@ -97,7 +114,10 @@ export class Ruleset implements RulesetMetadata { } /** - * SecurityRules service bound to the provided app. + * The Firebase `SecurityRules` service interface. + * + * Do not call this constructor directly. Instead, use + * [`admin.securityRules()`](admin.securityRules#securityRules). */ export class SecurityRules implements FirebaseServiceInterface { @@ -235,12 +255,21 @@ export class SecurityRules implements FirebaseServiceInterface { } /** - * Creates a `RulesFile` with the given name and source. Throws if any of the arguments are invalid. This is a - * local operation, and does not involve any network API calls. + * Creates a {@link admin.securityRules.RulesFile `RuleFile`} with the given name + * and source. Throws an error if any of the arguments are invalid. This is a local + * operation, and does not involve any network API calls. * - * @param {string} name Name to assign to the rules file. - * @param {string|Buffer} source Contents of the rules file. - * @returns {RulesFile} A new rules file instance. + * @example + * ```javascript + * const source = '// Some rules source'; + * const rulesFile = admin.securityRules().createRulesFileFromSource( + * 'firestore.rules', source); + * ``` + * + * @param name Name to assign to the rules file. This is usually a short file name that + * helps identify the file in a ruleset. + * @param source Contents of the rules file. + * @return A new rules file instance. */ public createRulesFileFromSource(name: string, source: string | Buffer): RulesFile { if (!validator.isNonEmptyString(name)) { @@ -265,10 +294,11 @@ export class SecurityRules implements FirebaseServiceInterface { } /** - * Creates a new `Ruleset` from the given `RulesFile`. + * Creates a new {@link admin.securityRules.Ruleset `Ruleset`} from the given + * {@link admin.securityRules.RulesFile `RuleFile`}. * - * @param {RulesFile} file Rules file to include in the new Ruleset. - * @returns {Promise} A promise that fulfills with the newly created Ruleset. + * @param file Rules file to include in the new `Ruleset`. + * @returns A promise that fulfills with the newly created `Ruleset`. */ public createRuleset(file: RulesFile): Promise { const ruleset: RulesetContent = { @@ -284,24 +314,27 @@ export class SecurityRules implements FirebaseServiceInterface { } /** - * Deletes the Ruleset identified by the given name. The input name should be the short name string without - * the project ID prefix. For example, to delete the `projects/project-id/rulesets/my-ruleset`, pass the - * short name "my-ruleset". Rejects with a `not-found` error if the specified Ruleset cannot be found. + * Deletes the {@link admin.securityRules.Ruleset `Ruleset`} identified by the given + * name. The input name should be the short name string without the project ID + * prefix. For example, to delete the `projects/project-id/rulesets/my-ruleset`, + * pass the short name "my-ruleset". Rejects with a `not-found` error if the + * specified `Ruleset` cannot be found. * - * @param {string} name Name of the Ruleset to delete. - * @returns {Promise} A promise that fulfills when the Ruleset is deleted. + * @param name Name of the `Ruleset` to delete. + * @return A promise that fulfills when the `Ruleset` is deleted. */ public deleteRuleset(name: string): Promise { return this.client.deleteRuleset(name); } /** - * Retrieves a page of rulesets. + * Retrieves a page of ruleset metadata. * - * @param {number=} pageSize The page size, 100 if undefined. This is also the maximum allowed limit. - * @param {string=} nextPageToken The next page token. If not specified, returns rulesets starting - * without any offset. - * @returns {Promise} A promise that fulfills a page of rulesets. + * @param pageSize The page size, 100 if undefined. This is also the maximum allowed + * limit. + * @param nextPageToken The next page token. If not specified, returns rulesets + * starting without any offset. + * @return A promise that fulfills with a page of rulesets. */ public listRulesetMetadata(pageSize = 100, nextPageToken?: string): Promise { return this.client.listRulesets(pageSize, nextPageToken) diff --git a/test/unit/security-rules/security-rules-api-client.spec.ts b/test/unit/security-rules/security-rules-api-client.spec.ts index 81b4472dc2..3a3c37a892 100644 --- a/test/unit/security-rules/security-rules-api-client.spec.ts +++ b/test/unit/security-rules/security-rules-api-client.spec.ts @@ -19,8 +19,8 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client'; -import { FirebaseSecurityRulesError } from '../../../src/security-rules/security-rules-utils'; +import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client-internal'; +import { FirebaseSecurityRulesError } from '../../../src/security-rules/security-rules-internal'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; diff --git a/test/unit/security-rules/security-rules.spec.ts b/test/unit/security-rules/security-rules.spec.ts index 8946d1d685..fd81fa7fd2 100644 --- a/test/unit/security-rules/security-rules.spec.ts +++ b/test/unit/security-rules/security-rules.spec.ts @@ -22,8 +22,8 @@ import * as sinon from 'sinon'; import { SecurityRules } from '../../../src/security-rules/security-rules'; import { FirebaseApp } from '../../../src/firebase-app'; import * as mocks from '../../resources/mocks'; -import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client'; -import { FirebaseSecurityRulesError } from '../../../src/security-rules/security-rules-utils'; +import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client-internal'; +import { FirebaseSecurityRulesError } from '../../../src/security-rules/security-rules-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; const expect = chai.expect; From 24ac03fc9d36ee675719bf64cb1ff8cd7007f39b Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Fri, 7 Aug 2020 17:04:10 -0400 Subject: [PATCH 024/160] Allow FCM to auto-generate typings, separate internal vs external APIs (#982) --- gulpfile.js | 6 +- ...h-request.ts => batch-request-internal.ts} | 0 src/messaging/index.ts | 77 + ...t.ts => messaging-api-request-internal.ts} | 4 +- ...errors.ts => messaging-errors-internal.ts} | 0 src/messaging/messaging-internal.ts | 608 +++++++ src/messaging/messaging-types.ts | 1439 ++++++++++------- src/messaging/messaging.ts | 237 +-- test/unit/messaging/batch-requests.spec.ts | 2 +- test/unit/messaging/messaging.spec.ts | 5 +- 10 files changed, 1677 insertions(+), 701 deletions(-) rename src/messaging/{batch-request.ts => batch-request-internal.ts} (100%) create mode 100644 src/messaging/index.ts rename src/messaging/{messaging-api-request.ts => messaging-api-request-internal.ts} (98%) rename src/messaging/{messaging-errors.ts => messaging-errors-internal.ts} (100%) create mode 100644 src/messaging/messaging-internal.ts diff --git a/gulpfile.js b/gulpfile.js index ab23375bd5..a3ef4a5b86 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -56,7 +56,8 @@ var paths = { 'src/*.d.ts', '!src/instance-id.d.ts', '!src/security-rules.d.ts', - '!src/project-management.d.ts' + '!src/project-management.d.ts', + '!src/messaging.d.ts', ], }; @@ -69,10 +70,9 @@ const TEMPORARY_TYPING_EXCLUDES = [ '!lib/database/*.d.ts', '!lib/firestore/*.d.ts', '!lib/machine-learning/*.d.ts', - '!lib/messaging/*.d.ts', '!lib/remote-config/*.d.ts', '!lib/storage/*.d.ts', - '!lib/utils/*.d.ts' + '!lib/utils/*.d.ts', ]; // Create a separate project for buildProject that overrides the rootDir. diff --git a/src/messaging/batch-request.ts b/src/messaging/batch-request-internal.ts similarity index 100% rename from src/messaging/batch-request.ts rename to src/messaging/batch-request-internal.ts diff --git a/src/messaging/index.ts b/src/messaging/index.ts new file mode 100644 index 0000000000..e11697e4a1 --- /dev/null +++ b/src/messaging/index.ts @@ -0,0 +1,77 @@ +/*! + * Copyright 2020 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 { FirebaseApp } from '../firebase-app'; +import * as messagingApi from './messaging'; +import * as messagingTypesApi from './messaging-types'; +import * as firebaseAdmin from '../index'; + +export function messaging(app?: FirebaseApp): messagingApi.Messaging { + if (typeof(app) === 'undefined') { + app = firebaseAdmin.app(); + } + return app.messaging(); +} + +/** + * We must define a namespace to make the typings work correctly. Otherwise + * `admin.messaging()` cannot be called like a function. Temporarily, + * admin.messaging is used as the namespace name because we cannot barrel + * re-export the contents from messsaging, and we want it to + * match the namespacing in the re-export inside src/index.d.ts + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace admin.messaging { + // See https://github.com/microsoft/TypeScript/issues/4336 + /* eslint-disable @typescript-eslint/no-unused-vars */ + // See https://github.com/typescript-eslint/typescript-eslint/issues/363 + export import AndroidConfig = messagingTypesApi.AndroidConfig; + export import AndroidFcmOptions = messagingTypesApi.AndroidFcmOptions; + export import AndroidNotification = messagingTypesApi.AndroidNotification; + export import ApnsConfig = messagingTypesApi.ApnsConfig; + export import ApnsFcmOptions = messagingTypesApi.ApnsFcmOptions; + export import ApnsPayload = messagingTypesApi.ApnsPayload; + export import Aps = messagingTypesApi.Aps; + export import ApsAlert = messagingTypesApi.ApsAlert; + export import BatchResponse = messagingTypesApi.BatchResponse; + export import CriticalSound = messagingTypesApi.CriticalSound; + export import FcmOptions = messagingTypesApi.FcmOptions; + export import LightSettings = messagingTypesApi.LightSettings; + export import Message = messagingTypesApi.Message; + export import MessagingTopicManagementResponse = messagingTypesApi.MessagingTopicManagementResponse; + export import MulticastMessage = messagingTypesApi.MulticastMessage; + export import Notification = messagingTypesApi.Notification; + export import SendResponse = messagingTypesApi.SendResponse; + export import WebpushConfig = messagingTypesApi.WebpushConfig; + export import WebpushFcmOptions = messagingTypesApi.WebpushFcmOptions; + export import WebpushNotification = messagingTypesApi.WebpushNotification; + + // See https://github.com/microsoft/TypeScript/issues/4336 + // Allows for exposing classes as interfaces in typings + /* eslint-disable @typescript-eslint/no-empty-interface */ + export interface Messaging extends messagingApi.Messaging {} + + // Legacy API types. + export import DataMessagePayload = messagingTypesApi.DataMessagePayload; + export import MessagingConditionResponse = messagingTypesApi.MessagingConditionResponse; + export import MessagingDeviceGroupResponse = messagingTypesApi.MessagingDeviceGroupResponse; + export import MessagingDevicesResponse = messagingTypesApi.MessagingDevicesResponse; + export import MessagingDeviceResult = messagingTypesApi.MessagingDeviceResult; + export import MessagingOptions = messagingTypesApi.MessagingOptions; + export import MessagingPayload = messagingTypesApi.MessagingPayload; + export import MessagingTopicResponse = messagingTypesApi.MessagingTopicResponse; + export import NotificationMessagePayload = messagingTypesApi.NotificationMessagePayload; +} diff --git a/src/messaging/messaging-api-request.ts b/src/messaging/messaging-api-request-internal.ts similarity index 98% rename from src/messaging/messaging-api-request.ts rename to src/messaging/messaging-api-request-internal.ts index 65b9944a04..99c55cce66 100644 --- a/src/messaging/messaging-api-request.ts +++ b/src/messaging/messaging-api-request-internal.ts @@ -18,8 +18,8 @@ import { FirebaseApp } from '../firebase-app'; import { HttpMethod, AuthorizedHttpClient, HttpRequestConfig, HttpError, HttpResponse, } from '../utils/api-request'; -import { createFirebaseError, getErrorCode } from './messaging-errors'; -import { SubRequest, BatchRequestClient } from './batch-request'; +import { createFirebaseError, getErrorCode } from './messaging-errors-internal'; +import { SubRequest, BatchRequestClient } from './batch-request-internal'; import { SendResponse, BatchResponse } from './messaging-types'; import { getSdkVersion } from '../utils/index'; diff --git a/src/messaging/messaging-errors.ts b/src/messaging/messaging-errors-internal.ts similarity index 100% rename from src/messaging/messaging-errors.ts rename to src/messaging/messaging-errors-internal.ts diff --git a/src/messaging/messaging-internal.ts b/src/messaging/messaging-internal.ts new file mode 100644 index 0000000000..1979b2bc14 --- /dev/null +++ b/src/messaging/messaging-internal.ts @@ -0,0 +1,608 @@ +/*! + * Copyright 2020 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 { renameProperties } from '../utils/index'; +import { MessagingClientErrorCode, FirebaseMessagingError, } from '../utils/error'; +import { + AndroidConfig, AndroidFcmOptions, AndroidNotification, + ApsAlert, ApnsConfig, ApnsFcmOptions, + ApnsPayload, Aps, CriticalSound, + FcmOptions, LightSettings, + Message, Notification, WebpushConfig +} from './messaging-types'; + +import * as validator from '../utils/validator'; + +// Keys which are not allowed in the messaging data payload object. +export const BLACKLISTED_DATA_PAYLOAD_KEYS = ['from']; + +// Keys which are not allowed in the messaging options object. +export const BLACKLISTED_OPTIONS_KEYS = [ + 'condition', 'data', 'notification', 'registrationIds', 'registration_ids', 'to', +]; + +/** + * Checks if the given Message object is valid. Recursively validates all the child objects + * included in the message (android, apns, data etc.). If successful, transforms the message + * in place by renaming the keys to what's expected by the remote FCM service. + * + * @param {Message} Message An object to be validated. + */ +export function validateMessage(message: Message): void { + if (!validator.isNonNullObject(message)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'Message must be a non-null object'); + } + + const anyMessage = message as any; + if (anyMessage.topic) { + // If the topic name is prefixed, remove it. + if (anyMessage.topic.startsWith('/topics/')) { + anyMessage.topic = anyMessage.topic.replace(/^\/topics\//, ''); + } + // Checks for illegal characters and empty string. + if (!/^[a-zA-Z0-9-_.~%]+$/.test(anyMessage.topic)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'Malformed topic name'); + } + } + + const targets = [anyMessage.token, anyMessage.topic, anyMessage.condition]; + if (targets.filter((v) => validator.isNonEmptyString(v)).length !== 1) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'Exactly one of topic, token or condition is required'); + } + + validateStringMap(message.data, 'data'); + validateAndroidConfig(message.android); + validateWebpushConfig(message.webpush); + validateApnsConfig(message.apns); + validateFcmOptions(message.fcmOptions); + validateNotification(message.notification); +} + +/** + * Checks if the given object only contains strings as child values. + * + * @param {object} map An object to be validated. + * @param {string} label A label to be included in the errors thrown. + */ +function validateStringMap(map: { [key: string]: any } | undefined, label: string): void { + if (typeof map === 'undefined') { + return; + } else if (!validator.isNonNullObject(map)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, `${label} must be a non-null object`); + } + Object.keys(map).forEach((key) => { + if (!validator.isString(map[key])) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, `${label} must only contain string values`); + } + }); +} + +/** + * Checks if the given WebpushConfig object is valid. The object must have valid headers and data. + * + * @param {WebpushConfig} config An object to be validated. + */ +function validateWebpushConfig(config: WebpushConfig | undefined): void { + if (typeof config === 'undefined') { + return; + } else if (!validator.isNonNullObject(config)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'webpush must be a non-null object'); + } + validateStringMap(config.headers, 'webpush.headers'); + validateStringMap(config.data, 'webpush.data'); +} + +/** + * Checks if the given ApnsConfig object is valid. The object must have valid headers and a + * payload. + * + * @param {ApnsConfig} config An object to be validated. + */ +function validateApnsConfig(config: ApnsConfig | undefined): void { + if (typeof config === 'undefined') { + return; + } else if (!validator.isNonNullObject(config)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'apns must be a non-null object'); + } + validateStringMap(config.headers, 'apns.headers'); + validateApnsPayload(config.payload); + validateApnsFcmOptions(config.fcmOptions); +} + +/** + * Checks if the given ApnsFcmOptions object is valid. + * + * @param {ApnsFcmOptions} fcmOptions An object to be validated. + */ +function validateApnsFcmOptions(fcmOptions: ApnsFcmOptions | undefined): void { + if (typeof fcmOptions === 'undefined') { + return; + } else if (!validator.isNonNullObject(fcmOptions)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); + } + + if (typeof fcmOptions.imageUrl !== 'undefined' && + !validator.isURL(fcmOptions.imageUrl)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'imageUrl must be a valid URL string'); + } + + if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); + } + + const propertyMappings: { [key: string]: string } = { + imageUrl: 'image', + }; + Object.keys(propertyMappings).forEach((key) => { + if (key in fcmOptions && propertyMappings[key] in fcmOptions) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + `Multiple specifications for ${key} in ApnsFcmOptions`); + } + }); + renameProperties(fcmOptions, propertyMappings); +} + +/** + * Checks if the given FcmOptions object is valid. + * + * @param {FcmOptions} fcmOptions An object to be validated. + */ +function validateFcmOptions(fcmOptions: FcmOptions | undefined): void { + if (typeof fcmOptions === 'undefined') { + return; + } else if (!validator.isNonNullObject(fcmOptions)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); + } + + if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); + } +} + +/** + * Checks if the given Notification object is valid. + * + * @param {Notification} notification An object to be validated. + */ +function validateNotification(notification: Notification | undefined): void { + if (typeof notification === 'undefined') { + return; + } else if (!validator.isNonNullObject(notification)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'notification must be a non-null object'); + } + + if (typeof notification.imageUrl !== 'undefined' && !validator.isURL(notification.imageUrl)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'notification.imageUrl must be a valid URL string'); + } + + const propertyMappings: { [key: string]: string } = { + imageUrl: 'image', + }; + Object.keys(propertyMappings).forEach((key) => { + if (key in notification && propertyMappings[key] in notification) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + `Multiple specifications for ${key} in Notification`); + } + }); + renameProperties(notification, propertyMappings); +} + +/** + * Checks if the given ApnsPayload object is valid. The object must have a valid aps value. + * + * @param {ApnsPayload} payload An object to be validated. + */ +function validateApnsPayload(payload: ApnsPayload | undefined): void { + if (typeof payload === 'undefined') { + return; + } else if (!validator.isNonNullObject(payload)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload must be a non-null object'); + } + validateAps(payload.aps); +} + +/** + * Checks if the given Aps object is valid. The object must have a valid alert. If the validation + * is successful, transforms the input object by renaming the keys to valid APNS payload keys. + * + * @param {Aps} aps An object to be validated. + */ +function validateAps(aps: Aps): void { + if (typeof aps === 'undefined') { + return; + } else if (!validator.isNonNullObject(aps)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps must be a non-null object'); + } + validateApsAlert(aps.alert); + validateApsSound(aps.sound); + + const propertyMappings: { [key: string]: string } = { + contentAvailable: 'content-available', + mutableContent: 'mutable-content', + threadId: 'thread-id', + }; + Object.keys(propertyMappings).forEach((key) => { + if (key in aps && propertyMappings[key] in aps) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in Aps`); + } + }); + renameProperties(aps, propertyMappings); + + const contentAvailable = aps['content-available']; + if (typeof contentAvailable !== 'undefined' && contentAvailable !== 1) { + if (contentAvailable === true) { + aps['content-available'] = 1; + } else { + delete aps['content-available']; + } + } + + const mutableContent = aps['mutable-content']; + if (typeof mutableContent !== 'undefined' && mutableContent !== 1) { + if (mutableContent === true) { + aps['mutable-content'] = 1; + } else { + delete aps['mutable-content']; + } + } +} + +function validateApsSound(sound: string | CriticalSound | undefined): void { + if (typeof sound === 'undefined' || validator.isNonEmptyString(sound)) { + return; + } else if (!validator.isNonNullObject(sound)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'apns.payload.aps.sound must be a non-empty string or a non-null object'); + } + + if (!validator.isNonEmptyString(sound.name)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'apns.payload.aps.sound.name must be a non-empty string'); + } + const volume = sound.volume; + if (typeof volume !== 'undefined') { + if (!validator.isNumber(volume)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'apns.payload.aps.sound.volume must be a number'); + } + if (volume < 0 || volume > 1) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'apns.payload.aps.sound.volume must be in the interval [0, 1]'); + } + } + const soundObject = sound as { [key: string]: any }; + const key = 'critical'; + const critical = soundObject[key]; + if (typeof critical !== 'undefined' && critical !== 1) { + if (critical === true) { + soundObject[key] = 1; + } else { + delete soundObject[key]; + } + } +} + +/** + * Checks if the given alert object is valid. Alert could be a string or a complex object. + * If specified as an object, it must have valid localization parameters. If successful, transforms + * the input object by renaming the keys to valid APNS payload keys. + * + * @param {string | ApsAlert} alert An alert string or an object to be validated. + */ +function validateApsAlert(alert: string | ApsAlert | undefined): void { + if (typeof alert === 'undefined' || validator.isString(alert)) { + return; + } else if (!validator.isNonNullObject(alert)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'apns.payload.aps.alert must be a string or a non-null object'); + } + + const apsAlert: ApsAlert = alert as ApsAlert; + if (validator.isNonEmptyArray(apsAlert.locArgs) && + !validator.isNonEmptyString(apsAlert.locKey)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'apns.payload.aps.alert.locKey is required when specifying locArgs'); + } + if (validator.isNonEmptyArray(apsAlert.titleLocArgs) && + !validator.isNonEmptyString(apsAlert.titleLocKey)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'apns.payload.aps.alert.titleLocKey is required when specifying titleLocArgs'); + } + if (validator.isNonEmptyArray(apsAlert.subtitleLocArgs) && + !validator.isNonEmptyString(apsAlert.subtitleLocKey)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'apns.payload.aps.alert.subtitleLocKey is required when specifying subtitleLocArgs'); + } + + const propertyMappings = { + locKey: 'loc-key', + locArgs: 'loc-args', + titleLocKey: 'title-loc-key', + titleLocArgs: 'title-loc-args', + subtitleLocKey: 'subtitle-loc-key', + subtitleLocArgs: 'subtitle-loc-args', + actionLocKey: 'action-loc-key', + launchImage: 'launch-image', + }; + renameProperties(apsAlert, propertyMappings); +} + +/** + * Checks if the given AndroidConfig object is valid. The object must have valid ttl, data, + * and notification fields. If successful, transforms the input object by renaming keys to valid + * Android keys. Also transforms the ttl value to the format expected by FCM service. + * + * @param {AndroidConfig} config An object to be validated. + */ +function validateAndroidConfig(config: AndroidConfig | undefined): void { + if (typeof config === 'undefined') { + return; + } else if (!validator.isNonNullObject(config)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'android must be a non-null object'); + } + + if (typeof config.ttl !== 'undefined') { + if (!validator.isNumber(config.ttl) || config.ttl < 0) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'TTL must be a non-negative duration in milliseconds'); + } + const duration: string = transformMillisecondsToSecondsString(config.ttl); + (config as any).ttl = duration; + } + validateStringMap(config.data, 'android.data'); + validateAndroidNotification(config.notification); + validateAndroidFcmOptions(config.fcmOptions); + + const propertyMappings = { + collapseKey: 'collapse_key', + restrictedPackageName: 'restricted_package_name', + }; + renameProperties(config, propertyMappings); +} + +/** + * Checks if the given AndroidNotification object is valid. The object must have valid color and + * localization parameters. If successful, transforms the input object by renaming keys to valid + * Android keys. + * + * @param {AndroidNotification} notification An object to be validated. + */ +function validateAndroidNotification(notification: AndroidNotification | undefined): void { + if (typeof notification === 'undefined') { + return; + } else if (!validator.isNonNullObject(notification)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification must be a non-null object'); + } + + if (typeof notification.color !== 'undefined' && !/^#[0-9a-fA-F]{6}$/.test(notification.color)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.color must be in the form #RRGGBB'); + } + if (validator.isNonEmptyArray(notification.bodyLocArgs) && + !validator.isNonEmptyString(notification.bodyLocKey)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.bodyLocKey is required when specifying bodyLocArgs'); + } + if (validator.isNonEmptyArray(notification.titleLocArgs) && + !validator.isNonEmptyString(notification.titleLocKey)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.titleLocKey is required when specifying titleLocArgs'); + } + if (typeof notification.imageUrl !== 'undefined' && + !validator.isURL(notification.imageUrl)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.imageUrl must be a valid URL string'); + } + + if (typeof notification.eventTimestamp !== 'undefined') { + if (!(notification.eventTimestamp instanceof Date)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.eventTimestamp must be a valid `Date` object'); + } + // Convert timestamp to RFC3339 UTC "Zulu" format, example "2014-10-02T15:01:23.045123456Z" + const zuluTimestamp = notification.eventTimestamp.toISOString(); + (notification as any).eventTimestamp = zuluTimestamp; + } + + if (typeof notification.vibrateTimingsMillis !== 'undefined') { + if (!validator.isNonEmptyArray(notification.vibrateTimingsMillis)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.vibrateTimingsMillis must be a non-empty array of numbers'); + } + const vibrateTimings: string[] = []; + notification.vibrateTimingsMillis.forEach((value) => { + if (!validator.isNumber(value) || value < 0) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.vibrateTimingsMillis must be non-negative durations in milliseconds'); + } + const duration = transformMillisecondsToSecondsString(value); + vibrateTimings.push(duration); + }); + (notification as any).vibrateTimingsMillis = vibrateTimings; + } + + if (typeof notification.priority !== 'undefined') { + const priority = 'PRIORITY_' + notification.priority.toUpperCase(); + (notification as any).priority = priority; + } + + if (typeof notification.visibility !== 'undefined') { + const visibility = notification.visibility.toUpperCase(); + (notification as any).visibility = visibility; + } + + validateLightSettings(notification.lightSettings); + + const propertyMappings = { + clickAction: 'click_action', + bodyLocKey: 'body_loc_key', + bodyLocArgs: 'body_loc_args', + titleLocKey: 'title_loc_key', + titleLocArgs: 'title_loc_args', + channelId: 'channel_id', + imageUrl: 'image', + eventTimestamp: 'event_time', + localOnly: 'local_only', + priority: 'notification_priority', + vibrateTimingsMillis: 'vibrate_timings', + defaultVibrateTimings: 'default_vibrate_timings', + defaultSound: 'default_sound', + lightSettings: 'light_settings', + defaultLightSettings: 'default_light_settings', + notificationCount: 'notification_count', + }; + renameProperties(notification, propertyMappings); +} + +/** + * Checks if the given LightSettings object is valid. The object must have valid color and + * light on/off duration parameters. If successful, transforms the input object by renaming + * keys to valid Android keys. + * + * @param {LightSettings} lightSettings An object to be validated. + */ +function validateLightSettings(lightSettings?: LightSettings): void { + if (typeof lightSettings === 'undefined') { + return; + } else if (!validator.isNonNullObject(lightSettings)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings must be a non-null object'); + } + + if (!validator.isNumber(lightSettings.lightOnDurationMillis) || lightSettings.lightOnDurationMillis < 0) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.lightSettings.lightOnDurationMillis must be a non-negative duration in milliseconds'); + } + const durationOn = transformMillisecondsToSecondsString(lightSettings.lightOnDurationMillis); + (lightSettings as any).lightOnDurationMillis = durationOn; + + if (!validator.isNumber(lightSettings.lightOffDurationMillis) || lightSettings.lightOffDurationMillis < 0) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.lightSettings.lightOffDurationMillis must be a non-negative duration in milliseconds'); + } + const durationOff = transformMillisecondsToSecondsString(lightSettings.lightOffDurationMillis); + (lightSettings as any).lightOffDurationMillis = durationOff; + + if (!validator.isString(lightSettings.color) || + (!/^#[0-9a-fA-F]{6}$/.test(lightSettings.color) && !/^#[0-9a-fA-F]{8}$/.test(lightSettings.color))) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'android.notification.lightSettings.color must be in the form #RRGGBB or #RRGGBBAA format'); + } + const colorString = lightSettings.color.length === 7 ? lightSettings.color + 'FF' : lightSettings.color; + const rgb = /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/i.exec(colorString); + if (!rgb || rgb.length < 4) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INTERNAL_ERROR, + 'regex to extract rgba values from ' + colorString + ' failed.'); + } + const color = { + red: parseInt(rgb[1], 16) / 255.0, + green: parseInt(rgb[2], 16) / 255.0, + blue: parseInt(rgb[3], 16) / 255.0, + alpha: parseInt(rgb[4], 16) / 255.0, + }; + (lightSettings as any).color = color; + + const propertyMappings = { + lightOnDurationMillis: 'light_on_duration', + lightOffDurationMillis: 'light_off_duration', + }; + renameProperties(lightSettings, propertyMappings); +} + +/** + * Checks if the given AndroidFcmOptions object is valid. + * + * @param {AndroidFcmOptions} fcmOptions An object to be validated. + */ +function validateAndroidFcmOptions(fcmOptions: AndroidFcmOptions | undefined): void { + if (typeof fcmOptions === 'undefined') { + return; + } else if (!validator.isNonNullObject(fcmOptions)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); + } + + if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); + } +} + +/** + * Transforms milliseconds to the format expected by FCM service. + * Returns the duration in seconds with up to nine fractional + * digits, terminated by 's'. Example: "3.5s". + * + * @param {number} milliseconds The duration in milliseconds. + * @return {string} The resulting formatted string in seconds with up to nine fractional + * digits, terminated by 's'. + */ +function transformMillisecondsToSecondsString(milliseconds: number): string { + let duration: string; + const seconds = Math.floor(milliseconds / 1000); + const nanos = (milliseconds - seconds * 1000) * 1000000; + if (nanos > 0) { + let nanoString = nanos.toString(); + while (nanoString.length < 9) { + nanoString = '0' + nanoString; + } + duration = `${seconds}.${nanoString}s`; + } else { + duration = `${seconds}s`; + } + return duration; +} diff --git a/src/messaging/messaging-types.ts b/src/messaging/messaging-types.ts index 795d0c1fb2..165aaf6e48 100644 --- a/src/messaging/messaging-types.ts +++ b/src/messaging/messaging-types.ts @@ -14,15 +14,12 @@ * limitations under the License. */ -import { renameProperties } from '../utils/index'; import { - MessagingClientErrorCode, FirebaseMessagingError, FirebaseArrayIndexError, FirebaseError, + FirebaseArrayIndexError, FirebaseError, } from '../utils/error'; -import * as validator from '../utils/validator'; - interface BaseMessage { - data?: {[key: string]: string}; + data?: { [key: string]: string }; notification?: Notification; android?: AndroidConfig; webpush?: WebpushConfig; @@ -56,78 +53,270 @@ export interface MulticastMessage extends BaseMessage { tokens: string[]; } +/** + * A notification that can be included in {@link admin.messaging.Message}. + */ export interface Notification { + /** + * The title of the notification. + */ title?: string; + /** + * The notification body + */ body?: string; + /** + * URL of an image to be displayed in the notification. + */ imageUrl?: string; } +/** + * Represents platform-independent options for features provided by the FCM SDKs. + */ export interface FcmOptions { + /** + * The label associated with the message's analytics data. + */ analyticsLabel?: string; } +/** + * Represents the WebPush protocol options that can be included in an + * {@link admin.messaging.Message}. + */ export interface WebpushConfig { - headers?: {[key: string]: string}; - data?: {[key: string]: string}; + + /** + * A collection of WebPush headers. Header values must be strings. + * + * See [WebPush specification](https://tools.ietf.org/html/rfc8030#section-5) + * for supported headers. + */ + headers?: { [key: string]: string }; + + /** + * A collection of data fields. + */ + data?: { [key: string]: string }; + + /** + * A WebPush notification payload to be included in the message. + */ notification?: WebpushNotification; + + /** + * Options for features provided by the FCM SDK for Web. + */ fcmOptions?: WebpushFcmOptions; } +/** Represents options for features provided by the FCM SDK for Web + * (which are not part of the Webpush standard). + */ export interface WebpushFcmOptions { + + /** + * The link to open when the user clicks on the notification. + * For all URL values, HTTPS is required. + */ link?: string; } +/** + * Represents the WebPush-specific notification options that can be included in + * {@link admin.messaging.WebpushConfig}. This supports most of the standard + * options as defined in the Web Notification + * [specification](https://developer.mozilla.org/en-US/docs/Web/API/notification/Notification). + */ export interface WebpushNotification { + + /** + * Title text of the notification. + */ title?: string; + + /** + * An array of notification actions representing the actions + * available to the user when the notification is presented. + */ actions?: Array<{ + + /** + * An action available to the user when the notification is presented + */ action: string; + + /** + * Optional icon for a notification action. + */ icon?: string; + + /** + * Title of the notification action. + */ title: string; }>; + + /** + * URL of the image used to represent the notification when there is + * not enough space to display the notification itself. + */ badge?: string; + + /** + * Body text of the notification. + */ body?: string; + + /** + * Arbitrary data that you want associated with the notification. + * This can be of any data type. + */ data?: any; + + /** + * The direction in which to display the notification. Must be one + * of `auto`, `ltr` or `rtl`. + */ dir?: 'auto' | 'ltr' | 'rtl'; + + /** + * URL to the notification icon. + */ icon?: string; + + /** + * URL of an image to be displayed in the notification. + */ image?: string; + + /** + * The notification's language as a BCP 47 language tag. + */ lang?: string; + + /** + * A boolean specifying whether the user should be notified after a + * new notification replaces an old one. Defaults to false. + */ renotify?: boolean; + + /** + * Indicates that a notification should remain active until the user + * clicks or dismisses it, rather than closing automatically. + * Defaults to false. + */ requireInteraction?: boolean; + + /** + * A boolean specifying whether the notification should be silent. + * Defaults to false. + */ silent?: boolean; + + /** + * An identifying tag for the notification. + */ tag?: string; + + /** + * Timestamp of the notification. Refer to + * https://developer.mozilla.org/en-US/docs/Web/API/notification/timestamp + * for details. + */ timestamp?: number; + + /** + * A vibration pattern for the device's vibration hardware to emit + * when the notification fires. + */ vibrate?: number | number[]; [key: string]: any; } +/** + * Represents the APNs-specific options that can be included in an + * {@link admin.messaging.Message}. Refer to + * [Apple documentation](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html) + * for various headers and payload fields supported by APNs. + */ export interface ApnsConfig { - headers?: {[key: string]: string}; + /** + * A collection of APNs headers. Header values must be strings. + */ + headers?: { [key: string]: string }; + + /** + * An APNs payload to be included in the message. + */ payload?: ApnsPayload; + + /** + * Options for features provided by the FCM SDK for iOS. + */ fcmOptions?: ApnsFcmOptions; } +/** + * Represents the payload of an APNs message. Mainly consists of the `aps` + * dictionary. But may also contain other arbitrary custom keys. + */ export interface ApnsPayload { + + /** + * The `aps` dictionary to be included in the message. + */ aps: Aps; [customData: string]: object; } +/** + * Represents the [aps dictionary](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) + * that is part of APNs messages. + */ export interface Aps { + + /** + * Alert to be included in the message. This may be a string or an object of + * type `admin.messaging.ApsAlert`. + */ alert?: string | ApsAlert; + + /** + * Badge to be displayed with the message. Set to 0 to remove the badge. When + * not specified, the badge will remain unchanged. + */ badge?: number; + + /** + * Sound to be played with the message. + */ sound?: string | CriticalSound; + + /** + * Specifies whether to configure a background update notification. + */ contentAvailable?: boolean; + + /** + * Specifies whether to set the `mutable-content` property on the message + * so the clients can modify the notification via app extensions. + */ + mutableContent?: boolean; + + /** + * Type of the notification. + */ category?: string; + + /** + * An app-specific identifier for grouping notifications. + */ threadId?: string; - mutableContent?: boolean; [customData: string]: any; } -export interface CriticalSound { - critical?: boolean; - name: string; - volume?: number; -} - export interface ApsAlert { title?: string; subtitle?: string; @@ -142,107 +331,636 @@ export interface ApsAlert { launchImage?: string; } +/** + * Represents a critical sound configuration that can be included in the + * `aps` dictionary of an APNs payload. + */ +export interface CriticalSound { + + /** + * The critical alert flag. Set to `true` to enable the critical alert. + */ + critical?: boolean; + + /** + * The name of a sound file in the app's main bundle or in the `Library/Sounds` + * folder of the app's container directory. Specify the string "default" to play + * the system sound. + */ + name: string; + + /** + * The volume for the critical alert's sound. Must be a value between 0.0 + * (silent) and 1.0 (full volume). + */ + volume?: number; +} + +/** + * Represents options for features provided by the FCM SDK for iOS. + */ export interface ApnsFcmOptions { + + /** + * The label associated with the message's analytics data. + */ analyticsLabel?: string; + + /** + * URL of an image to be displayed in the notification. + */ imageUrl?: string; } + +/** + * Represents the Android-specific options that can be included in an + * {@link admin.messaging.Message}. + */ export interface AndroidConfig { + + /** + * Collapse key for the message. Collapse key serves as an identifier for a + * group of messages that can be collapsed, so that only the last message gets + * sent when delivery can be resumed. A maximum of four different collapse keys + * may be active at any given time. + */ collapseKey?: string; + + /** + * Priority of the message. Must be either `normal` or `high`. + */ priority?: ('high' | 'normal'); + + /** + * Time-to-live duration of the message in milliseconds. + */ ttl?: number; + + /** + * Package name of the application where the registration tokens must match + * in order to receive the message. + */ restrictedPackageName?: string; - data?: {[key: string]: string}; + + /** + * A collection of data fields to be included in the message. All values must + * be strings. When provided, overrides any data fields set on the top-level + * `admin.messaging.Message`.} + */ + data?: { [key: string]: string }; + + /** + * Android notification to be included in the message. + */ notification?: AndroidNotification; + + /** + * Options for features provided by the FCM SDK for Android. + */ fcmOptions?: AndroidFcmOptions; } +/** + * Represents the Android-specific notification options that can be included in + * {@link admin.messaging.AndroidConfig}. + */ export interface AndroidNotification { + /** + * Title of the Android notification. When provided, overrides the title set via + * `admin.messaging.Notification`. + */ title?: string; + + /** + * Body of the Android notification. When provided, overrides the body set via + * `admin.messaging.Notification`. + */ body?: string; + + /** + * Icon resource for the Android notification. + */ icon?: string; + + /** + * Notification icon color in `#rrggbb` format. + */ color?: string; + + /** + * File name of the sound to be played when the device receives the + * notification. + */ sound?: string; + + /** + * Notification tag. This is an identifier used to replace existing + * notifications in the notification drawer. If not specified, each request + * creates a new notification. + */ tag?: string; + + /** + * URL of an image to be displayed in the notification. + */ imageUrl?: string; + + /** + * Action associated with a user click on the notification. If specified, an + * activity with a matching Intent Filter is launched when a user clicks on the + * notification. + */ clickAction?: string; + + /** + * Key of the body string in the app's string resource to use to localize the + * body text. + * + */ bodyLocKey?: string; + + /** + * An array of resource keys that will be used in place of the format + * specifiers in `bodyLocKey`. + */ bodyLocArgs?: string[]; + + /** + * Key of the title string in the app's string resource to use to localize the + * title text. + */ titleLocKey?: string; + + /** + * An array of resource keys that will be used in place of the format + * specifiers in `titleLocKey`. + */ titleLocArgs?: string[]; + + /** + * The Android notification channel ID (new in Android O). The app must create + * a channel with this channel ID before any notification with this channel ID + * can be received. If you don't send this channel ID in the request, or if the + * channel ID provided has not yet been created by the app, FCM uses the channel + * ID specified in the app manifest. + */ channelId?: string; + + /** + * Sets the "ticker" text, which is sent to accessibility services. Prior to + * API level 21 (Lollipop), sets the text that is displayed in the status bar + * when the notification first arrives. + */ ticker?: string; + + /** + * When set to `false` or unset, the notification is automatically dismissed when + * the user clicks it in the panel. When set to `true`, the notification persists + * even when the user clicks it. + */ sticky?: boolean; + + /** + * For notifications that inform users about events with an absolute time reference, sets + * the time that the event in the notification occurred. Notifications + * in the panel are sorted by this time. + */ eventTimestamp?: Date; + + /** + * Sets whether or not this notification is relevant only to the current device. + * Some notifications can be bridged to other devices for remote display, such as + * a Wear OS watch. This hint can be set to recommend this notification not be bridged. + * See [Wear OS guides](https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging) + */ localOnly?: boolean; + + /** + * Sets the relative priority for this notification. Low-priority notifications + * may be hidden from the user in certain situations. Note this priority differs + * from `AndroidMessagePriority`. This priority is processed by the client after + * the message has been delivered. Whereas `AndroidMessagePriority` is an FCM concept + * that controls when the message is delivered. + */ priority?: ('min' | 'low' | 'default' | 'high' | 'max'); + + /** + * Sets the vibration pattern to use. Pass in an array of milliseconds to + * turn the vibrator on or off. The first value indicates the duration to wait before + * turning the vibrator on. The next value indicates the duration to keep the + * vibrator on. Subsequent values alternate between duration to turn the vibrator + * off and to turn the vibrator on. If `vibrate_timings` is set and `default_vibrate_timings` + * is set to `true`, the default value is used instead of the user-specified `vibrate_timings`. + */ vibrateTimingsMillis?: number[]; + + /** + * If set to `true`, use the Android framework's default vibrate pattern for the + * notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * If `default_vibrate_timings` is set to `true` and `vibrate_timings` is also set, + * the default value is used instead of the user-specified `vibrate_timings`. + */ defaultVibrateTimings?: boolean; + + /** + * If set to `true`, use the Android framework's default sound for the notification. + * Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + */ defaultSound?: boolean; + + /** + * Settings to control the notification's LED blinking rate and color if LED is + * available on the device. The total blinking time is controlled by the OS. + */ lightSettings?: LightSettings; + + /** + * If set to `true`, use the Android framework's default LED light settings + * for the notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * If `default_light_settings` is set to `true` and `light_settings` is also set, + * the user-specified `light_settings` is used instead of the default value. + */ defaultLightSettings?: boolean; + + /** + * Sets the visibility of the notification. Must be either `private`, `public`, + * or `secret`. If unspecified, defaults to `private`. + */ visibility?: ('private' | 'public' | 'secret'); + + /** + * Sets the number of items this notification represents. May be displayed as a + * badge count for Launchers that support badging. See [`NotificationBadge`(https://developer.android.com/training/notify-user/badges). + * For example, this might be useful if you're using just one notification to + * represent multiple new messages but you want the count here to represent + * the number of total new messages. If zero or unspecified, systems + * that support badging use the default, which is to increment a number + * displayed on the long-press menu each time a new notification arrives. + */ notificationCount?: number; } +/** + * Represents settings to control notification LED that can be included in + * {@link admin.messaging.AndroidNotification}. + */ export interface LightSettings { + /** + * Required. Sets color of the LED in `#rrggbb` or `#rrggbbaa` format. + */ color: string; + + /** + * Required. Along with `light_off_duration`, defines the blink rate of LED flashes. + */ lightOnDurationMillis: number; + + /** + * Required. Along with `light_on_duration`, defines the blink rate of LED flashes. + */ lightOffDurationMillis: number; } +/** + * Represents options for features provided by the FCM SDK for Android. + */ export interface AndroidFcmOptions { + + /** + * The label associated with the message's analytics data. + */ analyticsLabel?: string; } -/* Payload for data messages */ +/** + * Interface representing an FCM legacy API data message payload. Data + * messages let developers send up to 4KB of custom key-value pairs. The + * keys and values must both be strings. Keys can be any custom string, + * except for the following reserved strings: + * + * * `"from"` + * * Anything starting with `"google."`. + * + * See [Build send requests](/docs/cloud-messaging/send-message) + * for code samples and detailed documentation. + */ export interface DataMessagePayload { [key: string]: string; } -/* Payload for notification messages */ +/** + * Interface representing an FCM legacy API notification message payload. + * Notification messages let developers send up to 4KB of predefined + * key-value pairs. Accepted keys are outlined below. + * + * See [Build send requests](/docs/cloud-messaging/send-message) + * for code samples and detailed documentation. + */ export interface NotificationMessagePayload { + + /** + * Identifier used to replace existing notifications in the notification drawer. + * + * If not specified, each request creates a new notification. + * + * If specified and a notification with the same tag is already being shown, + * the new notification replaces the existing one in the notification drawer. + * + * **Platforms:** Android + */ tag?: string; + + /** + * The notification's body text. + * + * **Platforms:** iOS, Android, Web + */ body?: string; + + /** + * The notification's icon. + * + * **Android:** Sets the notification icon to `myicon` for drawable resource + * `myicon`. If you don't send this key in the request, FCM displays the + * launcher icon specified in your app manifest. + * + * **Web:** The URL to use for the notification's icon. + * + * **Platforms:** Android, Web + */ icon?: string; + + /** + * The value of the badge on the home screen app icon. + * + * If not specified, the badge is not changed. + * + * If set to `0`, the badge is removed. + * + * **Platforms:** iOS + */ badge?: string; + + /** + * The notification icon's color, expressed in `#rrggbb` format. + * + * **Platforms:** Android + */ color?: string; + + /** + * The sound to be played when the device receives a notification. Supports + * "default" for the default notification sound of the device or the filename of a + * sound resource bundled in the app. + * Sound files must reside in `/res/raw/`. + * + * **Platforms:** Android + */ sound?: string; + + /** + * The notification's title. + * + * **Platforms:** iOS, Android, Web + */ title?: string; + + /** + * The key to the body string in the app's string resources to use to localize + * the body text to the user's current localization. + * + * **iOS:** Corresponds to `loc-key` in the APNs payload. See + * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) + * and + * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) + * for more information. + * + * **Android:** See + * [String Resources](http://developer.android.com/guide/topics/resources/string-resource.html) * for more information. + * + * **Platforms:** iOS, Android + */ bodyLocKey?: string; + + /** + * Variable string values to be used in place of the format specifiers in + * `body_loc_key` to use to localize the body text to the user's current + * localization. + * + * The value should be a stringified JSON array. + * + * **iOS:** Corresponds to `loc-args` in the APNs payload. See + * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) + * and + * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) + * for more information. + * + * **Android:** See + * [Formatting and Styling](http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling) + * for more information. + * + * **Platforms:** iOS, Android + */ bodyLocArgs?: string; + + /** + * Action associated with a user click on the notification. If specified, an + * activity with a matching Intent Filter is launched when a user clicks on the + * notification. + * + * * **Platforms:** Android + */ clickAction?: string; + + /** + * The key to the title string in the app's string resources to use to localize + * the title text to the user's current localization. + * + * **iOS:** Corresponds to `title-loc-key` in the APNs payload. See + * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) + * and + * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) + * for more information. + * + * **Android:** See + * [String Resources](http://developer.android.com/guide/topics/resources/string-resource.html) + * for more information. + * + * **Platforms:** iOS, Android + */ titleLocKey?: string; + + /** + * Variable string values to be used in place of the format specifiers in + * `title_loc_key` to use to localize the title text to the user's current + * localization. + * + * The value should be a stringified JSON array. + * + * **iOS:** Corresponds to `title-loc-args` in the APNs payload. See + * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) + * and + * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) + * for more information. + * + * **Android:** See + * [Formatting and Styling](http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling) + * for more information. + * + * **Platforms:** iOS, Android + */ titleLocArgs?: string; - [other: string]: string | undefined; + [key: string]: string | undefined; } -/* Composite messaging payload (data and notification payloads are both optional) */ +/** + * Interface representing a Firebase Cloud Messaging message payload. One or + * both of the `data` and `notification` keys are required. + * + * See + * [Build send requests](/docs/cloud-messaging/send-message) + * for code samples and detailed documentation. + */ export interface MessagingPayload { + + /** + * The data message payload. + */ data?: DataMessagePayload; + + /** + * The notification message payload. + */ notification?: NotificationMessagePayload; } -/* Options that can passed along with messages */ +/** + * Interface representing the options that can be provided when sending a + * message via the FCM legacy APIs. + * + * See [Build send requests](/docs/cloud-messaging/send-message) + * for code samples and detailed documentation. + */ export interface MessagingOptions { + + /** + * Whether or not the message should actually be sent. When set to `true`, + * allows developers to test a request without actually sending a message. When + * set to `false`, the message will be sent. + * + * **Default value:** `false` + */ dryRun?: boolean; + + /** + * The priority of the message. Valid values are `"normal"` and `"high".` On + * iOS, these correspond to APNs priorities `5` and `10`. + * + * By default, notification messages are sent with high priority, and data + * messages are sent with normal priority. Normal priority optimizes the client + * app's battery consumption and should be used unless immediate delivery is + * required. For messages with normal priority, the app may receive the message + * with unspecified delay. + * + * When a message is sent with high priority, it is sent immediately, and the + * app can wake a sleeping device and open a network connection to your server. + * + * For more information, see + * [Setting the priority of a message](/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message). + * + * **Default value:** `"high"` for notification messages, `"normal"` for data + * messages + */ priority?: string; + + /** + * How long (in seconds) the message should be kept in FCM storage if the device + * is offline. The maximum time to live supported is four weeks, and the default + * value is also four weeks. For more information, see + * [Setting the lifespan of a message](/docs/cloud-messaging/concept-options#ttl). + * + * **Default value:** `2419200` (representing four weeks, in seconds) + */ timeToLive?: number; + + /** + * String identifying a group of messages (for example, "Updates Available") + * that can be collapsed, so that only the last message gets sent when delivery + * can be resumed. This is used to avoid sending too many of the same messages + * when the device comes back online or becomes active. + * + * There is no guarantee of the order in which messages get sent. + * + * A maximum of four different collapse keys is allowed at any given time. This + * means FCM server can simultaneously store four different + * send-to-sync messages per client app. If you exceed this number, there is no + * guarantee which four collapse keys the FCM server will keep. + * + * **Default value:** None + */ collapseKey?: string; + + /** + * On iOS, use this field to represent `mutable-content` in the APNs payload. + * When a notification is sent and this is set to `true`, the content of the + * notification can be modified before it is displayed, using a + * [Notification Service app extension](https://developer.apple.com/reference/usernotifications/unnotificationserviceextension) + * + * On Android and Web, this parameter will be ignored. + * + * **Default value:** `false` + */ mutableContent?: boolean; + + /** + * On iOS, use this field to represent `content-available` in the APNs payload. + * When a notification or data message is sent and this is set to `true`, an + * inactive client app is awoken. On Android, data messages wake the app by + * default. On Chrome, this flag is currently not supported. + * + * **Default value:** `false` + */ contentAvailable?: boolean; + + /** + * The package name of the application which the registration tokens must match + * in order to receive the message. + * + * **Default value:** None + */ restrictedPackageName?: string; - [other: string]: any; + [key: string]: any | undefined; } /* Individual status response payload from single devices */ export interface MessagingDeviceResult { + /** + * The error that occurred when processing the message for the recipient. + */ error?: FirebaseError; + + /** + * A unique ID for the successfully processed message. + */ messageId?: string; + + /** + * The canonical registration token for the client app that the message was + * processed and sent to. You should use this value as the registration token + * for future requests. Otherwise, future messages might be rejected. + */ canonicalRegistrationToken?: string; } -/* Response payload from sending to a single device ID or array of device IDs */ +/** + * Interface representing the status of a message sent to an individual device + * via the FCM legacy APIs. + * + * See + * [Send to individual devices](/docs/cloud-messaging/admin/send-messages#send_to_individual_devices) + * for code samples and detailed documentation. + */ export interface MessagingDevicesResponse { canonicalRegistrationTokenCount: number; failureCount: number; @@ -251,614 +969,141 @@ export interface MessagingDevicesResponse { successCount: number; } -/* Response payload from sending to a device group */ -export interface MessagingDeviceGroupResponse { - successCount: number; - failureCount: number; - failedRegistrationTokens: string[]; -} - -/* Response payload from sending to a topic */ -export interface MessagingTopicResponse { - messageId: number; -} - -/* Response payload from sending to a condition */ -export interface MessagingConditionResponse { - messageId: number; -} - - -/* Response payload from sending to a single registration token or array of registration tokens */ -export interface MessagingTopicManagementResponse { - failureCount: number; - successCount: number; - errors: FirebaseArrayIndexError[]; -} - -/* Response from sending a batch of messages. */ -export interface BatchResponse { - responses: SendResponse[]; - failureCount: number; - successCount: number; -} - -/* The result of a sub request sent in a batch. */ -export interface SendResponse { - success: boolean; - messageId?: string; - error?: FirebaseError; -} - /** - * Checks if the given Message object is valid. Recursively validates all the child objects - * included in the message (android, apns, data etc.). If successful, transforms the message - * in place by renaming the keys to what's expected by the remote FCM service. + * Interface representing the server response from the + * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDeviceGroup `sendToDeviceGroup()`} + * method. * - * @param {Message} Message An object to be validated. + * See + * [Send messages to device groups](/docs/cloud-messaging/send-message?authuser=0#send_messages_to_device_groups) + * for code samples and detailed documentation. */ -export function validateMessage(message: Message): void { - if (!validator.isNonNullObject(message)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'Message must be a non-null object'); - } - - const anyMessage = message as any; - if (anyMessage.topic) { - // If the topic name is prefixed, remove it. - if (anyMessage.topic.startsWith('/topics/')) { - anyMessage.topic = anyMessage.topic.replace(/^\/topics\//, ''); - } - // Checks for illegal characters and empty string. - if (!/^[a-zA-Z0-9-_.~%]+$/.test(anyMessage.topic)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'Malformed topic name'); - } - } - - const targets = [anyMessage.token, anyMessage.topic, anyMessage.condition]; - if (targets.filter((v) => validator.isNonEmptyString(v)).length !== 1) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'Exactly one of topic, token or condition is required'); - } - - validateStringMap(message.data, 'data'); - validateAndroidConfig(message.android); - validateWebpushConfig(message.webpush); - validateApnsConfig(message.apns); - validateFcmOptions(message.fcmOptions); - validateNotification(message.notification); -} +export interface MessagingDeviceGroupResponse { -/** - * Checks if the given object only contains strings as child values. - * - * @param {object} map An object to be validated. - * @param {string} label A label to be included in the errors thrown. - */ -function validateStringMap(map: {[key: string]: any} | undefined, label: string): void { - if (typeof map === 'undefined') { - return; - } else if (!validator.isNonNullObject(map)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, `${label} must be a non-null object`); - } - Object.keys(map).forEach((key) => { - if (!validator.isString(map[key])) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, `${label} must only contain string values`); - } - }); -} + /** + * The number of messages that could not be processed and resulted in an error. + */ + successCount: number; -/** - * Checks if the given WebpushConfig object is valid. The object must have valid headers and data. - * - * @param {WebpushConfig} config An object to be validated. - */ -function validateWebpushConfig(config: WebpushConfig | undefined): void { - if (typeof config === 'undefined') { - return; - } else if (!validator.isNonNullObject(config)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'webpush must be a non-null object'); - } - validateStringMap(config.headers, 'webpush.headers'); - validateStringMap(config.data, 'webpush.data'); -} + /** + * The number of messages that could not be processed and resulted in an error. + */ + failureCount: number; -/** - * Checks if the given ApnsConfig object is valid. The object must have valid headers and a - * payload. - * - * @param {ApnsConfig} config An object to be validated. - */ -function validateApnsConfig(config: ApnsConfig | undefined): void { - if (typeof config === 'undefined') { - return; - } else if (!validator.isNonNullObject(config)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'apns must be a non-null object'); - } - validateStringMap(config.headers, 'apns.headers'); - validateApnsPayload(config.payload); - validateApnsFcmOptions(config.fcmOptions); + /** + * An array of registration tokens that failed to receive the message. + */ + failedRegistrationTokens: string[]; } /** - * Checks if the given ApnsFcmOptions object is valid. + * Interface representing the server response from the legacy + * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToTopic `sendToTopic()`} method. * - * @param {ApnsFcmOptions} fcmOptions An object to be validated. + * See + * [Send to a topic](/docs/cloud-messaging/admin/send-messages#send_to_a_topic) + * for code samples and detailed documentation. */ -function validateApnsFcmOptions(fcmOptions: ApnsFcmOptions | undefined): void { - if (typeof fcmOptions === 'undefined') { - return; - } else if (!validator.isNonNullObject(fcmOptions)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); - } - - if (typeof fcmOptions.imageUrl !== 'undefined' && - !validator.isURL(fcmOptions.imageUrl)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'imageUrl must be a valid URL string'); - } - - if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); - } - - const propertyMappings: {[key: string]: string} = { - imageUrl: 'image', - }; - Object.keys(propertyMappings).forEach((key) => { - if (key in fcmOptions && propertyMappings[key] in fcmOptions) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - `Multiple specifications for ${key} in ApnsFcmOptions`); - } - }); - renameProperties(fcmOptions, propertyMappings); +export interface MessagingTopicResponse { + /** + * The message ID for a successfully received request which FCM will attempt to + * deliver to all subscribed devices. + */ + messageId: number; } /** - * Checks if the given FcmOptions object is valid. + * Interface representing the server response from the legacy + * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToCondition `sendToCondition()`} method. * - * @param {FcmOptions} fcmOptions An object to be validated. + * See + * [Send to a condition](/docs/cloud-messaging/admin/send-messages#send_to_a_condition) + * for code samples and detailed documentation. */ -function validateFcmOptions(fcmOptions: FcmOptions | undefined): void { - if (typeof fcmOptions === 'undefined') { - return; - } else if (!validator.isNonNullObject(fcmOptions)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); - } - - if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); - } +export interface MessagingConditionResponse { + /** + * The message ID for a successfully received request which FCM will attempt to + * deliver to all subscribed devices. + */ + messageId: number; } /** - * Checks if the given Notification object is valid. + * Interface representing the server response from the + * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#subscribeToTopic `subscribeToTopic()`} and + * {@link + * admin.messaging.Messaging#unsubscribeFromTopic + * `unsubscribeFromTopic()`} + * methods. * - * @param {Notification} notification An object to be validated. + * See + * [Manage topics from the server](/docs/cloud-messaging/manage-topics) + * for code samples and detailed documentation. */ -function validateNotification(notification: Notification | undefined): void { - if (typeof notification === 'undefined') { - return; - } else if (!validator.isNonNullObject(notification)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'notification must be a non-null object'); - } - - if (typeof notification.imageUrl !== 'undefined' && !validator.isURL(notification.imageUrl)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'notification.imageUrl must be a valid URL string'); - } - - const propertyMappings: {[key: string]: string} = { - imageUrl: 'image', - }; - Object.keys(propertyMappings).forEach((key) => { - if (key in notification && propertyMappings[key] in notification) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - `Multiple specifications for ${key} in Notification`); - } - }); - renameProperties(notification, propertyMappings); -} +export interface MessagingTopicManagementResponse { + /** + * The number of registration tokens that could not be subscribed to the topic + * and resulted in an error. + */ + failureCount: number; -/** - * Checks if the given ApnsPayload object is valid. The object must have a valid aps value. - * - * @param {ApnsPayload} payload An object to be validated. - */ -function validateApnsPayload(payload: ApnsPayload | undefined): void { - if (typeof payload === 'undefined') { - return; - } else if (!validator.isNonNullObject(payload)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload must be a non-null object'); - } - validateAps(payload.aps); -} + /** + * The number of registration tokens that were successfully subscribed to the + * topic. + */ + successCount: number; -/** - * Checks if the given Aps object is valid. The object must have a valid alert. If the validation - * is successful, transforms the input object by renaming the keys to valid APNS payload keys. - * - * @param {Aps} aps An object to be validated. - */ -function validateAps(aps: Aps): void { - if (typeof aps === 'undefined') { - return; - } else if (!validator.isNonNullObject(aps)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps must be a non-null object'); - } - validateApsAlert(aps.alert); - validateApsSound(aps.sound); - - const propertyMappings: {[key: string]: string} = { - contentAvailable: 'content-available', - mutableContent: 'mutable-content', - threadId: 'thread-id', - }; - Object.keys(propertyMappings).forEach((key) => { - if (key in aps && propertyMappings[key] in aps) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in Aps`); - } - }); - renameProperties(aps, propertyMappings); - - const contentAvailable = aps['content-available']; - if (typeof contentAvailable !== 'undefined' && contentAvailable !== 1) { - if (contentAvailable === true) { - aps['content-available'] = 1; - } else { - delete aps['content-available']; - } - } - - const mutableContent = aps['mutable-content']; - if (typeof mutableContent !== 'undefined' && mutableContent !== 1) { - if (mutableContent === true) { - aps['mutable-content'] = 1; - } else { - delete aps['mutable-content']; - } - } -} - -function validateApsSound(sound: string | CriticalSound | undefined): void { - if (typeof sound === 'undefined' || validator.isNonEmptyString(sound)) { - return; - } else if (!validator.isNonNullObject(sound)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'apns.payload.aps.sound must be a non-empty string or a non-null object'); - } - - if (!validator.isNonEmptyString(sound.name)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'apns.payload.aps.sound.name must be a non-empty string'); - } - const volume = sound.volume; - if (typeof volume !== 'undefined') { - if (!validator.isNumber(volume)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'apns.payload.aps.sound.volume must be a number'); - } - if (volume < 0 || volume > 1) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'apns.payload.aps.sound.volume must be in the interval [0, 1]'); - } - } - const soundObject = sound as {[key: string]: any}; - const key = 'critical'; - const critical = soundObject[key]; - if (typeof critical !== 'undefined' && critical !== 1) { - if (critical === true) { - soundObject[key] = 1; - } else { - delete soundObject[key]; - } - } + /** + * An array of errors corresponding to the provided registration token(s). The + * length of this array will be equal to [`failureCount`](#failureCount). + */ + errors: FirebaseArrayIndexError[]; } /** - * Checks if the given alert object is valid. Alert could be a string or a complex object. - * If specified as an object, it must have valid localization parameters. If successful, transforms - * the input object by renaming the keys to valid APNS payload keys. - * - * @param {string | ApsAlert} alert An alert string or an object to be validated. + * Interface representing the server response from the + * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendAll `sendAll()`} and + * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendMulticast `sendMulticast()`} methods. */ -function validateApsAlert(alert: string | ApsAlert | undefined): void { - if (typeof alert === 'undefined' || validator.isString(alert)) { - return; - } else if (!validator.isNonNullObject(alert)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'apns.payload.aps.alert must be a string or a non-null object'); - } - - const apsAlert: ApsAlert = alert as ApsAlert; - if (validator.isNonEmptyArray(apsAlert.locArgs) && - !validator.isNonEmptyString(apsAlert.locKey)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'apns.payload.aps.alert.locKey is required when specifying locArgs'); - } - if (validator.isNonEmptyArray(apsAlert.titleLocArgs) && - !validator.isNonEmptyString(apsAlert.titleLocKey)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'apns.payload.aps.alert.titleLocKey is required when specifying titleLocArgs'); - } - if (validator.isNonEmptyArray(apsAlert.subtitleLocArgs) && - !validator.isNonEmptyString(apsAlert.subtitleLocKey)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'apns.payload.aps.alert.subtitleLocKey is required when specifying subtitleLocArgs'); - } - - const propertyMappings = { - locKey: 'loc-key', - locArgs: 'loc-args', - titleLocKey: 'title-loc-key', - titleLocArgs: 'title-loc-args', - subtitleLocKey: 'subtitle-loc-key', - subtitleLocArgs: 'subtitle-loc-args', - actionLocKey: 'action-loc-key', - launchImage: 'launch-image', - }; - renameProperties(apsAlert, propertyMappings); -} +export interface BatchResponse { -/** - * Checks if the given AndroidConfig object is valid. The object must have valid ttl, data, - * and notification fields. If successful, transforms the input object by renaming keys to valid - * Android keys. Also transforms the ttl value to the format expected by FCM service. - * - * @param {AndroidConfig} config An object to be validated. - */ -function validateAndroidConfig(config: AndroidConfig | undefined): void { - if (typeof config === 'undefined') { - return; - } else if (!validator.isNonNullObject(config)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'android must be a non-null object'); - } - - if (typeof config.ttl !== 'undefined') { - if (!validator.isNumber(config.ttl) || config.ttl < 0) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'TTL must be a non-negative duration in milliseconds'); - } - const duration: string = transformMillisecondsToSecondsString(config.ttl); - (config as any).ttl = duration; - } - validateStringMap(config.data, 'android.data'); - validateAndroidNotification(config.notification); - validateAndroidFcmOptions(config.fcmOptions); - - const propertyMappings = { - collapseKey: 'collapse_key', - restrictedPackageName: 'restricted_package_name', - }; - renameProperties(config, propertyMappings); -} + /** + * An array of responses, each corresponding to a message. + */ + responses: SendResponse[]; -/** - * Checks if the given AndroidNotification object is valid. The object must have valid color and - * localization parameters. If successful, transforms the input object by renaming keys to valid - * Android keys. - * - * @param {AndroidNotification} notification An object to be validated. - */ -function validateAndroidNotification(notification: AndroidNotification | undefined): void { - if (typeof notification === 'undefined') { - return; - } else if (!validator.isNonNullObject(notification)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification must be a non-null object'); - } - - if (typeof notification.color !== 'undefined' && !/^#[0-9a-fA-F]{6}$/.test(notification.color)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.color must be in the form #RRGGBB'); - } - if (validator.isNonEmptyArray(notification.bodyLocArgs) && - !validator.isNonEmptyString(notification.bodyLocKey)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'android.notification.bodyLocKey is required when specifying bodyLocArgs'); - } - if (validator.isNonEmptyArray(notification.titleLocArgs) && - !validator.isNonEmptyString(notification.titleLocKey)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'android.notification.titleLocKey is required when specifying titleLocArgs'); - } - if (typeof notification.imageUrl !== 'undefined' && - !validator.isURL(notification.imageUrl)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'android.notification.imageUrl must be a valid URL string'); - } - - if (typeof notification.eventTimestamp !== 'undefined') { - if (!(notification.eventTimestamp instanceof Date)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.eventTimestamp must be a valid `Date` object'); - } - // Convert timestamp to RFC3339 UTC "Zulu" format, example "2014-10-02T15:01:23.045123456Z" - const zuluTimestamp = notification.eventTimestamp.toISOString(); - (notification as any).eventTimestamp = zuluTimestamp; - } - - if (typeof notification.vibrateTimingsMillis !== 'undefined') { - if (!validator.isNonEmptyArray(notification.vibrateTimingsMillis)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'android.notification.vibrateTimingsMillis must be a non-empty array of numbers'); - } - const vibrateTimings: string[] = []; - notification.vibrateTimingsMillis.forEach((value) => { - if (!validator.isNumber(value) || value < 0) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'android.notification.vibrateTimingsMillis must be non-negative durations in milliseconds'); - } - const duration = transformMillisecondsToSecondsString(value); - vibrateTimings.push(duration); - }); - (notification as any).vibrateTimingsMillis = vibrateTimings; - } - - if (typeof notification.priority !== 'undefined') { - const priority = 'PRIORITY_' + notification.priority.toUpperCase(); - (notification as any).priority = priority; - } - - if (typeof notification.visibility !== 'undefined') { - const visibility = notification.visibility.toUpperCase(); - (notification as any).visibility = visibility; - } - - validateLightSettings(notification.lightSettings); - - const propertyMappings = { - clickAction: 'click_action', - bodyLocKey: 'body_loc_key', - bodyLocArgs: 'body_loc_args', - titleLocKey: 'title_loc_key', - titleLocArgs: 'title_loc_args', - channelId: 'channel_id', - imageUrl: 'image', - eventTimestamp: 'event_time', - localOnly: 'local_only', - priority: 'notification_priority', - vibrateTimingsMillis: 'vibrate_timings', - defaultVibrateTimings: 'default_vibrate_timings', - defaultSound: 'default_sound', - lightSettings: 'light_settings', - defaultLightSettings: 'default_light_settings', - notificationCount: 'notification_count', - }; - renameProperties(notification, propertyMappings); -} + /** + * The number of messages that were successfully handed off for sending. + */ + successCount: number; -/** - * Checks if the given LightSettings object is valid. The object must have valid color and - * light on/off duration parameters. If successful, transforms the input object by renaming - * keys to valid Android keys. - * - * @param {LightSettings} lightSettings An object to be validated. - */ -function validateLightSettings(lightSettings?: LightSettings): void { - if (typeof lightSettings === 'undefined') { - return; - } else if (!validator.isNonNullObject(lightSettings)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings must be a non-null object'); - } - - if (!validator.isNumber(lightSettings.lightOnDurationMillis) || lightSettings.lightOnDurationMillis < 0) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'android.notification.lightSettings.lightOnDurationMillis must be a non-negative duration in milliseconds'); - } - const durationOn = transformMillisecondsToSecondsString(lightSettings.lightOnDurationMillis); - (lightSettings as any).lightOnDurationMillis = durationOn; - - if (!validator.isNumber(lightSettings.lightOffDurationMillis) || lightSettings.lightOffDurationMillis < 0) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'android.notification.lightSettings.lightOffDurationMillis must be a non-negative duration in milliseconds'); - } - const durationOff = transformMillisecondsToSecondsString(lightSettings.lightOffDurationMillis); - (lightSettings as any).lightOffDurationMillis = durationOff; - - if (!validator.isString(lightSettings.color) || - (!/^#[0-9a-fA-F]{6}$/.test(lightSettings.color) && !/^#[0-9a-fA-F]{8}$/.test(lightSettings.color))) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, - 'android.notification.lightSettings.color must be in the form #RRGGBB or #RRGGBBAA format'); - } - const colorString = lightSettings.color.length === 7 ? lightSettings.color + 'FF' : lightSettings.color; - const rgb = /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/i.exec(colorString); - if (!rgb || rgb.length < 4) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INTERNAL_ERROR, - 'regex to extract rgba values from ' + colorString + ' failed.'); - } - const color = { - red: parseInt(rgb[1], 16) / 255.0, - green: parseInt(rgb[2], 16) / 255.0, - blue: parseInt(rgb[3], 16) / 255.0, - alpha: parseInt(rgb[4], 16) / 255.0, - }; - (lightSettings as any).color = color; - - const propertyMappings = { - lightOnDurationMillis: 'light_on_duration', - lightOffDurationMillis: 'light_off_duration', - }; - renameProperties(lightSettings, propertyMappings); + /** + * The number of messages that resulted in errors when sending. + */ + failureCount: number; } /** - * Checks if the given AndroidFcmOptions object is valid. - * - * @param {AndroidFcmOptions} fcmOptions An object to be validated. + * Interface representing the status of an individual message that was sent as + * part of a batch request. */ -function validateAndroidFcmOptions(fcmOptions: AndroidFcmOptions | undefined): void { - if (typeof fcmOptions === 'undefined') { - return; - } else if (!validator.isNonNullObject(fcmOptions)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); - } +export interface SendResponse { + /** + * A boolean indicating if the message was successfully handed off to FCM or + * not. When true, the `messageId` attribute is guaranteed to be set. When + * false, the `error` attribute is guaranteed to be set. + */ + success: boolean; - if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); - } -} + /** + * A unique message ID string, if the message was handed off to FCM for + * delivery. + * + */ + messageId?: string; -/** - * Transforms milliseconds to the format expected by FCM service. - * Returns the duration in seconds with up to nine fractional - * digits, terminated by 's'. Example: "3.5s". - * - * @param {number} milliseconds The duration in milliseconds. - * @return {string} The resulting formatted string in seconds with up to nine fractional - * digits, terminated by 's'. - */ -function transformMillisecondsToSecondsString(milliseconds: number): string { - let duration: string; - const seconds = Math.floor(milliseconds / 1000); - const nanos = (milliseconds - seconds * 1000) * 1000000; - if (nanos > 0) { - let nanoString = nanos.toString(); - while (nanoString.length < 9) { - nanoString = '0' + nanoString; - } - duration = `${seconds}.${nanoString}s`; - } else { - duration = `${seconds}s`; - } - return duration; + /** + * An error, if the message was not handed off to FCM successfully. + */ + error?: FirebaseError; } diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index e9eab2b43e..eec16df661 100644 --- a/src/messaging/messaging.ts +++ b/src/messaging/messaging.ts @@ -16,14 +16,15 @@ import { FirebaseApp } from '../firebase-app'; import { deepCopy, deepExtend } from '../utils/deep-copy'; -import { SubRequest } from './batch-request'; +import { SubRequest } from './batch-request-internal'; +import { validateMessage, BLACKLISTED_DATA_PAYLOAD_KEYS, BLACKLISTED_OPTIONS_KEYS } from './messaging-internal'; import { - Message, validateMessage, MessagingDevicesResponse, + Message, MessagingDevicesResponse, MessagingDeviceGroupResponse, MessagingTopicManagementResponse, MessagingPayload, MessagingOptions, MessagingTopicResponse, MessagingConditionResponse, BatchResponse, MulticastMessage, DataMessagePayload, NotificationMessagePayload, } from './messaging-types'; -import { FirebaseMessagingRequestHandler } from './messaging-api-request'; +import { FirebaseMessagingRequestHandler } from './messaging-api-request-internal'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError, @@ -94,14 +95,6 @@ const MESSAGING_CONDITION_RESPONSE_KEYS_MAP = { message_id: 'messageId', }; -// Keys which are not allowed in the messaging data payload object. -export const BLACKLISTED_DATA_PAYLOAD_KEYS = ['from']; - -// Keys which are not allowed in the messaging options object. -export const BLACKLISTED_OPTIONS_KEYS = [ - 'condition', 'data', 'notification', 'registrationIds', 'registration_ids', 'to', -]; - /** * Maps a raw FCM server response to a MessagingDevicesResponse object. * @@ -212,8 +205,17 @@ export class Messaging implements FirebaseServiceInterface { private readonly messagingRequestHandler: FirebaseMessagingRequestHandler; /** - * @param {FirebaseApp} app The app for this Messaging service. - * @constructor + * Gets the {@link admin.messaging.Messaging `Messaging`} service for the + * current app. + * + * @example + * ```javascript + * var messaging = app.messaging(); + * // The above is shorthand for: + * // var messaging = admin.messaging(app); + * ``` + * + * @return The `Messaging` service for the current app. */ constructor(app: FirebaseApp) { if (!validator.isNonNullObject(app) || !('options' in app)) { @@ -237,12 +239,14 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Sends a message via Firebase Cloud Messaging (FCM). - * - * @param {Message} message The message to be sent. - * @param {boolean=} dryRun Whether to send the message in the dry-run (validation only) mode. + * Sends the given message via FCM. * - * @return {Promise} A Promise fulfilled with a message ID string. + * @param message The message payload. + * @param dryRun Whether to send the message in the dry-run + * (validation only) mode. + * @return A promise fulfilled with a unique message ID + * string after the message has been successfully handed off to the FCM + * service for delivery. */ public send(message: Message, dryRun?: boolean): Promise { const copy: Message = deepCopy(message); @@ -253,7 +257,7 @@ export class Messaging implements FirebaseServiceInterface { } return this.getUrlPath() .then((urlPath) => { - const request: {message: Message; validate_only?: boolean} = { message: copy }; + const request: { message: Message; validate_only?: boolean } = { message: copy }; if (dryRun) { request.validate_only = true; } @@ -265,19 +269,23 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Sends all the messages in the given array via Firebase Cloud Messaging. Employs batching to - * send the entire list as a single RPC call. Compared to the send() method, this method is a - * significantly more efficient way to send multiple messages. + * Sends all the messages in the given array via Firebase Cloud Messaging. + * Employs batching to send the entire list as a single RPC call. Compared + * to the `send()` method, this method is a significantly more efficient way + * to send multiple messages. * - * The responses list obtained from the return value corresponds to the order of input messages. - * An error from this method indicates a total failure -- i.e. none of the messages in the - * list could be sent. Partial failures are indicated by a BatchResponse return value. + * The responses list obtained from the return value + * corresponds to the order of tokens in the `MulticastMessage`. An error + * from this method indicates a total failure -- i.e. none of the messages in + * the list could be sent. Partial failures are indicated by a `BatchResponse` + * return value. * - * @param {Message[]} messages A non-empty array containing up to 500 messages. - * @param {boolean=} dryRun Whether to send the message in the dry-run (validation only) mode. - * - * @return {Promise} A Promise fulfilled with an object representing the result - * of the send operation. + * @param messages A non-empty array + * containing up to 500 messages. + * @param dryRun Whether to send the messages in the dry-run + * (validation only) mode. + * @return A Promise fulfilled with an object representing the result of the + * send operation. */ public sendAll(messages: Message[], dryRun?: boolean): Promise { if (validator.isArray(messages) && messages.constructor !== Array) { @@ -306,7 +314,7 @@ export class Messaging implements FirebaseServiceInterface { .then((urlPath) => { const requests: SubRequest[] = copy.map((message) => { validateMessage(message); - const request: {message: Message; validate_only?: boolean} = { message }; + const request: { message: Message; validate_only?: boolean } = { message }; if (dryRun) { request.validate_only = true; } @@ -320,19 +328,22 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Sends the given multicast message to all the FCM registration tokens specified in it. - * - * This method uses the sendAll() API under the hood to send the given - * message to all the target recipients. The responses list obtained from the return value - * corresponds to the order of tokens in the MulticastMessage. An error from this method - * indicates a total failure -- i.e. none of the tokens in the list could be sent to. Partial - * failures are indicated by a BatchResponse return value. + * Sends the given multicast message to all the FCM registration tokens + * specified in it. * - * @param {MulticastMessage} message A multicast message containing up to 500 tokens. - * @param {boolean=} dryRun Whether to send the message in the dry-run (validation only) mode. + * This method uses the `sendAll()` API under the hood to send the given + * message to all the target recipients. The responses list obtained from the + * return value corresponds to the order of tokens in the `MulticastMessage`. + * An error from this method indicates a total failure -- i.e. the message was + * not sent to any of the tokens in the list. Partial failures are indicated by + * a `BatchResponse` return value. * - * @return {Promise} A Promise fulfilled with an object representing the result - * of the send operation. + * @param message A multicast message + * containing up to 500 tokens. + * @param dryRun Whether to send the message in the dry-run + * (validation only) mode. + * @return A Promise fulfilled with an object representing the result of the + * send operation. */ public sendMulticast(message: MulticastMessage, dryRun?: boolean): Promise { const copy: MulticastMessage = deepCopy(message); @@ -365,15 +376,24 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Sends an FCM message to a single device or an array of devices. + * Sends an FCM message to a single device corresponding to the provided + * registration token. * - * @param {string|string[]} registrationTokenOrTokens The registration token or an array of - * registration tokens for the device(s) to which to send the message. - * @param {MessagingPayload} payload The message payload. - * @param {MessagingOptions} [options = {}] Optional options to alter the message. + * See + * [Send to individual devices](/docs/cloud-messaging/admin/legacy-fcm#send_to_individual_devices) + * for code samples and detailed documentation. Takes either a + * `registrationToken` to send to a single device or a + * `registrationTokens` parameter containing an array of tokens to send + * to multiple devices. * - * @return {Promise} A Promise fulfilled - * with the server's response after the message has been sent. + * @param registrationToken A device registration token or an array of + * device registration tokens to which the message should be sent. + * @param payload The message payload. + * @param options Optional options to + * alter the message. + * + * @return A promise fulfilled with the server's response after the message + * has been sent. */ public sendToDevice( registrationTokenOrTokens: string | string[], @@ -423,15 +443,21 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Sends an FCM message to a device group. + * Sends an FCM message to a device group corresponding to the provided + * notification key. + * + * See + * [Send to a device group](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_device_group) + * for code samples and detailed documentation. * - * @param {string} notificationKey The notification key representing the device group to which to - * send the message. - * @param {MessagingPayload} payload The message payload. - * @param {MessagingOptions} [options = {}] Optional options to alter the message. + * @param notificationKey The notification key for the device group to + * which to send the message. + * @param payload The message payload. + * @param options Optional options to + * alter the message. * - * @return {Promise} A Promise fulfilled - * with the server's response after the message has been sent. + * @return A promise fulfilled with the server's response after the message + * has been sent. */ public sendToDeviceGroup( notificationKey: string, @@ -500,12 +526,17 @@ export class Messaging implements FirebaseServiceInterface { /** * Sends an FCM message to a topic. * - * @param {string} topic The name of the topic to which to send the message. - * @param {MessagingPayload} payload The message payload. - * @param {MessagingOptions} [options = {}] Optional options to alter the message. + * See + * [Send to a topic](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_topic) + * for code samples and detailed documentation. * - * @return {Promise} A Promise fulfilled with the server's response after - * the message has been sent. + * @param topic The topic to which to send the message. + * @param payload The message payload. + * @param options Optional options to + * alter the message. + * + * @return A promise fulfilled with the server's response after the message + * has been sent. */ public sendToTopic( topic: string, @@ -545,12 +576,18 @@ export class Messaging implements FirebaseServiceInterface { /** * Sends an FCM message to a condition. * - * @param {string} condition The condition to which to send the message. - * @param {MessagingPayload} payload The message payload. - * @param {MessagingOptions} [options = {}] Optional options to alter the message. + * See + * [Send to a condition](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_condition) + * for code samples and detailed documentation. + * + * @param condition The condition determining to which topics to send + * the message. + * @param payload The message payload. + * @param options Optional options to + * alter the message. * - * @return {Promise} A Promise fulfilled with the server's response - * after the message has been sent. + * @return A promise fulfilled with the server's response after the message + * has been sent. */ public sendToCondition( condition: string, @@ -595,14 +632,19 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Subscribes a single device or an array of devices to a topic. + * Subscribes a device to an FCM topic. * - * @param {string|string[]} registrationTokenOrTokens The registration token or an array of - * registration tokens to subscribe to the topic. - * @param {string} topic The topic to which to subscribe. + * See [Subscribe to a + * topic](/docs/cloud-messaging/manage-topics#suscribe_and_unsubscribe_using_the) + * for code samples and detailed documentation. Optionally, you can provide an + * array of tokens to subscribe multiple devices. + * + * @param registrationTokens A token or array of registration tokens + * for the devices to subscribe to the topic. + * @param topic The topic to which to subscribe. * - * @return {Promise} A Promise fulfilled with the parsed FCM - * server response. + * @return A promise fulfilled with the server's response after the device has been + * subscribed to the topic. */ public subscribeToTopic( registrationTokenOrTokens: string | string[], @@ -617,14 +659,19 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Unsubscribes a single device or an array of devices from a topic. + * Unsubscribes a device from an FCM topic. * - * @param {string|string[]} registrationTokenOrTokens The registration token or an array of - * registration tokens to unsubscribe from the topic. - * @param {string} topic The topic to which to subscribe. + * See [Unsubscribe from a + * topic](/docs/cloud-messaging/admin/manage-topic-subscriptions#unsubscribe_from_a_topic) + * for code samples and detailed documentation. Optionally, you can provide an + * array of tokens to unsubscribe multiple devices. + * + * @param registrationTokens A device registration token or an array of + * device registration tokens to unsubscribe from the topic. + * @param topic The topic from which to unsubscribe. * - * @return {Promise} A Promise fulfilled with the parsed FCM - * server response. + * @return A promise fulfilled with the server's response after the device has been + * unsubscribed from the topic. */ public unsubscribeFromTopic( registrationTokenOrTokens: string | string[], @@ -650,8 +697,8 @@ export class Messaging implements FirebaseServiceInterface { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_ARGUMENT, 'Failed to determine project ID for Messaging. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', ); } @@ -758,7 +805,7 @@ export class Messaging implements FirebaseServiceInterface { if (validPayloadKeys.indexOf(payloadKey) === -1) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - `Messaging payload contains an invalid "${ payloadKey }" property. Valid properties are ` + + `Messaging payload contains an invalid "${payloadKey}" property. Valid properties are ` + `"data" and "notification".`, ); } else { @@ -779,7 +826,7 @@ export class Messaging implements FirebaseServiceInterface { if (!validator.isNonNullObject(value)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - `Messaging payload contains an invalid value for the "${ payloadKey }" property. ` + + `Messaging payload contains an invalid value for the "${payloadKey}" property. ` + `Value must be an object.`, ); } @@ -789,14 +836,14 @@ export class Messaging implements FirebaseServiceInterface { // Validate all sub-keys have a string value throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - `Messaging payload contains an invalid value for the "${ payloadKey }.${ subKey }" ` + + `Messaging payload contains an invalid value for the "${payloadKey}.${subKey}" ` + `property. Values must be strings.`, ); } else if (payloadKey === 'data' && /^google\./.test(subKey)) { // Validate the data payload does not contain keys which start with 'google.'. throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - `Messaging payload contains the blacklisted "data.${ subKey }" property.`, + `Messaging payload contains the blacklisted "data.${subKey}" property.`, ); } }); @@ -815,7 +862,7 @@ export class Messaging implements FirebaseServiceInterface { if (blacklistedKey in payloadCopy.data!) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - `Messaging payload contains the blacklisted "data.${ blacklistedKey }" property.`, + `Messaging payload contains the blacklisted "data.${blacklistedKey}" property.`, ); } }); @@ -845,7 +892,7 @@ export class Messaging implements FirebaseServiceInterface { if (blacklistedKey in optionsCopy) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains the blacklisted "${ blacklistedKey }" property.`, + `Messaging options contains the blacklisted "${blacklistedKey}" property.`, ); } }); @@ -858,14 +905,14 @@ export class Messaging implements FirebaseServiceInterface { const keyName = ('collapseKey' in options) ? 'collapseKey' : 'collapse_key'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a non-empty string.', ); } else if ('dry_run' in optionsCopy && !validator.isBoolean((optionsCopy as any).dry_run)) { const keyName = ('dryRun' in options) ? 'dryRun' : 'dry_run'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a boolean.', ); } else if ('priority' in optionsCopy && !validator.isNonEmptyString(optionsCopy.priority)) { @@ -875,32 +922,32 @@ export class Messaging implements FirebaseServiceInterface { 'be a non-empty string.', ); } else if ('restricted_package_name' in optionsCopy && - !validator.isNonEmptyString((optionsCopy as any).restricted_package_name)) { + !validator.isNonEmptyString((optionsCopy as any).restricted_package_name)) { const keyName = ('restrictedPackageName' in options) ? 'restrictedPackageName' : 'restricted_package_name'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a non-empty string.', ); } else if ('time_to_live' in optionsCopy && !validator.isNumber((optionsCopy as any).time_to_live)) { const keyName = ('timeToLive' in options) ? 'timeToLive' : 'time_to_live'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a number.', ); } else if ('content_available' in optionsCopy && !validator.isBoolean((optionsCopy as any).content_available)) { const keyName = ('contentAvailable' in options) ? 'contentAvailable' : 'content_available'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a boolean.', ); } else if ('mutable_content' in optionsCopy && !validator.isBoolean((optionsCopy as any).mutable_content)) { const keyName = ('mutableContent' in options) ? 'mutableContent' : 'mutable_content'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a boolean.', ); } @@ -921,7 +968,7 @@ export class Messaging implements FirebaseServiceInterface { errorInfo: ErrorInfo = MessagingClientErrorCode.INVALID_ARGUMENT, ): void { if (!validator.isNonEmptyArray(registrationTokenOrTokens) && - !validator.isNonEmptyString(registrationTokenOrTokens)) { + !validator.isNonEmptyString(registrationTokenOrTokens)) { throw new FirebaseMessagingError( errorInfo, `Registration token(s) provided to ${methodName}() must be a non-empty string or a ` + @@ -1017,7 +1064,7 @@ export class Messaging implements FirebaseServiceInterface { */ private normalizeTopic(topic: string): string { if (!/^\/topics\//.test(topic)) { - topic = `/topics/${ topic }`; + topic = `/topics/${topic}`; } return topic; } diff --git a/test/unit/messaging/batch-requests.spec.ts b/test/unit/messaging/batch-requests.spec.ts index c7c96a75f1..982b16420f 100644 --- a/test/unit/messaging/batch-requests.spec.ts +++ b/test/unit/messaging/batch-requests.spec.ts @@ -24,7 +24,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import { HttpClient, HttpResponse, HttpRequestConfig, HttpError } from '../../../src/utils/api-request'; -import { SubRequest, BatchRequestClient } from '../../../src/messaging/batch-request'; +import { SubRequest, BatchRequestClient } from '../../../src/messaging/batch-request-internal'; chai.should(); chai.use(sinonChai); diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index b99542278d..fc89ad5ec3 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -31,9 +31,8 @@ import { Message, MessagingOptions, MessagingPayload, MessagingDevicesResponse, MessagingDeviceGroupResponse, MessagingTopicManagementResponse, BatchResponse, SendResponse, MulticastMessage, } from '../../../src/messaging/messaging-types'; -import { - Messaging, BLACKLISTED_OPTIONS_KEYS, BLACKLISTED_DATA_PAYLOAD_KEYS, -} from '../../../src/messaging/messaging'; +import { Messaging } from '../../../src/messaging/messaging'; +import { BLACKLISTED_OPTIONS_KEYS, BLACKLISTED_DATA_PAYLOAD_KEYS } from '../../../src/messaging/messaging-internal'; import { HttpClient } from '../../../src/utils/api-request'; import { getSdkVersion } from '../../../src/utils/index'; From 875d865f0822be6d8f9d652db6dbb2d025bead90 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Tue, 11 Aug 2020 10:58:04 -0400 Subject: [PATCH 025/160] Allow RemoteConfig to auto-generate typings, separate internal vs external APIs (#984) --- gulpfile.js | 2 +- src/remote-config/index.ts | 57 ++ .../remote-config-api-client-internal.ts | 445 +++++++++++++ src/remote-config/remote-config-api-client.ts | 604 ++++++------------ src/remote-config/remote-config-utils.ts | 43 -- src/remote-config/remote-config.ts | 79 ++- .../remote-config-api-client.spec.ts | 3 +- test/unit/remote-config/remote-config.spec.ts | 3 +- 8 files changed, 738 insertions(+), 498 deletions(-) create mode 100644 src/remote-config/index.ts create mode 100644 src/remote-config/remote-config-api-client-internal.ts delete mode 100644 src/remote-config/remote-config-utils.ts diff --git a/gulpfile.js b/gulpfile.js index a3ef4a5b86..b4f216a638 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -57,6 +57,7 @@ var paths = { '!src/instance-id.d.ts', '!src/security-rules.d.ts', '!src/project-management.d.ts', + '!src/remote-config.d.ts', '!src/messaging.d.ts', ], }; @@ -70,7 +71,6 @@ const TEMPORARY_TYPING_EXCLUDES = [ '!lib/database/*.d.ts', '!lib/firestore/*.d.ts', '!lib/machine-learning/*.d.ts', - '!lib/remote-config/*.d.ts', '!lib/storage/*.d.ts', '!lib/utils/*.d.ts', ]; diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts new file mode 100644 index 0000000000..d9450b83b1 --- /dev/null +++ b/src/remote-config/index.ts @@ -0,0 +1,57 @@ +/*! + * Copyright 2020 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 { FirebaseApp } from '../firebase-app'; +import * as remoteConfigApi from './remote-config'; +import * as remoteConfigClientApi from './remote-config-api-client'; +import * as firebaseAdmin from '../index'; + +export function remoteConfig(app?: FirebaseApp): remoteConfigApi.RemoteConfig { + if (typeof(app) === 'undefined') { + app = firebaseAdmin.app(); + } + return app.remoteConfig(); +} + +/** + * We must define a namespace to make the typings work correctly. Otherwise + * `admin.remoteConfig()` cannot be called like a function. Temporarily, + * admin.remoteConfig is used as the namespace name because we cannot barrel + * re-export the contents from remote-config, and we want it to + * match the namespacing in the re-export inside src/index.d.ts + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace admin.remoteConfig { + // See https://github.com/microsoft/TypeScript/issues/4336 + /* eslint-disable @typescript-eslint/no-unused-vars */ + // See https://github.com/typescript-eslint/typescript-eslint/issues/363 + export import ExplicitParameterValue = remoteConfigClientApi.ExplicitParameterValue; + export import ListVersionsOptions = remoteConfigClientApi.ListVersionsOptions; + export import ListVersionsResult = remoteConfigClientApi.ListVersionsResult; + export import InAppDefaultValue = remoteConfigClientApi.InAppDefaultValue; + export import RemoteConfigCondition = remoteConfigClientApi.RemoteConfigCondition; + export import RemoteConfigParameter = remoteConfigClientApi.RemoteConfigParameter; + export import RemoteConfigParameterGroup = remoteConfigClientApi.RemoteConfigParameterGroup; + export import RemoteConfigParameterValue = remoteConfigClientApi.RemoteConfigParameterValue; + export import RemoteConfigTemplate = remoteConfigClientApi.RemoteConfigTemplate; + export import RemoteConfigUser = remoteConfigClientApi.RemoteConfigUser; + export import TagColor = remoteConfigClientApi.TagColor; + export import Version = remoteConfigClientApi.Version; + + // Allows for exposing classes as interfaces in typings + /* eslint-disable @typescript-eslint/no-empty-interface */ + export interface RemoteConfig extends remoteConfigApi.RemoteConfig {} +} diff --git a/src/remote-config/remote-config-api-client-internal.ts b/src/remote-config/remote-config-api-client-internal.ts new file mode 100644 index 0000000000..315c9582b4 --- /dev/null +++ b/src/remote-config/remote-config-api-client-internal.ts @@ -0,0 +1,445 @@ +/*! + * Copyright 2020 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 { RemoteConfigTemplate, ListVersionsOptions, ListVersionsResult } from './remote-config-api-client'; +import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, HttpResponse } from '../utils/api-request'; +import { PrefixedFirebaseError } from '../utils/error'; +import { FirebaseApp } from '../firebase-app'; +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; +import { deepCopy } from '../utils/deep-copy'; + +// Remote Config backend constants +const FIREBASE_REMOTE_CONFIG_V1_API = 'https://firebaseremoteconfig.googleapis.com/v1'; +const FIREBASE_REMOTE_CONFIG_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, + // There is a known issue in which the ETag is not properly returned in cases where the request + // does not specify a compression type. Currently, it is required to include the header + // `Accept-Encoding: gzip` or equivalent in all requests. + // https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates + 'Accept-Encoding': 'gzip', +}; + + +/** + * Class that facilitates sending requests to the Firebase Remote Config backend API. + * + * @private + */ +export class RemoteConfigApiClient { + private readonly httpClient: HttpClient; + private projectIdPrefix?: string; + + constructor(private readonly app: FirebaseApp) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'First argument passed to admin.remoteConfig() must be a valid Firebase app instance.'); + } + + this.httpClient = new AuthorizedHttpClient(app); + } + + public getTemplate(): Promise { + return this.getUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'GET', + url: `${url}/remoteConfig`, + headers: FIREBASE_REMOTE_CONFIG_HEADERS + }; + return this.httpClient.send(request); + }) + .then((resp) => { + return this.toRemoteConfigTemplate(resp); + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + public getTemplateAtVersion(versionNumber: number | string): Promise { + const data = { versionNumber: this.validateVersionNumber(versionNumber) }; + return this.getUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'GET', + url: `${url}/remoteConfig`, + headers: FIREBASE_REMOTE_CONFIG_HEADERS, + data + }; + return this.httpClient.send(request); + }) + .then((resp) => { + return this.toRemoteConfigTemplate(resp); + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + public validateTemplate(template: RemoteConfigTemplate): Promise { + template = this.validateInputRemoteConfigTemplate(template); + return this.sendPutRequest(template, template.etag, true) + .then((resp) => { + // validating a template returns an etag with the suffix -0 means that your update + // was successfully validated. We set the etag back to the original etag of the template + // to allow future operations. + this.validateEtag(resp.headers['etag']); + return this.toRemoteConfigTemplate(resp, template.etag); + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + public publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise { + template = this.validateInputRemoteConfigTemplate(template); + let ifMatch: string = template.etag; + if (options && options.force == true) { + // setting `If-Match: *` forces the Remote Config template to be updated + // and circumvent the ETag, and the protection from that it provides. + ifMatch = '*'; + } + return this.sendPutRequest(template, ifMatch) + .then((resp) => { + return this.toRemoteConfigTemplate(resp); + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + public rollback(versionNumber: number | string): Promise { + const data = { versionNumber: this.validateVersionNumber(versionNumber) }; + return this.getUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'POST', + url: `${url}/remoteConfig:rollback`, + headers: FIREBASE_REMOTE_CONFIG_HEADERS, + data + }; + return this.httpClient.send(request); + }) + .then((resp) => { + return this.toRemoteConfigTemplate(resp); + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + public listVersions(options?: ListVersionsOptions): Promise { + if (typeof options !== 'undefined') { + options = this.validateListVersionsOptions(options); + } + return this.getUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'GET', + url: `${url}/remoteConfig:listVersions`, + headers: FIREBASE_REMOTE_CONFIG_HEADERS, + data: options + }; + return this.httpClient.send(request); + }) + .then((resp) => { + return resp.data; + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + private sendPutRequest(template: RemoteConfigTemplate, etag: string, validateOnly?: boolean): Promise { + let path = 'remoteConfig'; + if (validateOnly) { + path += '?validate_only=true'; + } + return this.getUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'PUT', + url: `${url}/${path}`, + headers: { ...FIREBASE_REMOTE_CONFIG_HEADERS, 'If-Match': etag }, + data: { + conditions: template.conditions, + parameters: template.parameters, + parameterGroups: template.parameterGroups, + version: template.version, + } + }; + return this.httpClient.send(request); + }); + } + + private getUrl(): Promise { + return this.getProjectIdPrefix() + .then((projectIdPrefix) => { + return `${FIREBASE_REMOTE_CONFIG_V1_API}/${projectIdPrefix}`; + }); + } + + private getProjectIdPrefix(): Promise { + if (this.projectIdPrefix) { + return Promise.resolve(this.projectIdPrefix); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseRemoteConfigError( + '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.projectIdPrefix = `projects/${projectId}`; + return this.projectIdPrefix; + }); + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + if (!response.isJson()) { + return new FirebaseRemoteConfigError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + const error: Error = (response.data as ErrorResponse).error || {}; + let code: RemoteConfigErrorCode = 'unknown-error'; + if (error.status && error.status in ERROR_CODE_MAPPING) { + code = ERROR_CODE_MAPPING[error.status]; + } + const message = error.message || `Unknown server error: ${response.text}`; + return new FirebaseRemoteConfigError(code, message); + } + + /** + * Creates a RemoteConfigTemplate from the API response. + * If provided, customEtag is used instead of the etag returned in the API response. + * + * @param {HttpResponse} resp API response object. + * @param {string} customEtag A custom etag to replace the etag fom the API response (Optional). + */ + private toRemoteConfigTemplate(resp: HttpResponse, customEtag?: string): RemoteConfigTemplate { + const etag = (typeof customEtag == 'undefined') ? resp.headers['etag'] : customEtag; + this.validateEtag(etag); + return { + conditions: resp.data.conditions, + parameters: resp.data.parameters, + parameterGroups: resp.data.parameterGroups, + etag, + version: resp.data.version, + }; + } + + /** + * Checks if the given RemoteConfigTemplate object is valid. + * The object must have valid parameters, parameter groups, conditions, and an etag. + * Removes output only properties from version metadata. + * + * @param {RemoteConfigTemplate} template A RemoteConfigTemplate object to be validated. + * + * @returns {RemoteConfigTemplate} The validated RemoteConfigTemplate object. + */ + private validateInputRemoteConfigTemplate(template: RemoteConfigTemplate): RemoteConfigTemplate { + const templateCopy = deepCopy(template); + if (!validator.isNonNullObject(templateCopy)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + `Invalid Remote Config template: ${JSON.stringify(templateCopy)}`); + } + if (!validator.isNonEmptyString(templateCopy.etag)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'ETag must be a non-empty string.'); + } + if (!validator.isNonNullObject(templateCopy.parameters)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Remote Config parameters must be a non-null object'); + } + if (!validator.isNonNullObject(templateCopy.parameterGroups)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Remote Config parameter groups must be a non-null object'); + } + if (!validator.isArray(templateCopy.conditions)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Remote Config conditions must be an array'); + } + if (typeof templateCopy.version !== 'undefined') { + // exclude output only properties and keep the only input property: description + templateCopy.version = { description: templateCopy.version.description }; + } + return templateCopy; + } + + /** + * Checks if a given version number is valid. + * A version number must be an integer or a string in int64 format. + * If valid, returns the string representation of the provided version number. + * + * @param {string|number} versionNumber A version number to be validated. + * + * @returns {string} The validated version number as a string. + */ + private validateVersionNumber(versionNumber: string | number, propertyName = 'versionNumber'): string { + if (!validator.isNonEmptyString(versionNumber) && + !validator.isNumber(versionNumber)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + `${propertyName} must be a non-empty string in int64 format or a number`); + } + if (!Number.isInteger(Number(versionNumber))) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + `${propertyName} must be an integer or a string in int64 format`); + } + return versionNumber.toString(); + } + + private validateEtag(etag?: string): void { + if (!validator.isNonEmptyString(etag)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'ETag header is not present in the server response.'); + } + } + + /** + * Checks if a given `ListVersionsOptions` object is valid. If successful, creates a copy of the + * options object and convert `startTime` and `endTime` to RFC3339 UTC "Zulu" format, if present. + * + * @param {ListVersionsOptions} options An options object to be validated. + * + * @return {ListVersionsOptions} A copy of the provided options object with timestamps converted + * to UTC Zulu format. + */ + private validateListVersionsOptions(options: ListVersionsOptions): ListVersionsOptions { + const optionsCopy = deepCopy(options); + if (!validator.isNonNullObject(optionsCopy)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'ListVersionsOptions must be a non-null object.'); + } + if (typeof optionsCopy.pageSize !== 'undefined') { + if (!validator.isNumber(optionsCopy.pageSize)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', 'pageSize must be a number.'); + } + if (optionsCopy.pageSize < 1 || optionsCopy.pageSize > 300) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', 'pageSize must be a number between 1 and 300 (inclusive).'); + } + } + if (typeof optionsCopy.pageToken !== 'undefined' && !validator.isNonEmptyString(optionsCopy.pageToken)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', 'pageToken must be a string value.'); + } + if (typeof optionsCopy.endVersionNumber !== 'undefined') { + optionsCopy.endVersionNumber = this.validateVersionNumber(optionsCopy.endVersionNumber, 'endVersionNumber'); + } + if (typeof optionsCopy.startTime !== 'undefined') { + if (!(optionsCopy.startTime instanceof Date) && !validator.isUTCDateString(optionsCopy.startTime)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', 'startTime must be a valid Date object or a UTC date string.'); + } + // Convert startTime to RFC3339 UTC "Zulu" format. + if (optionsCopy.startTime instanceof Date) { + optionsCopy.startTime = optionsCopy.startTime.toISOString(); + } else { + optionsCopy.startTime = new Date(optionsCopy.startTime).toISOString(); + } + } + if (typeof optionsCopy.endTime !== 'undefined') { + if (!(optionsCopy.endTime instanceof Date) && !validator.isUTCDateString(optionsCopy.endTime)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', 'endTime must be a valid Date object or a UTC date string.'); + } + // Convert endTime to RFC3339 UTC "Zulu" format. + if (optionsCopy.endTime instanceof Date) { + optionsCopy.endTime = optionsCopy.endTime.toISOString(); + } else { + optionsCopy.endTime = new Date(optionsCopy.endTime).toISOString(); + } + } + // Remove undefined fields from optionsCopy + Object.keys(optionsCopy).forEach(key => + (typeof (optionsCopy as any)[key] === 'undefined') && delete (optionsCopy as any)[key] + ); + return optionsCopy; + } +} + +interface ErrorResponse { + error?: Error; +} + +interface Error { + code?: number; + message?: string; + status?: string; +} + +const ERROR_CODE_MAPPING: { [key: string]: RemoteConfigErrorCode } = { + ABORTED: 'aborted', + ALREADY_EXISTS: `already-exists`, + INVALID_ARGUMENT: 'invalid-argument', + INTERNAL: 'internal-error', + FAILED_PRECONDITION: 'failed-precondition', + NOT_FOUND: 'not-found', + OUT_OF_RANGE: 'out-of-range', + PERMISSION_DENIED: 'permission-denied', + RESOURCE_EXHAUSTED: 'resource-exhausted', + UNAUTHENTICATED: 'unauthenticated', + UNKNOWN: 'unknown-error', +}; + +export type RemoteConfigErrorCode = + 'aborted' + | 'already-exists' + | 'failed-precondition' + | 'internal-error' + | 'invalid-argument' + | 'not-found' + | 'out-of-range' + | 'permission-denied' + | 'resource-exhausted' + | 'unauthenticated' + | 'unknown-error'; + +/** + * Firebase Remote Config error code structure. This extends PrefixedFirebaseError. + * + * @param {RemoteConfigErrorCode} code The error code. + * @param {string} message The error message. + * @constructor + */ +export class FirebaseRemoteConfigError extends PrefixedFirebaseError { + constructor(code: RemoteConfigErrorCode, message: string) { + super('remote-config', code, message); + } +} diff --git a/src/remote-config/remote-config-api-client.ts b/src/remote-config/remote-config-api-client.ts index 26bd1b8931..08621f972b 100644 --- a/src/remote-config/remote-config-api-client.ts +++ b/src/remote-config/remote-config-api-client.ts @@ -14,25 +14,9 @@ * limitations under the License. */ -import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, HttpResponse } from '../utils/api-request'; -import { PrefixedFirebaseError } from '../utils/error'; -import { FirebaseRemoteConfigError, RemoteConfigErrorCode } from './remote-config-utils'; -import { FirebaseApp } from '../firebase-app'; -import * as utils from '../utils/index'; -import * as validator from '../utils/validator'; -import { deepCopy } from '../utils/deep-copy'; - -// Remote Config backend constants -const FIREBASE_REMOTE_CONFIG_V1_API = 'https://firebaseremoteconfig.googleapis.com/v1'; -const FIREBASE_REMOTE_CONFIG_HEADERS = { - 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, - // There is a known issue in which the ETag is not properly returned in cases where the request - // does not specify a compression type. Currently, it is required to include the header - // `Accept-Encoding: gzip` or equivalent in all requests. - // https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates - 'Accept-Encoding': 'gzip', -}; - +/** + * Colors that are associated with conditions for display purposes. + */ export enum TagColor { BLUE = "Blue", BROWN = "Brown", @@ -47,464 +31,254 @@ export enum TagColor { TEAL = "Teal", } -/** Interface representing a Remote Config parameter `value` in value options. */ +/** + * Interface representing an explicit parameter value. + */ export interface ExplicitParameterValue { + /** + * The `string` value that the parameter is set to. + */ value: string; } -/** Interface representing a Remote Config parameter `useInAppDefault` in value options. */ +/** + * Interface representing an in-app-default value. + */ export interface InAppDefaultValue { + /** + * If `true`, the parameter is omitted from the parameter values returned to a client. + */ useInAppDefault: boolean; } +/** + * Type representing a Remote Config parameter value. + * A `RemoteConfigParameterValue` could be either an `ExplicitParameterValue` or + * an `InAppDefaultValue`. + */ export type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; -/** Interface representing a Remote Config parameter. */ +/** + * Interface representing a Remote Config parameter. + * At minimum, a `defaultValue` or a `conditionalValues` entry must be present for the + * parameter to have any effect. + */ export interface RemoteConfigParameter { + + /** + * The value to set the parameter to, when none of the named conditions evaluate to `true`. + */ defaultValue?: RemoteConfigParameterValue; + + /** + * A `(condition name, value)` map. The condition name of the highest priority + * (the one listed first in the Remote Config template's conditions list) determines the value of + * this parameter. + */ conditionalValues?: { [key: string]: RemoteConfigParameterValue }; + + /** + * A description for this parameter. Should not be over 100 characters and may contain any + * Unicode characters. + */ description?: string; } -/** Interface representing a Remote Config parameter group. */ +/** + * Interface representing a Remote Config parameter group. + * Grouping parameters is only for management purposes and does not affect client-side + * fetching of parameter values. + */ export interface RemoteConfigParameterGroup { + /** + * A description for the group. Its length must be less than or equal to 256 characters. + * A description may contain any Unicode characters. + */ description?: string; + + /** + * Map of parameter keys to their optional default values and optional conditional values for + * parameters that belong to this group. A parameter only appears once per + * Remote Config template. An ungrouped parameter appears at the top level, whereas a + * parameter organized within a group appears within its group's map of parameters. + */ parameters: { [key: string]: RemoteConfigParameter }; } -/** Interface representing a Remote Config condition. */ +/** + * Interface representing a Remote Config condition. + * A condition targets a specific group of users. A list of these conditions make up + * part of a Remote Config template. + */ export interface RemoteConfigCondition { + + /** + * A non-empty and unique name of this condition. + */ name: string; + + /** + * The logic of this condition. + * See the documentation on + * {@link https://firebase.google.com/docs/remote-config/condition-reference condition expressions} + * for the expected syntax of this field. + */ expression: string; + + /** + * The color associated with this condition for display purposes in the Firebase Console. + * Not specifying this value results in the console picking an arbitrary color to associate + * with the condition. + */ tagColor?: TagColor; } -/** Interface representing a Remote Config template. */ +/** + * Interface representing a Remote Config template. + */ export interface RemoteConfigTemplate { + /** + * A list of conditions in descending order by priority. + */ conditions: RemoteConfigCondition[]; + + /** + * Map of parameter keys to their optional default values and optional conditional values. + */ parameters: { [key: string]: RemoteConfigParameter }; + + /** + * Map of parameter group names to their parameter group objects. + * A group's name is mutable but must be unique among groups in the Remote Config template. + * The name is limited to 256 characters and intended to be human-readable. Any Unicode + * characters are allowed. + */ parameterGroups: { [key: string]: RemoteConfigParameterGroup }; + + /** + * ETag of the current Remote Config template (readonly). + */ readonly etag: string; + + /** + * Version information for the current Remote Config template. + */ version?: Version; } -/** Interface representing a Remote Config version. */ +/** + * Interface representing a Remote Config template version. + * Output only, except for the version description. Contains metadata about a particular + * version of the Remote Config template. All fields are set at the time the specified Remote + * Config template is published. A version's description field may be specified in + * `publishTemplate` calls. + */ export interface Version { - versionNumber?: string; // int64 format - updateTime?: string; // in UTC + /** + * The version number of a Remote Config template. + */ + versionNumber?: string; + + /** + * The timestamp of when this version of the Remote Config template was written to the + * Remote Config backend. + */ + updateTime?: string; + + /** + * The origin of the template update action. + */ updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | 'REST_API' | 'ADMIN_SDK_NODE'); + + /** + * The type of the template update action. + */ updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); + + /** + * Aggregation of all metadata fields about the account that performed the update. + */ updateUser?: RemoteConfigUser; + + /** + * The user-provided description of the corresponding Remote Config template. + */ description?: string; + + /** + * The version number of the Remote Config template that has become the current version + * due to a rollback. Only present if this version is the result of a rollback. + */ rollbackSource?: string; + + /** + * Indicates whether this Remote Config template was published before version history was + * supported. + */ isLegacy?: boolean; } /** Interface representing a list of Remote Config template versions. */ export interface ListVersionsResult { + /** + * A list of version metadata objects, sorted in reverse chronological order. + */ versions: Version[]; + + /** + * Token to retrieve the next page of results, or empty if there are no more results + * in the list. + */ nextPageToken?: string; } -/** Interface representing a Remote Config list version options. */ +/** Interface representing options for Remote Config list versions operation. */ export interface ListVersionsOptions { + /** + * The maximum number of items to return per page. + */ pageSize?: number; + + /** + * The `nextPageToken` value returned from a previous list versions request, if any. + */ pageToken?: string; + + /** + * Specifies the newest version number to include in the results. + * If specified, must be greater than zero. Defaults to the newest version. + */ endVersionNumber?: string | number; + + /** + * Specifies the earliest update time to include in the results. Any entries updated before this + * time are omitted. + */ startTime?: Date | string; + + /** + * Specifies the latest update time to include in the results. Any entries updated on or after + * this time are omitted. + */ endTime?: Date | string; } -/** Interface representing a Remote Config user. */ +/** Interface representing a Remote Config user.*/ export interface RemoteConfigUser { + /** + * Email address. Output only. + */ email: string; - name?: string; - imageUrl?: string; -} - -/** - * Class that facilitates sending requests to the Firebase Remote Config backend API. - * - * @private - */ -export class RemoteConfigApiClient { - - private readonly httpClient: HttpClient; - private projectIdPrefix?: string; - - constructor(private readonly app: FirebaseApp) { - if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'First argument passed to admin.remoteConfig() must be a valid Firebase app instance.'); - } - - this.httpClient = new AuthorizedHttpClient(app); - } - - public getTemplate(): Promise { - return this.getUrl() - .then((url) => { - const request: HttpRequestConfig = { - method: 'GET', - url: `${url}/remoteConfig`, - headers: FIREBASE_REMOTE_CONFIG_HEADERS - }; - return this.httpClient.send(request); - }) - .then((resp) => { - return this.toRemoteConfigTemplate(resp); - }) - .catch((err) => { - throw this.toFirebaseError(err); - }); - } - - public getTemplateAtVersion(versionNumber: number | string): Promise { - const data = { versionNumber: this.validateVersionNumber(versionNumber) }; - return this.getUrl() - .then((url) => { - const request: HttpRequestConfig = { - method: 'GET', - url: `${url}/remoteConfig`, - headers: FIREBASE_REMOTE_CONFIG_HEADERS, - data - }; - return this.httpClient.send(request); - }) - .then((resp) => { - return this.toRemoteConfigTemplate(resp); - }) - .catch((err) => { - throw this.toFirebaseError(err); - }); - } - - public validateTemplate(template: RemoteConfigTemplate): Promise { - template = this.validateInputRemoteConfigTemplate(template); - return this.sendPutRequest(template, template.etag, true) - .then((resp) => { - // validating a template returns an etag with the suffix -0 means that your update - // was successfully validated. We set the etag back to the original etag of the template - // to allow future operations. - this.validateEtag(resp.headers['etag']); - return this.toRemoteConfigTemplate(resp, template.etag); - }) - .catch((err) => { - throw this.toFirebaseError(err); - }); - } - - public publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise { - template = this.validateInputRemoteConfigTemplate(template); - let ifMatch: string = template.etag; - if (options && options.force == true) { - // setting `If-Match: *` forces the Remote Config template to be updated - // and circumvent the ETag, and the protection from that it provides. - ifMatch = '*'; - } - return this.sendPutRequest(template, ifMatch) - .then((resp) => { - return this.toRemoteConfigTemplate(resp); - }) - .catch((err) => { - throw this.toFirebaseError(err); - }); - } - - public rollback(versionNumber: number | string): Promise { - const data = { versionNumber: this.validateVersionNumber(versionNumber) }; - return this.getUrl() - .then((url) => { - const request: HttpRequestConfig = { - method: 'POST', - url: `${url}/remoteConfig:rollback`, - headers: FIREBASE_REMOTE_CONFIG_HEADERS, - data - }; - return this.httpClient.send(request); - }) - .then((resp) => { - return this.toRemoteConfigTemplate(resp); - }) - .catch((err) => { - throw this.toFirebaseError(err); - }); - } - - public listVersions(options?: ListVersionsOptions): Promise { - if (typeof options !== 'undefined') { - options = this.validateListVersionsOptions(options); - } - return this.getUrl() - .then((url) => { - const request: HttpRequestConfig = { - method: 'GET', - url: `${url}/remoteConfig:listVersions`, - headers: FIREBASE_REMOTE_CONFIG_HEADERS, - data: options - }; - return this.httpClient.send(request); - }) - .then((resp) => { - return resp.data; - }) - .catch((err) => { - throw this.toFirebaseError(err); - }); - } - - private sendPutRequest(template: RemoteConfigTemplate, etag: string, validateOnly?: boolean): Promise { - let path = 'remoteConfig'; - if (validateOnly) { - path += '?validate_only=true'; - } - return this.getUrl() - .then((url) => { - const request: HttpRequestConfig = { - method: 'PUT', - url: `${url}/${path}`, - headers: { ...FIREBASE_REMOTE_CONFIG_HEADERS, 'If-Match': etag }, - data: { - conditions: template.conditions, - parameters: template.parameters, - parameterGroups: template.parameterGroups, - version: template.version, - } - }; - return this.httpClient.send(request); - }); - } - - private getUrl(): Promise { - return this.getProjectIdPrefix() - .then((projectIdPrefix) => { - return `${FIREBASE_REMOTE_CONFIG_V1_API}/${projectIdPrefix}`; - }); - } - - private getProjectIdPrefix(): Promise { - if (this.projectIdPrefix) { - return Promise.resolve(this.projectIdPrefix); - } - - return utils.findProjectId(this.app) - .then((projectId) => { - if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseRemoteConfigError( - '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.projectIdPrefix = `projects/${projectId}`; - return this.projectIdPrefix; - }); - } - - private toFirebaseError(err: HttpError): PrefixedFirebaseError { - if (err instanceof PrefixedFirebaseError) { - return err; - } - - const response = err.response; - if (!response.isJson()) { - return new FirebaseRemoteConfigError( - 'unknown-error', - `Unexpected response with status: ${response.status} and body: ${response.text}`); - } - - const error: Error = (response.data as ErrorResponse).error || {}; - let code: RemoteConfigErrorCode = 'unknown-error'; - if (error.status && error.status in ERROR_CODE_MAPPING) { - code = ERROR_CODE_MAPPING[error.status]; - } - const message = error.message || `Unknown server error: ${response.text}`; - return new FirebaseRemoteConfigError(code, message); - } - - /** - * Creates a RemoteConfigTemplate from the API response. - * If provided, customEtag is used instead of the etag returned in the API response. - * - * @param {HttpResponse} resp API response object. - * @param {string} customEtag A custom etag to replace the etag fom the API response (Optional). - */ - private toRemoteConfigTemplate(resp: HttpResponse, customEtag?: string): RemoteConfigTemplate { - const etag = (typeof customEtag == 'undefined') ? resp.headers['etag'] : customEtag; - this.validateEtag(etag); - return { - conditions: resp.data.conditions, - parameters: resp.data.parameters, - parameterGroups: resp.data.parameterGroups, - etag, - version: resp.data.version, - }; - } - - /** - * Checks if the given RemoteConfigTemplate object is valid. - * The object must have valid parameters, parameter groups, conditions, and an etag. - * Removes output only properties from version metadata. - * - * @param {RemoteConfigTemplate} template A RemoteConfigTemplate object to be validated. - * - * @returns {RemoteConfigTemplate} The validated RemoteConfigTemplate object. - */ - private validateInputRemoteConfigTemplate(template: RemoteConfigTemplate): RemoteConfigTemplate { - const templateCopy = deepCopy(template); - if (!validator.isNonNullObject(templateCopy)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `Invalid Remote Config template: ${JSON.stringify(templateCopy)}`); - } - if (!validator.isNonEmptyString(templateCopy.etag)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'ETag must be a non-empty string.'); - } - if (!validator.isNonNullObject(templateCopy.parameters)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config parameters must be a non-null object'); - } - if (!validator.isNonNullObject(templateCopy.parameterGroups)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config parameter groups must be a non-null object'); - } - if (!validator.isArray(templateCopy.conditions)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config conditions must be an array'); - } - if (typeof templateCopy.version !== 'undefined') { - // exclude output only properties and keep the only input property: description - templateCopy.version = { description: templateCopy.version.description }; - } - return templateCopy; - } - - /** - * Checks if a given version number is valid. - * A version number must be an integer or a string in int64 format. - * If valid, returns the string representation of the provided version number. - * - * @param {string|number} versionNumber A version number to be validated. - * - * @returns {string} The validated version number as a string. - */ - private validateVersionNumber(versionNumber: string | number, propertyName = 'versionNumber'): string { - if (!validator.isNonEmptyString(versionNumber) && - !validator.isNumber(versionNumber)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `${propertyName} must be a non-empty string in int64 format or a number`); - } - if (!Number.isInteger(Number(versionNumber))) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `${propertyName} must be an integer or a string in int64 format`); - } - return versionNumber.toString(); - } - - private validateEtag(etag?: string): void { - if (!validator.isNonEmptyString(etag)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'ETag header is not present in the server response.'); - } - } - - /** - * Checks if a given `ListVersionsOptions` object is valid. If successful, creates a copy of the - * options object and convert `startTime` and `endTime` to RFC3339 UTC "Zulu" format, if present. - * - * @param {ListVersionsOptions} options An options object to be validated. - * - * @return {ListVersionsOptions} A copy of the provided options object with timestamps converted - * to UTC Zulu format. - */ - private validateListVersionsOptions(options: ListVersionsOptions): ListVersionsOptions { - const optionsCopy = deepCopy(options); - if (!validator.isNonNullObject(optionsCopy)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'ListVersionsOptions must be a non-null object.'); - } - if (typeof optionsCopy.pageSize !== 'undefined') { - if (!validator.isNumber(optionsCopy.pageSize)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', 'pageSize must be a number.'); - } - if (optionsCopy.pageSize < 1 || optionsCopy.pageSize > 300) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', 'pageSize must be a number between 1 and 300 (inclusive).'); - } - } - if (typeof optionsCopy.pageToken !== 'undefined' && !validator.isNonEmptyString(optionsCopy.pageToken)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', 'pageToken must be a string value.'); - } - if (typeof optionsCopy.endVersionNumber !== 'undefined') { - optionsCopy.endVersionNumber = this.validateVersionNumber(optionsCopy.endVersionNumber, 'endVersionNumber'); - } - if (typeof optionsCopy.startTime !== 'undefined') { - if (!(optionsCopy.startTime instanceof Date) && !validator.isUTCDateString(optionsCopy.startTime)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', 'startTime must be a valid Date object or a UTC date string.'); - } - // Convert startTime to RFC3339 UTC "Zulu" format. - if (optionsCopy.startTime instanceof Date) { - optionsCopy.startTime = optionsCopy.startTime.toISOString(); - } else { - optionsCopy.startTime = new Date(optionsCopy.startTime).toISOString(); - } - } - if (typeof optionsCopy.endTime !== 'undefined') { - if (!(optionsCopy.endTime instanceof Date) && !validator.isUTCDateString(optionsCopy.endTime)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', 'endTime must be a valid Date object or a UTC date string.'); - } - // Convert endTime to RFC3339 UTC "Zulu" format. - if (optionsCopy.endTime instanceof Date) { - optionsCopy.endTime = optionsCopy.endTime.toISOString(); - } else { - optionsCopy.endTime = new Date(optionsCopy.endTime).toISOString(); - } - } - // Remove undefined fields from optionsCopy - Object.keys(optionsCopy).forEach(key => - (typeof (optionsCopy as any)[key] === 'undefined') && delete (optionsCopy as any)[key] - ); - return optionsCopy; - } -} -interface ErrorResponse { - error?: Error; -} + /** + * Display name. Output only. + */ + name?: string; -interface Error { - code?: number; - message?: string; - status?: string; + /** + * Image URL. Output only. + */ + imageUrl?: string; } - -const ERROR_CODE_MAPPING: { [key: string]: RemoteConfigErrorCode } = { - ABORTED: 'aborted', - ALREADY_EXISTS: `already-exists`, - INVALID_ARGUMENT: 'invalid-argument', - INTERNAL: 'internal-error', - FAILED_PRECONDITION: 'failed-precondition', - NOT_FOUND: 'not-found', - OUT_OF_RANGE: 'out-of-range', - PERMISSION_DENIED: 'permission-denied', - RESOURCE_EXHAUSTED: 'resource-exhausted', - UNAUTHENTICATED: 'unauthenticated', - UNKNOWN: 'unknown-error', -}; diff --git a/src/remote-config/remote-config-utils.ts b/src/remote-config/remote-config-utils.ts deleted file mode 100644 index 523eeb10bd..0000000000 --- a/src/remote-config/remote-config-utils.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*! - * Copyright 2020 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'; - -export type RemoteConfigErrorCode = - 'aborted' - | 'already-exists' - | 'failed-precondition' - | 'internal-error' - | 'invalid-argument' - | 'not-found' - | 'out-of-range' - | 'permission-denied' - | 'resource-exhausted' - | 'unauthenticated' - | 'unknown-error'; - -/** - * Firebase Remote Config error code structure. This extends PrefixedFirebaseError. - * - * @param {RemoteConfigErrorCode} code The error code. - * @param {string} message The error message. - * @constructor - */ -export class FirebaseRemoteConfigError extends PrefixedFirebaseError { - constructor(code: RemoteConfigErrorCode, message: string) { - super('remote-config', code, message); - } -} diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index eface572db..4c67189a9f 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -17,9 +17,7 @@ import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { FirebaseApp } from '../firebase-app'; import * as validator from '../utils/validator'; -import { FirebaseRemoteConfigError } from './remote-config-utils'; import { - RemoteConfigApiClient, RemoteConfigTemplate, RemoteConfigParameter, RemoteConfigCondition, @@ -29,6 +27,7 @@ import { RemoteConfigUser, Version, } from './remote-config-api-client'; +import { FirebaseRemoteConfigError, RemoteConfigApiClient } from './remote-config-api-client-internal'; /** * Internals of an RemoteConfig service instance. @@ -62,10 +61,11 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Gets the current active version of the Remote Config template of the project. - * - * @return {Promise} A Promise that fulfills when the template is available. - */ + * Gets the current active version of the {@link admin.remoteConfig.RemoteConfigTemplate + * `RemoteConfigTemplate`} of the project. + * + * @return A promise that fulfills with a `RemoteConfigTemplate`. + */ public getTemplate(): Promise { return this.client.getTemplate() .then((templateResponse) => { @@ -74,12 +74,13 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Gets the requested version of the Remote Config template of the project. - * - * @param {number | string} versionNumber Version number of the Remote Config template to look up. - * - * @return {Promise} A Promise that fulfills when the template is available. - */ + * Gets the requested version of the {@link admin.remoteConfig.RemoteConfigTemplate + * `RemoteConfigTemplate`} of the project. + * + * @param versionNumber Version number of the Remote Config template to look up. + * + * @return A promise that fulfills with a `RemoteConfigTemplate`. + */ public getTemplateAtVersion(versionNumber: number | string): Promise { return this.client.getTemplateAtVersion(versionNumber) .then((templateResponse) => { @@ -88,11 +89,10 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Validates a Remote Config template. + * Validates a {@link admin.remoteConfig.RemoteConfigTemplate `RemoteConfigTemplate`}. * - * @param {RemoteConfigTemplate} template The Remote Config template to be validated. - * - * @return {Promise} A Promise that fulfills when a template is validated. + * @param template The Remote Config template to be validated. + * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. */ public validateTemplate(template: RemoteConfigTemplate): Promise { return this.client.validateTemplate(template) @@ -104,10 +104,16 @@ export class RemoteConfig implements FirebaseServiceInterface { /** * Publishes a Remote Config template. * - * @param {RemoteConfigTemplate} template The Remote Config template to be validated. - * @param {any=} options Optional options object when publishing a Remote Config template. + * @param template The Remote Config template to be published. + * @param options Optional options object when publishing a Remote Config template: + * - {boolean} `force` Setting this to `true` forces the Remote Config template to + * be updated and circumvent the ETag. This approach is not recommended + * because it risks causing the loss of updates to your Remote Config + * template if multiple clients are updating the Remote Config template. + * See {@link https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates + * ETag usage and forced updates}. * - * @return {Promise} A Promise that fulfills when a template is published. + * @return A Promise that fulfills with the published `RemoteConfigTemplate`. */ public publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise { return this.client.publishTemplate(template, options) @@ -117,13 +123,16 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Rollbacks a project's published Remote Config template to the specified version. + * Rolls back a project's published Remote Config template to the specified version. * A rollback is equivalent to getting a previously published Remote Config - * template, and re-publishing it using a force update. - * - * @param {number | string} versionNumber The version number of the Remote Config template - * to rollback to. - * @return {Promise} A Promise that fulfills with the published template. + * template and re-publishing it using a force update. + * + * @param versionNumber The version number of the Remote Config template to roll back to. + * The specified version number must be lower than the current version number, and not have + * been deleted due to staleness. Only the last 300 versions are stored. + * All versions that correspond to non-active Remote Config templates (that is, all except the + * template that is being fetched by clients) are also deleted if they are more than 90 days old. + * @return A promise that fulfills with the published `RemoteConfigTemplate`. */ public rollback(versionNumber: number | string): Promise { return this.client.rollback(versionNumber) @@ -133,14 +142,14 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Gets a list of Remote Config template versions that have been published, sorted in reverse - * chronological order. Only the last 300 versions are stored. - * All versions that correspond to non-active Remote Config templates (i.e., all except the - * template that is being fetched by clients) are also deleted if they are older than 90 days. - * - * @param {ListVersionsOptions} options Optional options object for getting a list of versions. - * @return A promise that fulfills with a `ListVersionsResult`. - */ + * Gets a list of Remote Config template versions that have been published, sorted in reverse + * chronological order. Only the last 300 versions are stored. + * All versions that correspond to non-active Remote Config templates (i.e., all except the + * template that is being fetched by clients) are also deleted if they are older than 90 days. + * + * @param {ListVersionsOptions} options Optional options object for getting a list of versions. + * @return A promise that fulfills with a `ListVersionsResult`. + */ public listVersions(options?: ListVersionsOptions): Promise { return this.client.listVersions(options) .then((listVersionsResponse) => { @@ -154,9 +163,9 @@ export class RemoteConfig implements FirebaseServiceInterface { /** * Creates and returns a new Remote Config template from a JSON string. * - * @param {string} json The JSON string to populate a Remote Config template. + * @param json The JSON string to populate a Remote Config template. * - * @return {RemoteConfigTemplate} A new template instance. + * @return A new template instance. */ public createTemplateFromJSON(json: string): RemoteConfigTemplate { if (!validator.isNonEmptyString(json)) { diff --git a/test/unit/remote-config/remote-config-api-client.spec.ts b/test/unit/remote-config/remote-config-api-client.spec.ts index 0561d0d40b..2b17171082 100644 --- a/test/unit/remote-config/remote-config-api-client.spec.ts +++ b/test/unit/remote-config/remote-config-api-client.spec.ts @@ -20,13 +20,12 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; import { - RemoteConfigApiClient, RemoteConfigTemplate, TagColor, ListVersionsResult, Version, } from '../../../src/remote-config/remote-config-api-client'; -import { FirebaseRemoteConfigError } from '../../../src/remote-config/remote-config-utils'; +import { FirebaseRemoteConfigError, RemoteConfigApiClient } from '../../../src/remote-config/remote-config-api-client-internal'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index 2490085bed..e7a3d08caa 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -23,13 +23,12 @@ import { RemoteConfig } from '../../../src/remote-config/remote-config'; import { FirebaseApp } from '../../../src/firebase-app'; import * as mocks from '../../resources/mocks'; import { - RemoteConfigApiClient, RemoteConfigTemplate, RemoteConfigCondition, TagColor, ListVersionsResult, } from '../../../src/remote-config/remote-config-api-client'; -import { FirebaseRemoteConfigError } from '../../../src/remote-config/remote-config-utils'; +import { FirebaseRemoteConfigError, RemoteConfigApiClient } from '../../../src/remote-config/remote-config-api-client-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; const expect = chai.expect; From be4b539fbfeef4479e863450a1fcf948de8e1cbf Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Tue, 11 Aug 2020 14:56:58 -0400 Subject: [PATCH 026/160] Allow Firestore to auto-generate typings, separate internal vs external APIs (#986) --- gulpfile.js | 1 - src/firebase-app.ts | 4 +- .../{firestore.ts => firestore-internal.ts} | 0 src/firestore/index.ts | 60 +++++++++++++++++++ test/unit/firestore/firestore.spec.ts | 2 +- 5 files changed, 63 insertions(+), 4 deletions(-) rename src/firestore/{firestore.ts => firestore-internal.ts} (100%) create mode 100644 src/firestore/index.ts diff --git a/gulpfile.js b/gulpfile.js index b4f216a638..9ed848daf7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -69,7 +69,6 @@ const TEMPORARY_TYPING_EXCLUDES = [ '!lib/firebase-service.d.ts', '!lib/auth/*.d.ts', '!lib/database/*.d.ts', - '!lib/firestore/*.d.ts', '!lib/machine-learning/*.d.ts', '!lib/storage/*.d.ts', '!lib/utils/*.d.ts', diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 4baf2de08d..194b792ce4 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -28,7 +28,7 @@ import { Storage } from './storage/storage'; import { Database } from '@firebase/database'; import { DatabaseService } from './database/database'; import { Firestore } from '@google-cloud/firestore'; -import { FirestoreService } from './firestore/firestore'; +import { FirestoreService } from './firestore/firestore-internal'; import { InstanceId } from './instance-id/instance-id'; import { ProjectManagement } from './project-management/project-management'; @@ -339,7 +339,7 @@ export class FirebaseApp { public firestore(): Firestore { const service: FirestoreService = this.ensureService_('firestore', () => { - const firestoreService: typeof FirestoreService = require('./firestore/firestore').FirestoreService; + const firestoreService: typeof FirestoreService = require('./firestore/firestore-internal').FirestoreService; return new firestoreService(this); }); return service.client; diff --git a/src/firestore/firestore.ts b/src/firestore/firestore-internal.ts similarity index 100% rename from src/firestore/firestore.ts rename to src/firestore/firestore-internal.ts diff --git a/src/firestore/index.ts b/src/firestore/index.ts new file mode 100644 index 0000000000..fb48b137b8 --- /dev/null +++ b/src/firestore/index.ts @@ -0,0 +1,60 @@ +/*! + * Copyright 2020 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 { FirebaseApp } from '../firebase-app'; +import * as _firestore from '@google-cloud/firestore'; +import * as firebaseAdmin from '../index'; + +export function firestore(app?: FirebaseApp): _firestore.Firestore { + if (typeof (app) === 'undefined') { + app = firebaseAdmin.app(); + } + return app.firestore(); +} + +/** + * We must define a namespace to make the typings work correctly. Otherwise + * `admin.firestore()` cannot be called like a function. Temporarily, + * admin.firestore is used as the namespace name because we cannot barrel + * re-export the contents from firestore, and we want it to + * match the namespacing in the re-export inside src/index.d.ts + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace admin.firestore { + // See https://github.com/microsoft/TypeScript/issues/4336 + /* eslint-disable @typescript-eslint/no-unused-vars */ + // See https://github.com/typescript-eslint/typescript-eslint/issues/363 + export import v1beta1 = _firestore.v1beta1; + export import v1 = _firestore.v1; + + export import CollectionReference = _firestore.CollectionReference; + export import DocumentData = _firestore.DocumentData; + export import DocumentReference = _firestore.DocumentReference; + export import DocumentSnapshot = _firestore.DocumentSnapshot; + export import FieldPath = _firestore.FieldPath; + export import FieldValue = _firestore.FieldValue; + export import Firestore = _firestore.Firestore; + export import GeoPoint = _firestore.GeoPoint; + export import Query = _firestore.Query; + export import QueryDocumentSnapshot = _firestore.QueryDocumentSnapshot; + export import QuerySnapshot = _firestore.QuerySnapshot; + export import Timestamp = _firestore.Timestamp; + export import Transaction = _firestore.Transaction; + export import WriteBatch = _firestore.WriteBatch; + export import WriteResult = _firestore.WriteResult; + + export import setLogFunction = _firestore.setLogFunction; +} diff --git a/test/unit/firestore/firestore.spec.ts b/test/unit/firestore/firestore.spec.ts index 236050dfbe..0935f58821 100644 --- a/test/unit/firestore/firestore.spec.ts +++ b/test/unit/firestore/firestore.spec.ts @@ -22,7 +22,7 @@ import { expect } from 'chai'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; import { ComputeEngineCredential, RefreshTokenCredential } from '../../../src/auth/credential'; -import { FirestoreService, getFirestoreOptions } from '../../../src/firestore/firestore'; +import { FirestoreService, getFirestoreOptions } from '../../../src/firestore/firestore-internal'; describe('Firestore', () => { let mockApp: FirebaseApp; From ef33c3c3aba59bc866101bf9ff07f8705b406ad4 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Tue, 11 Aug 2020 13:36:04 -0700 Subject: [PATCH 027/160] feat(auth): Adds ability to enable MFA on a Google Cloud Identity Platform tenant (#930) * feat(auth): Adds ability to enable MFA on a tenant. This includes the following capabilities: - Ability to enable disable MFA on a tenant. - Configure the MFA supported type. - Configure the test phone number / code pairs on the tenant. --- docgen/content-sources/node/toc.yaml | 2 + src/auth.d.ts | 49 ++++++ src/auth/auth-api-request.ts | 6 +- src/auth/auth-config.ts | 208 +++++++++++++++++++++++ src/auth/tenant.ts | 59 ++++++- src/utils/error.ts | 10 ++ src/utils/index.ts | 34 ++-- test/integration/auth.spec.ts | 33 ++++ test/unit/auth/auth-api-request.spec.ts | 46 ++++- test/unit/auth/auth-config.spec.ts | 213 +++++++++++++++++++++++- test/unit/auth/tenant.spec.ts | 141 +++++++++++++++- test/unit/utils/index.spec.ts | 54 +++--- 12 files changed, 807 insertions(+), 48 deletions(-) diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index 6af80e2356..595f55ae7a 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -40,6 +40,8 @@ toc: path: /docs/reference/admin/node/admin.auth.ListProviderConfigResults - title: "ListTenantsResult" path: /docs/reference/admin/node/admin.auth.ListTenantsResult + - title: "MultiFactorConfig" + path: /docs/reference/admin/node/admin.auth.MultiFactorConfig - title: "MultiFactorCreateSettings" path: /docs/reference/admin/node/admin.auth.MultiFactorCreateSettings - title: "MultiFactorInfo" diff --git a/src/auth.d.ts b/src/auth.d.ts index 39fa97c21b..02e8072455 100644 --- a/src/auth.d.ts +++ b/src/auth.d.ts @@ -971,12 +971,50 @@ export namespace admin.auth { passwordRequired?: boolean; }; + /** + * The multi-factor auth configuration on the current tenant. + */ + multiFactorConfig?: admin.auth.MultiFactorConfig; + + /** + * The map containing the test phone number / code pairs for the tenant. + */ + testPhoneNumbers?: {[phoneNumber: string]: string}; + /** * @return A JSON-serializable representation of this object. */ toJSON(): Object; } + /** + * Identifies a second factor type. + */ + type AuthFactorType = 'phone'; + + /** + * Identifies a multi-factor configuration state. + */ + type MultiFactorConfigState = 'ENABLED' | 'DISABLED'; + + /** + * Interface representing a multi-factor configuration. + * This can be used to define whether multi-factor authentication is enabled + * or disabled and the list of second factor challenges that are supported. + */ + interface MultiFactorConfig { + /** + * The multi-factor config state. + */ + state: admin.auth.MultiFactorConfigState; + + /** + * The list of identifiers for enabled second factors. + * Currently only ‘phone’ is supported. + */ + factorIds?: admin.auth.AuthFactorType[]; + } + /** * Interface representing the properties to update on the provided tenant. */ @@ -1003,6 +1041,17 @@ export namespace admin.auth { */ passwordRequired?: boolean; }; + + /** + * The multi-factor auth configuration to update on the tenant. + */ + multiFactorConfig?: admin.auth.MultiFactorConfig; + + /** + * The updated map containing the test phone number / code pairs for the tenant. + * Passing null clears the previously save phone number / code pairs. + */ + testPhoneNumbers?: {[phoneNumber: string]: string} | null; } /** diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index cf0df17b92..d6d6765280 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -1801,7 +1801,7 @@ const DELETE_TENANT = new ApiSettings('/tenants/{tenantId}', 'DELETE'); /** Instantiates the updateTenant endpoint settings. */ const UPDATE_TENANT = new ApiSettings('/tenants/{tenantId}?updateMask={updateMask}', 'PATCH') -// Set response validator. + // Set response validator. .setResponseValidator((response: any) => { // Response should always contain at least the tenant name. if (!validator.isNonEmptyString(response.name) || @@ -1982,7 +1982,9 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { try { // Construct backend request. const request = Tenant.buildServerRequest(tenantOptions, false); - const updateMask = utils.generateUpdateMask(request); + // 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, { tenantId, updateMask: updateMask.join(',') }) .then((response: any) => { diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index ed0c563e94..8c8df58fb4 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -18,6 +18,8 @@ import * as validator from '../utils/validator'; import { deepCopy } from '../utils/deep-copy'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +/** A maximum of 10 test phone number / code pairs can be configured. */ +export const MAXIMUM_TEST_PHONE_NUMBERS = 10; /** The filter interface used for listing provider configurations. */ export interface AuthProviderConfigFilter { @@ -160,6 +162,212 @@ export interface EmailSignInConfigServerRequest { enableEmailLinkSignin?: boolean; } +/** Identifies the public second factor type. */ +export type AuthFactorType = 'phone'; + +/** Identifies the server side second factor type. */ +export type AuthFactorServerType = 'PHONE_SMS'; + +/** Client Auth factor type to server auth factor type mapping. */ +export const AUTH_FACTOR_CLIENT_TO_SERVER_TYPE: {[key: string]: AuthFactorServerType} = { + phone: 'PHONE_SMS', +}; + +/** Server Auth factor type to client auth factor type mapping. */ +export const AUTH_FACTOR_SERVER_TO_CLIENT_TYPE: {[key: string]: AuthFactorType} = + Object.keys(AUTH_FACTOR_CLIENT_TO_SERVER_TYPE) + .reduce((res: {[key: string]: AuthFactorType}, key) => { + res[AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[key]] = key as AuthFactorType; + return res; + }, {}); + +/** Identifies a multi-factor configuration state. */ +export type MultiFactorConfigState = 'ENABLED' | 'DISABLED'; + +/** + * Public API interface representing a multi-factor configuration. + */ +export interface MultiFactorConfig { + /** + * The multi-factor config state. + */ + state: MultiFactorConfigState; + + /** + * The list of identifiers for enabled second factors. + * Currently only ‘phone’ is supported. + */ + factorIds?: AuthFactorType[]; +} + +/** Server side multi-factor configuration. */ +export interface MultiFactorAuthServerConfig { + state?: MultiFactorConfigState; + enabledProviders?: AuthFactorServerType[]; +} + + +/** + * Defines the multi-factor config class used to convert client side MultiFactorConfig + * to a format that is understood by the Auth server. + */ +export class MultiFactorAuthConfig implements MultiFactorConfig { + public readonly state: MultiFactorConfigState; + public readonly factorIds: AuthFactorType[]; + + /** + * Static method to convert a client side request to a MultiFactorAuthServerConfig. + * Throws an error if validation fails. + * + * @param options The options object to convert to a server request. + * @return The resulting server request. + */ + public static buildServerRequest(options: MultiFactorConfig): MultiFactorAuthServerConfig { + const request: MultiFactorAuthServerConfig = {}; + MultiFactorAuthConfig.validate(options); + if (Object.prototype.hasOwnProperty.call(options, 'state')) { + request.state = options.state; + } + if (Object.prototype.hasOwnProperty.call(options, 'factorIds')) { + (options.factorIds || []).forEach((factorId) => { + if (typeof request.enabledProviders === 'undefined') { + request.enabledProviders = []; + } + request.enabledProviders.push(AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[factorId]); + }); + // In case an empty array is passed. Ensure it gets populated so the array is cleared. + if (options.factorIds && options.factorIds.length === 0) { + request.enabledProviders = []; + } + } + return request; + } + + /** + * Validates the MultiFactorConfig options object. Throws an error on failure. + * + * @param options The options object to validate. + */ + private static validate(options: MultiFactorConfig): void { + const validKeys = { + state: true, + factorIds: true, + }; + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MultiFactorConfig" must be a non-null object.', + ); + } + // Check for unsupported top level attributes. + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid MultiFactorConfig parameter.`, + ); + } + } + // Validate content. + if (typeof options.state !== 'undefined' && + options.state !== 'ENABLED' && + options.state !== 'DISABLED') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".', + ); + } + + if (typeof options.factorIds !== 'undefined') { + if (!validator.isArray(options.factorIds)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MultiFactorConfig.factorIds" must be an array of valid "AuthFactorTypes".', + ); + } + + // Validate content of array. + options.factorIds.forEach((factorId) => { + if (typeof AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[factorId] === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${factorId}" is not a valid "AuthFactorType".`, + ); + } + }); + } + } + + /** + * The MultiFactorAuthConfig constructor. + * + * @param response The server side response used to initialize the + * MultiFactorAuthConfig object. + * @constructor + */ + constructor(response: MultiFactorAuthServerConfig) { + if (typeof response.state === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response'); + } + this.state = response.state; + this.factorIds = []; + (response.enabledProviders || []).forEach((enabledProvider) => { + // Ignore unsupported types. It is possible the current admin SDK version is + // not up to date and newer backend types are supported. + if (typeof AUTH_FACTOR_SERVER_TO_CLIENT_TYPE[enabledProvider] !== 'undefined') { + this.factorIds.push(AUTH_FACTOR_SERVER_TO_CLIENT_TYPE[enabledProvider]); + } + }) + } + + /** @return The plain object representation of the multi-factor config instance. */ + public toJSON(): object { + return { + state: this.state, + factorIds: this.factorIds, + }; + } +} + + +/** + * Validates the provided map of test phone number / code pairs. + * @param testPhoneNumbers The phone number / code pairs to validate. + */ +export function validateTestPhoneNumbers( + testPhoneNumbers: {[phoneNumber: string]: string}, +): void { + if (!validator.isObject(testPhoneNumbers)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"testPhoneNumbers" must be a map of phone number / code pairs.', + ); + } + if (Object.keys(testPhoneNumbers).length > MAXIMUM_TEST_PHONE_NUMBERS) { + throw new FirebaseAuthError(AuthClientErrorCode.MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED); + } + for (const phoneNumber in testPhoneNumbers) { + // Validate phone number. + if (!validator.isPhoneNumber(phoneNumber)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_TESTING_PHONE_NUMBER, + `"${phoneNumber}" is not a valid E.164 standard compliant phone number.` + ); + } + + // Validate code. + if (!validator.isString(testPhoneNumbers[phoneNumber]) || + !/^[\d]{6}$/.test(testPhoneNumbers[phoneNumber])) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_TESTING_PHONE_NUMBER, + `"${testPhoneNumbers[phoneNumber]}" is not a valid 6 digit code string.` + ); + } + } +} + /** * Defines the email sign-in config class used to convert client side EmailSignInConfig diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 2d449035fe..ce3bb36ecb 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -15,20 +15,27 @@ */ import * as validator from '../utils/validator'; +import { deepCopy } from '../utils/deep-copy'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { EmailSignInConfig, EmailSignInConfigServerRequest, EmailSignInProviderConfig, + MultiFactorConfig, MultiFactorAuthServerConfig, MultiFactorAuthConfig, + validateTestPhoneNumbers, } from './auth-config'; /** The TenantOptions interface used for create/read/update tenant operations. */ export interface TenantOptions { displayName?: string; emailSignInConfig?: EmailSignInProviderConfig; + multiFactorConfig?: MultiFactorConfig; + testPhoneNumbers?: {[phoneNumber: string]: string} | null; } /** The corresponding server side representation of a TenantOptions object. */ export interface TenantOptionsServerRequest extends EmailSignInConfigServerRequest { displayName?: string; + mfaConfig?: MultiFactorAuthServerConfig; + testPhoneNumbers?: {[key: string]: string}; } /** The tenant server response interface. */ @@ -37,6 +44,8 @@ export interface TenantServerResponse { displayName?: string; allowPasswordSignup?: boolean; enableEmailLinkSignin?: boolean; + mfaConfig?: MultiFactorAuthServerConfig; + testPhoneNumbers?: {[key: string]: string}; } /** The interface representing the listTenant API response. */ @@ -53,6 +62,8 @@ export class Tenant { public readonly tenantId: string; public readonly displayName?: string; public readonly emailSignInConfig?: EmailSignInConfig; + public readonly multiFactorConfig?: MultiFactorAuthConfig; + public readonly testPhoneNumbers?: {[phoneNumber: string]: string}; /** * Builds the corresponding server request for a TenantOptions object. @@ -71,6 +82,13 @@ export class Tenant { if (typeof tenantOptions.displayName !== 'undefined') { request.displayName = tenantOptions.displayName; } + if (typeof tenantOptions.multiFactorConfig !== 'undefined') { + request.mfaConfig = MultiFactorAuthConfig.buildServerRequest(tenantOptions.multiFactorConfig); + } + if (typeof tenantOptions.testPhoneNumbers !== 'undefined') { + // null will clear existing test phone numbers. Translate to empty object. + request.testPhoneNumbers = tenantOptions.testPhoneNumbers ?? {}; + } return request; } @@ -99,6 +117,8 @@ export class Tenant { const validKeys = { displayName: true, emailSignInConfig: true, + multiFactorConfig: true, + testPhoneNumbers: true, }; const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; if (!validator.isNonNullObject(request)) { @@ -129,15 +149,31 @@ export class Tenant { // This will throw an error if invalid. EmailSignInConfig.buildServerRequest(request.emailSignInConfig); } + // Validate test phone numbers if provided. + if (typeof request.testPhoneNumbers !== 'undefined' && + request.testPhoneNumbers !== null) { + validateTestPhoneNumbers(request.testPhoneNumbers); + } else if (request.testPhoneNumbers === null && createRequest) { + // null allowed only for update operations. + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${label}.testPhoneNumbers" must be a non-null object.`, + ); + } + // Validate multiFactorConfig type if provided. + if (typeof request.multiFactorConfig !== 'undefined') { + // This will throw an error if invalid. + MultiFactorAuthConfig.buildServerRequest(request.multiFactorConfig); + } } /** * The Tenant object constructor. * - * @param {any} response The server side response used to initialize the Tenant object. + * @param response The server side response used to initialize the Tenant object. * @constructor */ - constructor(response: any) { + constructor(response: TenantServerResponse) { const tenantId = Tenant.getTenantIdFromResourceName(response.name); if (!tenantId) { throw new FirebaseAuthError( @@ -155,15 +191,30 @@ export class Tenant { allowPasswordSignup: false, }); } + if (typeof response.mfaConfig !== 'undefined') { + this.multiFactorConfig = new MultiFactorAuthConfig(response.mfaConfig); + } + if (typeof response.testPhoneNumbers !== 'undefined') { + this.testPhoneNumbers = deepCopy(response.testPhoneNumbers || {}); + } } /** @return {object} The plain object representation of the tenant. */ public toJSON(): object { - return { + const json = { tenantId: this.tenantId, displayName: this.displayName, - emailSignInConfig: this.emailSignInConfig && this.emailSignInConfig.toJSON(), + emailSignInConfig: this.emailSignInConfig?.toJSON(), + multiFactorConfig: this.multiFactorConfig?.toJSON(), + testPhoneNumbers: this.testPhoneNumbers, }; + if (typeof json.multiFactorConfig === 'undefined') { + delete json.multiFactorConfig; + } + if (typeof json.testPhoneNumbers === 'undefined') { + delete json.testPhoneNumbers; + } + return json; } } diff --git a/src/utils/error.ts b/src/utils/error.ts index 8644511463..a894accc76 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -537,6 +537,10 @@ export class AuthClientErrorCode { code: 'invalid-tenant-type', message: 'Tenant type must be either "full_service" or "lightweight".', }; + public static INVALID_TESTING_PHONE_NUMBER = { + code: 'invalid-testing-phone-number', + message: 'Invalid testing phone number or invalid test code provided.', + }; public static INVALID_UID = { code: 'invalid-uid', message: 'The uid must be a non-empty string with at most 128 characters.', @@ -600,6 +604,10 @@ export class AuthClientErrorCode { code: 'missing-saml-relying-party-config', message: 'The SAML configuration provided is missing a relying party configuration.', }; + public static MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED = { + code: 'test-phone-number-limit-exceeded', + message: 'The maximum allowed number of test phone number / code pairs has been exceeded.', + }; public static MAXIMUM_USER_COUNT_EXCEEDED = { code: 'maximum-user-count-exceeded', message: 'The maximum allowed number of users to import has been exceeded.', @@ -875,6 +883,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { INVALID_PROVIDER_ID: 'INVALID_PROVIDER_ID', // Invalid service account. INVALID_SERVICE_ACCOUNT: 'INVALID_SERVICE_ACCOUNT', + // Invalid testing phone number. + INVALID_TESTING_PHONE_NUMBER: 'INVALID_TESTING_PHONE_NUMBER', // Invalid tenant type. INVALID_TENANT_TYPE: 'INVALID_TENANT_TYPE', // Missing Android package name. diff --git a/src/utils/index.ts b/src/utils/index.ts index 8c3a540dc0..fee064eab3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -152,23 +152,37 @@ export function formatString(str: string, params?: object): string { * Generates the update mask for the provided object. * Note this will ignore the last key with value undefined. * - * @param {[key: string]: any} obj The object to generate the update mask for. - * @return {Array} The computed update mask list. + * @param obj The object to generate the update mask for. + * @param terminalPaths The optional map of keys for maximum paths to traverse. + * Nested objects beyond that path will be ignored. This is useful for + * keys with variable object values. + * @param root The path so far. + * @return The computed update mask list. */ -export function generateUpdateMask(obj: {[key: string]: any}): string[] { +export function generateUpdateMask( + obj: any, terminalPaths: string[] = [], root = '' +): string[] { const updateMask: string[] = []; if (!validator.isNonNullObject(obj)) { return updateMask; } for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key) && typeof obj[key] !== 'undefined') { - const maskList = generateUpdateMask(obj[key]); - if (maskList.length > 0) { - maskList.forEach((mask) => { - updateMask.push(`${key}.${mask}`); - }); - } else { + if (typeof obj[key] !== 'undefined') { + const nextPath = root ? `${root}.${key}` : key; + // We hit maximum path. + // Consider switching to Set if the list grows too large. + if (terminalPaths.indexOf(nextPath) !== -1) { + // Add key and stop traversing this branch. updateMask.push(key); + } else { + const maskList = generateUpdateMask(obj[key], terminalPaths, nextPath); + if (maskList.length > 0) { + maskList.forEach((mask) => { + updateMask.push(`${key}.${mask}`); + }); + } else { + updateMask.push(key); + } } } } diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index e4617357fb..36185c8135 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -745,6 +745,15 @@ describe('admin.auth', () => { enabled: true, passwordRequired: true, }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, + // Add random phone number / code pairs. + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, }; const expectedCreatedTenant: any = { displayName: 'testTenant1', @@ -752,6 +761,14 @@ describe('admin.auth', () => { enabled: true, passwordRequired: true, }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, }; const expectedUpdatedTenant: any = { displayName: 'testTenantUpdated', @@ -759,6 +776,13 @@ describe('admin.auth', () => { enabled: false, passwordRequired: true, }, + multiFactorConfig: { + state: 'DISABLED', + factorIds: [], + }, + testPhoneNumbers: { + '+16505551234': '123456', + }, }; const expectedUpdatedTenant2: any = { displayName: 'testTenantUpdated', @@ -766,6 +790,10 @@ describe('admin.auth', () => { enabled: true, passwordRequired: false, }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, }; // https://mochajs.org/ @@ -1125,12 +1153,17 @@ describe('admin.auth', () => { emailSignInConfig: { enabled: false, }, + multiFactorConfig: deepCopy(expectedUpdatedTenant.multiFactorConfig), + testPhoneNumbers: deepCopy(expectedUpdatedTenant.testPhoneNumbers), }; const updatedOptions2: admin.auth.UpdateTenantRequest = { emailSignInConfig: { enabled: true, passwordRequired: false, }, + multiFactorConfig: deepCopy(expectedUpdatedTenant2.multiFactorConfig), + // Test clearing of phone numbers. + testPhoneNumbers: null, }; return admin.auth().tenantManager().updateTenant(createdTenantId, updatedOptions) .then((actualTenant) => { diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 3599aac026..19e1d59f9d 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -4454,11 +4454,27 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { enabled: true, passwordRequired: true, }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, }; const expectedRequest = { displayName: 'TENANT-DISPLAY-NAME', allowPasswordSignup: true, enableEmailLinkSignin: false, + mfaConfig: { + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, }; const expectedResult = utils.responseFrom(deepExtend({ name: 'projects/project_id/tenants/tenant-id', @@ -4560,24 +4576,41 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const path = '/v2/projects/project_id/tenants/tenant-id'; const patchMethod = 'PATCH'; const tenantId = 'tenant-id'; - const tenantOptions = { + const tenantOptions: TenantOptions = { displayName: 'TENANT-DISPLAY-NAME', emailSignInConfig: { enabled: true, passwordRequired: true, }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, }; const expectedRequest = { displayName: 'TENANT-DISPLAY-NAME', allowPasswordSignup: true, enableEmailLinkSignin: false, + mfaConfig: { + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, }; const expectedResult = utils.responseFrom(deepExtend({ name: 'projects/project_id/tenants/tenant-id', }, expectedRequest)); it('should be fulfilled given full parameters', () => { - const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName'; + const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -4663,7 +4696,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected when the backend returns a response missing name', () => { - const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName'; + const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; const expectedError = new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update tenant', @@ -4683,7 +4717,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected when the backend returns a response missing tenant ID in response name', () => { - const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName'; + const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; const expectedError = new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update tenant', @@ -4705,7 +4740,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected when the backend returns an error', () => { - const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName'; + const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; const expectedServerError = utils.errorFrom({ error: { message: 'INTERNAL_ERROR', diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts index d74a6c9666..03b52ed539 100644 --- a/test/unit/auth/auth-config.spec.ts +++ b/test/unit/auth/auth-config.spec.ts @@ -25,7 +25,8 @@ import { SAMLConfigServerResponse, OIDCConfigServerRequest, OIDCConfigServerResponse, SAMLUpdateAuthProviderRequest, OIDCUpdateAuthProviderRequest, SAMLAuthProviderConfig, OIDCAuthProviderConfig, - EmailSignInConfig, + EmailSignInConfig, MultiFactorAuthConfig, validateTestPhoneNumbers, + MAXIMUM_TEST_PHONE_NUMBERS, } from '../../../src/auth/auth-config'; @@ -152,6 +153,216 @@ describe('EmailSignInConfig', () => { }); }); +describe('MultiFactorAuthConfig', () => { + describe('constructor', () => { + const validConfig = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }); + + it('should throw on missing state', () => { + expect(() => new MultiFactorAuthConfig({ + enabledProviders: ['PHONE_SMS'], + } as any)).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor configuration response'); + }); + + it('should set readonly property "state" to ENABLED on state enabled', () => { + expect(validConfig.state).to.equals('ENABLED'); + }); + + it('should set readonly property "state" to DISABLED on state disabled', () => { + const disabledState = new MultiFactorAuthConfig({ + state: 'DISABLED', + enabledProviders: ['PHONE_SMS'], + }); + expect(disabledState.state).to.equals('DISABLED'); + }); + + it('should set readonly property "factorIds"', () => { + expect(validConfig.factorIds).to.deep.equal(['phone']); + }); + + it('should ignore unsupported backend types if found', () => { + const unsupportedType = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['UNSUPPORTED_TYPE', 'PHONE_SMS'], + } as any); + expect(unsupportedType.factorIds).to.deep.equal(['phone']); + }); + + it('should return empty factorIds array if no supported types are found', () => { + const unsupportedType = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['UNSUPPORTED_TYPE'], + } as any); + expect(unsupportedType.factorIds).to.deep.equal([]); + }); + }); + + describe('toJSON()', () => { + it('should return expected JSON representation', () => { + const config = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }); + expect(config.toJSON()).to.deep.equal({ + state: 'ENABLED', + factorIds: ['phone'], + }); + }); + }); + + describe('buildServerRequest()', () => { + it('should return expected server request on valid state and factorIds', () => { + expect(MultiFactorAuthConfig.buildServerRequest({ + state: 'ENABLED', + factorIds: ['phone'], + })).to.deep.equal({ + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }); + }); + + it('should return expected server request on valid state without factorIds', () => { + expect(MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + })).to.deep.equal({ + state: 'DISABLED', + }); + }); + + it('should return empty enabledProviders when an empty "options.factorIds" is provided', () => { + expect(MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + factorIds: [], + })).to.deep.equal({ + state: 'DISABLED', + enabledProviders: [], + }); + }); + + const invalidOptions = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidOptions.forEach((options) => { + it('should throw on invalid MultiFactorAuthConfig:' + JSON.stringify(options), () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest(options as any); + }).to.throw('"MultiFactorConfig" must be a non-null object.'); + }); + }); + + it('should throw on MultiFactorAuthConfig with unsupported attribute', () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + unsupported: true, + state: 'ENABLED', + factorIds: ['phone'], + } as any); + }).to.throw('"unsupported" is not a valid MultiFactorConfig parameter.'); + }); + + const invalidState = [ + null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop, true, false, + ]; + invalidState.forEach((state) => { + it('should throw on invalid MultiFactorConfig.state:' + JSON.stringify(state), () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state, + factorIds: ['phone'], + } as any); + }).to.throw('"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".'); + }); + }); + + it('should throw on non-array MultiFactorAuthConfig.factorIds', () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'ENABLED', + factorIds: 'phone', + } as any); + }).to.throw('"MultiFactorConfig.factorIds" must be an array of valid "AuthFactorTypes".'); + }); + + const invalidFactorIds = invalidState; + invalidFactorIds.forEach((factorId) => { + it('should throw on invalid MultiFactorConfig.factorIds:' + JSON.stringify(factorId), () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'ENABLED', + factorIds: [factorId], + } as any); + }).to.throw(`"${factorId}" is not a valid "AuthFactorType".`); + }); + }); + }); +}); + +describe('validateTestPhoneNumbers', () => { + it('should not throw an error on empty object', () => { + expect(() => validateTestPhoneNumbers({})).not.to.throw(); + }); + + it('should not throw an error on valid phone number / code pairs', () => { + const pairs = { + '+16505551234': '019287', + '+16505550676': '985235', + '+1 (123) 456-7890': '098765', + '+1 800 FLOwerS': '000000', + }; + + expect(() => validateTestPhoneNumbers(pairs)).not.to.throw(); + }); + + it(`should not throw when ${MAXIMUM_TEST_PHONE_NUMBERS} pairs are provided`, () => { + const pairs: {[key: string]: string} = {}; + for (let i = 0; i < MAXIMUM_TEST_PHONE_NUMBERS; i++) { + pairs[`+1650555${'0'.repeat(4 - i.toString().length)}${i}`] = '012938'; + } + + expect(() => validateTestPhoneNumbers(pairs)).not.to.throw(); + }); + + it(`should throw when >${MAXIMUM_TEST_PHONE_NUMBERS} pairs are provided`, () => { + const pairs: {[key: string]: string} = {}; + for (let i = 0; i < MAXIMUM_TEST_PHONE_NUMBERS + 1; i++) { + pairs[`+1650555${'0'.repeat(4 - i.toString().length)}${i}`] = '012938'; + } + + expect(() => validateTestPhoneNumbers(pairs)).to.throw(); + }); + + const nonObjects = [NaN, 0, 1, true, false, '', 'a', _.noop]; + nonObjects.forEach((nonObject) => { + it(`should throw when non-object ${JSON.stringify(nonObject)} is provided`, () => { + expect(() => validateTestPhoneNumbers(nonObject as any)).to.throw(); + }); + }); + + const invalidPhoneNumbers = [ + null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop, '+', '+ ()-', + ]; + invalidPhoneNumbers.forEach((invalidPhoneNumber) => { + it(`should throw when "${JSON.stringify(invalidPhoneNumber)}" is used as phone number`, () => { + const pairs = { + [invalidPhoneNumber as any]: '123456', + }; + expect(() => validateTestPhoneNumbers(pairs)).to.throw(); + }); + }); + + const invalidCodes = [ + NaN, 0, 1, true, false, '', 'a', _.noop, '12345', '1234567', '123a56', '12 345', 123456, + ]; + invalidCodes.forEach((invalidCode) => { + it(`should throw when an invalid code ${JSON.stringify(invalidCode)} is provided`, () => { + const pairs = { + '+16505551234': invalidCode, + }; + expect(() => validateTestPhoneNumbers(pairs as any)).to.throw(); + }); + }); +}); + describe('SAMLConfig', () => { const serverRequest: SAMLConfigServerRequest = { idpConfig: { diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 21440a1a8f..128c6840f5 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -20,7 +20,9 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import { deepCopy } from '../../../src/utils/deep-copy'; -import { EmailSignInConfig, EmailSignInProviderConfig } from '../../../src/auth/auth-config'; +import { + EmailSignInConfig, EmailSignInProviderConfig, MultiFactorAuthConfig, +} from '../../../src/auth/auth-config'; import { Tenant, TenantOptions, TenantServerResponse, } from '../../../src/auth/tenant'; @@ -38,6 +40,14 @@ describe('Tenant', () => { displayName: 'TENANT-DISPLAY-NAME', allowPasswordSignup: true, enableEmailLinkSignin: true, + mfaConfig: { + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, }; const clientRequest: TenantOptions = { @@ -46,13 +56,44 @@ describe('Tenant', () => { enabled: true, passwordRequired: false, }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + }; + + const serverRequestWithoutMfa: TenantServerResponse = { + name: 'projects/project1/tenants/TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + allowPasswordSignup: true, + enableEmailLinkSignin: true, + }; + + const clientRequestWithoutMfa: TenantOptions = { + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, }; describe('buildServerRequest()', () => { const createRequest = true; describe('for an update request', () => { - it('should return the expected server request', () => { + it('should return the expected server request without multi-factor and phone config', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithoutMfa); + const tenantOptionsServerRequest = deepCopy(serverRequestWithoutMfa); + delete tenantOptionsServerRequest.name; + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) + .to.deep.equal(tenantOptionsServerRequest); + }); + + it('should return the expected server request with multi-factor and phone config', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); const tenantOptionsServerRequest = deepCopy(serverRequest); delete tenantOptionsServerRequest.name; @@ -75,6 +116,33 @@ describe('Tenant', () => { }).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); }); + it('should throw on invalid MultiFactorConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.multiFactorConfig.state = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".'); + }); + + it('should throw on invalid testPhoneNumbers attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.testPhoneNumbers = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"testPhoneNumbers" must be a map of phone number / code pairs.'); + }); + + it('should not throw on null testPhoneNumbers attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + const tenantOptionsServerRequest = deepCopy(serverRequest); + tenantOptionsClientRequest.testPhoneNumbers = null; + delete tenantOptionsServerRequest.name; + tenantOptionsServerRequest.testPhoneNumbers = {}; + + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) + .to.deep.equal(tenantOptionsServerRequest); + }); + it('should not throw on valid client request object', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); expect(() => { @@ -112,7 +180,16 @@ describe('Tenant', () => { }); describe('for a create request', () => { - it('should return the expected server request', () => { + it('should return the expected server request without multi-factor and phone config', () => { + const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequestWithoutMfa); + const tenantOptionsServerRequest: TenantServerResponse = deepCopy(serverRequestWithoutMfa); + delete tenantOptionsServerRequest.name; + + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest)) + .to.deep.equal(tenantOptionsServerRequest); + }); + + it('should return the expected server request with multi-factor and phone config', () => { const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); const tenantOptionsServerRequest: TenantServerResponse = deepCopy(serverRequest); delete tenantOptionsServerRequest.name; @@ -129,6 +206,34 @@ describe('Tenant', () => { .to.throw('"EmailSignInConfig" must be a non-null object.'); }); + it('should throw on invalid MultiFactorConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.multiFactorConfig.factorIds = ['invalid']; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw(`"invalid" is not a valid "AuthFactorType".`,); + }); + + it('should throw on invalid testPhoneNumbers attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.testPhoneNumbers = { 'invalid': '123456' }; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw(`"invalid" is not a valid E.164 standard compliant phone number.`); + }); + + it('should throw on null testPhoneNumbers attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + const tenantOptionsServerRequest = deepCopy(serverRequest); + tenantOptionsClientRequest.testPhoneNumbers = null; + delete tenantOptionsServerRequest.name; + tenantOptionsServerRequest.testPhoneNumbers = {}; + + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw(`"CreateTenantRequest.testPhoneNumbers" must be a non-null object.`); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { @@ -198,6 +303,19 @@ describe('Tenant', () => { expect(tenant.emailSignInConfig).to.deep.equal(expectedEmailSignInConfig); }); + it('should set readonly property multiFactorConfig', () => { + const expectedMultiFactorConfig = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }); + expect(tenant.multiFactorConfig).to.deep.equal(expectedMultiFactorConfig); + }); + + it('should set readonly property testPhoneNumbers', () => { + expect(tenant.testPhoneNumbers).to.deep.equal( + deepCopy(clientRequest.testPhoneNumbers)); + }); + it('should throw when no tenant ID is provided', () => { const invalidOptions = deepCopy(serverRequest); // Use resource name that does not include a tenant ID. @@ -233,6 +351,23 @@ describe('Tenant', () => { enabled: true, passwordRequired: false, }, + multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), + testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), + }); + }); + + it('should not populate optional fields if not available', () => { + const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverRequest); + delete serverRequestCopyWithoutMfa.mfaConfig; + delete serverRequestCopyWithoutMfa.testPhoneNumbers; + + expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({ + tenantId: 'TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, }); }); }); diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index acb76f074a..1acbb6f4d6 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -335,10 +335,32 @@ describe('formatString()', () => { }); describe('generateUpdateMask()', () => { + const obj: any = { + a: undefined, + b: 'something', + c: ['stuff'], + d: false, + e: {}, + f: { + g: 1, + h: 0, + i: { + j: 2, + }, + }, + k: { + i: null, + j: undefined, + }, + l: { + m: undefined, + }, + n: [], + }; const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((nonObject) => { it(`should return empty array for non object ${JSON.stringify(nonObject)}`, () => { - expect(generateUpdateMask(nonObject as any)).to.deep.equal([]); + expect(generateUpdateMask(nonObject)).to.deep.equal([]); }); }); @@ -347,30 +369,16 @@ describe('generateUpdateMask()', () => { }); it('should return expected update mask array for nested object', () => { - const obj: any = { - a: undefined, - b: 'something', - c: ['stuff'], - d: false, - e: {}, - f: { - g: 1, - h: 0, - i: { - j: 2, - }, - }, - k: { - i: null, - j: undefined, - }, - l: { - m: undefined, - }, - }; const expectedMaskArray = [ - 'b', 'c', 'd', 'e', 'f.g', 'f.h', 'f.i.j', 'k.i', 'l', + 'b', 'c', 'd', 'e', 'f.g', 'f.h', 'f.i.j', 'k.i', 'l', 'n', ]; expect(generateUpdateMask(obj)).to.deep.equal(expectedMaskArray); }); + + it('should return expected update mask array with max paths for nested object', () => { + expect(generateUpdateMask(obj, ['f.i', 'k'])) + .to.deep.equal(['b', 'c', 'd', 'e', 'f.g', 'f.h', 'f.i', 'k', 'l', 'n']); + expect(generateUpdateMask(obj, ['notfound', 'b', 'f', 'k', 'l'])) + .to.deep.equal(['b', 'c', 'd', 'e', 'f', 'k', 'l', 'n']); + }); }); From 222fb3b971de34baa05c82b00dce0e03cf5941e0 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Wed, 12 Aug 2020 14:24:47 -0400 Subject: [PATCH 028/160] Allow RTDB to auto-generate typings, separate internal vs external APIs (#963) --- gulpfile.js | 2 +- package-lock.json | 142 +++++++++----- package.json | 11 +- src/database/database-internal.ts | 241 ++++++++++++++++++++++++ src/database/database.ts | 266 +++++---------------------- src/database/index.ts | 52 ++++++ src/firebase-app.ts | 6 +- src/firebase-namespace.ts | 2 +- test/integration/app.spec.ts | 14 ++ test/unit/database/database.spec.ts | 4 +- test/unit/firebase-app.spec.ts | 2 +- test/unit/firebase-namespace.spec.ts | 5 +- 12 files changed, 464 insertions(+), 283 deletions(-) create mode 100644 src/database/database-internal.ts create mode 100644 src/database/index.ts diff --git a/gulpfile.js b/gulpfile.js index 9ed848daf7..d123f14d81 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -54,6 +54,7 @@ var paths = { curatedTypings: [ 'src/*.d.ts', + '!src/database.d.ts', '!src/instance-id.d.ts', '!src/security-rules.d.ts', '!src/project-management.d.ts', @@ -68,7 +69,6 @@ const TEMPORARY_TYPING_EXCLUDES = [ '!lib/firebase-app.d.ts', '!lib/firebase-service.d.ts', '!lib/auth/*.d.ts', - '!lib/database/*.d.ts', '!lib/machine-learning/*.d.ts', '!lib/storage/*.d.ts', '!lib/utils/*.d.ts', diff --git a/package-lock.json b/package-lock.json index 623482b748..77678dded8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -156,15 +156,15 @@ } }, "@firebase/app": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.7.tgz", - "integrity": "sha512-6NpIZ3iMrCR2XOShK5oi3YYB0GXX5yxVD8p3+2N+X4CF5cERyIrDRf8+YXOFgr+bDHSbVcIyzpWv6ijhg4MJlw==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.9.tgz", + "integrity": "sha512-X2riRgK49IK8LCQ3j7BKLu3zqHDTJSaT6YgcLewtHuOVwtpHfGODiS1cL5VMvKm3ogxP84GA70tN3sdoL/vTog==", "dev": true, "requires": { "@firebase/app-types": "0.6.1", - "@firebase/component": "0.1.15", - "@firebase/logger": "0.2.5", - "@firebase/util": "0.2.50", + "@firebase/component": "0.1.17", + "@firebase/logger": "0.2.6", + "@firebase/util": "0.3.0", "dom-storage": "2.1.0", "tslib": "^1.11.1", "xmlhttprequest": "1.8.0" @@ -176,12 +176,20 @@ "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==" }, "@firebase/auth": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.13.6.tgz", - "integrity": "sha512-ERlda/t5RimNw5Err+5HJATC/qFkC64zR40G+4nK5b9eFJEm0MB+/DaismCwp6J6GoVL3NmejoVbuWU7sV4G1w==", + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.14.9.tgz", + "integrity": "sha512-PxYa2r5qUEdheXTvqROFrMstK8W4uPiP7NVfp+2Bec+AjY5PxZapCx/YFDLkU0D7YBI82H74PtZrzdJZw7TJ4w==", "dev": true, "requires": { - "@firebase/auth-types": "0.9.6" + "@firebase/auth-types": "0.10.1" + }, + "dependencies": { + "@firebase/auth-types": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.1.tgz", + "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==", + "dev": true + } } }, "@firebase/auth-interop-types": { @@ -190,51 +198,78 @@ "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==" }, "@firebase/auth-types": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.9.6.tgz", - "integrity": "sha512-HB1yXe5hgiwPMukLBEfC3TQX22U9qKczj8kEclKhL7rnds3FKZWMM0+EpKbcJREbU9Sj/rgwgaio7ovSN4ZQFA==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.1.tgz", + "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==", "dev": true }, "@firebase/component": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.15.tgz", - "integrity": "sha512-HqFb1qQl1vtlUMIzPM15plNz27jqM8DWjuQQuGeDfG+4iRRflwKfgNw1BOyoP4kQ8vOBCL7t/71yPXSomNdJdQ==", + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.17.tgz", + "integrity": "sha512-/tN5iLcFp9rdpTfCJPfQ/o2ziGHlDxOzNx6XD2FoHlu4pG/PPGu+59iRfQXIowBGhxcTGD/l7oJhZEY/PVg0KQ==", + "dev": true, "requires": { - "@firebase/util": "0.2.50", + "@firebase/util": "0.3.0", "tslib": "^1.11.1" } }, "@firebase/database": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.6.tgz", - "integrity": "sha512-TqUJOaCATF/h3wpqhPT9Fz1nZI6gBv/M2pHZztUjX4A9o9Bq93NyqUurYiZnGB7zpSkEADFCVT4f0VBrWdHlNw==", + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.10.tgz", + "integrity": "sha512-Hc8zIPAroIbAoRe6xFCI5oFHubcHKoDsbYE3J5G1/BhT6DnEUSoLgx8kJ2npybVSCVyb8BvsD6swh17DGEz+0g==", "requires": { "@firebase/auth-interop-types": "0.1.5", - "@firebase/component": "0.1.15", - "@firebase/database-types": "0.5.1", - "@firebase/logger": "0.2.5", - "@firebase/util": "0.2.50", + "@firebase/component": "0.1.17", + "@firebase/database-types": "0.5.2", + "@firebase/logger": "0.2.6", + "@firebase/util": "0.3.0", "faye-websocket": "0.11.3", "tslib": "^1.11.1" + }, + "dependencies": { + "@firebase/component": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.17.tgz", + "integrity": "sha512-/tN5iLcFp9rdpTfCJPfQ/o2ziGHlDxOzNx6XD2FoHlu4pG/PPGu+59iRfQXIowBGhxcTGD/l7oJhZEY/PVg0KQ==", + "requires": { + "@firebase/util": "0.3.0", + "tslib": "^1.11.1" + } + }, + "@firebase/logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" + }, + "@firebase/util": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.0.tgz", + "integrity": "sha512-GTwC+FSLeCPc44/TXCDReNQ5FPRIS5cb8Gr1XcD1TgiNBOvmyx61Om2YLwHp2GnN++6m6xmwmXARm06HOukATA==", + "requires": { + "tslib": "^1.11.1" + } + } } }, "@firebase/database-types": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.1.tgz", - "integrity": "sha512-onQxom1ZBYBJ648w/VNRzUewovEDAH7lvnrrpCd69ukkyrMk6rGEO/PQ9BcNEbhlNtukpsqRS0oNOFlHs0FaSA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.2.tgz", + "integrity": "sha512-ap2WQOS3LKmGuVFKUghFft7RxXTyZTDr0Xd8y2aqmWsbJVjgozi0huL/EUMgTjGFrATAjcf2A7aNs8AKKZ2a8g==", "requires": { "@firebase/app-types": "0.6.1" } }, "@firebase/logger": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.5.tgz", - "integrity": "sha512-qqw3m0tWs/qrg7axTZG/QZq24DIMdSY6dGoWuBn08ddq7+GLF5HiqkRj71XznYeUUbfRq5W9C/PSFnN4JxX+WA==" + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", + "dev": true }, "@firebase/util": { - "version": "0.2.50", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.50.tgz", - "integrity": "sha512-vFE6+Jfc25u0ViSpFxxq0q5s+XmuJ/y7CL3ud79RQe+WLFFg+j0eH1t23k0yNSG9vZNM7h3uHRIXbV97sYLAyw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.0.tgz", + "integrity": "sha512-GTwC+FSLeCPc44/TXCDReNQ5FPRIS5cb8Gr1XcD1TgiNBOvmyx61Om2YLwHp2GnN++6m6xmwmXARm06HOukATA==", + "dev": true, "requires": { "tslib": "^1.11.1" } @@ -572,7 +607,8 @@ "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true }, "@types/minimist": { "version": "1.2.0", @@ -794,6 +830,7 @@ "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" } @@ -839,7 +876,8 @@ "ansi-wrap": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true }, "any-promise": { "version": "1.3.0", @@ -909,7 +947,8 @@ "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true }, "arr-filter": { "version": "1.1.2", @@ -938,7 +977,8 @@ "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true }, "array-differ": { "version": "1.0.0", @@ -1068,7 +1108,8 @@ "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true }, "astral-regex": { "version": "1.0.0", @@ -1156,7 +1197,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "base": { "version": "0.11.2", @@ -1281,6 +1323,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1715,7 +1758,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "concat-stream": { "version": "2.0.0", @@ -2699,6 +2743,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -2708,6 +2753,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -3524,6 +3570,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-6.0.0.tgz", "integrity": "sha512-veQFW93kf6jBdWdF/RxMEIlDK2mkjHyPftM381DID2C9ImTVngwYpyyThxm4/EpgcNOT37BLefzMOjEKbyYg0Q==", + "dev": true, "requires": { "multimatch": "^4.0.0", "plugin-error": "^1.0.1", @@ -4346,6 +4393,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -4435,7 +4483,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "isstream": { "version": "0.1.2", @@ -5244,6 +5293,7 @@ "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" } @@ -5386,6 +5436,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz", "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==", + "dev": true, "requires": { "@types/minimatch": "^3.0.3", "array-differ": "^3.0.0", @@ -5397,12 +5448,14 @@ "array-differ": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==" + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true } } }, @@ -6353,6 +6406,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, "requires": { "ansi-colors": "^1.0.1", "arr-diff": "^4.0.0", @@ -7415,6 +7469,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-3.0.0.tgz", "integrity": "sha512-kvKNfXCmUyC8lAXSSHCIXBUlo/lhsLcCU/OmzACZYpRUdtKIH68xYhm/+HI15jFJYtNJGYtCgn2wmIiExY1VwA==", + "dev": true, "requires": { "readable-stream": "^3.0.6" }, @@ -7423,6 +7478,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", diff --git a/package.json b/package.json index 81008d942b..41f7b63281 100644 --- a/package.json +++ b/package.json @@ -54,10 +54,10 @@ ], "types": "./lib/index.d.ts", "dependencies": { - "@firebase/database": "^0.6.0", + "@firebase/database": "^0.6.10", + "@firebase/database-types": "^0.5.2", "@types/node": "^10.10.0", "dicer": "^0.3.0", - "gulp-filter": "^6.0.0", "jsonwebtoken": "^8.5.1", "node-forge": "^0.9.1" }, @@ -66,9 +66,9 @@ "@google-cloud/storage": "^5.0.0" }, "devDependencies": { - "@firebase/app": "^0.6.1", - "@firebase/auth": "^0.13.3", - "@firebase/auth-types": "^0.9.3", + "@firebase/app": "^0.6.9", + "@firebase/auth": "^0.14.9", + "@firebase/auth-types": "^0.10.1", "@types/bcrypt": "^2.0.0", "@types/chai": "^4.0.0", "@types/chai-as-promised": "^7.1.0", @@ -93,6 +93,7 @@ "eslint": "^6.8.0", "firebase-token-generator": "^2.0.0", "gulp": "^4.0.2", + "gulp-filter": "^6.0.0", "gulp-header": "^1.8.8", "gulp-replace": "^0.5.4", "gulp-typescript": "^5.0.1", diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts new file mode 100644 index 0000000000..fa955a9b7e --- /dev/null +++ b/src/database/database-internal.ts @@ -0,0 +1,241 @@ +/*! + * Copyright 2020 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 { URL } from 'url'; +import * as path from 'path'; + +import { FirebaseApp } from '../firebase-app'; +import { FirebaseDatabaseError, AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; +import { Database as DatabaseImpl } from '@firebase/database'; +import { Database } from './database'; + +import * as validator from '../utils/validator'; +import { AuthorizedHttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; +import { getSdkVersion } from '../utils/index'; + +/** + * Internals of a Database instance. + */ +class DatabaseInternals implements FirebaseServiceInternalsInterface { + + public databases: { + [dbUrl: string]: Database; + } = {}; + + /** + * Deletes the service and its associated resources. + * + * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. + */ + public delete(): Promise { + for (const dbUrl of Object.keys(this.databases)) { + const db: DatabaseImpl = ((this.databases[dbUrl] as any) as DatabaseImpl); + db.INTERNAL.delete(); + } + return Promise.resolve(undefined); + } +} + +export class DatabaseService implements FirebaseServiceInterface { + + public readonly INTERNAL: DatabaseInternals = new DatabaseInternals(); + + private readonly appInternal: FirebaseApp; + + constructor(app: FirebaseApp) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseDatabaseError({ + code: 'invalid-argument', + message: 'First argument passed to admin.database() must be a valid Firebase app instance.', + }); + } + this.appInternal = app; + } + + /** + * Returns the app associated with this DatabaseService instance. + * + * @return {FirebaseApp} The app associated with this DatabaseService instance. + */ + get app(): FirebaseApp { + return this.appInternal; + } + + public getDatabase(url?: string): Database { + const dbUrl: string = this.ensureUrl(url); + if (!validator.isNonEmptyString(dbUrl)) { + throw new FirebaseDatabaseError({ + code: 'invalid-argument', + message: 'Database URL must be a valid, non-empty URL string.', + }); + } + + let db: Database = this.INTERNAL.databases[dbUrl]; + if (typeof db === 'undefined') { + const rtdb = require('@firebase/database'); // eslint-disable-line @typescript-eslint/no-var-requires + db = rtdb.initStandalone(this.appInternal, dbUrl, getSdkVersion()).instance; + + const rulesClient = new DatabaseRulesClient(this.app, dbUrl); + db.getRules = () => { + return rulesClient.getRules(); + }; + db.getRulesJSON = () => { + return rulesClient.getRulesJSON(); + }; + db.setRules = (source: string) => { + return rulesClient.setRules(source); + }; + + this.INTERNAL.databases[dbUrl] = db; + } + return db; + } + + private ensureUrl(url?: string): string { + if (typeof url !== 'undefined') { + return url; + } else if (typeof this.appInternal.options.databaseURL !== 'undefined') { + return this.appInternal.options.databaseURL; + } + throw new FirebaseDatabaseError({ + code: 'invalid-argument', + message: 'Can\'t determine Firebase Database URL.', + }); + } +} + +const RULES_URL_PATH = '.settings/rules.json'; + +/** + * A helper client for managing RTDB security rules. + */ +class DatabaseRulesClient { + + private readonly dbUrl: string; + private readonly httpClient: AuthorizedHttpClient; + + constructor(app: FirebaseApp, dbUrl: string) { + const parsedUrl = new URL(dbUrl); + parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH); + this.dbUrl = parsedUrl.toString(); + this.httpClient = new AuthorizedHttpClient(app); + } + + /** + * Gets the currently applied security rules as a string. The return value consists of + * the rules source including comments. + * + * @return {Promise} A promise fulfilled with the rules as a raw string. + */ + public getRules(): Promise { + const req: HttpRequestConfig = { + method: 'GET', + url: this.dbUrl, + }; + return this.httpClient.send(req) + .then((resp) => { + if (!resp.text) { + throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.'); + } + return resp.text; + }) + .catch((err) => { + throw this.handleError(err); + }); + } + + /** + * Gets the currently applied security rules as a parsed JSON object. Any comments in + * the original source are stripped away. + * + * @return {Promise} A promise fulfilled with the parsed rules source. + */ + public getRulesJSON(): Promise { + const req: HttpRequestConfig = { + method: 'GET', + url: this.dbUrl, + data: { format: 'strict' }, + }; + return this.httpClient.send(req) + .then((resp) => { + return resp.data; + }) + .catch((err) => { + throw this.handleError(err); + }); + } + + /** + * Sets the specified rules on the Firebase Database instance. If the rules source is + * specified as a string or a Buffer, it may include comments. + * + * @param {string|Buffer|object} source Source of the rules to apply. Must not be `null` + * or empty. + * @return {Promise} Resolves when the rules are set on the Database. + */ + public setRules(source: string | Buffer | object): Promise { + if (!validator.isNonEmptyString(source) && + !validator.isBuffer(source) && + !validator.isNonNullObject(source)) { + const error = new FirebaseDatabaseError({ + code: 'invalid-argument', + message: 'Source must be a non-empty string, Buffer or an object.', + }); + return Promise.reject(error); + } + + const req: HttpRequestConfig = { + method: 'PUT', + url: this.dbUrl, + data: source, + headers: { + 'content-type': 'application/json; charset=utf-8', + }, + }; + return this.httpClient.send(req) + .then(() => { + return; + }) + .catch((err) => { + throw this.handleError(err); + }); + } + + private handleError(err: Error): Error { + if (err instanceof HttpError) { + return new FirebaseDatabaseError({ + code: AppErrorCodes.INTERNAL_ERROR, + message: this.getErrorMessage(err), + }); + } + return err; + } + + private getErrorMessage(err: HttpError): string { + const intro = 'Error while accessing security rules'; + try { + const body: { error?: string } = err.response.data; + if (body && body.error) { + return `${intro}: ${body.error.trim()}`; + } + } catch { + // Ignore parsing errors + } + + return `${intro}: ${err.response.text}`; + } +} diff --git a/src/database/database.ts b/src/database/database.ts index 686120909d..474426748f 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -1,233 +1,49 @@ -import { URL } from 'url'; -import * as path from 'path'; - -import { FirebaseApp } from '../firebase-app'; -import { FirebaseDatabaseError, AppErrorCodes, FirebaseAppError } from '../utils/error'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { Database } from '@firebase/database'; - -import * as validator from '../utils/validator'; -import { AuthorizedHttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; -import { getSdkVersion } from '../utils/index'; - - -/** - * Internals of a Database instance. +/*! + * Copyright 2020 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. */ -class DatabaseInternals implements FirebaseServiceInternalsInterface { - - public databases: { - [dbUrl: string]: Database; - } = {}; - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - for (const dbUrl of Object.keys(this.databases)) { - const db: Database = this.databases[dbUrl]; - db.INTERNAL.delete(); - } - return Promise.resolve(undefined); - } -} - -declare module '@firebase/database' { - interface Database { +// Required to perform module augmentation to FirebaseDatabase interface. +import { FirebaseDatabase } from '@firebase/database-types'; + +declare module '@firebase/database-types' { + interface FirebaseDatabase { + /** + * Gets the currently applied security rules as a string. The return value consists of + * the rules source including comments. + * + * @return A promise fulfilled with the rules as a raw string. + */ getRules(): Promise; - getRulesJSON(): Promise; - setRules(source: string | Buffer | object): Promise; - } -} - -export class DatabaseService implements FirebaseServiceInterface { - - public readonly INTERNAL: DatabaseInternals = new DatabaseInternals(); - - private readonly appInternal: FirebaseApp; - - constructor(app: FirebaseApp) { - if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseDatabaseError({ - code: 'invalid-argument', - message: 'First argument passed to admin.database() must be a valid Firebase app instance.', - }); - } - this.appInternal = app; - } - - /** - * Returns the app associated with this DatabaseService instance. - * - * @return {FirebaseApp} The app associated with this DatabaseService instance. - */ - get app(): FirebaseApp { - return this.appInternal; - } - - public getDatabase(url?: string): Database { - const dbUrl: string = this.ensureUrl(url); - if (!validator.isNonEmptyString(dbUrl)) { - throw new FirebaseDatabaseError({ - code: 'invalid-argument', - message: 'Database URL must be a valid, non-empty URL string.', - }); - } - - let db: Database = this.INTERNAL.databases[dbUrl]; - if (typeof db === 'undefined') { - const rtdb = require('@firebase/database'); // eslint-disable-line @typescript-eslint/no-var-requires - db = rtdb.initStandalone(this.appInternal, dbUrl, getSdkVersion()).instance; - - const rulesClient = new DatabaseRulesClient(this.app, dbUrl); - db.getRules = () => { - return rulesClient.getRules(); - }; - db.getRulesJSON = () => { - return rulesClient.getRulesJSON(); - }; - db.setRules = (source) => { - return rulesClient.setRules(source); - }; - this.INTERNAL.databases[dbUrl] = db; - } - return db; - } + /** + * Gets the currently applied security rules as a parsed JSON object. Any comments in + * the original source are stripped away. + * + * @return A promise fulfilled with the parsed rules object. + */ + getRulesJSON(): Promise; - private ensureUrl(url?: string): string { - if (typeof url !== 'undefined') { - return url; - } else if (typeof this.appInternal.options.databaseURL !== 'undefined') { - return this.appInternal.options.databaseURL; - } - throw new FirebaseDatabaseError({ - code: 'invalid-argument', - message: 'Can\'t determine Firebase Database URL.', - }); + /** + * Sets the specified rules on the Firebase Realtime Database instance. If the rules source is + * specified as a string or a Buffer, it may include comments. + * + * @param source Source of the rules to apply. Must not be `null` or empty. + * @return Resolves when the rules are set on the Realtime Database. + */ + setRules(source: string | Buffer | object): Promise; } } -const RULES_URL_PATH = '.settings/rules.json'; - -/** - * A helper client for managing RTDB security rules. - */ -class DatabaseRulesClient { - - private readonly dbUrl: string; - private readonly httpClient: AuthorizedHttpClient; - - constructor(app: FirebaseApp, dbUrl: string) { - const parsedUrl = new URL(dbUrl); - parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH); - this.dbUrl = parsedUrl.toString(); - this.httpClient = new AuthorizedHttpClient(app); - } - - /** - * Gets the currently applied security rules as a string. The return value consists of - * the rules source including comments. - * - * @return {Promise} A promise fulfilled with the rules as a raw string. - */ - public getRules(): Promise { - const req: HttpRequestConfig = { - method: 'GET', - url: this.dbUrl, - }; - return this.httpClient.send(req) - .then((resp) => { - if (!resp.text) { - throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.'); - } - return resp.text; - }) - .catch((err) => { - throw this.handleError(err); - }); - } - - /** - * Gets the currently applied security rules as a parsed JSON object. Any comments in - * the original source are stripped away. - * - * @return {Promise} A promise fulfilled with the parsed rules source. - */ - public getRulesJSON(): Promise { - const req: HttpRequestConfig = { - method: 'GET', - url: this.dbUrl, - data: { format: 'strict' }, - }; - return this.httpClient.send(req) - .then((resp) => { - return resp.data; - }) - .catch((err) => { - throw this.handleError(err); - }); - } - - /** - * Sets the specified rules on the Firebase Database instance. If the rules source is - * specified as a string or a Buffer, it may include comments. - * - * @param {string|Buffer|object} source Source of the rules to apply. Must not be `null` - * or empty. - * @return {Promise} Resolves when the rules are set on the Database. - */ - public setRules(source: string | Buffer | object): Promise { - if (!validator.isNonEmptyString(source) && - !validator.isBuffer(source) && - !validator.isNonNullObject(source)) { - const error = new FirebaseDatabaseError({ - code: 'invalid-argument', - message: 'Source must be a non-empty string, Buffer or an object.', - }); - return Promise.reject(error); - } - - const req: HttpRequestConfig = { - method: 'PUT', - url: this.dbUrl, - data: source, - headers: { - 'content-type': 'application/json; charset=utf-8', - }, - }; - return this.httpClient.send(req) - .then(() => { - return; - }) - .catch((err) => { - throw this.handleError(err); - }); - } - - private handleError(err: Error): Error { - if (err instanceof HttpError) { - return new FirebaseDatabaseError({ - code: AppErrorCodes.INTERNAL_ERROR, - message: this.getErrorMessage(err), - }); - } - return err; - } - - private getErrorMessage(err: HttpError): string { - const intro = 'Error while accessing security rules'; - try { - const body: {error?: string} = err.response.data; - if (body && body.error) { - return `${intro}: ${body.error.trim()}`; - } - } catch { - // Ignore parsing errors - } - - return `${intro}: ${err.response.text}`; - } -} +export { FirebaseDatabase as Database } diff --git a/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000000..30c6b1143a --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1,52 @@ +/*! + * Copyright 2020 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 { FirebaseApp } from '../firebase-app'; +import { ServerValue as sv } from '@firebase/database'; +import * as adminDb from './database'; +import * as firebaseDbTypesApi from '@firebase/database-types'; +import * as firebaseAdmin from '../index'; + +export function database(app?: FirebaseApp): adminDb.Database { + if (typeof(app) === 'undefined') { + app = firebaseAdmin.app(); + } + return app.database(); +} + +/** + * We must define a namespace to make the typings work correctly. Otherwise + * `admin.database()` cannot be called like a function. Temporarily, + * admin.database is used as the namespace name because we cannot barrel + * re-export the contents from @firebase/database-types, and we want it to + * match the namespacing in the re-export inside src/index.d.ts + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace admin.database { + // See https://github.com/microsoft/TypeScript/issues/4336 + /* eslint-disable @typescript-eslint/no-unused-vars */ + // See https://github.com/typescript-eslint/typescript-eslint/issues/363 + export import DataSnapshot = firebaseDbTypesApi.DataSnapshot; + export import Database = adminDb.Database; + export import EventType = firebaseDbTypesApi.EventType; + export import OnDisconnect = firebaseDbTypesApi.OnDisconnect; + export import Query = firebaseDbTypesApi.Query; + export import Reference = firebaseDbTypesApi.Reference; + export import ThenableReference = firebaseDbTypesApi.ThenableReference; + export import enableLogging = firebaseDbTypesApi.enableLogging; + + export const ServerValue: firebaseDbTypesApi.ServerValue = sv; +} diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 194b792ce4..fd8bd0f8d3 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -25,8 +25,8 @@ import { Auth } from './auth/auth'; import { MachineLearning } from './machine-learning/machine-learning'; import { Messaging } from './messaging/messaging'; import { Storage } from './storage/storage'; -import { Database } from '@firebase/database'; -import { DatabaseService } from './database/database'; +import { Database } from './database/database'; +import { DatabaseService } from './database/database-internal'; import { Firestore } from '@google-cloud/firestore'; import { FirestoreService } from './firestore/firestore-internal'; import { InstanceId } from './instance-id/instance-id'; @@ -307,7 +307,7 @@ export class FirebaseApp { */ public database(url?: string): Database { const service: DatabaseService = this.ensureService_('database', () => { - const dbService: typeof DatabaseService = require('./database/database').DatabaseService; + const dbService: typeof DatabaseService = require('./database/database-internal').DatabaseService; return new dbService(this); }); return service.getDatabase(url); diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 916132ebbf..b81b3c6b91 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -31,7 +31,7 @@ import { Auth } from './auth/auth'; import { MachineLearning } from './machine-learning/machine-learning'; import { Messaging } from './messaging/messaging'; import { Storage } from './storage/storage'; -import { Database } from '@firebase/database'; +import { Database } from './database/database'; import { Firestore } from '@google-cloud/firestore'; import { InstanceId } from './instance-id/instance-id'; import { ProjectManagement } from './project-management/project-management'; diff --git a/test/integration/app.spec.ts b/test/integration/app.spec.ts index 3cdf356245..cce88a52aa 100644 --- a/test/integration/app.spec.ts +++ b/test/integration/app.spec.ts @@ -27,6 +27,20 @@ describe('admin', () => { expect(storageBucket).to.be.not.empty; }); + it('does not load RTDB by default', () => { + const firebaseRtdb = require.cache[require.resolve('@firebase/database')]; + expect(firebaseRtdb).to.be.undefined; + const rtdbInternal = require.cache[require.resolve('../../lib/database/database-internal')]; + expect(rtdbInternal).to.be.undefined; + }); + + it('loads RTDB when calling admin.database', () => { + const rtdbNamespace = admin.database; + expect(rtdbNamespace).to.not.be.null; + const firebaseRtdb = require.cache[require.resolve('@firebase/database')]; + expect(firebaseRtdb).to.not.be.undefined; + }); + it('does not load Firestore by default', () => { const gcloud = require.cache[require.resolve('@google-cloud/firestore')]; expect(gcloud).to.be.undefined; diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 78d73f21d6..415ce4a94a 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -22,8 +22,8 @@ import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; -import { DatabaseService } from '../../../src/database/database'; -import { Database } from '@firebase/database'; +import { DatabaseService } from '../../../src/database/database-internal'; +import { Database } from '../../../src/database/database'; import * as utils from '../utils'; import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index dc89b6383c..2b3b9591d8 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -35,7 +35,7 @@ import { Messaging } from '../../src/messaging/messaging'; import { MachineLearning } from '../../src/machine-learning/machine-learning'; import { Storage } from '../../src/storage/storage'; import { Firestore } from '@google-cloud/firestore'; -import { Database } from '@firebase/database'; +import { Database } from '../../src/database/database'; import { InstanceId } from '../../src/instance-id/instance-id'; import { ProjectManagement } from '../../src/project-management/project-management'; import { SecurityRules } from '../../src/security-rules/security-rules'; diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 60b26ff91d..4cbc98df5e 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -36,6 +36,7 @@ import { Reference, ServerValue, } from '@firebase/database'; +import { Database as FirebaseDatabase } from '../../src/database/database'; import { Messaging } from '../../src/messaging/messaging'; import { MachineLearning } from '../../src/machine-learning/machine-learning'; import { Storage } from '../../src/storage/storage'; @@ -404,14 +405,14 @@ describe('FirebaseNamespace', () => { it('should return a valid namespace when the default app is initialized', () => { const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); - const db: Database = firebaseNamespace.database(); + const db: FirebaseDatabase = firebaseNamespace.database(); expect(db.app).to.be.deep.equal(app); return app.delete(); }); it('should return a valid namespace when the named app is initialized', () => { const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); - const db: Database = firebaseNamespace.database(app); + const db: FirebaseDatabase = firebaseNamespace.database(app); expect(db.app).to.be.deep.equal(app); return app.delete(); }); From a5c9da045526ec2b06e5e987d2298c1312ca76d7 Mon Sep 17 00:00:00 2001 From: ifielker Date: Wed, 12 Aug 2020 19:44:16 -0400 Subject: [PATCH 029/160] Firebase ML can gracefully accept unknown fields in ModelResponse (#989) --- package-lock.json | 20 ++++++------ src/machine-learning/machine-learning.ts | 2 +- .../machine-learning/machine-learning.spec.ts | 32 +++++++++++++++++++ 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77678dded8..74712bba20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -573,7 +573,7 @@ }, "@types/firebase-token-generator": { "version": "2.0.28", - "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", + "resolved": "http://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", "integrity": "sha1-Z1VIHZMk4mt6XItFXWgUg3aCw5Y=", "dev": true }, @@ -612,7 +612,7 @@ }, "@types/minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", "dev": true }, @@ -1299,7 +1299,7 @@ }, "binaryextensions": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", "dev": true }, @@ -3005,7 +3005,7 @@ }, "firebase-token-generator": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", "integrity": "sha1-l2fXWewTq9yZuhFf1eqZ2Lk9EgY=", "dev": true }, @@ -3393,7 +3393,7 @@ }, "globby": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "resolved": "http://registry.npmjs.org/globby/-/globby-5.0.0.tgz", "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", "dev": true, "requires": { @@ -4620,7 +4620,7 @@ }, "istextorbinary": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", "dev": true, "requires": { @@ -6278,7 +6278,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -6434,7 +6434,7 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, @@ -6980,7 +6980,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -7781,7 +7781,7 @@ }, "textextensions": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=", "dev": true }, diff --git a/src/machine-learning/machine-learning.ts b/src/machine-learning/machine-learning.ts index 3b09d07c23..39266c0b00 100644 --- a/src/machine-learning/machine-learning.ts +++ b/src/machine-learning/machine-learning.ts @@ -271,7 +271,7 @@ export class Model { if (model.modelHash) { this.modelHash = model.modelHash; } - if (model.tfliteModel) { + if (model.tfliteModel?.gcsTfliteUri) { this.tfliteModel = { gcsTfliteUri: model.tfliteModel.gcsTfliteUri, sizeBytes: model.tfliteModel.sizeBytes, diff --git a/test/unit/machine-learning/machine-learning.spec.ts b/test/unit/machine-learning/machine-learning.spec.ts index ece3057ba6..d966d3a6c6 100644 --- a/test/unit/machine-learning/machine-learning.spec.ts +++ b/test/unit/machine-learning/machine-learning.spec.ts @@ -105,6 +105,21 @@ describe('MachineLearning', () => { }; const MODEL2 = new Model(MODEL_RESPONSE2); + const MODEL_RESPONSE3: any = { + name: 'projects/test-project/models/3456789', + createTime: '2020-02-07T23:45:23.288047Z', + updateTime: '2020-02-08T23:45:23.288047Z', + etag: 'etag345', + modelHash: 'modelHash345', + displayName: 'model_3', + tags: ['tag_3', 'tag_4'], + state: { published: true }, + tfliteModel: { + managedUpload: true, + sizeBytes: 22200222, + }, + }; + const STATUS_ERROR_RESPONSE: { code: number; message: string; @@ -245,8 +260,25 @@ describe('MachineLearning', () => { 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite'); expect(tflite.sizeBytes).to.be.equal(16900988); }); + + it('should accept unknown fields gracefully', () => { + const model = new Model(MODEL_RESPONSE3); + expect(model.modelId).to.equal('3456789'); + expect(model.displayName).to.equal('model_3'); + expect(model.tags).to.deep.equal(['tag_3', 'tag_4']); + expect(model.createTime).to.equal(CREATE_TIME_UTC); + expect(model.updateTime).to.equal(UPDATE_TIME_UTC); + expect(model.validationError).to.be.undefined; + expect(model.published).to.be.true; + expect(model.etag).to.equal('etag345'); + expect(model.modelHash).to.equal('modelHash345'); + expect(model.tfliteModel).to.be.undefined; + }); + }); + + describe('getModel', () => { it('should propagate API errors', () => { const stub = sinon From e415188c700a9ba8c2f3eac4fbf09ce598d095b4 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 13 Aug 2020 14:35:29 -0400 Subject: [PATCH 030/160] [chore] Release 9.1.0 (#991) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41f7b63281..b1a1991c42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.0.0", + "version": "9.1.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 7a5ba241a034345b69c2e5c773e3ca13543b5732 Mon Sep 17 00:00:00 2001 From: Alejandro Ulate Date: Mon, 17 Aug 2020 12:16:01 -0600 Subject: [PATCH 031/160] Add missing class in RemoteConfig Namespace (#996) Just added a class that was missing from the Remote Config Namespace. RELEASE NOTE: Added missing type definition for ListVersionsOptions. --- src/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.d.ts b/src/index.d.ts index 6b4c8826f5..9de4488e12 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -857,6 +857,7 @@ declare namespace admin.remoteConfig { export import InAppDefaultValue = _remoteConfig.admin.remoteConfig.InAppDefaultValue; export import RemoteConfigParameterValue = _remoteConfig.admin.remoteConfig.RemoteConfigParameterValue; export import Version = _remoteConfig.admin.remoteConfig.Version; + export import ListVersionsOptions = _remoteConfig.admin.remoteConfig.ListVersionsOptions; export import ListVersionsResult = _remoteConfig.admin.remoteConfig.ListVersionsResult; export import RemoteConfigUser = _remoteConfig.admin.remoteConfig.RemoteConfigUser; export import RemoteConfig = _remoteConfig.admin.remoteConfig.RemoteConfig; From b3312ac2aa8bd8304da4cd3cec03791028fe2a7d Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Wed, 19 Aug 2020 09:31:49 -0400 Subject: [PATCH 032/160] fix(auth): Address several auth typing inconsistencies (#993) --- src/auth.d.ts | 2 +- src/auth/auth.ts | 2 +- src/auth/user-record.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/auth.d.ts b/src/auth.d.ts index 02e8072455..312f2c49f7 100644 --- a/src/auth.d.ts +++ b/src/auth.d.ts @@ -820,7 +820,7 @@ export namespace admin.auth { * When not provided in an `admin.auth.TenantAwareAuth` context, the user is uploaded * to the tenant corresponding to that `TenantAwareAuth` instance's tenant ID. */ - tenantId?: string | null; + tenantId?: string; /** * The user's multi-factor related properties. diff --git a/src/auth/auth.ts b/src/auth/auth.ts index d9d4df207d..d22f3776b6 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -99,6 +99,7 @@ export interface DecodedIdToken { sign_in_provider: string; sign_in_second_factor?: string; second_factor_identifier?: string; + tenant?: string; [key: string]: any; }; iat: number; @@ -106,7 +107,6 @@ export interface DecodedIdToken { phone_number?: string; picture?: string; sub: string; - tenant?: string; uid: string; [key: string]: any; } diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 257a21b220..8ec5c08a8e 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -148,9 +148,9 @@ export enum MultiFactorId { */ export abstract class MultiFactorInfo { public readonly uid: string; - public readonly displayName: string; + public readonly displayName?: string; public readonly factorId: MultiFactorId; - public readonly enrollmentTime: string; + public readonly enrollmentTime?: string; /** * Initializes the MultiFactorInfo associated subclass using the server side. From 8367fa7b9d8536f0d92b36dbd8c23a47dcfbbbee Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 20 Aug 2020 16:08:46 -0400 Subject: [PATCH 033/160] [chore] Release 9.1.1 (#1003) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b1a1991c42..061d5d0a1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.1.0", + "version": "9.1.1", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 242433514169599edef6e121799ae8935648cfc5 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Tue, 25 Aug 2020 17:01:48 -0400 Subject: [PATCH 034/160] auth: Add credential service (#1011) --- src/auth/token-generator.ts | 2 +- src/credential.d.ts | 147 ++++++++++++++++ src/{auth => credential}/credential.ts | 158 +++++++++++++++++- src/credential/index.ts | 35 ++++ src/firebase-app.ts | 4 +- src/firebase-namespace.ts | 39 +---- src/firestore/firestore-internal.ts | 2 +- src/index.d.ts | 148 +--------------- src/storage/storage.ts | 2 +- src/utils/index.ts | 2 +- test/integration/setup.ts | 2 +- test/resources/mocks.ts | 2 +- test/unit/auth/auth.spec.ts | 2 +- test/unit/auth/token-generator.spec.ts | 2 +- test/unit/auth/token-verifier.spec.ts | 2 +- .../{auth => credential}/credential.spec.ts | 2 +- test/unit/firebase-app.spec.ts | 2 +- test/unit/firebase.spec.ts | 4 +- test/unit/firestore/firestore.spec.ts | 2 +- test/unit/index.spec.ts | 4 +- test/unit/utils/index.spec.ts | 2 +- 21 files changed, 369 insertions(+), 196 deletions(-) create mode 100644 src/credential.d.ts rename src/{auth => credential}/credential.ts (74%) create mode 100644 src/credential/index.ts rename test/unit/{auth => credential}/credential.spec.ts (99%) diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 929986b92e..645b1afbf7 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -15,7 +15,7 @@ */ import { FirebaseApp } from '../firebase-app'; -import { ServiceAccountCredential } from './credential'; +import { ServiceAccountCredential } from '../credential/credential'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { AuthorizedHttpClient, HttpError, HttpRequestConfig, HttpClient } from '../utils/api-request'; diff --git a/src/credential.d.ts b/src/credential.d.ts new file mode 100644 index 0000000000..be99dc1c12 --- /dev/null +++ b/src/credential.d.ts @@ -0,0 +1,147 @@ +import * as _admin from './index.d'; +import { Agent } from 'http'; + +export namespace admin.credential { + /** + * Interface that provides Google OAuth2 access tokens used to authenticate + * with Firebase services. + * + * In most cases, you will not need to implement this yourself and can instead + * use the default implementations provided by + * {@link admin.credential `admin.credential`}. + */ + interface Credential { + + /** + * Returns a Google OAuth2 access token object used to authenticate with + * Firebase services. + * + * This object contains the following properties: + * * `access_token` (`string`): The actual Google OAuth2 access token. + * * `expires_in` (`number`): The number of seconds from when the token was + * issued that it expires. + * + * @return A Google OAuth2 access token object. + */ + getAccessToken(): Promise<_admin.GoogleOAuthAccessToken>; + } + + + /** + * Returns a credential created from the + * {@link + * https://developers.google.com/identity/protocols/application-default-credentials + * Google Application Default Credentials} + * that grants admin access to Firebase services. This credential can be used + * in the call to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * Google Application Default Credentials are available on any Google + * infrastructure, such as Google App Engine and Google Compute Engine. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * admin.initializeApp({ + * credential: admin.credential.applicationDefault(), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param {!Object=} httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return {!admin.credential.Credential} A credential authenticated via Google + * Application Default Credentials that can be used to initialize an app. + */ + function applicationDefault(httpAgent?: Agent): admin.credential.Credential; + + /** + * Returns a credential created from the provided service account that grants + * admin access to Firebase services. This credential can be used in the call + * to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * // Providing a path to a service account key JSON file + * var serviceAccount = require("path/to/serviceAccountKey.json"); + * admin.initializeApp({ + * credential: admin.credential.cert(serviceAccount), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @example + * ```javascript + * // Providing a service account object inline + * admin.initializeApp({ + * credential: admin.credential.cert({ + * projectId: "", + * clientEmail: "foo@.iam.gserviceaccount.com", + * privateKey: "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n" + * }), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param serviceAccountPathOrObject The path to a service + * account key JSON file or an object representing a service account key. + * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return A credential authenticated via the + * provided service account that can be used to initialize an app. + */ + function cert(serviceAccountPathOrObject: string | _admin.ServiceAccount, httpAgent?: Agent): admin.credential.Credential; + + /** + * Returns a credential created from the provided refresh token that grants + * admin access to Firebase services. This credential can be used in the call + * to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * // Providing a path to a refresh token JSON file + * var refreshToken = require("path/to/refreshToken.json"); + * admin.initializeApp({ + * credential: admin.credential.refreshToken(refreshToken), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param refreshTokenPathOrObject The path to a Google + * OAuth2 refresh token JSON file or an object representing a Google OAuth2 + * refresh token. + * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return A credential authenticated via the + * provided service account that can be used to initialize an app. + */ + function refreshToken(refreshTokenPathOrObject: string | object, httpAgent?: Agent): admin.credential.Credential; +} diff --git a/src/auth/credential.ts b/src/credential/credential.ts similarity index 74% rename from src/auth/credential.ts rename to src/credential/credential.ts index 9770185ac6..5789e1beeb 100644 --- a/src/auth/credential.ts +++ b/src/credential/credential.ts @@ -53,6 +53,10 @@ const REFRESH_TOKEN_PATH = '/oauth2/v4/token'; const ONE_HOUR_IN_SECONDS = 60 * 60; const JWT_ALGORITHM = 'RS256'; +let globalAppDefaultCred: Credential; +const globalCertCreds: { [key: string]: ServiceAccountCredential } = {}; +const globalRefreshTokenCreds: { [key: string]: RefreshTokenCredential } = {}; + /** * Interface for Google OAuth 2.0 access tokens. */ @@ -64,12 +68,164 @@ export interface GoogleOAuthAccessToken { } /** - * Interface for things that generate access tokens. + * Interface that provides Google OAuth2 access tokens used to authenticate + * with Firebase services. + * + * In most cases, you will not need to implement this yourself and can instead + * use the default implementations provided by + * {@link admin.credential `admin.credential`}. */ export interface Credential { + /** + * Returns a Google OAuth2 access token object used to authenticate with + * Firebase services. + * + * This object contains the following properties: + * * `access_token` (`string`): The actual Google OAuth2 access token. + * * `expires_in` (`number`): The number of seconds from when the token was + * issued that it expires. + * + * @return A Google OAuth2 access token object. + */ getAccessToken(): Promise; } +/** + * Returns a credential created from the + * {@link + * https://developers.google.com/identity/protocols/application-default-credentials + * Google Application Default Credentials} + * that grants admin access to Firebase services. This credential can be used + * in the call to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * Google Application Default Credentials are available on any Google + * infrastructure, such as Google App Engine and Google Compute Engine. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * admin.initializeApp({ + * credential: admin.credential.applicationDefault(), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param {!Object=} httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return {!admin.credential.Credential} A credential authenticated via Google + * Application Default Credentials that can be used to initialize an app. + */ +export function applicationDefault(httpAgent?: Agent): Credential { + if (typeof globalAppDefaultCred === 'undefined') { + globalAppDefaultCred = getApplicationDefault(httpAgent); + } + return globalAppDefaultCred; +} + +/** + * Returns a credential created from the provided service account that grants + * admin access to Firebase services. This credential can be used in the call + * to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * // Providing a path to a service account key JSON file + * var serviceAccount = require("path/to/serviceAccountKey.json"); + * admin.initializeApp({ + * credential: admin.credential.cert(serviceAccount), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @example + * ```javascript + * // Providing a service account object inline + * admin.initializeApp({ + * credential: admin.credential.cert({ + * projectId: "", + * clientEmail: "foo@.iam.gserviceaccount.com", + * privateKey: "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n" + * }), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param serviceAccountPathOrObject The path to a service + * account key JSON file or an object representing a service account key. + * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return A credential authenticated via the + * provided service account that can be used to initialize an app. + */ +export function cert(serviceAccountPathOrObject: string | object, httpAgent?: Agent): Credential { + const stringifiedServiceAccount = JSON.stringify(serviceAccountPathOrObject); + if (!(stringifiedServiceAccount in globalCertCreds)) { + globalCertCreds[stringifiedServiceAccount] = new ServiceAccountCredential(serviceAccountPathOrObject, httpAgent); + } + return globalCertCreds[stringifiedServiceAccount]; +} + +/** + * Returns a credential created from the provided refresh token that grants + * admin access to Firebase services. This credential can be used in the call + * to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * // Providing a path to a refresh token JSON file + * var refreshToken = require("path/to/refreshToken.json"); + * admin.initializeApp({ + * credential: admin.credential.refreshToken(refreshToken), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param refreshTokenPathOrObject The path to a Google + * OAuth2 refresh token JSON file or an object representing a Google OAuth2 + * refresh token. + * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return A credential authenticated via the + * provided service account that can be used to initialize an app. + */ +export function refreshToken(refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential { + const stringifiedRefreshToken = JSON.stringify(refreshTokenPathOrObject); + if (!(stringifiedRefreshToken in globalRefreshTokenCreds)) { + globalRefreshTokenCreds[stringifiedRefreshToken] = new RefreshTokenCredential( + refreshTokenPathOrObject, httpAgent); + } + return globalRefreshTokenCreds[stringifiedRefreshToken]; +} + /** * Implementation of Credential that uses a service account. */ diff --git a/src/credential/index.ts b/src/credential/index.ts new file mode 100644 index 0000000000..ff194f4e97 --- /dev/null +++ b/src/credential/index.ts @@ -0,0 +1,35 @@ +/*! + * Copyright 2020 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 credentialApi from './credential'; + +/** + * Temporarily, admin.credential is used as the namespace name because we + * cannot barrel re-export the contents from credential.ts, and we want it to + * match the namespacing in the re-export inside src/index.d.ts + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace admin.credential { + // See https://github.com/microsoft/TypeScript/issues/4336 + /* eslint-disable @typescript-eslint/no-unused-vars */ + // See https://github.com/typescript-eslint/typescript-eslint/issues/363 + // Allows for exposing classes as interfaces in typings + /* eslint-disable @typescript-eslint/no-empty-interface */ + export import Credential = credentialApi.Credential; + export const applicationDefault = credentialApi.applicationDefault; + export const cert = credentialApi.cert; + export const refreshToken = credentialApi.refreshToken; +} diff --git a/src/firebase-app.ts b/src/firebase-app.ts index fd8bd0f8d3..b3db9aaccc 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { Credential, GoogleOAuthAccessToken, getApplicationDefault } from './auth/credential'; +import { + Credential, GoogleOAuthAccessToken, getApplicationDefault +} from './credential/credential'; import * as validator from './utils/validator'; import { deepCopy, deepExtend } from './utils/deep-copy'; import { FirebaseServiceInterface } from './firebase-service'; diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index b81b3c6b91..9a639d426e 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -15,17 +15,13 @@ */ import fs = require('fs'); -import { Agent } from 'http'; import { deepExtend } from './utils/deep-copy'; import { AppErrorCodes, FirebaseAppError } from './utils/error'; import { AppHook, FirebaseApp, FirebaseAppOptions } from './firebase-app'; import { FirebaseServiceFactory, FirebaseServiceInterface } from './firebase-service'; import { - Credential, - RefreshTokenCredential, - ServiceAccountCredential, - getApplicationDefault, -} from './auth/credential'; + getApplicationDefault, cert, refreshToken, applicationDefault +} from './credential/credential'; import { Auth } from './auth/auth'; import { MachineLearning } from './machine-learning/machine-learning'; @@ -50,12 +46,6 @@ const DEFAULT_APP_NAME = '[DEFAULT]'; */ export const FIREBASE_CONFIG_VAR = 'FIREBASE_CONFIG'; - -let globalAppDefaultCred: Credential; -const globalCertCreds: { [key: string]: ServiceAccountCredential } = {}; -const globalRefreshTokenCreds: { [key: string]: RefreshTokenCredential } = {}; - - export interface FirebaseServiceNamespace { (app?: FirebaseApp): T; [key: string]: any; @@ -272,31 +262,8 @@ export class FirebaseNamespaceInternals { } } - const firebaseCredential = { - cert: (serviceAccountPathOrObject: string | object, httpAgent?: Agent): Credential => { - const stringifiedServiceAccount = JSON.stringify(serviceAccountPathOrObject); - if (!(stringifiedServiceAccount in globalCertCreds)) { - globalCertCreds[stringifiedServiceAccount] = new ServiceAccountCredential(serviceAccountPathOrObject, httpAgent); - } - return globalCertCreds[stringifiedServiceAccount]; - }, - - refreshToken: (refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential => { - const stringifiedRefreshToken = JSON.stringify(refreshTokenPathOrObject); - if (!(stringifiedRefreshToken in globalRefreshTokenCreds)) { - globalRefreshTokenCreds[stringifiedRefreshToken] = new RefreshTokenCredential( - refreshTokenPathOrObject, httpAgent); - } - return globalRefreshTokenCreds[stringifiedRefreshToken]; - }, - - applicationDefault: (httpAgent?: Agent): Credential => { - if (typeof globalAppDefaultCred === 'undefined') { - globalAppDefaultCred = getApplicationDefault(httpAgent); - } - return globalAppDefaultCred; - }, + cert, refreshToken, applicationDefault }; diff --git a/src/firestore/firestore-internal.ts b/src/firestore/firestore-internal.ts index 28bf7e0272..9cf5a8a5a8 100644 --- a/src/firestore/firestore-internal.ts +++ b/src/firestore/firestore-internal.ts @@ -17,7 +17,7 @@ import { FirebaseApp } from '../firebase-app'; import { FirebaseFirestoreError } from '../utils/error'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { ServiceAccountCredential, isApplicationDefault } from '../auth/credential'; +import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential'; import { Firestore, Settings } from '@google-cloud/firestore'; import * as validator from '../utils/validator'; diff --git a/src/index.d.ts b/src/index.d.ts index 9de4488e12..fff3085772 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -19,6 +19,7 @@ import * as _firestore from '@google-cloud/firestore'; import { Agent } from 'http'; import * as _auth from './auth'; +import * as _credential from './credential'; import * as _database from './database'; import * as _messaging from './messaging'; import * as _instanceId from './instance-id'; @@ -596,149 +597,10 @@ declare namespace admin.auth { } declare namespace admin.credential { - - /** - * Interface that provides Google OAuth2 access tokens used to authenticate - * with Firebase services. - * - * In most cases, you will not need to implement this yourself and can instead - * use the default implementations provided by - * {@link admin.credential `admin.credential`}. - */ - interface Credential { - - /** - * Returns a Google OAuth2 access token object used to authenticate with - * Firebase services. - * - * This object contains the following properties: - * * `access_token` (`string`): The actual Google OAuth2 access token. - * * `expires_in` (`number`): The number of seconds from when the token was - * issued that it expires. - * - * @return A Google OAuth2 access token object. - */ - getAccessToken(): Promise; - } - - - /** - * Returns a credential created from the - * {@link - * https://developers.google.com/identity/protocols/application-default-credentials - * Google Application Default Credentials} - * that grants admin access to Firebase services. This credential can be used - * in the call to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * Google Application Default Credentials are available on any Google - * infrastructure, such as Google App Engine and Google Compute Engine. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * admin.initializeApp({ - * credential: admin.credential.applicationDefault(), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param {!Object=} httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return {!admin.credential.Credential} A credential authenticated via Google - * Application Default Credentials that can be used to initialize an app. - */ - function applicationDefault(httpAgent?: Agent): admin.credential.Credential; - - /** - * Returns a credential created from the provided service account that grants - * admin access to Firebase services. This credential can be used in the call - * to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * // Providing a path to a service account key JSON file - * var serviceAccount = require("path/to/serviceAccountKey.json"); - * admin.initializeApp({ - * credential: admin.credential.cert(serviceAccount), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @example - * ```javascript - * // Providing a service account object inline - * admin.initializeApp({ - * credential: admin.credential.cert({ - * projectId: "", - * clientEmail: "foo@.iam.gserviceaccount.com", - * privateKey: "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n" - * }), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param serviceAccountPathOrObject The path to a service - * account key JSON file or an object representing a service account key. - * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return A credential authenticated via the - * provided service account that can be used to initialize an app. - */ - function cert(serviceAccountPathOrObject: string | admin.ServiceAccount, httpAgent?: Agent): admin.credential.Credential; - - /** - * Returns a credential created from the provided refresh token that grants - * admin access to Firebase services. This credential can be used in the call - * to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * // Providing a path to a refresh token JSON file - * var refreshToken = require("path/to/refreshToken.json"); - * admin.initializeApp({ - * credential: admin.credential.refreshToken(refreshToken), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param refreshTokenPathOrObject The path to a Google - * OAuth2 refresh token JSON file or an object representing a Google OAuth2 - * refresh token. - * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return A credential authenticated via the - * provided service account that can be used to initialize an app. - */ - function refreshToken(refreshTokenPathOrObject: string | object, httpAgent?: Agent): admin.credential.Credential; + export import Credential = _credential.admin.credential.Credential; + export import applicationDefault = _credential.admin.credential.applicationDefault; + export import cert = _credential.admin.credential.cert; + export import refreshToken = _credential.admin.credential.refreshToken; } declare namespace admin.database { diff --git a/src/storage/storage.ts b/src/storage/storage.ts index af490eedd5..61cb6a80c9 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -17,7 +17,7 @@ import { FirebaseApp } from '../firebase-app'; import { FirebaseError } from '../utils/error'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { ServiceAccountCredential, isApplicationDefault } from '../auth/credential'; +import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential'; import { Bucket, Storage as StorageClient } from '@google-cloud/storage'; import * as utils from '../utils/index'; diff --git a/src/utils/index.ts b/src/utils/index.ts index fee064eab3..2a32c46146 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -15,7 +15,7 @@ */ import { FirebaseApp, FirebaseAppOptions } from '../firebase-app'; -import { ServiceAccountCredential, ComputeEngineCredential } from '../auth/credential'; +import { ServiceAccountCredential, ComputeEngineCredential } from '../credential/credential'; import * as validator from './validator'; diff --git a/test/integration/setup.ts b/test/integration/setup.ts index 4d58cede9c..9e8e789ec8 100644 --- a/test/integration/setup.ts +++ b/test/integration/setup.ts @@ -19,7 +19,7 @@ import fs = require('fs'); import minimist = require('minimist'); import path = require('path'); import { random } from 'lodash'; -import { Credential, GoogleOAuthAccessToken } from '../../src/auth/credential'; +import { Credential, GoogleOAuthAccessToken } from '../../src/credential/credential'; // eslint-disable-next-line @typescript-eslint/no-var-requires const chalk = require('chalk'); diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index 28884b30d2..9033e3d798 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -27,7 +27,7 @@ import * as jwt from 'jsonwebtoken'; import { FirebaseNamespace } from '../../src/firebase-namespace'; import { FirebaseServiceInterface } from '../../src/firebase-service'; import { FirebaseApp, FirebaseAppOptions } from '../../src/firebase-app'; -import { Credential, GoogleOAuthAccessToken, ServiceAccountCredential } from '../../src/auth/credential'; +import { Credential, GoogleOAuthAccessToken, ServiceAccountCredential } from '../../src/credential/credential'; const ALGORITHM = 'RS256'; const ONE_HOUR_IN_SECONDS = 60 * 60; diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index c5ae2ddbbb..8b32625736 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -42,7 +42,7 @@ import { } from '../../../src/auth/auth-config'; import { deepCopy } from '../../../src/utils/deep-copy'; import { TenantManager } from '../../../src/auth/tenant-manager'; -import { ServiceAccountCredential } from '../../../src/auth/credential'; +import { ServiceAccountCredential } from '../../../src/credential/credential'; import { HttpClient } from '../../../src/utils/api-request'; chai.should(); diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index cc733270b2..69ad13b9e9 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -28,7 +28,7 @@ import { BLACKLISTED_CLAIMS, FirebaseTokenGenerator, ServiceAccountSigner, IAMSigner, } from '../../../src/auth/token-generator'; -import { ServiceAccountCredential } from '../../../src/auth/credential'; +import { ServiceAccountCredential } from '../../../src/credential/credential'; import { AuthorizedHttpClient, HttpClient } from '../../../src/utils/api-request'; import { FirebaseApp } from '../../../src/firebase-app'; import * as utils from '../utils'; diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index d4f4722af9..1c7a73f62d 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -31,7 +31,7 @@ import * as mocks from '../../resources/mocks'; import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator'; import * as verifier from '../../../src/auth/token-verifier'; -import { ServiceAccountCredential } from '../../../src/auth/credential'; +import { ServiceAccountCredential } from '../../../src/credential/credential'; import { AuthClientErrorCode } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; diff --git a/test/unit/auth/credential.spec.ts b/test/unit/credential/credential.spec.ts similarity index 99% rename from test/unit/auth/credential.spec.ts rename to test/unit/credential/credential.spec.ts index dc66ae317b..df635004ef 100644 --- a/test/unit/auth/credential.spec.ts +++ b/test/unit/credential/credential.spec.ts @@ -33,7 +33,7 @@ import * as mocks from '../../resources/mocks'; import { GoogleOAuthAccessToken, RefreshTokenCredential, ServiceAccountCredential, ComputeEngineCredential, getApplicationDefault, isApplicationDefault, Credential, -} from '../../../src/auth/credential'; +} from '../../../src/credential/credential'; import { HttpClient } from '../../../src/utils/api-request'; import { Agent } from 'https'; import { FirebaseAppError } from '../../../src/utils/error'; diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index 2b3b9591d8..e710ea1613 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -25,7 +25,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from './utils'; import * as mocks from '../resources/mocks'; -import { GoogleOAuthAccessToken, ServiceAccountCredential } from '../../src/auth/credential'; +import { GoogleOAuthAccessToken, ServiceAccountCredential } from '../../src/credential/credential'; import { FirebaseServiceInterface } from '../../src/firebase-service'; import { FirebaseApp, FirebaseAccessToken } from '../../src/firebase-app'; import { FirebaseNamespace, FirebaseNamespaceInternals, FIREBASE_CONFIG_VAR } from '../../src/firebase-namespace'; diff --git a/test/unit/firebase.spec.ts b/test/unit/firebase.spec.ts index b697afa541..9017f6805b 100644 --- a/test/unit/firebase.spec.ts +++ b/test/unit/firebase.spec.ts @@ -27,7 +27,9 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../resources/mocks'; import * as firebaseAdmin from '../../src/index'; -import { RefreshTokenCredential, ServiceAccountCredential, isApplicationDefault } from '../../src/auth/credential'; +import { + RefreshTokenCredential, ServiceAccountCredential, isApplicationDefault +} from '../../src/credential/credential'; chai.should(); chai.use(chaiAsPromised); diff --git a/test/unit/firestore/firestore.spec.ts b/test/unit/firestore/firestore.spec.ts index 0935f58821..08bc2f5cd3 100644 --- a/test/unit/firestore/firestore.spec.ts +++ b/test/unit/firestore/firestore.spec.ts @@ -21,7 +21,7 @@ import { expect } from 'chai'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; -import { ComputeEngineCredential, RefreshTokenCredential } from '../../../src/auth/credential'; +import { ComputeEngineCredential, RefreshTokenCredential } from '../../../src/credential/credential'; import { FirestoreService, getFirestoreOptions } from '../../../src/firestore/firestore-internal'; describe('Firestore', () => { diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index 70583f73a6..fe22da0aca 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -27,7 +27,6 @@ import './utils/api-request.spec'; // Auth import './auth/auth.spec'; -import './auth/credential.spec'; import './auth/user-record.spec'; import './auth/token-generator.spec'; import './auth/token-verifier.spec'; @@ -38,6 +37,9 @@ import './auth/auth-config.spec'; import './auth/tenant.spec'; import './auth/tenant-manager.spec'; +// Credential +import './credential/credential.spec'; + // Database import './database/database.spec'; diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 1acbb6f4d6..02d63f06cb 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -25,7 +25,7 @@ import { } from '../../../src/utils/index'; import { isNonEmptyString } from '../../../src/utils/validator'; import { FirebaseApp, FirebaseAppOptions } from '../../../src/firebase-app'; -import { ComputeEngineCredential } from '../../../src/auth/credential'; +import { ComputeEngineCredential } from '../../../src/credential/credential'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; import { FirebaseAppError } from '../../../src/utils/error'; From 1b1dbb78ec364ad872a20608152a9c867570c2f8 Mon Sep 17 00:00:00 2001 From: Horatiu Lazu Date: Wed, 26 Aug 2020 22:47:36 +0000 Subject: [PATCH 035/160] Allow Credential to auto-generate typings, separate internal vs external APIs (#1012) --- gulpfile.js | 1 + src/auth/token-generator.ts | 2 +- src/credential/credential-interfaces.ts | 50 +++ src/credential/credential-internal.ts | 472 +++++++++++++++++++++++ src/credential/credential.ts | 489 +----------------------- src/credential/index.ts | 3 +- src/firebase-app.ts | 5 +- src/firebase-namespace.ts | 3 +- src/firestore/firestore-internal.ts | 2 +- src/storage/storage.ts | 2 +- src/utils/index.ts | 2 +- test/integration/setup.ts | 2 +- test/resources/mocks.ts | 3 +- test/unit/auth/auth.spec.ts | 2 +- test/unit/auth/token-generator.spec.ts | 2 +- test/unit/auth/token-verifier.spec.ts | 2 +- test/unit/credential/credential.spec.ts | 9 +- test/unit/firebase-app.spec.ts | 3 +- test/unit/firebase.spec.ts | 2 +- test/unit/firestore/firestore.spec.ts | 4 +- test/unit/utils/index.spec.ts | 2 +- 21 files changed, 556 insertions(+), 506 deletions(-) create mode 100644 src/credential/credential-interfaces.ts create mode 100644 src/credential/credential-internal.ts diff --git a/gulpfile.js b/gulpfile.js index d123f14d81..8af679e13d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -54,6 +54,7 @@ var paths = { curatedTypings: [ 'src/*.d.ts', + '!src/credential.d.ts', '!src/database.d.ts', '!src/instance-id.d.ts', '!src/security-rules.d.ts', diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 645b1afbf7..194ed48cbf 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -15,7 +15,7 @@ */ import { FirebaseApp } from '../firebase-app'; -import { ServiceAccountCredential } from '../credential/credential'; +import { ServiceAccountCredential } from '../credential/credential-internal'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { AuthorizedHttpClient, HttpError, HttpRequestConfig, HttpClient } from '../utils/api-request'; diff --git a/src/credential/credential-interfaces.ts b/src/credential/credential-interfaces.ts new file mode 100644 index 0000000000..d627586020 --- /dev/null +++ b/src/credential/credential-interfaces.ts @@ -0,0 +1,50 @@ +/*! + * Copyright 2020 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. + */ + +// TODO: According to the typings this is part of the Firebase Namespace today +// and not credential; it will need to be moved accordingly. +/** + * Interface for Google OAuth 2.0 access tokens. + */ +export interface GoogleOAuthAccessToken { + /* tslint:disable:variable-name */ + access_token: string; + expires_in: number; + /* tslint:enable:variable-name */ +} + +/** + * Interface that provides Google OAuth2 access tokens used to authenticate + * with Firebase services. + * + * In most cases, you will not need to implement this yourself and can instead + * use the default implementations provided by + * {@link admin.credential `admin.credential`}. + */ +export interface Credential { + /** + * Returns a Google OAuth2 access token object used to authenticate with + * Firebase services. + * + * This object contains the following properties: + * * `access_token` (`string`): The actual Google OAuth2 access token. + * * `expires_in` (`number`): The number of seconds from when the token was + * issued that it expires. + * + * @return A Google OAuth2 access token object. + */ + getAccessToken(): Promise; +} diff --git a/src/credential/credential-internal.ts b/src/credential/credential-internal.ts new file mode 100644 index 0000000000..a1037857af --- /dev/null +++ b/src/credential/credential-internal.ts @@ -0,0 +1,472 @@ +/*! + * Copyright 2020 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 untyped import syntax for Node built-ins +import fs = require('fs'); +import os = require('os'); +import path = require('path'); + +import { GoogleOAuthAccessToken, Credential } from './credential-interfaces'; +import { AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { HttpClient, HttpRequestConfig, HttpError, HttpResponse } from '../utils/api-request'; +import { Agent } from 'http'; +import * as util from '../utils/validator'; + +const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token'; +const GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com'; +const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token'; + +// NOTE: the Google Metadata Service uses HTTP over a vlan +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 configDir = (() => { + // Windows has a dedicated low-rights location for apps at ~/Application Data + const sys = os.platform(); + if (sys && sys.length >= 3 && sys.substring(0, 3).toLowerCase() === 'win') { + return process.env.APPDATA; + } + + // On *nix the gcloud cli creates a . dir. + return process.env.HOME && path.resolve(process.env.HOME, '.config'); +})(); + +const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json'; +const GCLOUD_CREDENTIAL_PATH = configDir && path.resolve(configDir, GCLOUD_CREDENTIAL_SUFFIX); + +const REFRESH_TOKEN_HOST = 'www.googleapis.com'; +const REFRESH_TOKEN_PATH = '/oauth2/v4/token'; + +const ONE_HOUR_IN_SECONDS = 60 * 60; +const JWT_ALGORITHM = 'RS256'; + +/** + * Implementation of Credential that uses a service account. + */ +export class ServiceAccountCredential implements Credential { + + public readonly projectId: string; + public readonly privateKey: string; + public readonly clientEmail: string; + + private readonly httpClient: HttpClient; + + /** + * Creates a new ServiceAccountCredential from the given parameters. + * + * @param serviceAccountPathOrObject Service account json object or path to a service account json file. + * @param httpAgent Optional http.Agent to use when calling the remote token server. + * @param implicit An optinal boolean indicating whether this credential was implicitly discovered from the + * environment, as opposed to being explicitly specified by the developer. + * + * @constructor + */ + constructor( + serviceAccountPathOrObject: string | object, + private readonly httpAgent?: Agent, + readonly implicit: boolean = false) { + + const serviceAccount = (typeof serviceAccountPathOrObject === 'string') ? + ServiceAccount.fromPath(serviceAccountPathOrObject) + : new ServiceAccount(serviceAccountPathOrObject); + this.projectId = serviceAccount.projectId; + this.privateKey = serviceAccount.privateKey; + this.clientEmail = serviceAccount.clientEmail; + this.httpClient = new HttpClient(); + } + + public getAccessToken(): Promise { + const token = this.createAuthJwt_(); + const postData = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3A' + + 'grant-type%3Ajwt-bearer&assertion=' + token; + const request: HttpRequestConfig = { + method: 'POST', + url: `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: postData, + httpAgent: this.httpAgent, + }; + return requestAccessToken(this.httpClient, request); + } + + private createAuthJwt_(): string { + const claims = { + scope: [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/firebase.database', + 'https://www.googleapis.com/auth/firebase.messaging', + 'https://www.googleapis.com/auth/identitytoolkit', + 'https://www.googleapis.com/auth/userinfo.email', + ].join(' '), + }; + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const jwt = require('jsonwebtoken'); + // This method is actually synchronous so we can capture and return the buffer. + return jwt.sign(claims, this.privateKey, { + audience: GOOGLE_TOKEN_AUDIENCE, + expiresIn: ONE_HOUR_IN_SECONDS, + issuer: this.clientEmail, + algorithm: JWT_ALGORITHM, + }); + } +} + +/** + * A struct containing the properties necessary to use service account JSON credentials. + */ +class ServiceAccount { + + public readonly projectId: string; + public readonly privateKey: string; + public readonly clientEmail: string; + + public static fromPath(filePath: string): ServiceAccount { + try { + return new ServiceAccount(JSON.parse(fs.readFileSync(filePath, 'utf8'))); + } catch (error) { + // Throw a nicely formed error message if the file contents cannot be parsed + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Failed to parse service account json file: ' + error, + ); + } + } + + constructor(json: object) { + if (!util.isNonNullObject(json)) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Service account must be an object.', + ); + } + + copyAttr(this, json, 'projectId', 'project_id'); + copyAttr(this, json, 'privateKey', 'private_key'); + copyAttr(this, json, 'clientEmail', 'client_email'); + + let errorMessage; + if (!util.isNonEmptyString(this.projectId)) { + errorMessage = 'Service account object must contain a string "project_id" property.'; + } else if (!util.isNonEmptyString(this.privateKey)) { + errorMessage = 'Service account object must contain a string "private_key" property.'; + } else if (!util.isNonEmptyString(this.clientEmail)) { + errorMessage = 'Service account object must contain a string "client_email" property.'; + } + + if (typeof errorMessage !== 'undefined') { + throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); + } + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const forge = require('node-forge'); + try { + forge.pki.privateKeyFromPem(this.privateKey); + } catch (error) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Failed to parse private key: ' + error); + } + } +} + +/** + * Implementation of Credential that gets access tokens from the metadata service available + * in the Google Cloud Platform. This authenticates the process as the default service account + * of an App Engine instance or Google Compute Engine machine. + */ +export class ComputeEngineCredential implements Credential { + + private readonly httpClient = new HttpClient(); + private readonly httpAgent?: Agent; + private projectId?: string; + + constructor(httpAgent?: Agent) { + this.httpAgent = httpAgent; + } + + public getAccessToken(): Promise { + const request = this.buildRequest(GOOGLE_METADATA_SERVICE_TOKEN_PATH); + return requestAccessToken(this.httpClient, request); + } + + public getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + + const request = this.buildRequest(GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH); + return this.httpClient.send(request) + .then((resp) => { + this.projectId = resp.text!; + return this.projectId; + }) + .catch((err) => { + const detail: string = (err instanceof HttpError) ? getDetailFromResponse(err.response) : err.message; + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + `Failed to determine project ID: ${detail}`); + }); + } + + private buildRequest(urlPath: string): HttpRequestConfig { + return { + method: 'GET', + url: `http://${GOOGLE_METADATA_SERVICE_HOST}${urlPath}`, + headers: { + 'Metadata-Flavor': 'Google', + }, + httpAgent: this.httpAgent, + }; + } +} + +/** + * Implementation of Credential that gets access tokens from refresh tokens. + */ +export class RefreshTokenCredential implements Credential { + + private readonly refreshToken: RefreshToken; + private readonly httpClient: HttpClient; + + /** + * Creates a new RefreshTokenCredential from the given parameters. + * + * @param refreshTokenPathOrObject Refresh token json object or path to a refresh token (user credentials) json file. + * @param httpAgent Optional http.Agent to use when calling the remote token server. + * @param implicit An optinal boolean indicating whether this credential was implicitly discovered from the + * environment, as opposed to being explicitly specified by the developer. + * + * @constructor + */ + constructor( + refreshTokenPathOrObject: string | object, + private readonly httpAgent?: Agent, + readonly implicit: boolean = false) { + + this.refreshToken = (typeof refreshTokenPathOrObject === 'string') ? + RefreshToken.fromPath(refreshTokenPathOrObject) + : new RefreshToken(refreshTokenPathOrObject); + this.httpClient = new HttpClient(); + } + + public getAccessToken(): Promise { + const postData = + 'client_id=' + this.refreshToken.clientId + '&' + + 'client_secret=' + this.refreshToken.clientSecret + '&' + + 'refresh_token=' + this.refreshToken.refreshToken + '&' + + 'grant_type=refresh_token'; + const request: HttpRequestConfig = { + method: 'POST', + url: `https://${REFRESH_TOKEN_HOST}${REFRESH_TOKEN_PATH}`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: postData, + httpAgent: this.httpAgent, + }; + return requestAccessToken(this.httpClient, request); + } +} + +class RefreshToken { + + public readonly clientId: string; + public readonly clientSecret: string; + public readonly refreshToken: string; + public readonly type: string; + + /* + * Tries to load a RefreshToken from a path. Throws if the path doesn't exist or the + * data at the path is invalid. + */ + public static fromPath(filePath: string): RefreshToken { + try { + return new RefreshToken(JSON.parse(fs.readFileSync(filePath, 'utf8'))); + } catch (error) { + // Throw a nicely formed error message if the file contents cannot be parsed + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Failed to parse refresh token file: ' + error, + ); + } + } + + constructor(json: object) { + copyAttr(this, json, 'clientId', 'client_id'); + copyAttr(this, json, 'clientSecret', 'client_secret'); + copyAttr(this, json, 'refreshToken', 'refresh_token'); + copyAttr(this, json, 'type', 'type'); + + let errorMessage; + if (!util.isNonEmptyString(this.clientId)) { + errorMessage = 'Refresh token must contain a "client_id" property.'; + } else if (!util.isNonEmptyString(this.clientSecret)) { + errorMessage = 'Refresh token must contain a "client_secret" property.'; + } else if (!util.isNonEmptyString(this.refreshToken)) { + errorMessage = 'Refresh token must contain a "refresh_token" property.'; + } else if (!util.isNonEmptyString(this.type)) { + errorMessage = 'Refresh token must contain a "type" property.'; + } + + if (typeof errorMessage !== 'undefined') { + throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); + } + } +} + + +/** + * Checks if the given credential was loaded via the application default credentials mechanism. This + * includes all ComputeEngineCredential instances, and the ServiceAccountCredential and RefreshTokenCredential + * instances that were loaded from well-known files or environment variables, rather than being explicitly + * instantiated. + * + * @param credential The credential instance to check. + */ +export function isApplicationDefault(credential?: Credential): boolean { + return credential instanceof ComputeEngineCredential || + (credential instanceof ServiceAccountCredential && credential.implicit) || + (credential instanceof RefreshTokenCredential && credential.implicit); +} + +export function getApplicationDefault(httpAgent?: Agent): Credential { + if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { + return credentialFromFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, httpAgent); + } + + // It is OK to not have this file. If it is present, it must be valid. + if (GCLOUD_CREDENTIAL_PATH) { + const refreshToken = readCredentialFile(GCLOUD_CREDENTIAL_PATH, true); + if (refreshToken) { + return new RefreshTokenCredential(refreshToken, httpAgent, true); + } + } + + return new ComputeEngineCredential(httpAgent); +} + +/** + * Copies the specified property from one object to another. + * + * If no property exists by the given "key", looks for a property identified by "alt", and copies it instead. + * This can be used to implement behaviors such as "copy property myKey or my_key". + * + * @param to Target object to copy the property into. + * @param from Source object to copy the property from. + * @param key Name of the property to copy. + * @param alt Alternative name of the property to copy. + */ +function copyAttr(to: {[key: string]: any}, from: {[key: string]: any}, key: string, alt: string): void { + const tmp = from[key] || from[alt]; + if (typeof tmp !== 'undefined') { + to[key] = tmp; + } +} + +/** + * Obtain a new OAuth2 token by making a remote service call. + */ +function requestAccessToken(client: HttpClient, request: HttpRequestConfig): Promise { + return client.send(request).then((resp) => { + const json = resp.data; + if (!json.access_token || !json.expires_in) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + `Unexpected response while fetching access token: ${ JSON.stringify(json) }`, + ); + } + return json; + }).catch((err) => { + throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, getErrorMessage(err)); + }); +} + +/** + * Constructs a human-readable error message from the given Error. + */ +function getErrorMessage(err: Error): string { + const detail: string = (err instanceof HttpError) ? getDetailFromResponse(err.response) : err.message; + return `Error fetching access token: ${detail}`; +} + +/** + * Extracts details from the given HTTP error response, and returns a human-readable description. If + * the response is JSON-formatted, looks up the error and error_description fields sent by the + * Google Auth servers. Otherwise returns the entire response payload as the error detail. + */ +function getDetailFromResponse(response: HttpResponse): string { + if (response.isJson() && response.data.error) { + const json = response.data; + let detail = json.error; + if (json.error_description) { + detail += ' (' + json.error_description + ')'; + } + return detail; + } + return response.text || 'Missing error payload'; +} + +function credentialFromFile(filePath: string, httpAgent?: Agent): Credential { + const credentialsFile = readCredentialFile(filePath); + if (typeof credentialsFile !== 'object' || credentialsFile === null) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Failed to parse contents of the credentials file as an object', + ); + } + + if (credentialsFile.type === 'service_account') { + return new ServiceAccountCredential(credentialsFile, httpAgent, true); + } + + if (credentialsFile.type === 'authorized_user') { + return new RefreshTokenCredential(credentialsFile, httpAgent, true); + } + + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Invalid contents in the credentials file', + ); +} + +function readCredentialFile(filePath: string, ignoreMissing?: boolean): {[key: string]: any} | null { + let fileText: string; + try { + fileText = fs.readFileSync(filePath, 'utf8'); + } catch (error) { + if (ignoreMissing) { + return null; + } + + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + `Failed to read credentials from file ${filePath}: ` + error, + ); + } + + try { + return JSON.parse(fileText); + } catch (error) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Failed to parse contents of the credentials file as an object: ' + error, + ); + } +} diff --git a/src/credential/credential.ts b/src/credential/credential.ts index 5789e1beeb..f05b40676b 100644 --- a/src/credential/credential.ts +++ b/src/credential/credential.ts @@ -15,81 +15,16 @@ */ // Use untyped import syntax for Node built-ins -import fs = require('fs'); -import os = require('os'); -import path = require('path'); - -import { AppErrorCodes, FirebaseAppError } from '../utils/error'; -import { HttpClient, HttpRequestConfig, HttpError, HttpResponse } from '../utils/api-request'; import { Agent } from 'http'; -import * as util from '../utils/validator'; - -const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token'; -const GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com'; -const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token'; - -// NOTE: the Google Metadata Service uses HTTP over a vlan -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 configDir = (() => { - // Windows has a dedicated low-rights location for apps at ~/Application Data - const sys = os.platform(); - if (sys && sys.length >= 3 && sys.substring(0, 3).toLowerCase() === 'win') { - return process.env.APPDATA; - } - - // On *nix the gcloud cli creates a . dir. - return process.env.HOME && path.resolve(process.env.HOME, '.config'); -})(); - -const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json'; -const GCLOUD_CREDENTIAL_PATH = configDir && path.resolve(configDir, GCLOUD_CREDENTIAL_SUFFIX); - -const REFRESH_TOKEN_HOST = 'www.googleapis.com'; -const REFRESH_TOKEN_PATH = '/oauth2/v4/token'; - -const ONE_HOUR_IN_SECONDS = 60 * 60; -const JWT_ALGORITHM = 'RS256'; +import { + ServiceAccountCredential, RefreshTokenCredential, getApplicationDefault +} from './credential-internal'; +import { Credential } from './credential-interfaces'; let globalAppDefaultCred: Credential; const globalCertCreds: { [key: string]: ServiceAccountCredential } = {}; const globalRefreshTokenCreds: { [key: string]: RefreshTokenCredential } = {}; -/** - * Interface for Google OAuth 2.0 access tokens. - */ -export interface GoogleOAuthAccessToken { - /* tslint:disable:variable-name */ - access_token: string; - expires_in: number; - /* tslint:enable:variable-name */ -} - -/** - * Interface that provides Google OAuth2 access tokens used to authenticate - * with Firebase services. - * - * In most cases, you will not need to implement this yourself and can instead - * use the default implementations provided by - * {@link admin.credential `admin.credential`}. - */ -export interface Credential { - /** - * Returns a Google OAuth2 access token object used to authenticate with - * Firebase services. - * - * This object contains the following properties: - * * `access_token` (`string`): The actual Google OAuth2 access token. - * * `expires_in` (`number`): The number of seconds from when the token was - * issued that it expires. - * - * @return A Google OAuth2 access token object. - */ - getAccessToken(): Promise; -} - /** * Returns a credential created from the * {@link @@ -225,419 +160,3 @@ export function refreshToken(refreshTokenPathOrObject: string | object, httpAgen } return globalRefreshTokenCreds[stringifiedRefreshToken]; } - -/** - * Implementation of Credential that uses a service account. - */ -export class ServiceAccountCredential implements Credential { - - public readonly projectId: string; - public readonly privateKey: string; - public readonly clientEmail: string; - - private readonly httpClient: HttpClient; - - /** - * Creates a new ServiceAccountCredential from the given parameters. - * - * @param serviceAccountPathOrObject Service account json object or path to a service account json file. - * @param httpAgent Optional http.Agent to use when calling the remote token server. - * @param implicit An optinal boolean indicating whether this credential was implicitly discovered from the - * environment, as opposed to being explicitly specified by the developer. - * - * @constructor - */ - constructor( - serviceAccountPathOrObject: string | object, - private readonly httpAgent?: Agent, - readonly implicit: boolean = false) { - - const serviceAccount = (typeof serviceAccountPathOrObject === 'string') ? - ServiceAccount.fromPath(serviceAccountPathOrObject) - : new ServiceAccount(serviceAccountPathOrObject); - this.projectId = serviceAccount.projectId; - this.privateKey = serviceAccount.privateKey; - this.clientEmail = serviceAccount.clientEmail; - this.httpClient = new HttpClient(); - } - - public getAccessToken(): Promise { - const token = this.createAuthJwt_(); - const postData = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3A' + - 'grant-type%3Ajwt-bearer&assertion=' + token; - const request: HttpRequestConfig = { - method: 'POST', - url: `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - data: postData, - httpAgent: this.httpAgent, - }; - return requestAccessToken(this.httpClient, request); - } - - private createAuthJwt_(): string { - const claims = { - scope: [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/firebase.database', - 'https://www.googleapis.com/auth/firebase.messaging', - 'https://www.googleapis.com/auth/identitytoolkit', - 'https://www.googleapis.com/auth/userinfo.email', - ].join(' '), - }; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const jwt = require('jsonwebtoken'); - // This method is actually synchronous so we can capture and return the buffer. - return jwt.sign(claims, this.privateKey, { - audience: GOOGLE_TOKEN_AUDIENCE, - expiresIn: ONE_HOUR_IN_SECONDS, - issuer: this.clientEmail, - algorithm: JWT_ALGORITHM, - }); - } -} - -/** - * A struct containing the properties necessary to use service account JSON credentials. - */ -class ServiceAccount { - - public readonly projectId: string; - public readonly privateKey: string; - public readonly clientEmail: string; - - public static fromPath(filePath: string): ServiceAccount { - try { - return new ServiceAccount(JSON.parse(fs.readFileSync(filePath, 'utf8'))); - } catch (error) { - // Throw a nicely formed error message if the file contents cannot be parsed - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse service account json file: ' + error, - ); - } - } - - constructor(json: object) { - if (!util.isNonNullObject(json)) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Service account must be an object.', - ); - } - - copyAttr(this, json, 'projectId', 'project_id'); - copyAttr(this, json, 'privateKey', 'private_key'); - copyAttr(this, json, 'clientEmail', 'client_email'); - - let errorMessage; - if (!util.isNonEmptyString(this.projectId)) { - errorMessage = 'Service account object must contain a string "project_id" property.'; - } else if (!util.isNonEmptyString(this.privateKey)) { - errorMessage = 'Service account object must contain a string "private_key" property.'; - } else if (!util.isNonEmptyString(this.clientEmail)) { - errorMessage = 'Service account object must contain a string "client_email" property.'; - } - - if (typeof errorMessage !== 'undefined') { - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); - } - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const forge = require('node-forge'); - try { - forge.pki.privateKeyFromPem(this.privateKey); - } catch (error) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse private key: ' + error); - } - } -} - -/** - * Implementation of Credential that gets access tokens from the metadata service available - * in the Google Cloud Platform. This authenticates the process as the default service account - * of an App Engine instance or Google Compute Engine machine. - */ -export class ComputeEngineCredential implements Credential { - - private readonly httpClient = new HttpClient(); - private readonly httpAgent?: Agent; - private projectId?: string; - - constructor(httpAgent?: Agent) { - this.httpAgent = httpAgent; - } - - public getAccessToken(): Promise { - const request = this.buildRequest(GOOGLE_METADATA_SERVICE_TOKEN_PATH); - return requestAccessToken(this.httpClient, request); - } - - public getProjectId(): Promise { - if (this.projectId) { - return Promise.resolve(this.projectId); - } - - const request = this.buildRequest(GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH); - return this.httpClient.send(request) - .then((resp) => { - this.projectId = resp.text!; - return this.projectId; - }) - .catch((err) => { - const detail: string = (err instanceof HttpError) ? getDetailFromResponse(err.response) : err.message; - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Failed to determine project ID: ${detail}`); - }); - } - - private buildRequest(urlPath: string): HttpRequestConfig { - return { - method: 'GET', - url: `http://${GOOGLE_METADATA_SERVICE_HOST}${urlPath}`, - headers: { - 'Metadata-Flavor': 'Google', - }, - httpAgent: this.httpAgent, - }; - } -} - -/** - * Implementation of Credential that gets access tokens from refresh tokens. - */ -export class RefreshTokenCredential implements Credential { - - private readonly refreshToken: RefreshToken; - private readonly httpClient: HttpClient; - - /** - * Creates a new RefreshTokenCredential from the given parameters. - * - * @param refreshTokenPathOrObject Refresh token json object or path to a refresh token (user credentials) json file. - * @param httpAgent Optional http.Agent to use when calling the remote token server. - * @param implicit An optinal boolean indicating whether this credential was implicitly discovered from the - * environment, as opposed to being explicitly specified by the developer. - * - * @constructor - */ - constructor( - refreshTokenPathOrObject: string | object, - private readonly httpAgent?: Agent, - readonly implicit: boolean = false) { - - this.refreshToken = (typeof refreshTokenPathOrObject === 'string') ? - RefreshToken.fromPath(refreshTokenPathOrObject) - : new RefreshToken(refreshTokenPathOrObject); - this.httpClient = new HttpClient(); - } - - public getAccessToken(): Promise { - const postData = - 'client_id=' + this.refreshToken.clientId + '&' + - 'client_secret=' + this.refreshToken.clientSecret + '&' + - 'refresh_token=' + this.refreshToken.refreshToken + '&' + - 'grant_type=refresh_token'; - const request: HttpRequestConfig = { - method: 'POST', - url: `https://${REFRESH_TOKEN_HOST}${REFRESH_TOKEN_PATH}`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - data: postData, - httpAgent: this.httpAgent, - }; - return requestAccessToken(this.httpClient, request); - } -} - -class RefreshToken { - - public readonly clientId: string; - public readonly clientSecret: string; - public readonly refreshToken: string; - public readonly type: string; - - /* - * Tries to load a RefreshToken from a path. Throws if the path doesn't exist or the - * data at the path is invalid. - */ - public static fromPath(filePath: string): RefreshToken { - try { - return new RefreshToken(JSON.parse(fs.readFileSync(filePath, 'utf8'))); - } catch (error) { - // Throw a nicely formed error message if the file contents cannot be parsed - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse refresh token file: ' + error, - ); - } - } - - constructor(json: object) { - copyAttr(this, json, 'clientId', 'client_id'); - copyAttr(this, json, 'clientSecret', 'client_secret'); - copyAttr(this, json, 'refreshToken', 'refresh_token'); - copyAttr(this, json, 'type', 'type'); - - let errorMessage; - if (!util.isNonEmptyString(this.clientId)) { - errorMessage = 'Refresh token must contain a "client_id" property.'; - } else if (!util.isNonEmptyString(this.clientSecret)) { - errorMessage = 'Refresh token must contain a "client_secret" property.'; - } else if (!util.isNonEmptyString(this.refreshToken)) { - errorMessage = 'Refresh token must contain a "refresh_token" property.'; - } else if (!util.isNonEmptyString(this.type)) { - errorMessage = 'Refresh token must contain a "type" property.'; - } - - if (typeof errorMessage !== 'undefined') { - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); - } - } -} - -export function getApplicationDefault(httpAgent?: Agent): Credential { - if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { - return credentialFromFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, httpAgent); - } - - // It is OK to not have this file. If it is present, it must be valid. - if (GCLOUD_CREDENTIAL_PATH) { - const refreshToken = readCredentialFile(GCLOUD_CREDENTIAL_PATH, true); - if (refreshToken) { - return new RefreshTokenCredential(refreshToken, httpAgent, true); - } - } - - return new ComputeEngineCredential(httpAgent); -} - -/** - * Checks if the given credential was loaded via the application default credentials mechanism. This - * includes all ComputeEngineCredential instances, and the ServiceAccountCredential and RefreshTokenCredential - * instances that were loaded from well-known files or environment variables, rather than being explicitly - * instantiated. - * - * @param credential The credential instance to check. - */ -export function isApplicationDefault(credential?: Credential): boolean { - return credential instanceof ComputeEngineCredential || - (credential instanceof ServiceAccountCredential && credential.implicit) || - (credential instanceof RefreshTokenCredential && credential.implicit); -} - -/** - * Copies the specified property from one object to another. - * - * If no property exists by the given "key", looks for a property identified by "alt", and copies it instead. - * This can be used to implement behaviors such as "copy property myKey or my_key". - * - * @param to Target object to copy the property into. - * @param from Source object to copy the property from. - * @param key Name of the property to copy. - * @param alt Alternative name of the property to copy. - */ -function copyAttr(to: {[key: string]: any}, from: {[key: string]: any}, key: string, alt: string): void { - const tmp = from[key] || from[alt]; - if (typeof tmp !== 'undefined') { - to[key] = tmp; - } -} - -/** - * Obtain a new OAuth2 token by making a remote service call. - */ -function requestAccessToken(client: HttpClient, request: HttpRequestConfig): Promise { - return client.send(request).then((resp) => { - const json = resp.data; - if (!json.access_token || !json.expires_in) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Unexpected response while fetching access token: ${ JSON.stringify(json) }`, - ); - } - return json; - }).catch((err) => { - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, getErrorMessage(err)); - }); -} - -/** - * Constructs a human-readable error message from the given Error. - */ -function getErrorMessage(err: Error): string { - const detail: string = (err instanceof HttpError) ? getDetailFromResponse(err.response) : err.message; - return `Error fetching access token: ${detail}`; -} - -/** - * Extracts details from the given HTTP error response, and returns a human-readable description. If - * the response is JSON-formatted, looks up the error and error_description fields sent by the - * Google Auth servers. Otherwise returns the entire response payload as the error detail. - */ -function getDetailFromResponse(response: HttpResponse): string { - if (response.isJson() && response.data.error) { - const json = response.data; - let detail = json.error; - if (json.error_description) { - detail += ' (' + json.error_description + ')'; - } - return detail; - } - return response.text || 'Missing error payload'; -} - -function credentialFromFile(filePath: string, httpAgent?: Agent): Credential { - const credentialsFile = readCredentialFile(filePath); - if (typeof credentialsFile !== 'object' || credentialsFile === null) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse contents of the credentials file as an object', - ); - } - - if (credentialsFile.type === 'service_account') { - return new ServiceAccountCredential(credentialsFile, httpAgent, true); - } - - if (credentialsFile.type === 'authorized_user') { - return new RefreshTokenCredential(credentialsFile, httpAgent, true); - } - - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Invalid contents in the credentials file', - ); -} - -function readCredentialFile(filePath: string, ignoreMissing?: boolean): {[key: string]: any} | null { - let fileText: string; - try { - fileText = fs.readFileSync(filePath, 'utf8'); - } catch (error) { - if (ignoreMissing) { - return null; - } - - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Failed to read credentials from file ${filePath}: ` + error, - ); - } - - try { - return JSON.parse(fileText); - } catch (error) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse contents of the credentials file as an object: ' + error, - ); - } -} diff --git a/src/credential/index.ts b/src/credential/index.ts index ff194f4e97..db1e5258bb 100644 --- a/src/credential/index.ts +++ b/src/credential/index.ts @@ -15,6 +15,7 @@ */ import * as credentialApi from './credential'; +import * as credentialInterfacesApi from './credential-interfaces'; /** * Temporarily, admin.credential is used as the namespace name because we @@ -28,7 +29,7 @@ export namespace admin.credential { // See https://github.com/typescript-eslint/typescript-eslint/issues/363 // Allows for exposing classes as interfaces in typings /* eslint-disable @typescript-eslint/no-empty-interface */ - export import Credential = credentialApi.Credential; + export import Credential = credentialInterfacesApi.Credential; export const applicationDefault = credentialApi.applicationDefault; export const cert = credentialApi.cert; export const refreshToken = credentialApi.refreshToken; diff --git a/src/firebase-app.ts b/src/firebase-app.ts index b3db9aaccc..b432fa8e4a 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { - Credential, GoogleOAuthAccessToken, getApplicationDefault -} from './credential/credential'; +import { Credential, GoogleOAuthAccessToken } from './credential/credential-interfaces'; +import { getApplicationDefault } from './credential/credential-internal'; import * as validator from './utils/validator'; import { deepCopy, deepExtend } from './utils/deep-copy'; import { FirebaseServiceInterface } from './firebase-service'; diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 9a639d426e..f4b52b5a6d 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -20,8 +20,9 @@ import { AppErrorCodes, FirebaseAppError } from './utils/error'; import { AppHook, FirebaseApp, FirebaseAppOptions } from './firebase-app'; import { FirebaseServiceFactory, FirebaseServiceInterface } from './firebase-service'; import { - getApplicationDefault, cert, refreshToken, applicationDefault + cert, refreshToken, applicationDefault } from './credential/credential'; +import { getApplicationDefault } from './credential/credential-internal'; import { Auth } from './auth/auth'; import { MachineLearning } from './machine-learning/machine-learning'; diff --git a/src/firestore/firestore-internal.ts b/src/firestore/firestore-internal.ts index 9cf5a8a5a8..25ce0f903a 100644 --- a/src/firestore/firestore-internal.ts +++ b/src/firestore/firestore-internal.ts @@ -17,7 +17,7 @@ import { FirebaseApp } from '../firebase-app'; import { FirebaseFirestoreError } from '../utils/error'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential'; +import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential-internal'; import { Firestore, Settings } from '@google-cloud/firestore'; import * as validator from '../utils/validator'; diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 61cb6a80c9..d7b5d789d5 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -17,7 +17,7 @@ import { FirebaseApp } from '../firebase-app'; import { FirebaseError } from '../utils/error'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential'; +import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential-internal'; import { Bucket, Storage as StorageClient } from '@google-cloud/storage'; import * as utils from '../utils/index'; diff --git a/src/utils/index.ts b/src/utils/index.ts index 2a32c46146..057a9eeffa 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -15,7 +15,7 @@ */ import { FirebaseApp, FirebaseAppOptions } from '../firebase-app'; -import { ServiceAccountCredential, ComputeEngineCredential } from '../credential/credential'; +import { ServiceAccountCredential, ComputeEngineCredential } from '../credential/credential-internal'; import * as validator from './validator'; diff --git a/test/integration/setup.ts b/test/integration/setup.ts index 9e8e789ec8..8e39226bb3 100644 --- a/test/integration/setup.ts +++ b/test/integration/setup.ts @@ -19,7 +19,7 @@ import fs = require('fs'); import minimist = require('minimist'); import path = require('path'); import { random } from 'lodash'; -import { Credential, GoogleOAuthAccessToken } from '../../src/credential/credential'; +import { Credential, GoogleOAuthAccessToken } from '../../src/credential/credential-interfaces'; // eslint-disable-next-line @typescript-eslint/no-var-requires const chalk = require('chalk'); diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index 9033e3d798..664ec660be 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -27,7 +27,8 @@ import * as jwt from 'jsonwebtoken'; import { FirebaseNamespace } from '../../src/firebase-namespace'; import { FirebaseServiceInterface } from '../../src/firebase-service'; import { FirebaseApp, FirebaseAppOptions } from '../../src/firebase-app'; -import { Credential, GoogleOAuthAccessToken, ServiceAccountCredential } from '../../src/credential/credential'; +import { Credential, GoogleOAuthAccessToken } from '../../src/credential/credential-interfaces'; +import { ServiceAccountCredential } from '../../src/credential/credential-internal'; const ALGORITHM = 'RS256'; const ONE_HOUR_IN_SECONDS = 60 * 60; diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 8b32625736..7fb5715eb7 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -42,7 +42,7 @@ import { } from '../../../src/auth/auth-config'; import { deepCopy } from '../../../src/utils/deep-copy'; import { TenantManager } from '../../../src/auth/tenant-manager'; -import { ServiceAccountCredential } from '../../../src/credential/credential'; +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { HttpClient } from '../../../src/utils/api-request'; chai.should(); diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index 69ad13b9e9..e7e5dbaaca 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -28,7 +28,7 @@ import { BLACKLISTED_CLAIMS, FirebaseTokenGenerator, ServiceAccountSigner, IAMSigner, } from '../../../src/auth/token-generator'; -import { ServiceAccountCredential } from '../../../src/credential/credential'; +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { AuthorizedHttpClient, HttpClient } from '../../../src/utils/api-request'; import { FirebaseApp } from '../../../src/firebase-app'; import * as utils from '../utils'; diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 1c7a73f62d..5a9749851c 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -31,7 +31,7 @@ import * as mocks from '../../resources/mocks'; import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator'; import * as verifier from '../../../src/auth/token-verifier'; -import { ServiceAccountCredential } from '../../../src/credential/credential'; +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { AuthClientErrorCode } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; diff --git a/test/unit/credential/credential.spec.ts b/test/unit/credential/credential.spec.ts index df635004ef..42f965dbd1 100644 --- a/test/unit/credential/credential.spec.ts +++ b/test/unit/credential/credential.spec.ts @@ -31,9 +31,12 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { - GoogleOAuthAccessToken, RefreshTokenCredential, ServiceAccountCredential, - ComputeEngineCredential, getApplicationDefault, isApplicationDefault, Credential, -} from '../../../src/credential/credential'; + GoogleOAuthAccessToken, Credential +} from '../../../src/credential/credential-interfaces'; +import { + RefreshTokenCredential, ServiceAccountCredential, + ComputeEngineCredential, getApplicationDefault, isApplicationDefault +} from '../../../src/credential/credential-internal'; import { HttpClient } from '../../../src/utils/api-request'; import { Agent } from 'https'; import { FirebaseAppError } from '../../../src/utils/error'; diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index e710ea1613..af923ef423 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -25,7 +25,8 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from './utils'; import * as mocks from '../resources/mocks'; -import { GoogleOAuthAccessToken, ServiceAccountCredential } from '../../src/credential/credential'; +import { GoogleOAuthAccessToken } from '../../src/credential/credential-interfaces'; +import { ServiceAccountCredential } from '../../src/credential/credential-internal'; import { FirebaseServiceInterface } from '../../src/firebase-service'; import { FirebaseApp, FirebaseAccessToken } from '../../src/firebase-app'; import { FirebaseNamespace, FirebaseNamespaceInternals, FIREBASE_CONFIG_VAR } from '../../src/firebase-namespace'; diff --git a/test/unit/firebase.spec.ts b/test/unit/firebase.spec.ts index 9017f6805b..98ca68d5f8 100644 --- a/test/unit/firebase.spec.ts +++ b/test/unit/firebase.spec.ts @@ -29,7 +29,7 @@ import * as mocks from '../resources/mocks'; import * as firebaseAdmin from '../../src/index'; import { RefreshTokenCredential, ServiceAccountCredential, isApplicationDefault -} from '../../src/credential/credential'; +} from '../../src/credential/credential-internal'; chai.should(); chai.use(chaiAsPromised); diff --git a/test/unit/firestore/firestore.spec.ts b/test/unit/firestore/firestore.spec.ts index 08bc2f5cd3..0b80a846a7 100644 --- a/test/unit/firestore/firestore.spec.ts +++ b/test/unit/firestore/firestore.spec.ts @@ -21,7 +21,9 @@ import { expect } from 'chai'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; -import { ComputeEngineCredential, RefreshTokenCredential } from '../../../src/credential/credential'; +import { + ComputeEngineCredential, RefreshTokenCredential +} from '../../../src/credential/credential-internal'; import { FirestoreService, getFirestoreOptions } from '../../../src/firestore/firestore-internal'; describe('Firestore', () => { diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 02d63f06cb..7a742e1b20 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -25,7 +25,7 @@ import { } from '../../../src/utils/index'; import { isNonEmptyString } from '../../../src/utils/validator'; import { FirebaseApp, FirebaseAppOptions } from '../../../src/firebase-app'; -import { ComputeEngineCredential } from '../../../src/credential/credential'; +import { ComputeEngineCredential } from '../../../src/credential/credential-internal'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; import { FirebaseAppError } from '../../../src/utils/error'; From 699aa9be5bcc306ce186607c227e4fe6a48d5a57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2020 14:28:39 -0700 Subject: [PATCH 036/160] build(deps-dev): bump bcrypt from 3.0.8 to 5.0.0 (#1002) Bumps [bcrypt](https://github.com/kelektiv/node.bcrypt.js) from 3.0.8 to 5.0.0. - [Release notes](https://github.com/kelektiv/node.bcrypt.js/releases) - [Changelog](https://github.com/kelektiv/node.bcrypt.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/kelektiv/node.bcrypt.js/compare/v3.0.8...v5.0.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Hiranya Jayathilaka --- package-lock.json | 31 +++++++++++++++++++------------ package.json | 2 +- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74712bba20..aa2d0766c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.0.0", + "version": "9.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1261,13 +1261,13 @@ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "bcrypt": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.8.tgz", - "integrity": "sha512-jKV6RvLhI36TQnPDvUFqBEnGX9c8dRRygKxCZu7E+MgLfKZbmmXL8a7/SFFOyHoPNX9nV81cKRC5tbQfvEQtpw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz", + "integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==", "dev": true, "requires": { - "nan": "2.14.0", - "node-pre-gyp": "0.14.0" + "node-addon-api": "^3.0.0", + "node-pre-gyp": "0.15.0" } }, "bcrypt-pbkdf": { @@ -5495,7 +5495,8 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true + "dev": true, + "optional": true }, "nanomatch": { "version": "1.2.13", @@ -5615,6 +5616,12 @@ } } }, + "node-addon-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz", + "integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg==", + "dev": true + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -5627,14 +5634,14 @@ "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" }, "node-pre-gyp": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", - "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz", + "integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==", "dev": true, "requires": { "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", + "mkdirp": "^0.5.3", + "needle": "^2.5.0", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", diff --git a/package.json b/package.json index 061d5d0a1c..bb80b12f47 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@types/sinon-chai": "^3.0.0", "@typescript-eslint/eslint-plugin": "^2.20.0", "@typescript-eslint/parser": "^2.20.0", - "bcrypt": "^3.0.6", + "bcrypt": "^5.0.0", "chai": "^4.2.0", "chai-as-promised": "^7.0.0", "chalk": "^1.1.3", From ddce5b6fbc683fd908af30f5f4bd72c961f60571 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 31 Aug 2020 14:12:12 -0700 Subject: [PATCH 037/160] chore: Enabling max-len lint rule (#1014) --- .eslintrc.js | 7 +++++ src/auth/auth-api-request.ts | 3 +- src/credential.d.ts | 4 ++- test/integration/auth.spec.ts | 10 ++++-- test/integration/remote-config.spec.ts | 2 +- test/resources/mocks.ts | 8 ++--- .../project-management/android-app.spec.ts | 4 ++- test/unit/project-management/ios-app.spec.ts | 4 ++- .../project-management-api-request.spec.ts | 4 ++- .../project-management.spec.ts | 4 ++- .../remote-config-api-client.spec.ts | 31 +++++++++++++++---- test/unit/remote-config/remote-config.spec.ts | 26 +++++++++++++--- 12 files changed, 83 insertions(+), 24 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 37125d1535..11dc9d2a80 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,6 +38,13 @@ module.exports = { // Required checks 'indent': ['error', 2], 'keyword-spacing': ['error'], + 'max-len': [ + 'error', + { + 'code': 120, + 'ignoreUrls': true + } + ], "object-curly-spacing": [2, "always"], '@typescript-eslint/explicit-function-return-type': [ 'error', diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index d6d6765280..6043341b80 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -1532,7 +1532,8 @@ export abstract class AbstractAuthRequestHandler { return Promise.reject(e); } const providerId = options.providerId; - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), CREATE_OAUTH_IDP_CONFIG, request, { providerId }) + return this.invokeRequestHandler( + this.getProjectConfigUrlBuilder(), CREATE_OAUTH_IDP_CONFIG, request, { providerId }) .then((response: any) => { if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( diff --git a/src/credential.d.ts b/src/credential.d.ts index be99dc1c12..1a3b8a97d7 100644 --- a/src/credential.d.ts +++ b/src/credential.d.ts @@ -108,7 +108,9 @@ export namespace admin.credential { * @return A credential authenticated via the * provided service account that can be used to initialize an app. */ - function cert(serviceAccountPathOrObject: string | _admin.ServiceAccount, httpAgent?: Agent): admin.credential.Credential; + function cert( + serviceAccountPathOrObject: string | _admin.ServiceAccount, + httpAgent?: Agent): admin.credential.Credential; /** * Returns a credential created from the provided refresh token that grants diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 36185c8135..805e0393cf 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -171,14 +171,16 @@ describe('admin.auth', () => { const firstMultiFactor = userRecord.multiFactor!.enrolledFactors[0]; expect(firstMultiFactor.uid).not.to.be.undefined; expect(firstMultiFactor.enrollmentTime).not.to.be.undefined; - expect((firstMultiFactor as admin.auth.PhoneMultiFactorInfo).phoneNumber).to.equal(enrolledFactors[0].phoneNumber); + expect((firstMultiFactor as admin.auth.PhoneMultiFactorInfo).phoneNumber).to.equal( + enrolledFactors[0].phoneNumber); expect(firstMultiFactor.displayName).to.equal(enrolledFactors[0].displayName); expect(firstMultiFactor.factorId).to.equal(enrolledFactors[0].factorId); // Confirm second enrolled second factor. const secondMultiFactor = userRecord.multiFactor!.enrolledFactors[1]; expect(secondMultiFactor.uid).not.to.be.undefined; expect(secondMultiFactor.enrollmentTime).not.to.be.undefined; - expect((secondMultiFactor as admin.auth.PhoneMultiFactorInfo).phoneNumber).to.equal(enrolledFactors[1].phoneNumber); + expect((secondMultiFactor as admin.auth.PhoneMultiFactorInfo).phoneNumber).to.equal( + enrolledFactors[1].phoneNumber); expect(secondMultiFactor.displayName).to.equal(enrolledFactors[1].displayName); expect(secondMultiFactor.factorId).to.equal(enrolledFactors[1].factorId); }); @@ -218,7 +220,9 @@ describe('admin.auth', () => { * the uid, email, and phoneNumber fields. Works with at least UserRecord * and UserImportRecord instances. */ - function mapUserRecordsToUidEmailPhones(values: Array<{ uid: string; email?: string; phoneNumber?: string}>): Array<{ uid: string; email?: string; phoneNumber?: string}> { + function mapUserRecordsToUidEmailPhones( + values: Array<{ uid: string; email?: string; phoneNumber?: string}> + ): Array<{ uid: string; email?: string; phoneNumber?: string}> { return values.map((ur) => ({ uid: ur.uid, email: ur.email, phoneNumber: ur.phoneNumber })); } diff --git a/test/integration/remote-config.spec.ts b/test/integration/remote-config.spec.ts index 330fd0efca..7c4ef39d4b 100644 --- a/test/integration/remote-config.spec.ts +++ b/test/integration/remote-config.spec.ts @@ -246,7 +246,7 @@ describe('admin.remoteConfig', () => { INVALID_JSON_STRINGS.forEach((invalidJson) => { it(`should throw if the json string is ${JSON.stringify(invalidJson)}`, () => { expect(() => admin.remoteConfig().createTemplateFromJSON(invalidJson)) - .to.throw(/^Failed to parse the JSON string: ([\D\w]*)\. SyntaxError: Unexpected token ([\D\w]*) in JSON at position ([0-9]*)$/); + .to.throw(/Failed to parse the JSON string/); }); }); diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index 664ec660be..d8a75aff71 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -150,7 +150,7 @@ export const certificateObject = require('./mock.key.json'); // Randomly generated key pairs that don't correspond to anything related to Firebase or GCP export const keyPairs = [ - /* tslint:disable:max-line-length */ + /* eslint-disable max-len */ // The private key for this key pair is identical to the one used in ./mock.key.json { public: '-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAwJENcRev+eXZKvhhWLiV3Lz2MvO+naQRHo59g3vaNQnbgyduN/L4krlrJ5c6\nFiikXdtJNb/QrsAHSyJWCu8j3T9CruiwbidGAk2W0RuViTVspjHUTsIHExx9euWM0UomGvYk\noqXahdhPL/zViVSJt+Rt8bHLsMvpb8RquTIb9iKY3SMV2tCofNmyCSgVbghq/y7lKORtV/IR\nguWs6R22fbkb0r2MCYoNAbZ9dqnbRIFNZBC7itYtUoTEresRWcyFMh0zfAIJycWOJlVLDLqk\nY2SmIx8u7fuysCg1wcoSZoStuDq02nZEMw1dx8HGzE0hynpHlloRLByuIuOAfMCCYwIDAQAB\n-----END RSA PUBLIC KEY-----\n', @@ -160,12 +160,12 @@ export const keyPairs = [ public: '-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAzhI/CMRtNO45R0DD4NBXFRDYAjlB/UVGGdMJKbCIrD3Uq7r/ivedqRYUIccO\nqpeYeu9IH9iotkKq8TM0eCJAUr9WT0o5YzpGvaB8ut87xLh8SqK42VmYAvemUjI257LtDbms\nhoqzqt9Yq0sgC05b7L3r2xDTxnefeMUHYBwaerCr8PTBCu7NjK3eIWHGPouEwT46WoUpnoNm\nxdI16CoSMqtuxteG8c14qJbGR9AZujkRDntWOuL1m5KaUIc7XcAaXBt4FiPwoDoQmmCmydVC\njln3YwSrvL60iAQM6pzCxNRrJRWPYd2u7fgjir/W88w5KHOvdbUyemZWnd6SBExHuQIDAQAB\n-----END RSA PUBLIC KEY-----\n', private: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAzhI/CMRtNO45R0DD4NBXFRDYAjlB/UVGGdMJKbCIrD3Uq7r/ivedqRYU\nIccOqpeYeu9IH9iotkKq8TM0eCJAUr9WT0o5YzpGvaB8ut87xLh8SqK42VmYAvemUjI257Lt\nDbmshoqzqt9Yq0sgC05b7L3r2xDTxnefeMUHYBwaerCr8PTBCu7NjK3eIWHGPouEwT46WoUp\nnoNmxdI16CoSMqtuxteG8c14qJbGR9AZujkRDntWOuL1m5KaUIc7XcAaXBt4FiPwoDoQmmCm\nydVCjln3YwSrvL60iAQM6pzCxNRrJRWPYd2u7fgjir/W88w5KHOvdbUyemZWnd6SBExHuQID\nAQABAoIBAQDJ9iv9BbYaGBfe82SGIuoV5Uou87ru5EPN73yddTydwoN6Q21L316PZuoYKKUB\nIE36viSrwYWoCzLJ7etQihEMiCWo1A/mZikKlA1qgHptVHnMFCqiKiLHVbuV90zETCH0P7MM\nsUdhAkA+sQQY0JVbMs/DBXzomDic/k06LpDtCBNdjL7UIT5KyFbBqit+cV6H91Ujqg8MmzrU\ntOSw+63oSqZJkT6WPuA/NJNXqtFF+0aOKNX1ttrrTzSDhyp6AxOO7Wm++dpYBtcfnOc3EG65\nul9PfKsJwVZFVO+AAZwdLCeKjtCtWeJc/yXvSj2NTsjs3FKJkRAmmiMp5tH+vbE5AoGBAOhn\nKTXGI+ofA3iggByt2InCU+YIXsw1EbbhH4LGB8yyUA2SIjZybwUMKCkoMxmEumFP/FWgOL2w\nLlClqf9vZg9dBy8bDINJHm+9roYRO0/EhHA6IDSC+0X5BPZOexrBI07HJI7w7Y0WHFU8jK53\n55ps2YGT20n7haRMbbPMrq/3AoGBAOL+pY8bgCnKmeG2inun4FuD+0/aXAySXi70/BAABeHH\npogEfc0jv5SgygTiuC/2T84Jmsg0Y6M2l86srMrMA07xtyMbfRq7zih+K+EDoQ9HAwhDqxX5\nM7E8fPXscDzH2Y361QiGAQpjUcMix3hDV8oK537rYOmCYku18ZsVkjnPAoGAbE1u4fVlVTyA\ntJ0vNq45Q/GAgamS690rVStSMPIyPk02iyx3ryHi5NpGeO+X6KN269SHhiu1ZYiN/N1G/Jeg\nWzaCG4yiZygS/AXMKAQtvL2a7mXYDkCf8nrauiHWsqAg4RxiyA401dPg/kPKV5/fGZLyRbVu\nsup43BkV4n1XRv8CgYAmUIE1dJjfdPkgZiVd1epCyDZFNkBPRu1q06MwODDF+WMcllV9qMkP\nl0xCItqgDd1Ok8RygpVG2VIqam8IFAOC8b3NyTgGqSiVISba5jfrUjsqy/E21kdpZSJaiDwx\npjIMiwgmVigazsTgQSCWJhfNXKXSgHxtLbrVuLI9URjLdQKBgQDProyaG7pspt6uUdqMTa4+\nGVkUg+gIt5aVTf/Lb25K3SHA1baPamtbTDDf6vUjeJtTG+O+RMGqK5mB2MywjVHJdMGcJ44e\nogIh9eWY450oUoVBjEsdUd7Ef5KcpMFDUVFJwzCY371+Loqh2KYAk8WUSRzwGuw2QtLPO/L/\nQkKj4Q==\n-----END RSA PRIVATE KEY-----\n', }, - /* tslint:enable:max-line-length */ + /* eslint-enable max-len */ ]; // Randomly generated an X.509 certs using https://www.samltool.com/self_signed_certs.php export const x509CertPairs = [ - /* tslint:disable:max-line-length */ + /* eslint-disable max-len */ { public: '-----BEGIN CERTIFICATE-----\nMIICZjCCAc+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBQMQswCQYDVQQGEwJ1czEL\nMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAPBgNVBAMMCGFjbWUuY29tMRIw\nEAYDVQQHDAlTdW5ueXZhbGUwHhcNMTgxMjA2MDc1MTUxWhcNMjgxMjAzMDc1MTUx\nWjBQMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAP\nBgNVBAMMCGFjbWUuY29tMRIwEAYDVQQHDAlTdW5ueXZhbGUwgZ8wDQYJKoZIhvcN\nAQEBBQADgY0AMIGJAoGBAKphmggjiVgqMLXyzvI7cKphscIIQ+wcv7Dld6MD4aKv\n7Jqr8ltujMxBUeY4LFEKw8Terb01snYpDotfilaG6NxpF/GfVVmMalzwWp0mT8+H\nyzyPj89mRcozu17RwuooR6n1ofXjGcBE86lqC21UhA3WVgjPOLqB42rlE9gPnZLB\nAgMBAAGjUDBOMB0GA1UdDgQWBBS0iM7WnbCNOnieOP1HIA+Oz/ML+zAfBgNVHSME\nGDAWgBS0iM7WnbCNOnieOP1HIA+Oz/ML+zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\nDQEBDQUAA4GBAF3jBgS+wP+K/jTupEQur6iaqS4UvXd//d4vo1MV06oTLQMTz+rP\nOSMDNwxzfaOn6vgYLKP/Dcy9dSTnSzgxLAxfKvDQZA0vE3udsw0Bd245MmX4+GOp\nlbrN99XP1u+lFxCSdMUzvQ/jW4ysw/Nq4JdJ0gPAyPvL6Qi/3mQdIQwx\n-----END CERTIFICATE-----\n', private: '-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKphmggjiVgqMLXy\nzvI7cKphscIIQ+wcv7Dld6MD4aKv7Jqr8ltujMxBUeY4LFEKw8Terb01snYpDotf\nilaG6NxpF/GfVVmMalzwWp0mT8+HyzyPj89mRcozu17RwuooR6n1ofXjGcBE86lq\nC21UhA3WVgjPOLqB42rlE9gPnZLBAgMBAAECgYAwZ7g2FbqAZMQf/RKUORTiIw04\nXdbGLsi6/gZGNuUUrjxfGPiqxzaTFP+qk0zr3U4PEWB0v9uqvDFYoVURDhT7isxm\nH5bc6dxwvBRIy8tLtvxo0jMTotJaBhEHP3YMKxbC7lxo3PV5HLIve5nf9ChOypKp\n4zbP4d1IJjpu8ggrbQJBANoPCrJyXjsDgh8WAEpALAsgM4ugyJwdk8AHJUTy3IeJ\niYYB/RLVpYW8LI1dmqN5NPKbyKE+dsdSiiEpclsocl8CQQDIBt5DbO+tEGr5BGsk\nBi+P3E1M3KVV2eJv+inlgYkYeS/cdd5CJczCDwxeDk8DXsKvmOp0LCHeU2sCKjSy\nF07fAkB86KLjB1ptCZxu/CZcYhgYo3CDai2gJ90r4av6q/eheCqb5eW29UUkr18B\n932OaO7ojk5F90cI9IIFbv1/tFKXAkEAnrXUZWtqQMdmGW+IE21VD7CdJP9tsFDR\nekfkNlYxkVmWwDZFw/Z6IQAPsBFqYCIwF2Qdo0/hD6bgoTcb2LLlwQJATqOMr7yr\neYKLJ+edhwMHx4U5ZIT8l/MjDv4/6L6FgGYVo7gNjjIIsDXUOo3PlBOWe6fxb5+f\ntFlwxZNz+g9ONg==\n-----END PRIVATE KEY-----\n', @@ -174,7 +174,7 @@ export const x509CertPairs = [ public: '-----BEGIN CERTIFICATE-----\nMIICZjCCAc+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBQMQswCQYDVQQGEwJ1czEL\nMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAPBgNVBAMMCGFjbWUuY29tMRIw\nEAYDVQQHDAlTdW5ueXZhbGUwHhcNMTgxMjA2MDc1ODE4WhcNMjgxMjAzMDc1ODE4\nWjBQMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAP\nBgNVBAMMCGFjbWUuY29tMRIwEAYDVQQHDAlTdW5ueXZhbGUwgZ8wDQYJKoZIhvcN\nAQEBBQADgY0AMIGJAoGBAKuzYKfDZGA6DJgQru3wNUqv+S0hMZfP/jbp8ou/8UKu\nrNeX7cfCgt3yxoGCJYKmF6t5mvo76JY0MWwA53BxeP/oyXmJ93uHG5mFRAsVAUKs\ncVVb0Xi6ujxZGVdDWFV696L0BNOoHTfXmac6IBoZQzNNK4n1AATqwo+z7a0pfRrJ\nAgMBAAGjUDBOMB0GA1UdDgQWBBSKmi/ZKMuLN0ES7/jPa7q7jAjPiDAfBgNVHSME\nGDAWgBSKmi/ZKMuLN0ES7/jPa7q7jAjPiDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\nDQEBDQUAA4GBAAg2a2kSn05NiUOuWOHwPUjW3wQRsGxPXtbhWMhmNdCfKKteM2+/\nLd/jz5F3qkOgGQ3UDgr3SHEoWhnLaJMF4a2tm6vL2rEIfPEK81KhTTRxSsAgMVbU\nJXBz1md6Ur0HlgQC7d1CHC8/xi2DDwHopLyxhogaZUxy9IaRxUEa2vJW\n-----END CERTIFICATE-----\n', private: '-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKuzYKfDZGA6DJgQ\nru3wNUqv+S0hMZfP/jbp8ou/8UKurNeX7cfCgt3yxoGCJYKmF6t5mvo76JY0MWwA\n53BxeP/oyXmJ93uHG5mFRAsVAUKscVVb0Xi6ujxZGVdDWFV696L0BNOoHTfXmac6\nIBoZQzNNK4n1AATqwo+z7a0pfRrJAgMBAAECgYBG15vpnBSuH0VS+I80XQef6TtG\nA4wStx6MSbppLqi8epWV3nmdEgQszx5YEPqpDR53AZWP6WftkVtS1IypOChTwRIh\n73vheFJ4XYqjoU+2OUtj7hhMMHDBFhw7W3Jvz4PkPu9drmzBS8N5Dd38ROwhwoS3\nUD/18pxXXyd61s/+gQJBANSuA7fRna1qXmRmdwpQR1Mebh0dw2ZgOn4ekIgsfmgP\nGPznhsjWQEuT1BxIS8R8x4ZmCJY4W89GfUBLtWprBTsCQQDOrIyHCOzOmNYXcgRT\nhW+ZiSi+46FAYqCKawIwlq2M0GsJaMTdXFQFKTmnxiNvWxxDOeZcIsrc5uwEwr6A\n3I/LAkEAwuFBHurAZOsW20DYy2aMNKmplJx1NBXxAyfWoDDFE2ziJLuyUc2g1J/8\nuH22j7EW0xwjuiKiXeflVUkKTx0JiQJAUQb5OV/YZ88n8J008QHZlRpfLSfVaobA\nZkQ54Y7Rj+mObWvz8s1l63gUMKDP97KCzCCBHhJN8nlegydOxPq0LQJADBjkunGt\nfIGv6A3SG5/5nRYI1gHQsq30BaAPwx6BuDBtnaf5BpzcFvu1JMNHoVFYzmiykpwX\n1zUhaAtcX2BV9g==\n-----END PRIVATE KEY-----\n', }, - /* tslint:enable:max-line-length */ + /* eslint-enable max-len */ ]; /** diff --git a/test/unit/project-management/android-app.spec.ts b/test/unit/project-management/android-app.spec.ts index ff17f0d3e1..edc2d464d3 100644 --- a/test/unit/project-management/android-app.spec.ts +++ b/test/unit/project-management/android-app.spec.ts @@ -21,7 +21,9 @@ import * as _ from 'lodash'; import * as sinon from 'sinon'; import { FirebaseApp } from '../../../src/firebase-app'; import { AndroidApp, ShaCertificate } from '../../../src/project-management/android-app'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; +import { + ProjectManagementRequestHandler +} from '../../../src/project-management/project-management-api-request-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; diff --git a/test/unit/project-management/ios-app.spec.ts b/test/unit/project-management/ios-app.spec.ts index 3445076cf7..9f00aaff34 100644 --- a/test/unit/project-management/ios-app.spec.ts +++ b/test/unit/project-management/ios-app.spec.ts @@ -21,7 +21,9 @@ import * as _ from 'lodash'; import * as sinon from 'sinon'; import { FirebaseApp } from '../../../src/firebase-app'; import { IosApp } from '../../../src/project-management/ios-app'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; +import { + ProjectManagementRequestHandler +} from '../../../src/project-management/project-management-api-request-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; diff --git a/test/unit/project-management/project-management-api-request.spec.ts b/test/unit/project-management/project-management-api-request.spec.ts index 44a9c46cc5..23a5153ff5 100644 --- a/test/unit/project-management/project-management-api-request.spec.ts +++ b/test/unit/project-management/project-management-api-request.spec.ts @@ -22,7 +22,9 @@ import * as _ from 'lodash'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import { FirebaseApp } from '../../../src/firebase-app'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; +import { + ProjectManagementRequestHandler +} from '../../../src/project-management/project-management-api-request-internal'; import { HttpClient } from '../../../src/utils/api-request'; import * as mocks from '../../resources/mocks'; import * as utils from '../utils'; diff --git a/test/unit/project-management/project-management.spec.ts b/test/unit/project-management/project-management.spec.ts index 62f819f264..0e109a6d9b 100644 --- a/test/unit/project-management/project-management.spec.ts +++ b/test/unit/project-management/project-management.spec.ts @@ -22,7 +22,9 @@ import * as sinon from 'sinon'; import { FirebaseApp } from '../../../src/firebase-app'; import { AndroidApp } from '../../../src/project-management/android-app'; import { ProjectManagement } from '../../../src/project-management/project-management'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; +import { + ProjectManagementRequestHandler +} from '../../../src/project-management/project-management-api-request-internal'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; import { IosApp } from '../../../src/project-management/ios-app'; diff --git a/test/unit/remote-config/remote-config-api-client.spec.ts b/test/unit/remote-config/remote-config-api-client.spec.ts index 2b17171082..4a01433f30 100644 --- a/test/unit/remote-config/remote-config-api-client.spec.ts +++ b/test/unit/remote-config/remote-config-api-client.spec.ts @@ -25,7 +25,10 @@ import { ListVersionsResult, Version, } from '../../../src/remote-config/remote-config-api-client'; -import { FirebaseRemoteConfigError, RemoteConfigApiClient } from '../../../src/remote-config/remote-config-api-client-internal'; +import { + FirebaseRemoteConfigError, + RemoteConfigApiClient +} from '../../../src/remote-config/remote-config-api-client-internal'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; @@ -47,7 +50,8 @@ describe('RemoteConfigApiClient', () => { }; const VALIDATION_ERROR_MESSAGES = [ - "[VALIDATION_ERROR]: [foo] are not valid condition names. All keys in all conditional value maps must be valid condition names.", + "[VALIDATION_ERROR]: [foo] are not valid condition names. All keys in all conditional value" + + " maps must be valid condition names.", "[VERSION_MISMATCH]: Expected version 6, found 8 for project: 123456789012" ]; @@ -529,11 +533,19 @@ describe('RemoteConfigApiClient', () => { }); }); - ['', 'abc', 'a123b', 'a123', '123a', 1.2, '70.2', null, NaN, true, [], {}].forEach( + ['', null, NaN, true, [], {}].forEach( + (invalidVersion) => { + it(`should throw if the endVersionNumber is: ${invalidVersion}`, () => { + expect(() => apiClient.listVersions({ endVersionNumber: invalidVersion } as any)) + .to.throw(/^endVersionNumber must be a non-empty string in int64 format or a number$/); + }); + }); + + ['abc', 'a123b', 'a123', '123a', 1.2, '70.2'].forEach( (invalidVersion) => { it(`should throw if the endVersionNumber is: ${invalidVersion}`, () => { expect(() => apiClient.listVersions({ endVersionNumber: invalidVersion } as any)) - .to.throw(/^endVersionNumber must be (a non-empty string in int64 format or a number|an integer or a string in int64 format)$/); + .to.throw(/^endVersionNumber must be an integer or a string in int64 format$/); }); }); @@ -655,10 +667,17 @@ describe('RemoteConfigApiClient', () => { }); function runTemplateVersionNumberTests(rcOperation: Function): void { - ['', 'abc', 'a123b', 'a123', '123a', 1.2, '70.2', null, NaN, true, [], {}].forEach((invalidVersion) => { + ['', null, NaN, true, [], {}].forEach((invalidVersion) => { + it(`should reject if the versionNumber is: ${invalidVersion}`, () => { + expect(() => rcOperation(invalidVersion as any)) + .to.throw(/^versionNumber must be a non-empty string in int64 format or a number$/); + }); + }); + + ['abc', 'a123b', 'a123', '123a', 1.2, '70.2'].forEach((invalidVersion) => { it(`should reject if the versionNumber is: ${invalidVersion}`, () => { expect(() => rcOperation(invalidVersion as any)) - .to.throw(/^versionNumber must be (a non-empty string in int64 format or a number|an integer or a string in int64 format)$/); + .to.throw(/^versionNumber must be an integer or a string in int64 format$/); }); }); } diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index e7a3d08caa..a6512cf3fa 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -28,7 +28,10 @@ import { TagColor, ListVersionsResult, } from '../../../src/remote-config/remote-config-api-client'; -import { FirebaseRemoteConfigError, RemoteConfigApiClient } from '../../../src/remote-config/remote-config-api-client-internal'; +import { + FirebaseRemoteConfigError, + RemoteConfigApiClient +} from '../../../src/remote-config/remote-config-api-client-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; const expect = chai.expect; @@ -254,7 +257,21 @@ describe('RemoteConfig', () => { .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); }); - ['', 'abc', 'a123b', 'a123', '123a', 1.2, '70.2', null, NaN, true, [], {}].forEach((invalidVersion) => { + ['', null, NaN, true, [], {}].forEach((invalidVersion) => { + it(`should reject if the versionNumber is: ${invalidVersion}`, () => { + const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + response.versions[0].versionNumber = invalidVersion as any; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .resolves(response); + stubs.push(stub); + return remoteConfig.listVersions() + .should.eventually.be.rejected + .and.to.match(/^Error: Version number must be a non-empty string in int64 format or a number$/); + }); + }); + + ['abc', 'a123b', 'a123', '123a', 1.2, '70.2'].forEach((invalidVersion) => { it(`should reject if the versionNumber is: ${invalidVersion}`, () => { const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); response.versions[0].versionNumber = invalidVersion as any; @@ -263,7 +280,8 @@ describe('RemoteConfig', () => { .resolves(response); stubs.push(stub); return remoteConfig.listVersions() - .should.eventually.be.rejected.and.to.match(/^Error: Version number must be (a non-empty string in int64 format or a number|an integer or a string in int64 format)$/); + .should.eventually.be.rejected + .and.to.match(/^Error: Version number must be an integer or a string in int64 format$/); }); }); @@ -415,7 +433,7 @@ describe('RemoteConfig', () => { INVALID_JSON_STRINGS.forEach((invalidJson) => { it(`should throw if the json string is ${JSON.stringify(invalidJson)}`, () => { expect(() => remoteConfig.createTemplateFromJSON(invalidJson)) - .to.throw(/^Failed to parse the JSON string: ([\D\w]*)\. SyntaxError: Unexpected token ([\D\w]*) in JSON at position ([0-9]*)$/); + .to.throw(/Failed to parse the JSON string: ([\D\w]*)\./); }); }); From f11aed754d30fabb2b936e9b8958669c65750f5f Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 3 Sep 2020 11:41:44 -0700 Subject: [PATCH 038/160] fix(storage): Support typing generation for the storage API (#1019) * fix(storage): Support typing generation for the storage API * fix(storage): Added newline at eof * fix(storage): Fixing an accidentally inserted import --- gulpfile.js | 12 +++--- src/index.d.ts | 21 +--------- src/storage.d.ts | 39 +++++++++++++++++++ src/storage/index.ts | 32 +++++++++++++++ src/storage/storage.ts | 19 +++++---- .../typescript/src/example.test.ts | 3 +- 6 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 src/storage.d.ts create mode 100644 src/storage/index.ts diff --git a/gulpfile.js b/gulpfile.js index 8af679e13d..4e585f3f79 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -57,10 +57,11 @@ var paths = { '!src/credential.d.ts', '!src/database.d.ts', '!src/instance-id.d.ts', - '!src/security-rules.d.ts', + '!src/messaging.d.ts', '!src/project-management.d.ts', '!src/remote-config.d.ts', - '!src/messaging.d.ts', + '!src/security-rules.d.ts', + '!src/storage.d.ts', ], }; @@ -71,7 +72,6 @@ const TEMPORARY_TYPING_EXCLUDES = [ '!lib/firebase-service.d.ts', '!lib/auth/*.d.ts', '!lib/machine-learning/*.d.ts', - '!lib/storage/*.d.ts', '!lib/utils/*.d.ts', ]; @@ -110,10 +110,10 @@ gulp.task('compile', function() { // Add header .pipe(header(banner)); - + // Exclude typings that are unintended (currently excludes all auto-generated // typings, but as services are refactored to auto-generate typings this will - // change). Moreover, all *-internal.d.ts typings should not be exposed to + // change). Moreover, all *-internal.d.ts typings should not be exposed to // developers as it denotes internally used types. if (declaration) { const configuration = [ @@ -153,7 +153,7 @@ gulp.task('copyTypings', function() { let workflow = gulp.src('src/*.d.ts') // Add header .pipe(header(banner)); - + if (declaration) { workflow = workflow.pipe(filter(paths.curatedTypings)); } diff --git a/src/index.d.ts b/src/index.d.ts index fff3085772..3cf835fd11 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Bucket } from '@google-cloud/storage'; import * as _firestore from '@google-cloud/firestore'; import { Agent } from 'http'; @@ -26,6 +25,7 @@ import * as _instanceId from './instance-id'; import * as _projectManagement from './project-management'; import * as _remoteConfig from './remote-config'; import * as _securityRules from './security-rules'; +import * as _storage from './storage'; /* eslint-disable @typescript-eslint/ban-types */ @@ -651,24 +651,7 @@ declare namespace admin.messaging { } declare namespace admin.storage { - - /** - * The default `Storage` service if no - * app is provided or the `Storage` service associated with the provided - * app. - */ - interface Storage { - /** - * Optional app whose `Storage` service to - * return. If not provided, the default `Storage` service will be returned. - */ - app: admin.app.App; - /** - * @returns A [Bucket](https://cloud.google.com/nodejs/docs/reference/storage/latest/Bucket) - * instance as defined in the `@google-cloud/storage` package. - */ - bucket(name?: string): Bucket; - } + export import Storage = _storage.admin.storage.Storage; } declare namespace admin.firestore { diff --git a/src/storage.d.ts b/src/storage.d.ts new file mode 100644 index 0000000000..00f499a890 --- /dev/null +++ b/src/storage.d.ts @@ -0,0 +1,39 @@ +/*! + * Copyright 2020 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 { Bucket } from '@google-cloud/storage'; +import * as _admin from './index.d'; + +export namespace admin.storage { + + /** + * The default `Storage` service if no + * app is provided or the `Storage` service associated with the provided + * app. + */ + export interface Storage { + /** + * Optional app whose `Storage` service to + * return. If not provided, the default `Storage` service will be returned. + */ + app: _admin.app.App; + /** + * @returns A [Bucket](https://cloud.google.com/nodejs/docs/reference/storage/latest/Bucket) + * instance as defined in the `@google-cloud/storage` package. + */ + bucket(name?: string): Bucket; + } +} diff --git a/src/storage/index.ts b/src/storage/index.ts new file mode 100644 index 0000000000..a85f4b60d1 --- /dev/null +++ b/src/storage/index.ts @@ -0,0 +1,32 @@ +/*! + * Copyright 2020 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 _storage from './storage'; +import { FirebaseApp } from '../firebase-app'; +import * as firebaseAdmin from '../index'; + +export function storage(app?: FirebaseApp): _storage.Storage { + if (typeof(app) === 'undefined') { + app = firebaseAdmin.app(); + } + return app.storage(); +} + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace admin.storage { + /* eslint-disable @typescript-eslint/no-empty-interface */ + export interface Storage extends _storage.Storage {} +} diff --git a/src/storage/storage.ts b/src/storage/storage.ts index d7b5d789d5..eb20194843 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -39,7 +39,9 @@ class StorageInternals implements FirebaseServiceInternalsInterface { } /** - * Storage service bound to the provided app. + * The default `Storage` service if no + * app is provided or the `Storage` service associated with the provided + * app. */ export class Storage implements FirebaseServiceInterface { public readonly INTERNAL: StorageInternals = new StorageInternals(); @@ -50,6 +52,7 @@ export class Storage implements FirebaseServiceInterface { /** * @param {FirebaseApp} app The app for this Storage service. * @constructor + * @internal */ constructor(app: FirebaseApp) { if (!validator.isNonNullObject(app) || !('options' in app)) { @@ -98,12 +101,10 @@ export class Storage implements FirebaseServiceInterface { } /** - * Returns a reference to a Google Cloud Storage bucket. Returned reference can be used to upload - * and download content from Google Cloud Storage. - * - * @param {string=} name Optional name of the bucket to be retrieved. If name is not specified, - * retrieves a reference to the default bucket. - * @return {Bucket} A Bucket object from the @google-cloud/storage library. + * @param name Optional name of the bucket to be retrieved. If name is not specified, + * retrieves a reference to the default bucket. + * @returns A [Bucket](https://cloud.google.com/nodejs/docs/reference/storage/latest/Bucket) + * instance as defined in the `@google-cloud/storage` package. */ public bucket(name?: string): Bucket { const bucketName = (typeof name !== 'undefined') @@ -120,9 +121,7 @@ export class Storage implements FirebaseServiceInterface { } /** - * Returns the app associated with this Storage instance. - * - * @return {FirebaseApp} The app associated with this Storage instance. + * @return The app associated with this Storage instance. */ get app(): FirebaseApp { return this.appInternal; diff --git a/test/integration/typescript/src/example.test.ts b/test/integration/typescript/src/example.test.ts index 8b730ea3f6..52f865ee58 100644 --- a/test/integration/typescript/src/example.test.ts +++ b/test/integration/typescript/src/example.test.ts @@ -71,7 +71,8 @@ describe('Init App', () => { }); it('Should return a Cloud Storage client', () => { - const bucket: Bucket = app.storage().bucket('TestBucket'); + const storage: admin.storage.Storage = app.storage(); + const bucket: Bucket = storage.bucket('TestBucket'); expect(bucket.name).to.equal('TestBucket'); }); From b4ffd0dbb28ac2b75a1aaba0adec18e7416d2b5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2020 11:02:02 -0700 Subject: [PATCH 039/160] build(deps): bump node-fetch from 2.6.0 to 2.6.1 (#1025) Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1. - [Release notes](https://github.com/bitinn/node-fetch/releases) - [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1) 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 aa2d0766c3..3924825348 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5623,9 +5623,9 @@ "dev": true }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", "optional": true }, "node-forge": { From 151bc86d71d565a2ee8598afb3fe26d8f7094d10 Mon Sep 17 00:00:00 2001 From: ifielker Date: Mon, 14 Sep 2020 13:07:18 -0400 Subject: [PATCH 040/160] feat(ml): Adding Firebase ML support for AutoML models (#1024) * Added support for AutoML models RELEASE NOTES: Added support for creating, updating, getting, listing, publishing, unpublishing and deleting Firebase-hosted custom ML models created with AutoML. --- package-lock.json | 101 ++---- package.json | 2 +- src/index.d.ts | 146 ++++---- .../machine-learning-api-client.ts | 159 +++++++- src/machine-learning/machine-learning.ts | 217 +++++++---- ...project-management-api-request-internal.ts | 2 +- src/utils/api-request.ts | 14 +- test/integration/machine-learning.spec.ts | 195 +++++++--- .../machine-learning-api-client.spec.ts | 342 ++++++++++++++++-- .../machine-learning/machine-learning.spec.ts | 136 ++++++- 10 files changed, 997 insertions(+), 317 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3924825348..405c715af4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -275,9 +275,9 @@ } }, "@google-cloud/common": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.3.1.tgz", - "integrity": "sha512-bJamcNvZ2j5xS01uFBT1GqfHIKrtwpyUhIU/Xn3uwMZkK/t6JA3mlID0wuZlo7XjbjFSRT2iLBEmDWv9T2hP8g==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.3.3.tgz", + "integrity": "sha512-2PwPDE47N4WiWQK/F35vE5aWVoCjKQ2NW8r8OFAg6QslkLMjX6WNcmUO8suYlSkavc58qOvzA4jG6eVkC90i8Q==", "optional": true, "requires": { "@google-cloud/projectify": "^2.0.0", @@ -328,9 +328,9 @@ } }, "@google-cloud/paginator": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.2.tgz", - "integrity": "sha512-kXK+Dbz4pNvv8bKU80Aw5HsIdgOe0WuMTd8/fI6tkANUxzvJOVJQQRsWVqcHSWK2RXHPTA9WBniUCwY6gAJDXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz", + "integrity": "sha512-N4Uk4BT1YuskfRhKXBs0n9Lg2YTROZc6IMpkO/8DIHODtm5s3xY8K5vVBo23v/2XulY3azwITQlYWgT4GdLsUw==", "optional": true, "requires": { "arrify": "^2.0.0", @@ -344,52 +344,37 @@ "optional": true }, "@google-cloud/promisify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.1.tgz", - "integrity": "sha512-82EQzwrNauw1fkbUSr3f+50Bcq7g4h0XvLOk8C5e9ABkXYHei7ZPi9tiMMD7Vh3SfcdH97d1ibJ3KBWp2o1J+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", "optional": true }, "@google-cloud/storage": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.1.1.tgz", - "integrity": "sha512-w/64V+eJl+vpYUXT15sBcO8pX0KTmb9Ni2ZNuQQ8HmyhAbEA3//G8JFaLPCXGBWO2/b0OQZytUT6q2wII9a9aQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.3.0.tgz", + "integrity": "sha512-3t5UF3SZ14Bw2kcBHubCai6EIugU2GnQOstYWVSFuoO8IJ94RAaIOPq/dtexvQbUTpBTAGpd5smVR9WPL1mJVw==", "optional": true, "requires": { - "@google-cloud/common": "^3.0.0", + "@google-cloud/common": "^3.3.0", "@google-cloud/paginator": "^3.0.0", "@google-cloud/promisify": "^2.0.0", "arrify": "^2.0.0", "compressible": "^2.0.12", "concat-stream": "^2.0.0", - "date-and-time": "^0.13.0", + "date-and-time": "^0.14.0", "duplexify": "^3.5.0", "extend": "^3.0.2", "gaxios": "^3.0.0", - "gcs-resumable-upload": "^3.0.0", + "gcs-resumable-upload": "^3.1.0", "hash-stream-validation": "^0.2.2", "mime": "^2.2.0", "mime-types": "^2.0.8", "onetime": "^5.1.0", "p-limit": "^3.0.1", "pumpify": "^2.0.0", - "readable-stream": "^3.4.0", "snakeize": "^0.1.0", "stream-events": "^1.0.1", - "through2": "^3.0.0", "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "optional": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } } }, "@grpc/grpc-js": { @@ -1986,9 +1971,9 @@ } }, "date-and-time": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.13.1.tgz", - "integrity": "sha512-/Uge9DJAT+s+oAcDxtBhyR8+sKjUnZbYmyhbmWjTHNtX7B7oWD8YyYdeXcBRbwSj6hVvj+IQegJam7m7czhbFw==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.1.tgz", + "integrity": "sha512-M4RggEH5OF2ZuCOxgOU67R6Z9ohjKbxGvAQz48vj53wLmL0bAgumkBvycR32f30pK+Og9pIR+RFDyChbaE4oLA==", "optional": true }, "dateformat": { @@ -2220,9 +2205,9 @@ } }, "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "optional": true, "requires": { "is-obj": "^2.0.0" @@ -3214,9 +3199,9 @@ } }, "gcs-resumable-upload": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.0.tgz", - "integrity": "sha512-gB8xH6EjYCv9lfBEL4FK5+AMgTY0feYoNHAYOV5nCuOrDPhy5MOiyJE8WosgxhbKBPS361H7fkwv6CTufEh9bg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.1.tgz", + "integrity": "sha512-RS1osvAicj9+MjCc6jAcVL1Pt3tg7NK2C2gXM5nqD1Gs0klF2kj5nnAFSBy97JrtslMIQzpb7iSuxaG8rFWd2A==", "optional": true, "requires": { "abort-controller": "^3.0.0", @@ -3863,25 +3848,10 @@ } }, "hash-stream-validation": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.3.tgz", - "integrity": "sha512-OEohGLoUOh+bwsIpHpdvhIXFyRGjeLqJbT8Yc5QTZPbRM7LKywagTQxnX/6mghLDOrD9YGz88hy5mLN2eKflYQ==", - "optional": true, - "requires": { - "through2": "^2.0.0" - }, - "dependencies": { - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "optional": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", + "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==", + "optional": true }, "hasha": { "version": "3.0.0", @@ -6170,9 +6140,9 @@ } }, "p-limit": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.1.tgz", - "integrity": "sha512-mw/p92EyOzl2MhauKodw54Rx5ZK4624rNfgNaBguFZkHzyUG9WsDzFF5/yQVEJinbJDdP4jEfMN+uBquiGnaLg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", "optional": true, "requires": { "p-try": "^2.0.0" @@ -8301,9 +8271,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", - "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==", "optional": true }, "v8-compile-cache": { @@ -8613,7 +8583,8 @@ "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true }, "y18n": { "version": "3.2.1", diff --git a/package.json b/package.json index bb80b12f47..983a8b3de3 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "optionalDependencies": { "@google-cloud/firestore": "^4.0.0", - "@google-cloud/storage": "^5.0.0" + "@google-cloud/storage": "^5.3.0" }, "devDependencies": { "@firebase/app": "^0.6.9", diff --git a/src/index.d.ts b/src/index.d.ts index 3cf835fd11..997fcb8779 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -718,46 +718,29 @@ declare namespace admin.securityRules { declare namespace admin.machineLearning { /** - * Interface representing options for listing Models. + * Firebase ML Model input objects */ - interface ListModelsOptions { - /** - * An expression that specifies how to filter the results. - * - * Examples: - * - * ``` - * display_name = your_model - * display_name : experimental_* - * tags: face_detector AND tags: experimental - * state.published = true - * ``` - * - * See https://firebase.google.com/docs/ml-kit/manage-hosted-models#list_your_projects_models - */ - filter?: string; - - /** The number of results to return in each page. */ - pageSize?: number; - - /** A token that specifies the result page to return. */ - pageToken?: string; + interface ModelOptionsBase { + displayName?: string; + tags?: string[]; } - - /** Response object for a listModels operation. */ - interface ListModelsResult { - /** A list of models in your project. */ - readonly models: Model[]; - - /** - * A token you can use to retrieve the next page of results. If null, the - * current page is the final page. - */ - readonly pageToken?: string; + interface GcsTfliteModelOptions extends ModelOptionsBase { + tfliteModel: { + gcsTfliteUri: string; + }; + } + interface AutoMLTfliteModelOptions extends ModelOptionsBase { + tfliteModel: { + automlModel: string; + }; } + type ModelOptions = ModelOptionsBase | GcsTfliteModelOptions | AutoMLTfliteModelOptions; /** * A TensorFlow Lite Model output object + * + * One of either the `gcsTfliteUri` or `automlModel` properties will be + * defined. */ interface TFLiteModel { /** The size of the model. */ @@ -765,24 +748,11 @@ declare namespace admin.machineLearning { /** The URI from which the model was originally provided to Firebase. */ readonly gcsTfliteUri?: string; - } - - /** - * A Firebase ML Model input object - */ - interface ModelOptions { - /** A name for the model. This is the name you use from your app to load the model. */ - displayName?: string; - - /** Tags for easier model management. */ - tags?: string[]; - /** - * An object containing the URI of the model in Cloud Storage. - * - * Example: `tfliteModel: { gcsTfliteUri: 'gs://your-bucket/your-model.tflite' }` + * The AutoML model reference from which the model was originally provided + * to Firebase. */ - tfliteModel?: { gcsTfliteUri: string }; + readonly automlModel?: string; } /** @@ -792,10 +762,16 @@ declare namespace admin.machineLearning { /** The ID of the model. */ readonly modelId: string; - /** The model's name. This is the name you use from your app to load the model. */ + /** + * The model's name. This is the name you use from your app to load the + * model. + */ readonly displayName: string; - /** The model's tags. */ + /** + * The model's tags, which can be used to group or filter models in list + * operations. + */ readonly tags?: string[]; /** The timestamp of the model's creation. */ @@ -831,17 +807,63 @@ declare namespace admin.machineLearning { /** * Wait for the model to be unlocked. * - * @param {number} maxTimeSeconds The maximum time in seconds to wait. + * @param {number} maxTimeMillis The maximum time in milliseconds to wait. + * If not specified, a default maximum of 2 minutes is used. * * @return {Promise} A promise that resolves when the model is unlocked * or the maximum wait time has passed. */ - waitForUnlocked(maxTimeSeconds?: number): Promise; + waitForUnlocked(maxTimeMillis?: number): Promise; + + /** + * Return the model as a JSON object. + */ + toJSON(): {[key: string]: any}; /** Metadata about the model's TensorFlow Lite model file. */ readonly tfliteModel?: TFLiteModel; } + /** + * Interface representing options for listing Models. + */ + interface ListModelsOptions { + /** + * An expression that specifies how to filter the results. + * + * Examples: + * + * ``` + * display_name = your_model + * display_name : experimental_* + * tags: face_detector AND tags: experimental + * state.published = true + * ``` + * + * See https://firebase.google.com/docs/ml/manage-hosted-models#list_your_projects_models + */ + filter?: string; + + /** The number of results to return in each page. */ + pageSize?: number; + + /** A token that specifies the result page to return. */ + pageToken?: string; + } + + /** Response object for a listModels operation. */ + interface ListModelsResult { + /** A list of models in your project. */ + readonly models: Model[]; + + /** + * A token you can use to retrieve the next page of results. If null, the + * current page is the final page. + */ + readonly pageToken?: string; + } + + /** * The Firebase `MachineLearning` service interface. * @@ -856,7 +878,7 @@ declare namespace admin.machineLearning { app: admin.app.App; /** - * Creates a model in Firebase ML. + * Creates a model in the current Firebase project. * * @param {ModelOptions} model The model to create. * @@ -865,7 +887,7 @@ declare namespace admin.machineLearning { createModel(model: ModelOptions): Promise; /** - * Updates a model in Firebase ML. + * Updates a model's metadata or model file. * * @param {string} modelId The ID of the model to update. * @param {ModelOptions} model The model fields to update. @@ -875,7 +897,9 @@ declare namespace admin.machineLearning { updateModel(modelId: string, model: ModelOptions): Promise; /** - * Publishes a model in Firebase ML. + * Publishes a Firebase ML model. + * + * A published model can be downloaded to client apps. * * @param {string} modelId The ID of the model to publish. * @@ -884,7 +908,7 @@ declare namespace admin.machineLearning { publishModel(modelId: string): Promise; /** - * Unpublishes a model in Firebase ML. + * Unpublishes a Firebase ML model. * * @param {string} modelId The ID of the model to unpublish. * @@ -893,7 +917,7 @@ declare namespace admin.machineLearning { unpublishModel(modelId: string): Promise; /** - * Gets a model from Firebase ML. + * Gets the model specified by the given ID. * * @param {string} modelId The ID of the model to get. * @@ -902,7 +926,7 @@ declare namespace admin.machineLearning { getModel(modelId: string): Promise; /** - * Lists models from Firebase ML. + * Lists the current project's models. * * @param {ListModelsOptions} options The listing options. * @@ -914,7 +938,7 @@ declare namespace admin.machineLearning { listModels(options?: ListModelsOptions): Promise; /** - * Deletes a model from Firebase ML. + * Deletes a model from the current project. * * @param {string} modelId The ID of the model to delete. */ diff --git a/src/machine-learning/machine-learning-api-client.ts b/src/machine-learning/machine-learning-api-client.ts index 4e7a1a7383..e39c94db9a 100644 --- a/src/machine-learning/machine-learning-api-client.ts +++ b/src/machine-learning/machine-learning-api-client.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient } from '../utils/api-request'; +import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, + ExponentialBackoffPoller } from '../utils/api-request'; import { PrefixedFirebaseError } from '../utils/error'; import { FirebaseMachineLearningError, MachineLearningErrorCode } from './machine-learning-utils'; import * as utils from '../utils/index'; @@ -26,23 +27,39 @@ const FIREBASE_VERSION_HEADER = { 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, }; +// Operation polling defaults +const POLL_DEFAULT_MAX_TIME_MILLISECONDS = 120000; // Maximum overall 2 minutes +const POLL_BASE_WAIT_TIME_MILLISECONDS = 3000; // Start with 3 second delay +const POLL_MAX_WAIT_TIME_MILLISECONDS = 30000; // Maximum 30 second delay + export interface StatusErrorResponse { readonly code: number; readonly message: string; } /** - * A Firebase ML Model input object + * Firebase ML Model input objects */ -export interface ModelOptions { +export interface ModelOptionsBase { displayName?: string; tags?: string[]; - - tfliteModel?: { gcsTfliteUri: string }; } +export interface GcsTfliteModelOptions extends ModelOptionsBase { + tfliteModel: { + gcsTfliteUri: string; + }; +} +export interface AutoMLTfliteModelOptions extends ModelOptionsBase { + tfliteModel: { + automlModel: string; + }; +} +export type ModelOptions = ModelOptionsBase | GcsTfliteModelOptions | AutoMLTfliteModelOptions; +export type ModelUpdateOptions = ModelOptions & { state?: { published?: boolean }}; -export interface ModelUpdateOptions extends ModelOptions { - state?: { published?: boolean }; +export function isGcsTfliteModelOptions(options: ModelOptions): options is GcsTfliteModelOptions { + const gcsUri = (options as GcsTfliteModelOptions)?.tfliteModel?.gcsTfliteUri; + return typeof gcsUri !== 'undefined' } /** Interface representing listModels options. */ @@ -60,7 +77,9 @@ export interface ModelContent { readonly published?: boolean; }; readonly tfliteModel?: { - readonly gcsTfliteUri: string; + readonly gcsTfliteUri?: string; + readonly automlModel?: string; + readonly sizeBytes: number; }; } @@ -71,6 +90,7 @@ export interface ModelResponse extends ModelContent { readonly updateTime: string; readonly etag: string; readonly modelHash?: string; + readonly activeOperations?: OperationResponse[]; } export interface ListModelsResponse { @@ -80,6 +100,7 @@ export interface ListModelsResponse { export interface OperationResponse { readonly name?: string; + readonly metadata?: {[key: string]: any}; readonly done: boolean; readonly error?: StatusErrorResponse; readonly response?: ModelResponse; @@ -112,7 +133,7 @@ export class MachineLearningApiClient { const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid model content.'); return Promise.reject(err); } - return this.getUrl() + return this.getProjectUrl() .then((url) => { const request: HttpRequestConfig = { method: 'POST', @@ -130,7 +151,7 @@ export class MachineLearningApiClient { const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid model or mask content.'); return Promise.reject(err); } - return this.getUrl() + return this.getProjectUrl() .then((url) => { const request: HttpRequestConfig = { method: 'PATCH', @@ -141,14 +162,20 @@ export class MachineLearningApiClient { }); } - public getModel(modelId: string): Promise { return Promise.resolve() .then(() => { return this.getModelName(modelId); }) .then((modelName) => { - return this.getResource(modelName); + return this.getResourceWithShortName(modelName); + }); + } + + public getOperation(operationName: string): Promise { + return Promise.resolve() + .then(() => { + return this.getResourceWithFullName(operationName); }); } @@ -177,7 +204,7 @@ export class MachineLearningApiClient { 'invalid-argument', 'Next page token must be a non-empty string.'); return Promise.reject(err); } - return this.getUrl() + return this.getProjectUrl() .then((url) => { const request: HttpRequestConfig = { method: 'GET', @@ -189,7 +216,7 @@ export class MachineLearningApiClient { } public deleteModel(modelId: string): Promise { - return this.getUrl() + return this.getProjectUrl() .then((url) => { const modelName = this.getModelName(modelId); const request: HttpRequestConfig = { @@ -200,15 +227,93 @@ export class MachineLearningApiClient { }); } + /** + * Handles a Long Running Operation coming back from the server. + * + * @param op The operation to handle + * @param options The options for polling + */ + public handleOperation( + op: OperationResponse, + options?: { + wait?: boolean; + maxTimeMillis?: number; + baseWaitMillis?: number; + maxWaitMillis?: number; + }): + Promise { + if (op.done) { + if (op.response) { + return Promise.resolve(op.response); + } else if (op.error) { + const err = FirebaseMachineLearningError.fromOperationError( + op.error.code, op.error.message); + return Promise.reject(err); + } + + // Done operations must have either a response or an error. + throw new FirebaseMachineLearningError('invalid-server-response', + 'Invalid operation response.'); + } + + // Operation is not done + if (options?.wait) { + return this.pollOperationWithExponentialBackoff(op.name!, options); + } + + const metadata = op.metadata || {}; + const metadataType: string = metadata['@type'] || ''; + if (!metadataType.includes('ModelOperationMetadata')) { + throw new FirebaseMachineLearningError('invalid-server-response', + `Unknown Metadata type: ${JSON.stringify(metadata)}`); + } + + return this.getModel(extractModelId(metadata.name)); + } + + // baseWaitMillis and maxWaitMillis should only ever be modified by unit tests to run faster. + private pollOperationWithExponentialBackoff( + opName: string, + options?: { + maxTimeMillis?: number; + baseWaitMillis?: number; + maxWaitMillis?: number; + }): Promise { + + const maxTimeMilliseconds = options?.maxTimeMillis ?? POLL_DEFAULT_MAX_TIME_MILLISECONDS; + const baseWaitMillis = options?.baseWaitMillis ?? POLL_BASE_WAIT_TIME_MILLISECONDS; + const maxWaitMillis = options?.maxWaitMillis ?? POLL_MAX_WAIT_TIME_MILLISECONDS; + + const poller = new ExponentialBackoffPoller( + baseWaitMillis, + maxWaitMillis, + maxTimeMilliseconds); + + return poller.poll(() => { + return this.getOperation(opName) + .then((responseData: {[key: string]: any}) => { + if (!responseData.done) { + return null; + } + if (responseData.error) { + const err = FirebaseMachineLearningError.fromOperationError( + responseData.error.code, responseData.error.message); + throw err; + } + return responseData.response; + }); + }); + } + /** * Gets the specified resource from the ML API. Resource names must be the short names without project * ID prefix (e.g. `models/123456789`). * - * @param {string} name Full qualified name of the resource to get. + * @param {string} name Short name of the resource to get. e.g. 'models/12345' * @returns {Promise} A promise that fulfills with the resource. */ - private getResource(name: string): Promise { - return this.getUrl() + private getResourceWithShortName(name: string): Promise { + return this.getProjectUrl() .then((url) => { const request: HttpRequestConfig = { method: 'GET', @@ -218,6 +323,20 @@ export class MachineLearningApiClient { }); } + /** + * Gets the specified resource from the ML API. Resource names must be the full names including project + * number prefix. + * @param fullName Full resource name of the resource to get. e.g. projects/123465/operations/987654 + * @returns {Promise} A promise that fulfulls with the resource. + */ + private getResourceWithFullName(fullName: string): Promise { + const request: HttpRequestConfig = { + method: 'GET', + url: `${ML_V1BETA2_API}/${fullName}` + }; + return this.sendRequest(request); + } + private sendRequest(request: HttpRequestConfig): Promise { request.headers = FIREBASE_VERSION_HEADER; return this.httpClient.send(request) @@ -250,7 +369,7 @@ export class MachineLearningApiClient { return new FirebaseMachineLearningError(code, message); } - private getUrl(): Promise { + private getProjectUrl(): Promise { return this.getProjectIdPrefix() .then((projectIdPrefix) => { return `${ML_V1BETA2_API}/${projectIdPrefix}`; @@ -309,3 +428,7 @@ const ERROR_CODE_MAPPING: {[key: string]: MachineLearningErrorCode} = { UNAUTHENTICATED: 'authentication-error', UNKNOWN: 'unknown-error', }; + +function extractModelId(resourceName: string): string { + return resourceName.split('/').pop()!; +} diff --git a/src/machine-learning/machine-learning.ts b/src/machine-learning/machine-learning.ts index 39266c0b00..8430b66cfc 100644 --- a/src/machine-learning/machine-learning.ts +++ b/src/machine-learning/machine-learning.ts @@ -16,8 +16,8 @@ import { FirebaseApp } from '../firebase-app'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { MachineLearningApiClient, ModelResponse, OperationResponse, - ModelOptions, ModelUpdateOptions, ListModelsOptions } from './machine-learning-api-client'; +import { MachineLearningApiClient, ModelResponse, ModelOptions, + ModelUpdateOptions, ListModelsOptions, isGcsTfliteModelOptions } from './machine-learning-api-client'; import { FirebaseError } from '../utils/error'; import * as validator from '../utils/validator'; @@ -92,7 +92,8 @@ export class MachineLearning implements FirebaseServiceInterface { public createModel(model: ModelOptions): Promise { return this.signUrlIfPresent(model) .then((modelContent) => this.client.createModel(modelContent)) - .then((operation) => handleOperation(operation)); + .then((operation) => this.client.handleOperation(operation)) + .then((modelResponse) => new Model(modelResponse, this.client)); } /** @@ -107,7 +108,8 @@ export class MachineLearning implements FirebaseServiceInterface { const updateMask = utils.generateUpdateMask(model); return this.signUrlIfPresent(model) .then((modelContent) => this.client.updateModel(modelId, modelContent, updateMask)) - .then((operation) => handleOperation(operation)); + .then((operation) => this.client.handleOperation(operation)) + .then((modelResponse) => new Model(modelResponse, this.client)); } /** @@ -141,7 +143,7 @@ export class MachineLearning implements FirebaseServiceInterface { */ public getModel(modelId: string): Promise { return this.client.getModel(modelId) - .then((modelResponse) => new Model(modelResponse)); + .then((modelResponse) => new Model(modelResponse, this.client)); } /** @@ -164,7 +166,7 @@ export class MachineLearning implements FirebaseServiceInterface { } let models: Model[] = []; if (resp.models) { - models = resp.models.map((rs) => new Model(rs)); + models = resp.models.map((rs) => new Model(rs, this.client)); } const result: ListModelsResult = { models }; if (resp.nextPageToken) { @@ -187,15 +189,16 @@ export class MachineLearning implements FirebaseServiceInterface { const updateMask = ['state.published']; const options: ModelUpdateOptions = { state: { published: publish } }; return this.client.updateModel(modelId, options, updateMask) - .then((operation) => handleOperation(operation)); + .then((operation) => this.client.handleOperation(operation)) + .then((modelResponse) => new Model(modelResponse, this.client)); } private signUrlIfPresent(options: ModelOptions): Promise { const modelOptions = deepCopy(options); - if (modelOptions.tfliteModel?.gcsTfliteUri) { + if (isGcsTfliteModelOptions(modelOptions)) { return this.signUrl(modelOptions.tfliteModel.gcsTfliteUri) - .then ((uri: string) => { - modelOptions.tfliteModel!.gcsTfliteUri = uri; + .then((uri: string) => { + modelOptions.tfliteModel.gcsTfliteUri = uri; return modelOptions; }) .catch((err: Error) => { @@ -229,67 +232,145 @@ export class MachineLearning implements FirebaseServiceInterface { } } - /** * A Firebase ML Model output object. */ export class Model { - public readonly modelId: string; - public readonly displayName: string; - public readonly tags?: string[]; - public readonly createTime: string; - public readonly updateTime: string; - public readonly validationError?: string; - public readonly published: boolean; - public readonly etag: string; - public readonly modelHash?: string; - - public readonly tfliteModel?: TFLiteModel; - - constructor(model: ModelResponse) { - if (!validator.isNonNullObject(model) || - !validator.isNonEmptyString(model.name) || - !validator.isNonEmptyString(model.createTime) || - !validator.isNonEmptyString(model.updateTime) || - !validator.isNonEmptyString(model.displayName) || - !validator.isNonEmptyString(model.etag)) { - throw new FirebaseMachineLearningError( - 'invalid-server-response', - `Invalid Model response: ${JSON.stringify(model)}`); - } + private model: ModelResponse; + private readonly client?: MachineLearningApiClient; + + constructor(model: ModelResponse, client: MachineLearningApiClient) { + this.model = Model.validateAndClone(model); + this.client = client; + } + + get modelId(): string { + return extractModelId(this.model.name); + } + + get displayName(): string { + return this.model.displayName!; + } + + get tags(): string[] { + return this.model.tags || []; + } + + get createTime(): string { + return new Date(this.model.createTime).toUTCString(); + } + + get updateTime(): string { + return new Date(this.model.updateTime).toUTCString(); + } + + get validationError(): string | undefined { + return this.model.state?.validationError?.message; + } + + get published(): boolean { + return this.model.state?.published || false; + } - this.modelId = extractModelId(model.name); - this.displayName = model.displayName; - this.tags = model.tags || []; - this.createTime = new Date(model.createTime).toUTCString(); - this.updateTime = new Date(model.updateTime).toUTCString(); - if (model.state?.validationError?.message) { - this.validationError = model.state?.validationError?.message; + get etag(): string { + return this.model.etag; + } + + get modelHash(): string | undefined { + return this.model.modelHash; + } + + get tfliteModel(): TFLiteModel | undefined { + // Make a copy so people can't directly modify the private this.model object. + return deepCopy(this.model.tfliteModel); + } + + /** + * Locked indicates if there are active long running operations on the model. + * Models may not be modified when they are locked. + */ + public get locked(): boolean { + return (this.model.activeOperations?.length ?? 0) > 0; + } + + public toJSON(): {[key: string]: any} { + // We can't just return this.model because it has extra fields and + // different formats etc. So we build the expected model object. + const jsonModel: {[key: string]: any} = { + modelId: this.modelId, + displayName: this.displayName, + tags: this.tags, + createTime: this.createTime, + updateTime: this.updateTime, + published: this.published, + etag: this.etag, + locked: this.locked, + }; + + // Also add possibly undefined fields if they exist. + + if (this.validationError) { + jsonModel['validationError'] = this.validationError; } - this.published = model.state?.published || false; - this.etag = model.etag; - if (model.modelHash) { - this.modelHash = model.modelHash; + + if (this.modelHash) { + jsonModel['modelHash'] = this.modelHash; } - if (model.tfliteModel?.gcsTfliteUri) { - this.tfliteModel = { - gcsTfliteUri: model.tfliteModel.gcsTfliteUri, - sizeBytes: model.tfliteModel.sizeBytes, - }; + + if (this.tfliteModel) { + jsonModel['tfliteModel'] = this.tfliteModel; } - } - public get locked(): boolean { - // Backend does not currently return locked models. - // This will likely change in future. - return false; + return jsonModel; } - public waitForUnlocked(_maxTimeSeconds?: number): Promise { - // Backend does not currently return locked models. - // This will likely change in future. + + /** + * Wait for the active operations on the model to complete. + * @param maxTimeMillis The number of milliseconds to wait for the model to be unlocked. If unspecified, + * a default will be used. + */ + public waitForUnlocked(maxTimeMillis?: number): Promise { + if ((this.model.activeOperations?.length ?? 0) > 0) { + // The client will always be defined on Models that have activeOperations + // because models with active operations came back from the server and + // were constructed with a non-empty client. + return this.client!.handleOperation(this.model.activeOperations![0], { wait: true, maxTimeMillis }) + .then((modelResponse) => { + this.model = Model.validateAndClone(modelResponse); + }); + } return Promise.resolve(); } + + private static validateAndClone(model: ModelResponse): ModelResponse { + if (!validator.isNonNullObject(model) || + !validator.isNonEmptyString(model.name) || + !validator.isNonEmptyString(model.createTime) || + !validator.isNonEmptyString(model.updateTime) || + !validator.isNonEmptyString(model.displayName) || + !validator.isNonEmptyString(model.etag)) { + throw new FirebaseMachineLearningError( + 'invalid-server-response', + `Invalid Model response: ${JSON.stringify(model)}`); + } + const tmpModel = deepCopy(model); + + // If tflite Model is specified, it must have a source consisting of + // oneof {gcsTfliteUri, automlModel} + if (model.tfliteModel && + !validator.isNonEmptyString(model.tfliteModel.gcsTfliteUri) && + !validator.isNonEmptyString(model.tfliteModel.automlModel)) { + // If we have some other source, ignore the whole tfliteModel. + delete (tmpModel as any).tfliteModel; + } + + // Remove '@type' field. We don't need it. + if ((tmpModel as any)["@type"]) { + delete (tmpModel as any)["@type"]; + } + return tmpModel; + } } /** @@ -298,25 +379,11 @@ export class Model { export interface TFLiteModel { readonly sizeBytes: number; - readonly gcsTfliteUri: string; + // Oneof these two + readonly gcsTfliteUri?: string; + readonly automlModel?: string; } function extractModelId(resourceName: string): string { return resourceName.split('/').pop()!; } - -function handleOperation(op: OperationResponse): Model { - // Backend currently does not return operations that are not done. - if (op.done) { - // Done operations must have either a response or an error. - if (op.response) { - return new Model(op.response); - } else if (op.error) { - throw FirebaseMachineLearningError.fromOperationError( - op.error.code, op.error.message); - } - } - throw new FirebaseMachineLearningError( - 'invalid-server-response', - `Invalid Operation response: ${JSON.stringify(op)}`); -} diff --git a/src/project-management/project-management-api-request-internal.ts b/src/project-management/project-management-api-request-internal.ts index 95dcb76a56..3d4fc654ce 100644 --- a/src/project-management/project-management-api-request-internal.ts +++ b/src/project-management/project-management-api-request-internal.ts @@ -276,7 +276,7 @@ export class ProjectManagementRequestHandler { private pollRemoteOperationWithExponentialBackoff( operationResourceName: string): Promise { - const poller = new ExponentialBackoffPoller(); + const poller = new ExponentialBackoffPoller(); return poller.poll(() => { return this.invokeRequestHandler('GET', operationResourceName, /* requestData */ null) diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index 03f52d0240..3b83221d5f 100644 --- a/src/utils/api-request.ts +++ b/src/utils/api-request.ts @@ -909,15 +909,15 @@ export class ApiSettings { * }); * ``` */ -export class ExponentialBackoffPoller extends EventEmitter { +export class ExponentialBackoffPoller extends EventEmitter { private numTries = 0; private completed = false; private masterTimer: NodeJS.Timer; private repollTimer: NodeJS.Timer; - private pollCallback?: () => Promise; - private resolve: (result: object) => void; + private pollCallback?: () => Promise; + private resolve: (result: T) => void; private reject: (err: object) => void; constructor( @@ -930,13 +930,13 @@ export class ExponentialBackoffPoller extends EventEmitter { /** * Poll the provided callback with exponential backoff. * - * @param {() => Promise} callback The callback to be called for each poll. If the + * @param {() => Promise} callback The callback to be called for each poll. If the * callback resolves to a falsey value, polling will continue. Otherwise, the truthy * resolution will be used to resolve the promise returned by this method. - * @return {Promise} A Promise which resolves to the truthy value returned by the provided + * @return {Promise} A Promise which resolves to the truthy value returned by the provided * callback when polling is complete. */ - public poll(callback: () => Promise): Promise { + public poll(callback: () => Promise): Promise { if (this.pollCallback) { throw new Error('poll() can only be called once per instance of ExponentialBackoffPoller'); } @@ -953,7 +953,7 @@ export class ExponentialBackoffPoller extends EventEmitter { this.reject(new Error('ExponentialBackoffPoller deadline exceeded - Master timeout reached')); }, this.masterTimeoutMillis); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; this.repoll(); diff --git a/test/integration/machine-learning.spec.ts b/test/integration/machine-learning.spec.ts index a679c31111..f0ed2b93cd 100644 --- a/test/integration/machine-learning.spec.ts +++ b/test/integration/machine-learning.spec.ts @@ -18,10 +18,15 @@ import path = require('path'); import * as chai from 'chai'; import * as admin from '../../lib/index'; +import { projectId } from './setup'; import { Bucket } from '@google-cloud/storage'; +import { GcsTfliteModelOptions, AutoMLTfliteModelOptions } from + '../../src/machine-learning/machine-learning-api-client'; const expect = chai.expect; + + describe('admin.machineLearning', () => { const modelsToDelete: string[] = []; @@ -74,8 +79,9 @@ describe('admin.machineLearning', () => { describe('createModel()', () => { it('creates a new Model without ModelFormat', () => { const modelOptions: admin.machineLearning.ModelOptions = { - displayName: 'node-integration-test-create-1', - tags: ['tag123', 'tag345'] }; + displayName: 'node-integ-test-create-1', + tags: ['tag123', 'tag345'] + }; return admin.machineLearning().createModel(modelOptions) .then((model) => { scheduleForDelete(model); @@ -83,9 +89,9 @@ describe('admin.machineLearning', () => { }); }); - it('creates a new Model with valid ModelFormat', () => { + it('creates a new Model with valid GCS TFLite ModelFormat', () => { const modelOptions: admin.machineLearning.ModelOptions = { - displayName: 'node-integration-test-create-2', + displayName: 'node-integ-test-create-2', tags: ['tag234', 'tag456'], tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, }; @@ -100,10 +106,35 @@ describe('admin.machineLearning', () => { }); }); + it('creates a new Model with valid AutoML TFLite ModelFormat', function () { + // AutoML models require verification. This takes between 20 and 60 seconds + this.timeout(60000); // Allow up to 60 seconds for this test. + return getAutoMLModelReference() + .then((automlRef: string) => { + if (!automlRef) { + this.skip(); + return; + } + const modelOptions: admin.machineLearning.ModelOptions = { + displayName: 'node-integ-test-create-automl', + tags: ['tagAutoml'], + tfliteModel: { automlModel: automlRef } + }; + return admin.machineLearning().createModel(modelOptions) + .then((model) => { + return model.waitForUnlocked(55000) + .then(() => { + scheduleForDelete(model); + verifyModel(model, modelOptions); + }); + }); + }); + }); + it('creates a new Model with invalid ModelFormat', () => { // Upload a file to default gcs bucket const modelOptions: admin.machineLearning.ModelOptions = { - displayName: 'node-integration-test-create-3', + displayName: 'node-integ-test-create-3', tags: ['tag234', 'tag456'], tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, }; @@ -150,15 +181,15 @@ describe('admin.machineLearning', () => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: 'Invalid Name#*^!', }; - return createTemporaryModel({ displayName: 'node-integration-invalid-arg' }) + return createTemporaryModel({ displayName: 'node-integ-invalid-argument' }) .then((model) => admin.machineLearning().updateModel(model.modelId, modelOptions) .should.eventually.be.rejected.and.have.property( 'code', 'machine-learning/invalid-argument')); }); it('updates the displayName', () => { - const DISPLAY_NAME = 'node-integration-test-update-1b'; - return createTemporaryModel({ displayName: 'node-integration-test-update-1a' }) + const DISPLAY_NAME = 'node-integ-test-update-1b'; + return createTemporaryModel({ displayName: 'node-integ-test-update-1a' }) .then((model) => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: DISPLAY_NAME, @@ -175,7 +206,7 @@ describe('admin.machineLearning', () => { const NEW_TAGS = ['tag-node-update-2', 'tag-node-update-3']; return createTemporaryModel({ - displayName: 'node-integration-test-update-2', + displayName: 'node-integ-test-update-2', tags: ORIGINAL_TAGS, }).then((expectedModel) => { const modelOptions: admin.machineLearning.ModelOptions = { @@ -204,10 +235,37 @@ describe('admin.machineLearning', () => { }); }); + it('updates the automl model', function () { + // AutoML models require verification. This takes between 20 and 60 seconds + this.timeout(60000); // Allow up to 60 seconds for this test. + return createTemporaryModel({ + displayName: 'node-integ-test-update-automl' + }).then((model) => { + + return getAutoMLModelReference() + .then((automlRef: string) => { + if (!automlRef) { + this.skip(); + return; + } + const modelOptions: admin.machineLearning.ModelOptions = { + tfliteModel: { automlModel: automlRef }, + }; + return admin.machineLearning().updateModel(model.modelId, modelOptions) + .then((updatedModel) => { + return updatedModel.waitForUnlocked(55000) + .then(() => { + verifyModel(updatedModel, modelOptions); + }); + }); + }); + }); + }); + it('can update more than 1 field', () => { - const DISPLAY_NAME = 'node-integration-test-update-3b'; - const TAGS = ['node-integration-tag-1', 'node-integration-tag-2']; - return createTemporaryModel({ displayName: 'node-integration-test-update-3a' }) + const DISPLAY_NAME = 'node-integ-test-update-3b'; + const TAGS = ['node-integ-tag-1', 'node-integ-tag-2']; + return createTemporaryModel({ displayName: 'node-integ-test-update-3a' }) .then((model) => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: DISPLAY_NAME, @@ -238,7 +296,7 @@ describe('admin.machineLearning', () => { it('publishes the model successfully', () => { const modelOptions: admin.machineLearning.ModelOptions = { - displayName: 'node-integration-test-publish-1', + displayName: 'node-integ-test-publish-1', tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, }; return uploadModelToGcs('model1.tflite', 'valid_model.tflite') @@ -273,7 +331,7 @@ describe('admin.machineLearning', () => { it('unpublishes the model successfully', () => { const modelOptions: admin.machineLearning.ModelOptions = { - displayName: 'node-integration-test-unpublish1', + displayName: 'node-integ-test-unpublish-1', tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, }; return uploadModelToGcs('model1.tflite', 'valid_model.tflite') @@ -330,16 +388,16 @@ describe('admin.machineLearning', () => { before(() => { return Promise.all([ admin.machineLearning().createModel({ - displayName: 'node-integration-list1', - tags: ['node-integration-tag-1'], + displayName: 'node-integ-list1', + tags: ['node-integ-tag-1'], }), admin.machineLearning().createModel({ - displayName: 'node-integration-list2', - tags: ['node-integration-tag-1'], + displayName: 'node-integ-list2', + tags: ['node-integ-tag-1'], }), admin.machineLearning().createModel({ - displayName: 'node-integration-list3', - tags: ['node-integration-tag-1'], + displayName: 'node-integ-list3', + tags: ['node-integ-tag-1'], })]) .then(([m1, m2, m3]: admin.machineLearning.Model[]) => { model1 = m1; @@ -370,12 +428,12 @@ describe('admin.machineLearning', () => { return admin.machineLearning().listModels({ pageSize: 2 }) .then((modelList) => { expect(modelList.models.length).to.equal(2); - expect(modelList.pageToken).not.to.be.undefined; + expect(modelList.pageToken).not.to.be.empty; }); }); it('filters by exact displayName', () => { - return admin.machineLearning().listModels({ filter: 'displayName=node-integration-list1' }) + return admin.machineLearning().listModels({ filter: 'displayName=node-integ-list1' }) .then((modelList) => { expect(modelList.models.length).to.equal(1); expect(modelList.models[0]).to.deep.equal(model1); @@ -384,7 +442,7 @@ describe('admin.machineLearning', () => { }); it('filters by displayName prefix', () => { - return admin.machineLearning().listModels({ filter: 'displayName:node-integration-list*', pageSize: 100 }) + return admin.machineLearning().listModels({ filter: 'displayName:node-integ-list*', pageSize: 100 }) .then((modelList) => { expect(modelList.models.length).to.be.at.least(3); expect(modelList.models).to.deep.include(model1); @@ -395,7 +453,7 @@ describe('admin.machineLearning', () => { }); it('filters by tag', () => { - return admin.machineLearning().listModels({ filter: 'tags:node-integration-tag-1', pageSize: 100 }) + return admin.machineLearning().listModels({ filter: 'tags:node-integ-tag-1', pageSize: 100 }) .then((modelList) => { expect(modelList.models.length).to.be.at.least(3); expect(modelList.models).to.deep.include(model1); @@ -406,14 +464,15 @@ describe('admin.machineLearning', () => { }); it('handles pageTokens properly', () => { - return admin.machineLearning().listModels({ filter: 'displayName:node-integration-list*', pageSize: 2 }) + return admin.machineLearning().listModels({ filter: 'displayName:node-integ-list*', pageSize: 2 }) .then((modelList) => { expect(modelList.models.length).to.equal(2); - expect(modelList.pageToken).not.to.be.empty; + expect(modelList.pageToken).not.to.be.undefined; return admin.machineLearning().listModels({ - filter: 'displayName:node-integration-list*', + filter: 'displayName:node-integ-list*', pageSize: 2, - pageToken: modelList.pageToken }) + pageToken: modelList.pageToken + }) .then((modelList2) => { expect(modelList2.models.length).to.be.at.least(1); expect(modelList2.pageToken).to.be.undefined; @@ -464,30 +523,34 @@ describe('admin.machineLearning', () => { }); }); - function verifyModel(model: admin.machineLearning.Model, expectedOptions: admin.machineLearning.ModelOptions): void { - if (expectedOptions.displayName) { - expect(model.displayName).to.equal(expectedOptions.displayName); - } else { - expect(model.displayName).not.to.be.empty; - } - expect(model.createTime).to.not.be.empty; - expect(model.updateTime).to.not.be.empty; - expect(model.etag).to.not.be.empty; - if (expectedOptions.tags) { - expect(model.tags).to.deep.equal(expectedOptions.tags); - } else { - expect(model.tags).to.be.empty; - } - if (expectedOptions.tfliteModel) { - verifyTfliteModel(model, expectedOptions.tfliteModel.gcsTfliteUri); - } else { - expect(model.validationError).to.equal('No model file has been uploaded.'); - } - expect(model.locked).to.be.false; - } }); -function verifyTfliteModel(model: admin.machineLearning.Model, expectedGcsTfliteUri: string): void { +function verifyModel(model: admin.machineLearning.Model, expectedOptions: admin.machineLearning.ModelOptions): void { + if (expectedOptions.displayName) { + expect(model.displayName).to.equal(expectedOptions.displayName); + } else { + expect(model.displayName).not.to.be.empty; + } + expect(model.createTime).to.not.be.empty; + expect(model.updateTime).to.not.be.empty; + expect(model.etag).to.not.be.empty; + expect(model.locked).to.be.false; + if (expectedOptions.tags) { + expect(model.tags).to.deep.equal(expectedOptions.tags); + } else { + expect(model.tags).to.be.empty; + } + if ((expectedOptions as GcsTfliteModelOptions).tfliteModel?.gcsTfliteUri !== undefined) { + verifyGcsTfliteModel(model, (expectedOptions as GcsTfliteModelOptions)); + } else if ((expectedOptions as AutoMLTfliteModelOptions).tfliteModel?.automlModel !== undefined) { + verifyAutomlTfliteModel(model, (expectedOptions as AutoMLTfliteModelOptions)); + } else { + expect(model.validationError).to.equal('No model file has been uploaded.'); + } +} + +function verifyGcsTfliteModel(model: admin.machineLearning.Model, expectedOptions: GcsTfliteModelOptions): void { + const expectedGcsTfliteUri = expectedOptions.tfliteModel.gcsTfliteUri; expect(model.tfliteModel!.gcsTfliteUri).to.equal(expectedGcsTfliteUri); if (expectedGcsTfliteUri.endsWith('invalid_model.tflite')) { expect(model.modelHash).to.be.undefined; @@ -497,3 +560,35 @@ function verifyTfliteModel(model: admin.machineLearning.Model, expectedGcsTflite expect(model.validationError).to.be.undefined; } } + +function verifyAutomlTfliteModel(model: admin.machineLearning.Model, expectedOptions: AutoMLTfliteModelOptions): void { + const expectedAutomlReference = expectedOptions.tfliteModel.automlModel; + expect(model.tfliteModel!.automlModel).to.equal(expectedAutomlReference); + expect(model.validationError).to.be.undefined; + expect(model.tfliteModel!.sizeBytes).to.not.be.undefined; + expect(model.modelHash).to.not.be.undefined; +} + +function getAutoMLModelReference(): Promise { + let automl; + try { + const { AutoMlClient } = require('@google-cloud/automl').v1; + automl = new AutoMlClient(); + } + catch (error) { + // Returning an empty string will result in skipping the test. + return Promise.resolve(""); + } + + const parent = automl.locationPath(projectId, 'us-central1'); + return automl.listModels({ parent, filter:"displayName=admin_sdk_integ_test1" }) + .then(([models]: [any]) => { + let modelRef = ""; + for (const model of models) { + modelRef = model.name; + } + return modelRef; + }) + // Skip the test if anything goes wrong with listing the models. + .catch(() => ''); +} diff --git a/test/unit/machine-learning/machine-learning-api-client.spec.ts b/test/unit/machine-learning/machine-learning-api-client.spec.ts index aa2dc536d6..ae5fe16244 100644 --- a/test/unit/machine-learning/machine-learning-api-client.spec.ts +++ b/test/unit/machine-learning/machine-learning-api-client.spec.ts @@ -19,8 +19,8 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { MachineLearningApiClient, ModelContent, - ListModelsOptions } from '../../../src/machine-learning/machine-learning-api-client'; +import { MachineLearningApiClient, ListModelsOptions, + ModelOptions } from '../../../src/machine-learning/machine-learning-api-client'; import { FirebaseMachineLearningError } from '../../../src/machine-learning/machine-learning-utils'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; @@ -64,10 +64,29 @@ describe('MachineLearningApiClient', () => { sizeBytes: 2220022, }, }; + const MODEL_RESPONSE_AUTOML = { + name: 'projects/test-project/models/3456789', + createTime: '2020-07-15T18:12:25.123987Z', + updateTime: '2020-07-15T19:15:32.965435Z', + etag: 'etag345', + modelHash: 'modelHash345', + displayName: 'model_automl', + tags: ['tag_automl'], + state: { published: true }, + tfliteModel: { + automlModel: 'projects/65432/models/ICN123', + sizeBytes: 3330033, + }, + }; + const PROJECT_ID = 'test-project'; + const PROJECT_NUMBER = '1234567'; + const OPERATION_ID = '987654'; + const OPERATION_NAME = `projects/${PROJECT_NUMBER}/operations/${OPERATION_ID}`; + const STATUS_ERROR_MESSAGE = 'Invalid Argument message' const STATUS_ERROR_RESPONSE = { code: 3, - message: 'Invalid Argument message', + message: STATUS_ERROR_MESSAGE, }; const OPERATION_SUCCESS_RESPONSE = { done: true, @@ -77,6 +96,34 @@ describe('MachineLearningApiClient', () => { done: true, error: STATUS_ERROR_RESPONSE, }; + const OPERATION_NOT_DONE_RESPONSE = { + name: OPERATION_NAME, + metadata: { + '@type': 'type.googleapis.com/google.firebase.ml.v1beta2.ModelOperationMetadata', + name: `projects/${PROJECT_ID}/models/${MODEL_ID}`, + basicOperationStatus: 'BASIC_OPERATION_STATUS_UPLOADING' + }, + done: false, + }; + const OPERATION_AUTOML_RESPONSE = { + done: true, + response: MODEL_RESPONSE_AUTOML, + }; + const LOCKED_MODEL_RESPONSE = { + name: 'projects/test-project/models/1234567', + createTime: '2020-02-07T23:45:23.288047Z', + updateTime: '2020-02-08T23:45:23.288047Z', + etag: 'etag123', + modelHash: 'modelHash123', + displayName: 'model_1', + tags: ['tag_1', 'tag_2'], + activeOperations: [OPERATION_NOT_DONE_RESPONSE], + state: { published: true }, + tfliteModel: { + gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', + sizeBytes: 16900988, + }, + }; const ERROR_RESPONSE = { error: { @@ -125,8 +172,19 @@ describe('MachineLearningApiClient', () => { }); describe('createModel', () => { - const NAME_ONLY_CONTENT: ModelContent = { displayName: 'name1' }; - + const NAME_ONLY_OPTIONS: ModelOptions = { displayName: 'name1' }; + const GCS_OPTIONS: ModelOptions = { + displayName: 'name2', + tfliteModel: { + gcsTfliteUri: 'gcsUri1', + }, + }; + const AUTOML_OPTIONS: ModelOptions = { + displayName: 'name3', + tfliteModel: { + automlModel: 'automlModel', + }, + }; const invalidContent: any[] = [null, undefined, {}, { tags: [] }]; invalidContent.forEach((content) => { @@ -138,7 +196,7 @@ describe('MachineLearningApiClient', () => { }); it('should reject when project id is not available', () => { - return clientWithoutProjectId.createModel(NAME_ONLY_CONTENT) + return clientWithoutProjectId.createModel(NAME_ONLY_OPTIONS) .should.eventually.be.rejectedWith(noProjectId); }); @@ -148,7 +206,7 @@ describe('MachineLearningApiClient', () => { .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); - return apiClient.createModel(NAME_ONLY_CONTENT) + return apiClient.createModel(NAME_ONLY_OPTIONS) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -157,7 +215,20 @@ describe('MachineLearningApiClient', () => { .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); stubs.push(stub); - return apiClient.createModel(NAME_ONLY_CONTENT) + return apiClient.createModel(NAME_ONLY_OPTIONS) + .then((resp) => { + expect(resp.done).to.be.true; + expect(resp.name).to.be.undefined; + expect(resp.response).to.deep.equal(MODEL_RESPONSE); + }); + }); + + it('should accept TFLite GCS options', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); + stubs.push(stub); + return apiClient.createModel(GCS_OPTIONS) .then((resp) => { expect(resp.done).to.be.true; expect(resp.name).to.be.undefined; @@ -165,12 +236,25 @@ describe('MachineLearningApiClient', () => { }); }); + it('should accept AutoML options', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_AUTOML_RESPONSE)); + stubs.push(stub); + return apiClient.createModel(AUTOML_OPTIONS) + .then((resp) => { + expect(resp.done).to.be.true; + expect(resp.name).to.be.undefined; + expect(resp.response).to.deep.equal(MODEL_RESPONSE_AUTOML); + }); + }); + it('should resolve with error when the operation fails', () => { const stub = sinon .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(OPERATION_ERROR_RESPONSE)); stubs.push(stub); - return apiClient.createModel(NAME_ONLY_CONTENT) + return apiClient.createModel(NAME_ONLY_OPTIONS) .then((resp) => { expect(resp.done).to.be.true; expect(resp.name).to.be.undefined; @@ -184,7 +268,7 @@ describe('MachineLearningApiClient', () => { .rejects(utils.errorFrom({}, 404)); stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); - return apiClient.createModel(NAME_ONLY_CONTENT) + return apiClient.createModel(NAME_ONLY_OPTIONS) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -195,7 +279,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); - return apiClient.createModel(NAME_ONLY_CONTENT) + return apiClient.createModel(NAME_ONLY_OPTIONS) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -205,19 +289,38 @@ describe('MachineLearningApiClient', () => { .stub(HttpClient.prototype, 'send') .rejects(expected); stubs.push(stub); - return apiClient.createModel(NAME_ONLY_CONTENT) + return apiClient.createModel(NAME_ONLY_OPTIONS) .should.eventually.be.rejected.and.deep.include(expected); }); }); describe('updateModel', () => { - const NAME_ONLY_CONTENT: ModelContent = { displayName: 'name1' }; - const NAME_ONLY_MASK = ['displayName']; + const NAME_ONLY_OPTIONS: ModelOptions = { displayName: 'name1' }; + const GCS_OPTIONS: ModelOptions = { + displayName: 'name2', + tfliteModel: { + gcsTfliteUri: 'gcsUri1', + }, + }; + const AUTOML_OPTIONS: ModelOptions = { + displayName: 'name3', + tfliteModel: { + automlModel: 'automlModel', + }, + }; - const invalidContent: any[] = [null, undefined]; - invalidContent.forEach((content) => { - it(`should reject when called with: ${JSON.stringify(content)}`, () => { - return apiClient.updateModel(MODEL_ID, content, NAME_ONLY_MASK) + const NAME_ONLY_MASK_LIST = ['displayName']; + const GCS_MASK_LIST = ['displayName', 'tfliteModel.gcsTfliteUri']; + const AUTOML_MASK_LIST = ['displayName', 'tfliteModel.automlModel']; + + const NAME_ONLY_UPDATE_MASK_STRING = "updateMask=displayName"; + const GCS_UPDATE_MASK_STRING = "updateMask=displayName,tfliteModel.gcsTfliteUri"; + const AUTOML_UPDATE_MASK_STRING = "updateMask=displayName,tfliteModel.automlModel"; + + const invalidOptions: any[] = [null, undefined]; + invalidOptions.forEach((option) => { + it(`should reject when called with: ${JSON.stringify(option)}`, () => { + return apiClient.updateModel(MODEL_ID, option, NAME_ONLY_MASK_LIST) .should.eventually.be.rejected.and.have.property( 'message', 'Invalid model or mask content.'); }); @@ -230,7 +333,7 @@ describe('MachineLearningApiClient', () => { }); it('should reject when project id is not available', () => { - return clientWithoutProjectId.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) + return clientWithoutProjectId.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) .should.eventually.be.rejectedWith(noProjectId); }); @@ -240,7 +343,7 @@ describe('MachineLearningApiClient', () => { .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) + return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -249,7 +352,7 @@ describe('MachineLearningApiClient', () => { .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); stubs.push(stub); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) + return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) .then((resp) => { expect(resp.done).to.be.true; expect(resp.name).to.be.undefined; @@ -257,8 +360,46 @@ describe('MachineLearningApiClient', () => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'PATCH', headers: EXPECTED_HEADERS, - url: `${BASE_URL}/projects/test-project/models/${MODEL_ID}?updateMask=displayName`, - data: NAME_ONLY_CONTENT, + url: `${BASE_URL}/projects/test-project/models/${MODEL_ID}?${NAME_ONLY_UPDATE_MASK_STRING}`, + data: NAME_ONLY_OPTIONS, + }); + }); + }); + + it('should resolve with the updated GCS resource on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); + stubs.push(stub); + return apiClient.updateModel(MODEL_ID, GCS_OPTIONS, GCS_MASK_LIST) + .then((resp) => { + expect(resp.done).to.be.true; + expect(resp.name).to.be.undefined; + expect(resp.response).to.deep.equal(MODEL_RESPONSE); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PATCH', + headers: EXPECTED_HEADERS, + url: `${BASE_URL}/projects/test-project/models/${MODEL_ID}?${GCS_UPDATE_MASK_STRING}`, + data: GCS_OPTIONS, + }); + }); + }); + + it('should resolve with the updated AutoML resource on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); + stubs.push(stub); + return apiClient.updateModel(MODEL_ID, AUTOML_OPTIONS, AUTOML_MASK_LIST) + .then((resp) => { + expect(resp.done).to.be.true; + expect(resp.name).to.be.undefined; + expect(resp.response).to.deep.equal(MODEL_RESPONSE); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PATCH', + headers: EXPECTED_HEADERS, + url: `${BASE_URL}/projects/test-project/models/${MODEL_ID}?${AUTOML_UPDATE_MASK_STRING}`, + data: AUTOML_OPTIONS, }); }); }); @@ -268,7 +409,7 @@ describe('MachineLearningApiClient', () => { .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(OPERATION_ERROR_RESPONSE)); stubs.push(stub); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) + return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) .then((resp) => { expect(resp.done).to.be.true; expect(resp.name).to.be.undefined; @@ -282,7 +423,7 @@ describe('MachineLearningApiClient', () => { .rejects(utils.errorFrom({}, 404)); stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) + return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -293,7 +434,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) + return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -303,7 +444,7 @@ describe('MachineLearningApiClient', () => { .stub(HttpClient.prototype, 'send') .rejects(expected); stubs.push(stub); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) + return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -387,6 +528,153 @@ describe('MachineLearningApiClient', () => { }); }); + describe('getOperation', () => { + it('should resolve with the requested operation on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); + stubs.push(stub); + return apiClient.getOperation(OPERATION_NAME) + .then((resp) => { + expect(resp).to.deep.equal(OPERATION_SUCCESS_RESPONSE); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: `${BASE_URL}/projects/${PROJECT_NUMBER}/operations/${OPERATION_ID}`, + headers: EXPECTED_HEADERS, + }); + }); + }); + + 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 FirebaseMachineLearningError('not-found', 'Requested entity not found'); + return apiClient.getOperation(OPERATION_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 FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); + return apiClient.getOperation(OPERATION_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 FirebaseMachineLearningError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.getOperation(OPERATION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject when failed 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.getOperation(OPERATION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + describe('handleOperation', () => { + it('handles a done operation with result', () => { + return apiClient.handleOperation(OPERATION_SUCCESS_RESPONSE) + .then((resp) => { + expect(resp).deep.equals(MODEL_RESPONSE); + }); + }); + + it('handles a done operation with error', () => { + const expected = new FirebaseMachineLearningError('invalid-argument', STATUS_ERROR_MESSAGE); + return apiClient.handleOperation(OPERATION_ERROR_RESPONSE) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('handles a running operation with no wait', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(LOCKED_MODEL_RESPONSE)); + stubs.push(stub); + return apiClient.handleOperation(OPERATION_NOT_DONE_RESPONSE) + .then((resp) => { + expect(resp).to.deep.equal(LOCKED_MODEL_RESPONSE); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: `${BASE_URL}/projects/${PROJECT_ID}/models/${MODEL_ID}`, + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('handles a running operation with wait', () => { + const stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).resolves(utils.responseFrom(OPERATION_NOT_DONE_RESPONSE)); + stub.onCall(1).resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); + stubs.push(stub); + return apiClient.handleOperation(OPERATION_NOT_DONE_RESPONSE, { + wait: true, + maxTimeMillis: 1000, + baseWaitMillis: 2, + maxWaitMillis: 5 }) + .then((resp) => { + expect(resp).to.deep.equal(MODEL_RESPONSE); + expect(stub).to.have.been.calledTwice.and.calledWith({ + method: 'GET', + url: `${BASE_URL}/projects/${PROJECT_NUMBER}/operations/${OPERATION_ID}`, + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('handles a running operation with wait ending in error', () => { + const stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).resolves(utils.responseFrom(OPERATION_NOT_DONE_RESPONSE)); + stub.onCall(1).resolves(utils.responseFrom(OPERATION_ERROR_RESPONSE)); + stubs.push(stub); + const expected = new FirebaseMachineLearningError('invalid-argument', STATUS_ERROR_MESSAGE); + return apiClient.handleOperation(OPERATION_NOT_DONE_RESPONSE, { + wait: true, + maxTimeMillis: 1000, + baseWaitMillis: 2, + maxWaitMillis: 5 }) + .should.eventually.be.rejected.and.deep.include(expected) + .then(() => { + expect(stub).to.have.been.calledTwice.and.calledWith({ + method: 'GET', + url: `${BASE_URL}/projects/${PROJECT_NUMBER}/operations/${OPERATION_ID}`, + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('handles a running operation with wait ending in timeout', () => { + const stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).resolves(utils.responseFrom(OPERATION_NOT_DONE_RESPONSE)); + stub.onCall(1).resolves(utils.responseFrom(OPERATION_NOT_DONE_RESPONSE)); + stub.onCall(2).resolves(utils.responseFrom(OPERATION_NOT_DONE_RESPONSE)); + stubs.push(stub); + const expected = new Error('ExponentialBackoffPoller dealine exceeded - Master timeout reached'); + return apiClient.handleOperation(OPERATION_NOT_DONE_RESPONSE, { + wait: true, + maxTimeMillis: 1000, + baseWaitMillis: 500, + maxWaitMillis: 1000 }) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + }); + describe('listModels', () => { const LIST_RESPONSE = { models: [MODEL_RESPONSE, MODEL_RESPONSE2], diff --git a/test/unit/machine-learning/machine-learning.spec.ts b/test/unit/machine-learning/machine-learning.spec.ts index d966d3a6c6..8d32619dff 100644 --- a/test/unit/machine-learning/machine-learning.spec.ts +++ b/test/unit/machine-learning/machine-learning.spec.ts @@ -23,7 +23,7 @@ import { MachineLearning, Model } from '../../../src/machine-learning/machine-le import { FirebaseApp } from '../../../src/firebase-app'; import * as mocks from '../../resources/mocks'; import { MachineLearningApiClient, StatusErrorResponse, - ModelOptions, ModelResponse } from '../../../src/machine-learning/machine-learning-api-client'; + ModelOptions, ModelResponse, OperationResponse } from '../../../src/machine-learning/machine-learning-api-client'; import { FirebaseMachineLearningError } from '../../../src/machine-learning/machine-learning-utils'; import { deepCopy } from '../../../src/utils/deep-copy'; @@ -32,6 +32,10 @@ const expect = chai.expect; describe('MachineLearning', () => { const MODEL_ID = '1234567'; + const PROJECT_ID = 'test-project'; + const PROJECT_NUMBER = '987654'; + const OPERATION_ID = '456789'; + const OPERATION_NAME = `projects/${PROJECT_NUMBER}/operations/${OPERATION_ID}` const EXPECTED_ERROR = new FirebaseMachineLearningError('internal-error', 'message'); const CREATE_TIME_UTC = 'Fri, 07 Feb 2020 23:45:23 GMT'; const UPDATE_TIME_UTC = 'Sat, 08 Feb 2020 23:45:23 GMT'; @@ -68,7 +72,7 @@ describe('MachineLearning', () => { sizeBytes: 16900988, }, }; - const MODEL1 = new Model(MODEL_RESPONSE); + const MODEL_RESPONSE2: { name: string; @@ -103,7 +107,6 @@ describe('MachineLearning', () => { sizeBytes: 22200222, }, }; - const MODEL2 = new Model(MODEL_RESPONSE2); const MODEL_RESPONSE3: any = { name: 'projects/test-project/models/3456789', @@ -130,6 +133,7 @@ describe('MachineLearning', () => { const OPERATION_RESPONSE: { name?: string; + metadata?: any; done: boolean; error?: StatusErrorResponse; response?: { @@ -159,6 +163,7 @@ describe('MachineLearning', () => { const OPERATION_RESPONSE_ERROR: { name?: string; + metadata?: any; done: boolean; error?: { code: number; @@ -170,18 +175,79 @@ describe('MachineLearning', () => { error: STATUS_ERROR_RESPONSE, }; + const OPERATION_RESPONSE_NOT_DONE: { + name?: string; + metadata?: any; + done: boolean; + error?: { + code: number; + message: string; + }; + response?: ModelResponse; + } = { + name: OPERATION_NAME, + metadata: { + '@type': 'type.googleapis.com/google.firebase.ml.v1beta2.ModelOperationMetadata', + name: `projects/${PROJECT_ID}/models/${MODEL_ID}`, + basicOperationStatus: 'BASIC_OPERATION_STATUS_UPLOADING' + }, + done: false, + }; + + const MODEL_RESPONSE_LOCKED: { + name: string; + createTime: string; + updateTime: string; + etag: string; + modelHash: string; + displayName?: string; + tags?: string[]; + activeOperations?: OperationResponse[]; + state?: { + validationError?: { + code: number; + message: string; + }; + published?: boolean; + }; + tfliteModel?: { + gcsTfliteUri: string; + sizeBytes: number; + }; + } = { + name: 'projects/test-project/models/1234567', + createTime: '2020-02-07T23:45:23.288047Z', + updateTime: '2020-02-08T23:45:23.288047Z', + etag: 'etag123', + modelHash: 'modelHash123', + displayName: 'model_1', + tags: ['tag_1', 'tag_2'], + activeOperations: [OPERATION_RESPONSE_NOT_DONE], + state: { published: true }, + tfliteModel: { + gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', + sizeBytes: 16900988, + }, + }; let machineLearning: MachineLearning; let mockApp: FirebaseApp; + let mockClient: MachineLearningApiClient; let mockCredentialApp: FirebaseApp; + let model1: Model; + let model2: Model; + const stubs: sinon.SinonStub[] = []; before(() => { mockApp = mocks.app(); + mockClient = new MachineLearningApiClient(mockApp); mockCredentialApp = mocks.mockCredentialApp(); machineLearning = new MachineLearning(mockApp); + model1 = new Model(MODEL_RESPONSE, mockClient); + model2 = new Model(MODEL_RESPONSE2, mockClient); }); after(() => { @@ -244,7 +310,7 @@ describe('MachineLearning', () => { describe('Model', () => { it('should successfully construct a model', () => { - const model = new Model(MODEL_RESPONSE); + const model = new Model(MODEL_RESPONSE, mockClient); expect(model.modelId).to.equal(MODEL_ID); expect(model.displayName).to.equal('model_1'); expect(model.tags).to.deep.equal(['tag_1', 'tag_2']); @@ -262,7 +328,7 @@ describe('MachineLearning', () => { }); it('should accept unknown fields gracefully', () => { - const model = new Model(MODEL_RESPONSE3); + const model = new Model(MODEL_RESPONSE3, mockClient); expect(model.modelId).to.equal('3456789'); expect(model.displayName).to.equal('model_3'); expect(model.tags).to.deep.equal(['tag_3', 'tag_4']); @@ -275,6 +341,52 @@ describe('MachineLearning', () => { expect(model.tfliteModel).to.be.undefined; }); + it('should successfully serialize a model to JSON', () => { + const model = new Model(MODEL_RESPONSE, mockClient); + const expectedModel = { + modelId: MODEL_ID, + displayName: 'model_1', + tags: ['tag_1', 'tag_2'], + createTime: CREATE_TIME_UTC, + updateTime: UPDATE_TIME_UTC, + published: true, + etag: 'etag123', + locked: false, + modelHash: 'modelHash123', + tfliteModel: { + gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', + sizeBytes: 16900988, + } + } + const jsonString = JSON.stringify(model); + expect(JSON.parse(jsonString)).to.deep.equal(expectedModel); + }) + + it('should return locked when active operations are present', () => { + const model = new Model(MODEL_RESPONSE_LOCKED, mockClient); + expect(model.locked).to.be.true; + }); + + it('should return locked as false when no active operations are present', () => { + const model = new Model(MODEL_RESPONSE, mockClient); + expect(model.locked).to.be.false; + }); + + it('should successfully update a model from a Response', () => { + const model = new Model(MODEL_RESPONSE_LOCKED, mockClient); + expect(model.locked).to.be.true; + + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'handleOperation') + .resolves(MODEL_RESPONSE2); + stubs.push(stub); + + model.waitForUnlocked() + .then(() => { + expect(model.locked).to.be.false; + expect(model).to.deep.equal(model2); + }); + }); }); @@ -367,7 +479,7 @@ describe('MachineLearning', () => { return machineLearning.getModel(MODEL_ID) .then((model) => { - expect(model).to.deep.equal(MODEL1); + expect(model).to.deep.equal(model1); }); }); }); @@ -409,8 +521,8 @@ describe('MachineLearning', () => { return machineLearning.listModels() .then((result) => { expect(result.models.length).equals(2); - expect(result.models[0]).to.deep.equal(MODEL1); - expect(result.models[1]).to.deep.equal(MODEL2); + expect(result.models[0]).to.deep.equal(model1); + expect(result.models[1]).to.deep.equal(model2); expect(result.pageToken).to.equal(LIST_MODELS_RESPONSE.nextPageToken); }); }); @@ -537,7 +649,7 @@ describe('MachineLearning', () => { return machineLearning.createModel(MODEL_OPTIONS_WITH_GCS) .then((model) => { - expect(model).to.deep.equal(MODEL1); + expect(model).to.deep.equal(model1); }); }); @@ -654,7 +766,7 @@ describe('MachineLearning', () => { return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_WITH_GCS) .then((model) => { - expect(model).to.deep.equal(MODEL1); + expect(model).to.deep.equal(model1); }); }); @@ -758,7 +870,7 @@ describe('MachineLearning', () => { return machineLearning.publishModel(MODEL_ID) .then((model) => { - expect(model).to.deep.equal(MODEL1); + expect(model).to.deep.equal(model1); }); }); @@ -862,7 +974,7 @@ describe('MachineLearning', () => { return machineLearning.unpublishModel(MODEL_ID) .then((model) => { - expect(model).to.deep.equal(MODEL1); + expect(model).to.deep.equal(model1); }); }); From 32b025d874b13a80f453864815b8be778cae87d3 Mon Sep 17 00:00:00 2001 From: ifielker Date: Mon, 14 Sep 2020 17:19:26 -0400 Subject: [PATCH 041/160] Adding More ModelOptions to toc.yaml (#1027) * Adding More ModelOptions to toc.yaml * Remove ModelOptions * removed extra new line --- docgen/content-sources/node/toc.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index 595f55ae7a..ee0071f5f3 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -155,8 +155,12 @@ toc: path: /docs/reference/admin/node/admin.machineLearning.MachineLearning - title: "Model" path: /docs/reference/admin/node/admin.machineLearning.Model - - title: "ModelOptions" - path: /docs/reference/admin/node/admin.machineLearning.ModelOptions + - title: "ModelOptionsBase" + path: /docs/reference/admin/node/admin.machineLearning.ModelOptionsBase + - title: "GcsTfliteModelOptions" + path: /docs/reference/admin/node/admin.machineLearning.GcsTfliteModelOptions + - title: "AutoMLTfliteModelOptions" + path: /docs/reference/admin/node/admin.machineLearning.AutoMLTfliteModelOptions - title: "TFLiteModel" path: /docs/reference/admin/node/admin.machineLearning.TFLiteModel From f087375711f45f6814ac713dbefbd65fc18482ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 19:50:14 -0400 Subject: [PATCH 042/160] build(deps): bump node-forge from 0.9.1 to 0.10.0 (#1028) Bumps [node-forge](https://github.com/digitalbazaar/forge) from 0.9.1 to 0.10.0. - [Release notes](https://github.com/digitalbazaar/forge/releases) - [Changelog](https://github.com/digitalbazaar/forge/blob/master/CHANGELOG.md) - [Commits](https://github.com/digitalbazaar/forge/compare/0.9.1...0.10.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++++++--- package.json | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 405c715af4..f17615e944 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3453,6 +3453,14 @@ "optional": true, "requires": { "node-forge": "^0.9.0" + }, + "dependencies": { + "node-forge": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.2.tgz", + "integrity": "sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==", + "optional": true + } } }, "graceful-fs": { @@ -5599,9 +5607,9 @@ "optional": true }, "node-forge": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", - "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, "node-pre-gyp": { "version": "0.15.0", diff --git a/package.json b/package.json index 983a8b3de3..70efc31769 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@types/node": "^10.10.0", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", - "node-forge": "^0.9.1" + "node-forge": "^0.10.0" }, "optionalDependencies": { "@google-cloud/firestore": "^4.0.0", From 59f2203dc6d0881dce7fc1af28f7b4dfebaa6bc6 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 15 Sep 2020 13:58:19 -0400 Subject: [PATCH 043/160] [chore] Release 9.2.0 (#1030) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 70efc31769..b8babb1bc0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.1.1", + "version": "9.2.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 738250da37df9261ab0b76a39cf8c6d223cb766e Mon Sep 17 00:00:00 2001 From: egilmorez Date: Wed, 23 Sep 2020 17:58:00 -0700 Subject: [PATCH 044/160] Update default.hbs (#1040) Adding meta tag that will be used by recommendations feature of devsite. --- docgen/theme/layouts/default.hbs | 1 + 1 file changed, 1 insertion(+) diff --git a/docgen/theme/layouts/default.hbs b/docgen/theme/layouts/default.hbs index 72111fb4e7..a5e05fc73e 100644 --- a/docgen/theme/layouts/default.hbs +++ b/docgen/theme/layouts/default.hbs @@ -7,6 +7,7 @@ + {{#ifCond model.name '==' project.name}}{{project.name}}{{else}}{{model.name}} | {{project.name}}{{/ifCond}} From dbb0c785c9b72590c19655d3ab5a39338e0cd393 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Fri, 16 Oct 2020 16:59:44 -0400 Subject: [PATCH 045/160] Add support for Auth Emulator (#1044) * Basic URL replacement and custom tokens * Fix test * Tests actually pass * Mock verifyIdToken * Add another test * Small style change * Small simplification * Significant cleanup of all the useEmulator stuff * Make sure we don't use env var to short-circuit ID Token verification * Make lint pass * Add unit tests that go through the Auth interface * Hiranya nits * Make private method even more private and scary, require env * Make the private method throw * Fix test --- package-lock.json | 26 +++++----- package.json | 2 +- src/auth/auth-api-request.ts | 27 +++++++++- src/auth/auth.ts | 44 ++++++++++++++-- src/auth/token-generator.ts | 43 +++++++++++++-- src/auth/token-verifier.ts | 45 +++++++++------- test/resources/mocks.ts | 12 +++-- test/unit/auth/auth.spec.ts | 61 ++++++++++++++++++++++ test/unit/auth/token-generator.spec.ts | 25 ++++++++- test/unit/auth/token-verifier.spec.ts | 72 +++++++++++++++++++++++--- 10 files changed, 303 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index f17615e944..4b4098dc78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.1.1", + "version": "9.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -558,7 +558,7 @@ }, "@types/firebase-token-generator": { "version": "2.0.28", - "resolved": "http://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", + "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", "integrity": "sha1-Z1VIHZMk4mt6XItFXWgUg3aCw5Y=", "dev": true }, @@ -569,9 +569,9 @@ "dev": true }, "@types/jsonwebtoken": { - "version": "7.2.8", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-7.2.8.tgz", - "integrity": "sha512-XENN3YzEB8D6TiUww0O8SRznzy1v+77lH7UmuN54xq/IHIsyWjWOzZuFFTtoiRuaE782uAoRwBe/wwow+vQXZw==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz", + "integrity": "sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg==", "dev": true, "requires": { "@types/node": "*" @@ -1284,7 +1284,7 @@ }, "binaryextensions": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", "dev": true }, @@ -2990,7 +2990,7 @@ }, "firebase-token-generator": { "version": "2.0.0", - "resolved": "http://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", "integrity": "sha1-l2fXWewTq9yZuhFf1eqZ2Lk9EgY=", "dev": true }, @@ -4598,7 +4598,7 @@ }, "istextorbinary": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", "dev": true, "requires": { @@ -5903,7 +5903,7 @@ }, "find-up": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "resolved": false, "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { @@ -6263,7 +6263,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -6419,7 +6419,7 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, @@ -6965,7 +6965,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -7766,7 +7766,7 @@ }, "textextensions": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=", "dev": true }, diff --git a/package.json b/package.json index b8babb1bc0..0c63c8ab3b 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@types/chai": "^4.0.0", "@types/chai-as-promised": "^7.1.0", "@types/firebase-token-generator": "^2.0.28", - "@types/jsonwebtoken": "^7.2.8", + "@types/jsonwebtoken": "^8.5.0", "@types/lodash": "^4.14.104", "@types/minimist": "^1.2.0", "@types/mocha": "^2.2.48", diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 6043341b80..81789d17e2 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -89,10 +89,19 @@ const MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE = 100; const FIREBASE_AUTH_BASE_URL_FORMAT = 'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}'; +/** Firebase Auth base URlLformat when using the auth emultor. */ +const FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT = + 'http://{host}/identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}'; + /** The Firebase Auth backend multi-tenancy base URL format. */ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace( 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); +/** Firebase Auth base URL format when using the auth emultor with multi-tenancy. */ +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; @@ -121,7 +130,14 @@ class AuthResourceUrlBuilder { * @constructor */ constructor(protected app: FirebaseApp, protected version: string = 'v1') { - this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; + const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST; + if (emulatorHost) { + this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, { + host: emulatorHost + }); + } else { + this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; + } } /** @@ -181,7 +197,14 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { */ constructor(protected app: FirebaseApp, protected version: string, protected tenantId: string) { super(app, version); - this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; + const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST + if (emulatorHost) { + this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, { + host: emulatorHost + }); + } else { + this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; + } } /** diff --git a/src/auth/auth.ts b/src/auth/auth.ts index d22f3776b6..b9904eadfe 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -19,7 +19,7 @@ import { UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, } from './identifier'; import { FirebaseApp } from '../firebase-app'; -import { FirebaseTokenGenerator, cryptoSignerFromApp } from './token-generator'; +import { FirebaseTokenGenerator, EmulatedSigner, cryptoSignerFromApp } from './token-generator'; import { AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, } from './auth-api-request'; @@ -31,7 +31,9 @@ import { import * as utils from '../utils/index'; import * as validator from '../utils/validator'; -import { FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier } from './token-verifier'; +import { + FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier, ALGORITHM_RS256 +} from './token-verifier'; import { ActionCodeSettings } from './action-code-settings-builder'; import { AuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults, UpdateAuthProviderRequest, @@ -141,7 +143,7 @@ export class BaseAuth { if (tokenGenerator) { this.tokenGenerator = tokenGenerator; } else { - const cryptoSigner = cryptoSignerFromApp(app); + const cryptoSigner = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app); this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner); } @@ -735,6 +737,28 @@ export class BaseAuth { return decodedIdToken; }); } + + /** + * Enable or disable ID token verification. This is used to safely short-circuit token verification with the + * Auth emulator. When disabled ONLY unsigned tokens will pass verification, production tokens will not pass. + * + * WARNING: This is a dangerous method that will compromise your app's security and break your app in + * production. Developers should never call this method, it is for internal testing use only. + * + * @internal + */ + // @ts-expect-error: this method appears unused but is used privately. + private setJwtVerificationEnabled(enabled: boolean): void { + if (!enabled && !useEmulator()) { + // We only allow verification to be disabled in conjunction with + // the emulator environment variable. + throw new Error('This method is only available when connected to the Authentication emulator.'); + } + + const algorithm = enabled ? ALGORITHM_RS256 : 'none'; + this.idTokenVerifier.setAlgorithm(algorithm); + this.sessionCookieVerifier.setAlgorithm(algorithm); + } } @@ -752,7 +776,7 @@ export class TenantAwareAuth extends BaseAuth { * @constructor */ constructor(app: FirebaseApp, tenantId: string) { - const cryptoSigner = cryptoSignerFromApp(app); + const cryptoSigner = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app); const tokenGenerator = new FirebaseTokenGenerator(cryptoSigner, tenantId); super(app, new TenantAwareAuthRequestHandler(app, tenantId), tokenGenerator); utils.addReadonlyGetter(this, 'tenantId', tenantId); @@ -868,3 +892,15 @@ export class Auth extends BaseAuth implements FirebaseServic return this.tenantManager_; } } + +/** + * When true the SDK should communicate with the Auth Emulator for all API + * calls and also produce unsigned tokens. + * + * This alone does NOT short-circuit ID Token verification. + * For security reasons that must be explicitly disabled through + * setJwtVerificationEnabled(false); + */ +function useEmulator(): boolean { + return !!process.env.FIREBASE_AUTH_EMULATOR_HOST; +} diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 194ed48cbf..8f6850f6c6 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -21,9 +21,12 @@ import { AuthorizedHttpClient, HttpError, HttpRequestConfig, HttpClient } from ' import * as validator from '../utils/validator'; import { toWebSafeBase64 } from '../utils'; +import { Algorithm } from 'jsonwebtoken'; -const ALGORITHM_RS256 = 'RS256'; +const ALGORITHM_RS256: Algorithm = 'RS256' as const; +const ALGORITHM_NONE: Algorithm = 'none' as const; + const ONE_HOUR_IN_SECONDS = 60 * 60; // List of blacklisted claims which cannot be provided when creating a custom token @@ -39,6 +42,12 @@ const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identit * CryptoSigner interface represents an object that can be used to sign JWTs. */ export interface CryptoSigner { + + /** + * The name of the signing algorithm. + */ + readonly algorithm: Algorithm; + /** * Cryptographically signs a buffer of data. * @@ -82,6 +91,8 @@ interface JWTBody { * sign data. Performs all operations locally, and does not make any RPC calls. */ export class ServiceAccountSigner implements CryptoSigner { + + algorithm = ALGORITHM_RS256; /** * Creates a new CryptoSigner instance from the given service account credential. @@ -124,6 +135,8 @@ export class ServiceAccountSigner implements CryptoSigner { * @see https://cloud.google.com/compute/docs/storing-retrieving-metadata */ export class IAMSigner implements CryptoSigner { + algorithm = ALGORITHM_RS256; + private readonly httpClient: AuthorizedHttpClient; private serviceAccountId?: string; @@ -215,6 +228,29 @@ export class IAMSigner implements CryptoSigner { } } +/** + * A CryptoSigner implementation that is used when communicating with the Auth emulator. + * It produces unsigned tokens. + */ +export class EmulatedSigner implements CryptoSigner { + + algorithm = ALGORITHM_NONE; + + /** + * @inheritDoc + */ + public sign(_: Buffer): Promise { + return Promise.resolve(Buffer.from('')); + } + + /** + * @inheritDoc + */ + public getAccountId(): Promise { + return Promise.resolve('firebase-auth-emulator@example.com'); + } +} + /** * Create a new CryptoSigner instance for the given app. If the app has been initialized with a service * account credential, creates a ServiceAccountSigner. Otherwise creates an IAMSigner. @@ -250,7 +286,7 @@ export class FirebaseTokenGenerator { 'INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator.', ); } - if (typeof tenantId !== 'undefined' && !validator.isNonEmptyString(tenantId)) { + if (typeof this.tenantId !== 'undefined' && !validator.isNonEmptyString(this.tenantId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, '`tenantId` argument must be a non-empty string.'); @@ -298,7 +334,7 @@ export class FirebaseTokenGenerator { } return this.signer.getAccountId().then((account) => { const header: JWTHeader = { - alg: ALGORITHM_RS256, + alg: this.signer.algorithm, typ: 'JWT', }; const iat = Math.floor(Date.now() / 1000); @@ -319,6 +355,7 @@ export class FirebaseTokenGenerator { } const token = `${this.encodeSegment(header)}.${this.encodeSegment(body)}`; const signPromise = this.signer.sign(Buffer.from(token)); + return Promise.all([token, signPromise]); }).then(([token, signature]) => { return `${token}.${this.encodeSegment(signature)}`; diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index 1f8e3e825a..768cc5bb7b 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -75,9 +75,10 @@ export class FirebaseTokenVerifier { private publicKeysExpireAt: number; private readonly shortNameArticle: string; - constructor(private clientCertUrl: string, private algorithm: string, + constructor(private clientCertUrl: string, private algorithm: jwt.Algorithm, private issuer: string, private tokenInfo: FirebaseTokenInfo, private readonly app: FirebaseApp) { + if (!validator.isURL(clientCertUrl)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -150,6 +151,14 @@ export class FirebaseTokenVerifier { }); } + /** + * Override the JWT signing algorithm. + * @param algorithm the new signing algorithm. + */ + public setAlgorithm(algorithm: jwt.Algorithm): void { + this.algorithm = algorithm; + } + private verifyJWTWithProjectId(jwtToken: string, projectId: string | null): Promise { if (!validator.isNonEmptyString(projectId)) { throw new FirebaseAuthError( @@ -175,7 +184,7 @@ export class FirebaseTokenVerifier { if (!fullDecodedToken) { errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed the entire string JWT ` + `which represents ${this.shortNameArticle} ${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; - } else if (typeof header.kid === 'undefined') { + } else if (typeof header.kid === 'undefined' && this.algorithm !== 'none') { const isCustomToken = (payload.aud === FIREBASE_AUDIENCE); const isLegacyCustomToken = (header.alg === 'HS256' && payload.v === 0 && 'd' in payload && 'uid' in payload.d); @@ -213,6 +222,12 @@ export class FirebaseTokenVerifier { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); } + // When the algorithm is set to 'none' there will be no signature and therefore we don't check + // the public keys. + if (this.algorithm === 'none') { + return this.verifyJwtSignatureWithKey(jwtToken, null); + } + return this.fetchPublicKeys().then((publicKeys) => { if (!Object.prototype.hasOwnProperty.call(publicKeys, header.kid)) { return Promise.reject( @@ -237,13 +252,13 @@ export class FirebaseTokenVerifier { * @return {Promise} A promise that resolves with the decoded JWT claims on successful * verification. */ - private verifyJwtSignatureWithKey(jwtToken: string, publicKey: string): Promise { + private verifyJwtSignatureWithKey(jwtToken: string, publicKey: string | null): Promise { const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; return new Promise((resolve, reject) => { - jwt.verify(jwtToken, publicKey, { + jwt.verify(jwtToken, publicKey || "", { algorithms: [this.algorithm], - }, (error: jwt.VerifyErrors, decodedToken: string | object) => { + }, (error: jwt.VerifyErrors | null, decodedToken: object | undefined) => { if (error) { if (error.name === 'TokenExpiredError') { const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + @@ -256,19 +271,9 @@ export class FirebaseTokenVerifier { } return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message)); } else { - // TODO(rsgowman): I think the typing on jwt.verify is wrong. It claims that this can be either a string or an - // object, but the code always seems to call it as an object. Investigate and upstream typing changes if this - // is actually correct. - if (typeof decodedToken === 'string') { - return reject(new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - "Unexpected decodedToken. Expected an object but got a string: '" + decodedToken + "'", - )); - } else { - const decodedIdToken = (decodedToken as DecodedIdToken); - decodedIdToken.uid = decodedIdToken.sub; - resolve(decodedIdToken); - } + const decodedIdToken = (decodedToken as DecodedIdToken); + decodedIdToken.uid = decodedIdToken.sub; + resolve(decodedIdToken); } }); }); @@ -343,7 +348,7 @@ export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier { ALGORITHM_RS256, 'https://securetoken.google.com/', ID_TOKEN_INFO, - app, + app ); } @@ -359,6 +364,6 @@ export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVeri ALGORITHM_RS256, 'https://session.firebase.google.com/', SESSION_COOKIE_INFO, - app, + app ); } diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index d8a75aff71..caf919e192 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -30,7 +30,7 @@ import { FirebaseApp, FirebaseAppOptions } from '../../src/firebase-app'; import { Credential, GoogleOAuthAccessToken } from '../../src/credential/credential-interfaces'; import { ServiceAccountCredential } from '../../src/credential/credential-internal'; -const ALGORITHM = 'RS256'; +const ALGORITHM = 'RS256' as const; const ONE_HOUR_IN_SECONDS = 60 * 60; export const uid = 'someUid'; @@ -181,9 +181,10 @@ export const x509CertPairs = [ * Generates a mocked Firebase ID token. * * @param {object} overrides Overrides for the generated token's attributes. + * @param {object} claims Extra claims to add to the token. * @return {string} A mocked Firebase ID token with any provided overrides included. */ -export function generateIdToken(overrides?: object): string { +export function generateIdToken(overrides?: object, claims?: object): string { const options = _.assign({ audience: projectId, expiresIn: ONE_HOUR_IN_SECONDS, @@ -195,7 +196,12 @@ export function generateIdToken(overrides?: object): string { }, }, overrides); - return jwt.sign(developerClaims, certificateObject.private_key, options); + const payload = { + ...developerClaims, + ...claims, + }; + + return jwt.sign(payload, certificateObject.private_key, options); } /** diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 7fb5715eb7..e51d0da2c8 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -3192,5 +3192,66 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); } + + describe('auth emulator support', () => { + + let mockAuth = testConfig.init(mocks.app()); + + beforeEach(() => { + process.env.FIREBASE_AUTH_EMULATOR_HOST = 'localhost:9099'; + mockAuth = testConfig.init(mocks.app()); + }); + + afterEach(() => { + delete process.env.FIREBASE_AUTH_EMULATOR_HOST; + }); + + it('createCustomToken() generates an unsigned token', async () => { + const token = await mockAuth.createCustomToken('uid1'); + + // Check the decoded token has the right algorithm + const decoded = jwt.decode(token, { complete: true }); + expect(decoded).to.have.property('header').that.has.property('alg', 'none'); + expect(decoded).to.have.property('payload').that.has.property('uid', 'uid1'); + + // Make sure this doesn't throw + jwt.verify(token, '', { algorithms: ['none'] }); + }); + + it('verifyIdToken() rejects an unsigned token when only the env var is set', async () => { + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none' + }); + + await expect(mockAuth.verifyIdToken(unsignedToken)) + .to.be.rejectedWith('Firebase ID token has incorrect algorithm. Expected "RS256"'); + }); + + it('verifyIdToken() accepts an unsigned token when private method is called and env var is set', async () => { + (mockAuth as any).setJwtVerificationEnabled(false); + + let claims = {}; + if (testConfig.Auth === TenantAwareAuth) { + claims = { + firebase: { + tenant: TENANT_ID + } + } + } + + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none' + }, claims); + + const decoded = await mockAuth.verifyIdToken(unsignedToken); + expect(decoded).to.be.ok; + }); + + it('private method throws when env var is unset', async () => { + delete process.env.FIREBASE_AUTH_EMULATOR_HOST; + await expect(() => (mockAuth as any).setJwtVerificationEnabled(false)) + .to.throw('This method is only available when connected to the Authentication emulator.') + }); + }); }); }); diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index e7e5dbaaca..4164d008be 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -25,7 +25,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../../resources/mocks'; import { - BLACKLISTED_CLAIMS, FirebaseTokenGenerator, ServiceAccountSigner, IAMSigner, + BLACKLISTED_CLAIMS, FirebaseTokenGenerator, ServiceAccountSigner, IAMSigner, EmulatedSigner } from '../../../src/auth/token-generator'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; @@ -308,6 +308,29 @@ describe('FirebaseTokenGenerator', () => { tokenGenerator: new FirebaseTokenGenerator(new ServiceAccountSigner(cert), tenantId), }]; + describe('Emulator', () => { + const signer = new EmulatedSigner(); + const tokenGenerator = new FirebaseTokenGenerator(signer); + + it('should generate a valid unsigned token', async () => { + const uid = 'uid123'; + const claims = { foo: 'bar' }; + const token = await tokenGenerator.createCustomToken(uid, claims); + + // Check that verify doesn't throw + // Note: the types for jsonwebtoken are wrong so we have to disguise the 'null' + jwt.verify(token, '', { algorithms: ['none'] }); + + // Decode and check all three segments + const { header, payload, signature } = jwt.decode(token, { complete: true }) as { [key: string]: any }; + expect(header).to.deep.equal({ alg: 'none', typ: 'JWT' }); + expect(payload['uid']).to.equal(uid); + expect(payload['claims']).to.deep.equal(claims); + expect(signature).to.equal(''); + }); + + }); + tokenGeneratorConfigs.forEach((tokenGeneratorConfig) => { describe(tokenGeneratorConfig.name, () => { const tokenGenerator = tokenGeneratorConfig.tokenGenerator; diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 5a9749851c..7aae3255b8 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -34,6 +34,7 @@ import * as verifier from '../../../src/auth/token-verifier'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { AuthClientErrorCode } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; +import { Algorithm } from 'jsonwebtoken'; chai.should(); chai.use(sinonChai); @@ -104,6 +105,20 @@ function mockFailedFetchPublicKeys(): nock.Scope { .replyWithError('message'); } +function createTokenVerifier( + app: FirebaseApp, + options: { algorithm?: Algorithm } = {} +): verifier.FirebaseTokenVerifier { + const algorithm = options.algorithm || "RS256"; + return new verifier.FirebaseTokenVerifier( + 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', + algorithm, + 'https://securetoken.google.com/', + verifier.ID_TOKEN_INFO, + app + ); +} + describe('FirebaseTokenVerifier', () => { let app: FirebaseApp; @@ -116,13 +131,7 @@ describe('FirebaseTokenVerifier', () => { app = mocks.app(); const cert = new ServiceAccountCredential(mocks.certificateObject); tokenGenerator = new FirebaseTokenGenerator(new ServiceAccountSigner(cert)); - tokenVerifier = new verifier.FirebaseTokenVerifier( - 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', - 'RS256', - 'https://securetoken.google.com/', - verifier.ID_TOKEN_INFO, - app, - ); + tokenVerifier = createTokenVerifier(app); httpsSpy = sinon.spy(https, 'request'); }); @@ -535,6 +544,55 @@ describe('FirebaseTokenVerifier', () => { }); }); + it('should decode an unsigned token when the algorithm is set to none (emulator)', async () => { + clock = sinon.useFakeTimers(1000); + + const emulatorVerifier = createTokenVerifier(app, { algorithm: 'none' }); + const mockIdToken = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }); + + const decoded = await emulatorVerifier.verifyJWT(mockIdToken); + expect(decoded).to.deep.equal({ + one: 'uno', + two: 'dos', + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: mocks.projectId, + iss: 'https://securetoken.google.com/' + mocks.projectId, + sub: mocks.uid, + uid: mocks.uid, + }); + }); + + it('should not decode a signed token when the algorithm is set to none (emulator)', async () => { + clock = sinon.useFakeTimers(1000); + + const emulatorVerifier = createTokenVerifier(app, { algorithm: 'none' }); + const mockIdToken = mocks.generateIdToken(); + + await emulatorVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has incorrect algorithm. Expected "none"'); + }); + + it('should not decode an unsigned token when the algorithm is not overridden (emulator)', async () => { + clock = sinon.useFakeTimers(1000); + + const idTokenNoAlg = mocks.generateIdToken({ + algorithm: 'none', + }); + await tokenVerifier.verifyJWT(idTokenNoAlg) + .should.eventually.be.rejectedWith('Firebase ID token has incorrect algorithm.'); + + const idTokenNoHeader = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }); + await tokenVerifier.verifyJWT(idTokenNoHeader) + .should.eventually.be.rejectedWith('Firebase ID token has no "kid" claim.'); + }); + it('should use the given HTTP Agent', () => { const agent = new https.Agent(); const appWithAgent = mocks.appWithOptions({ From 4aba84a24373f8f4ca0090b4bb5fb9dbfa499436 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2020 14:10:46 -0700 Subject: [PATCH 046/160] build(deps): bump @actions/core in /.github/actions/send-tweet (#1052) Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 1.2.2 to 1.2.6. - [Release notes](https://github.com/actions/toolkit/releases) - [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md) - [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/send-tweet/package-lock.json | 6 +++--- .github/actions/send-tweet/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/send-tweet/package-lock.json b/.github/actions/send-tweet/package-lock.json index e59bab6c1d..6caa0a69f5 100644 --- a/.github/actions/send-tweet/package-lock.json +++ b/.github/actions/send-tweet/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@actions/core": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.2.tgz", - "integrity": "sha512-IbCx7oefq+Gi6FWbSs2Fnw8VkEI6Y4gvjrYprY3RV//ksq/KPMlClOerJ4jRosyal6zkUIc8R9fS/cpRMlGClg==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", + "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" }, "@zeit/ncc": { "version": "0.21.1", diff --git a/.github/actions/send-tweet/package.json b/.github/actions/send-tweet/package.json index 0f22b3fa1d..440567dc26 100644 --- a/.github/actions/send-tweet/package.json +++ b/.github/actions/send-tweet/package.json @@ -14,7 +14,7 @@ "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", "dependencies": { - "@actions/core": "^1.2.2", + "@actions/core": "^1.2.6", "twitter": "^1.7.1" }, "devDependencies": { From 21ff4bab6be84a7be2dab36ea70df39539957cde Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 22 Oct 2020 16:09:03 -0400 Subject: [PATCH 047/160] [chore] Release 9.3.0 (#1070) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c63c8ab3b..d292e6f6c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.2.0", + "version": "9.3.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From f0f720d0248b976dc73c80a2509442dc351c8393 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 23 Oct 2020 11:25:20 -0700 Subject: [PATCH 048/160] fix: Upgraded dev dependency on yargs (#1073) --- package-lock.json | 280 ++++++++++++++++++++++++++++++---------------- package.json | 2 +- 2 files changed, 187 insertions(+), 95 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b4098dc78..189d92aa98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.2.0", + "version": "9.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -558,7 +558,7 @@ }, "@types/firebase-token-generator": { "version": "2.0.28", - "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", + "resolved": "http://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", "integrity": "sha1-Z1VIHZMk4mt6XItFXWgUg3aCw5Y=", "dev": true }, @@ -1284,7 +1284,7 @@ }, "binaryextensions": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", "dev": true }, @@ -2396,6 +2396,12 @@ "es6-symbol": "^3.1.1" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -2990,7 +2996,7 @@ }, "firebase-token-generator": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", "integrity": "sha1-l2fXWewTq9yZuhFf1eqZ2Lk9EgY=", "dev": true }, @@ -4598,7 +4604,7 @@ }, "istextorbinary": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", "dev": true, "requires": { @@ -5895,21 +5901,65 @@ "yargs-parser": "^13.0.0" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, "find-up": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { "locate-path": "^3.0.0" } }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -5926,18 +5976,85 @@ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, "yargs-parser": { "version": "13.1.2", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", @@ -6263,7 +6380,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -6419,7 +6536,7 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, @@ -6965,7 +7082,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -7766,7 +7883,7 @@ }, "textextensions": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=", "dev": true }, @@ -8606,70 +8723,61 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.1.0.tgz", + "integrity": "sha512-upWFJOmDdHN0syLuESuvXDmrRcWd1QafJolHskzaw79uZa7/x53gxQKiR07W59GWY1tFhhU/Th9DrtSfpS782g==", "dev": true, "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" + "string-width": "^4.2.0", + "y18n": "^5.0.2", + "yargs-parser": "^20.2.2" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.3.tgz", + "integrity": "sha512-Gj3QHTkVMPKqwP3f7B4KPkBZRMR9r4rfi5bXFpg1a+Svvj8l7q5CnkBkVQzfxT5DFSsGk2+PascOgL0JYkL2kw==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "color-name": "~1.1.4" } }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -8677,69 +8785,53 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.4.tgz", + "integrity": "sha512-deLOfD+RvFgrpAmSZgfGdWYE+OKyHcVHaRQ7NphG/63scpRvTHHeQMAxGGvaLVGJ+HYVcCXlzcTK0ZehFf+eHQ==", "dev": true }, "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.3.tgz", + "integrity": "sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww==", + "dev": true } } }, diff --git a/package.json b/package.json index d292e6f6c4..1dea780c1f 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,6 @@ "ts-node": "^3.3.0", "typedoc": "^0.15.0", "typescript": "^3.7.3", - "yargs": "^13.2.2" + "yargs": "^16.0.0" } } From 5d72c1b40ef9383060d500e4f08678cb37ab8c0e Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 2 Nov 2020 12:03:25 -0800 Subject: [PATCH 049/160] chore: Auto-generating typings from the source (#1072) * chore: Removing ambient typing files (#1032) * chore: Removing ambient typing files * chore: Added some TODOs to keep track of WIP * chore: Added the credentials API signatures and exposed admin.credential (#1035) * chore: Added the credentials API signatures and exposed admin.credential * fix: Updated ServiceAccountCredential constructor signature * chore: Declared admin and admin.app namespaces (#1037) * chore: Added some TODOs to keep track of WIP * chore: Declared admin and admin.app namespaces * fix: Fixing an indentation issue * fix(rc): Exposed admin.remoteConfig namespace (#1038) * fix(fcm): Exposed admin.messaging namespace (#1039) * chore(iid): Exposed admin.instanceId namespace (#1046) * chore(iid): Exposed admin.instanceId namespace * fix: Fixing some bad indentation in comments * chore(rtdb): Exposed admin.database namespace (#1043) * chore(rules): Exposed admin.securiryRules namespace (#1050) * chore: Exposed admin.projectManagement namespace (#1054) * chore(auth): Exposed admin.auth namespace (#1053) * chore(auth): Exposed admin.auth namespace * fix(auth): Fixing unit tests for SAMLConfig * fix(auth): Removing more auth. prefixed direct references * fix(auth): Using CreateTenantRequest explicitly where appropriate * chore(ml): Exposed admin.machineLearning namespace (#1055) * chore(ml): Exposed admin.machineLearning namespace * fix(ml): Fixing some code formatting issues * chore: Exposed admin.storage and admin.firestore namespaces (#1056) * chore(ml): Exposed admin.machineLearning namespace * chore: Exposed admin.firestore and admin.storage namespaces * chore: Merged with upstream; Re-enabled build:tests CI task * chore: Using public API types in FirebaseNamespace impl (#1057) * chore: Updated TypeDoc workflow to generate API docs (#1060) * chore: Experimental typedoc pipeline for doc generation * chore: Adding RTDB type aliases to the API docs * Updated comments * Cleaned up the docgen script; Fixed a file name check warning * fix: Fixed cross links; Fixed a faulty regex replacement in links * fix: Fixing the remaining cross linkage errors * chore: Enabled quotes rule in eslint config (#1067) * chore: Enabled quotes rule in esline config * fix: Enabled avoidEscape option * fix: Fixed a typo in an error message --- .eslintrc.js | 15 +- docgen/content-sources/node/toc.yaml | 44 +- docgen/generate-docs.js | 86 +- gulpfile.js | 77 +- src/auth/action-code-settings-builder.ts | 16 +- src/auth/auth-api-request.ts | 67 +- src/auth/auth-config.ts | 146 +- src/auth/auth.ts | 124 +- src/auth/identifier.ts | 49 +- src/{auth.d.ts => auth/index.ts} | 366 ++-- src/auth/tenant-manager.ts | 16 +- src/auth/tenant.ts | 26 +- src/auth/token-generator.ts | 9 +- src/auth/token-verifier.ts | 41 +- src/auth/user-import-builder.ts | 112 +- src/auth/user-record.ts | 87 +- src/credential.d.ts | 149 -- src/credential/credential-interfaces.ts | 50 - src/credential/credential-internal.ts | 7 +- src/credential/credential.ts | 126 +- src/credential/index.ts | 171 +- src/database.d.ts | 1662 ----------------- src/database/database-internal.ts | 4 +- src/database/database.ts | 49 - src/database/index.ts | 101 +- src/firebase-app.ts | 72 +- src/firebase-namespace-api.ts | 266 +++ src/firebase-namespace.d.ts | 28 + src/firebase-namespace.ts | 153 +- src/firebase-service.ts | 4 +- src/firestore/index.ts | 20 +- src/index.d.ts | 932 +-------- src/instance-id.d.ts | 36 - src/instance-id/index.ts | 86 +- src/instance-id/instance-id.ts | 6 +- src/machine-learning/index.ts | 282 +++ .../machine-learning-api-client.ts | 36 +- src/machine-learning/machine-learning.ts | 41 +- src/messaging.d.ts | 1340 ------------- src/messaging/index.ts | 1378 +++++++++++++- .../messaging-api-request-internal.ts | 5 +- src/messaging/messaging-internal.ts | 24 +- src/messaging/messaging-types.ts | 1109 ----------- src/messaging/messaging.ts | 56 +- src/project-management.d.ts | 360 ---- src/project-management/android-app.ts | 17 +- src/project-management/app-metadata.ts | 130 -- src/project-management/index.ts | 416 ++++- src/project-management/ios-app.ts | 10 +- ...project-management-api-request-internal.ts | 8 +- src/project-management/project-management.ts | 18 +- src/remote-config.d.ts | 349 ---- src/remote-config/index.ts | 414 +++- .../remote-config-api-client-internal.ts | 18 +- src/remote-config/remote-config-api-client.ts | 284 --- src/remote-config/remote-config.ts | 45 +- src/security-rules.d.ts | 191 -- src/security-rules/index.ts | 241 ++- src/security-rules/security-rules.ts | 60 +- src/storage.d.ts | 39 - src/storage/index.ts | 55 +- src/storage/storage.ts | 6 +- src/utils/error.ts | 8 +- src/utils/index.ts | 21 +- test/integration/auth.spec.ts | 9 +- test/integration/machine-learning.spec.ts | 13 +- test/integration/remote-config.spec.ts | 4 +- test/integration/setup.ts | 4 +- test/resources/mocks.ts | 24 +- test/unit/auth/auth-api-request.spec.ts | 98 +- test/unit/auth/auth-config.spec.ts | 17 +- test/unit/auth/auth.spec.ts | 18 +- test/unit/auth/tenant-manager.spec.ts | 13 +- test/unit/auth/tenant.spec.ts | 32 +- test/unit/auth/token-generator.spec.ts | 6 +- test/unit/auth/token-verifier.spec.ts | 2 +- test/unit/auth/user-import-builder.spec.ts | 48 +- test/unit/credential/credential.spec.ts | 6 +- test/unit/database/database.spec.ts | 4 +- test/unit/firebase-app.spec.ts | 45 +- test/unit/firebase-namespace.spec.ts | 123 +- test/unit/firebase.spec.ts | 13 +- .../machine-learning-api-client.spec.ts | 21 +- .../machine-learning/machine-learning.spec.ts | 13 +- test/unit/messaging/batch-requests.spec.ts | 8 +- test/unit/messaging/messaging.spec.ts | 37 +- .../project-management/android-app.spec.ts | 7 +- test/unit/project-management/ios-app.spec.ts | 7 +- .../project-management-api-request.spec.ts | 4 +- .../project-management.spec.ts | 7 +- .../remote-config-api-client.spec.ts | 35 +- test/unit/remote-config/remote-config.spec.ts | 34 +- .../security-rules-api-client.spec.ts | 16 +- test/unit/utils.ts | 9 +- test/unit/utils/index.spec.ts | 8 +- tsconfig.eslint.json | 12 - tsconfig.json | 1 + 97 files changed, 4435 insertions(+), 8427 deletions(-) rename src/{auth.d.ts => auth/index.ts} (86%) delete mode 100644 src/credential.d.ts delete mode 100644 src/credential/credential-interfaces.ts delete mode 100644 src/database.d.ts delete mode 100644 src/database/database.ts create mode 100644 src/firebase-namespace-api.ts create mode 100644 src/firebase-namespace.d.ts delete mode 100644 src/instance-id.d.ts create mode 100644 src/machine-learning/index.ts delete mode 100644 src/messaging.d.ts delete mode 100644 src/messaging/messaging-types.ts delete mode 100644 src/project-management.d.ts delete mode 100644 src/project-management/app-metadata.ts delete mode 100644 src/remote-config.d.ts delete mode 100644 src/remote-config/remote-config-api-client.ts delete mode 100644 src/security-rules.d.ts delete mode 100644 src/storage.d.ts delete mode 100644 tsconfig.eslint.json diff --git a/.eslintrc.js b/.eslintrc.js index 11dc9d2a80..e44e804d89 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,7 +33,6 @@ module.exports = { // Disabled checks '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/no-use-before-define': 0, - '@typescript-eslint/no-unused-vars': 0, // Required checks 'indent': ['error', 2], @@ -45,7 +44,7 @@ module.exports = { 'ignoreUrls': true } ], - "object-curly-spacing": [2, "always"], + 'object-curly-spacing': [2, 'always'], '@typescript-eslint/explicit-function-return-type': [ 'error', { @@ -54,12 +53,8 @@ module.exports = { 'allowHigherOrderFunctions': true } ], - '@typescript-eslint/no-unused-vars-experimental': 2, - }, - // Required by the @typescript-eslint/no-unused-vars-experimental rule. - // We use a separate tsconfig file for linting to reduce the time complexity of the operation. - // See github.com/typescript-eslint/typescript-eslint/issues/1320 - parserOptions: { - project: './tsconfig.eslint.json', - }, + 'no-unused-vars': 'off', // Must be disabled to enable the next rule + '@typescript-eslint/no-unused-vars': ['error'], + 'quotes': ['error', 'single', {'avoidEscape': true}] + } }; diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index ee0071f5f3..a563b7417a 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -17,13 +17,13 @@ toc: path: /docs/reference/admin/node/admin.app section: - title: "App" - path: /docs/reference/admin/node/admin.app.App + path: /docs/reference/admin/node/admin.app.App-1 - title: "admin.auth" path: /docs/reference/admin/node/admin.auth section: - title: "Auth" - path: /docs/reference/admin/node/admin.auth.Auth + path: /docs/reference/admin/node/admin.auth.Auth-1 - title: "ActionCodeSettings" path: /docs/reference/admin/node/admin.auth.ActionCodeSettings - title: "AuthProviderConfig" @@ -36,6 +36,8 @@ toc: path: /docs/reference/admin/node/admin.auth.CreatePhoneMultiFactorInfoRequest - title: "CreateRequest" path: /docs/reference/admin/node/admin.auth.CreateRequest + - title: "EmailSignInProviderConfig" + path: /docs/reference/admin/node/admin.auth.EmailSignInProviderConfig - title: "ListProviderConfigResults" path: /docs/reference/admin/node/admin.auth.ListProviderConfigResults - title: "ListTenantsResult" @@ -115,25 +117,13 @@ toc: path: /docs/reference/admin/node/admin.credential section: - title: "Credential" - path: /docs/reference/admin/node/admin.credential.Credential + path: /docs/reference/admin/node/admin.credential.Credential-1 - title: "admin.database" path: /docs/reference/admin/node/admin.database section: - title: "Database" - path: /docs/reference/admin/node/admin.database.Database - - title: "DataSnapshot" - path: /docs/reference/admin/node/admin.database.DataSnapshot - - title: "OnDisconnect" - path: /docs/reference/admin/node/admin.database.OnDisconnect - - title: "Query" - path: /docs/reference/admin/node/admin.database.Query - - title: "Reference" - path: /docs/reference/admin/node/admin.database.Reference - - title: "ServerValue" - path: /docs/reference/admin/node/admin.database.ServerValue - - title: "ThenableReference" - path: /docs/reference/admin/node/admin.database.ThenableReference + path: /docs/reference/admin/node/admin.database.Database-1 - title: "admin.firestore" path: /docs/reference/admin/node/admin.firestore @@ -142,7 +132,7 @@ toc: path: /docs/reference/admin/node/admin.instanceId section: - title: "InstanceId" - path: /docs/reference/admin/node/admin.instanceId.InstanceId + path: /docs/reference/admin/node/admin.instanceId.InstanceId-1 - title: "admin.machineLearning" path: /docs/reference/admin/node/admin.machineLearning @@ -152,7 +142,7 @@ toc: - title: "ListModelsResult" path: /docs/reference/admin/node/admin.machineLearning.ListModelsResult - title: "MachineLearning" - path: /docs/reference/admin/node/admin.machineLearning.MachineLearning + path: /docs/reference/admin/node/admin.machineLearning.MachineLearning-1 - title: "Model" path: /docs/reference/admin/node/admin.machineLearning.Model - title: "ModelOptionsBase" @@ -167,12 +157,14 @@ toc: - title: "admin.messaging" path: /docs/reference/admin/node/admin.messaging section: + - title: "BaseMessage" + path: /docs/reference/admin/node/admin.messaging.BaseMessage - title: "TopicMessage" - path: /docs/reference/admin/node/TopicMessage + path: /docs/reference/admin/node/admin.messaging.TopicMessage - title: "TokenMessage" - path: /docs/reference/admin/node/TokenMessage + path: /docs/reference/admin/node/admin.messaging.TokenMessage - title: "ConditionMessage" - path: /docs/reference/admin/node/ConditionMessage + path: /docs/reference/admin/node/admin.messaging.ConditionMessage - title: "AndroidConfig" path: /docs/reference/admin/node/admin.messaging.AndroidConfig - title: "AndroidFcmOptions" @@ -184,7 +176,7 @@ toc: - title: "LightSettings" path: /docs/reference/admin/node/admin.messaging.LightSettings - title: "Messaging" - path: /docs/reference/admin/node/admin.messaging.Messaging + path: /docs/reference/admin/node/admin.messaging.Messaging-1 - title: "MessagingConditionResponse" path: /docs/reference/admin/node/admin.messaging.MessagingConditionResponse - title: "MessagingDeviceGroupResponse" @@ -248,7 +240,7 @@ toc: - title: "IosAppMetadata" path: /docs/reference/admin/node/admin.projectManagement.IosAppMetadata - title: "ProjectManagement" - path: /docs/reference/admin/node/admin.projectManagement.ProjectManagement + path: /docs/reference/admin/node/admin.projectManagement.ProjectManagement-1 - title: "ShaCertificate" path: /docs/reference/admin/node/admin.projectManagement.ShaCertificate @@ -264,19 +256,19 @@ toc: - title: "RulesetMetadataList" path: /docs/reference/admin/node/admin.securityRules.RulesetMetadataList - title: "SecurityRules" - path: /docs/reference/admin/node/admin.securityRules.SecurityRules + path: /docs/reference/admin/node/admin.securityRules.SecurityRules-1 - title: "admin.storage" path: /docs/reference/admin/node/admin.storage section: - title: "Storage" - path: /docs/reference/admin/node/admin.storage.Storage + path: /docs/reference/admin/node/admin.storage.Storage-1 - title: "admin.remoteConfig" path: /docs/reference/admin/node/admin.remoteConfig section: - title: "RemoteConfig" - path: /docs/reference/admin/node/admin.remoteConfig.RemoteConfig + path: /docs/reference/admin/node/admin.remoteConfig.RemoteConfig-1 - title: "RemoteConfigTemplate" path: /docs/reference/admin/node/admin.remoteConfig.RemoteConfigTemplate - title: "RemoteConfigParameter" diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js index bc04538bfb..4f8b8df736 100644 --- a/docgen/generate-docs.js +++ b/docgen/generate-docs.js @@ -25,10 +25,16 @@ const yaml = require('js-yaml'); const repoPath = path.resolve(`${__dirname}/..`); +const defaultSources = [ + `${repoPath}/lib/firebase-namespace.d.ts`, + `${repoPath}/lib/firebase-namespace-api.d.ts`, + `${repoPath}/lib/**/*.d.ts`, +]; + // Command-line options. const { source: sourceFile } = yargs .option('source', { - default: `${repoPath}/src/*.d.ts`, + default: defaultSources.join(' '), describe: 'Typescript source file(s)', type: 'string' }) @@ -51,6 +57,17 @@ const firestoreHeader = `
    `; const firestoreFooter = '\n
\n
\n'; +const databaseExcludes = ['enableLogging']; +const databaseHtmlPath = `${docPath}/admin.database.html`; +const databaseHeader = `
+

Type aliases

+
+

Following types are defined in the @firebase/database package + and re-exported from this namespace for convenience.

+
+
    `; +const databaseFooter = '\n
\n
\n'; + /** * Strips path prefix and returns only filename. * @param {string} path @@ -99,7 +116,7 @@ function fixLinks(file) { .replace(/(modules|interfaces|classes|enums)\//g, ''); let caseFixedLinks = flattenedLinks; for (const lower in lowerToUpperLookup) { - const re = new RegExp(lower, 'g'); + const re = new RegExp('\\b' + lower, 'g'); caseFixedLinks = caseFixedLinks.replace(re, lowerToUpperLookup[lower]); } return fs.writeFile(file, caseFixedLinks); @@ -119,7 +136,7 @@ function generateTempHomeMdFile(tocRaw, homeRaw) { const { toc } = yaml.safeLoad(tocRaw); let tocPageLines = [homeRaw, '# API Reference']; toc.forEach(group => { - tocPageLines.push(`\n## [${group.title}](${stripPath(group.path)})`); + tocPageLines.push(`\n## [${group.title}](${stripPath(group.path)}.html)`); const section = group.section || []; section.forEach(item => { tocPageLines.push(`- [${item.title}](${stripPath(item.path)}.html)`); @@ -150,13 +167,13 @@ function checkForMissingFilesAndFixFilenameCase() { // Preferred filename for devsite should be capitalized and taken from // toc.yaml. const tocFilePath = `${docPath}/${filename}.html`; - // Generated filename from Typedoc will be lowercase. - const generatedFilePath = `${docPath}/${filename.toLowerCase()}.html`; + // Generated filename from Typedoc will be lowercase and won't have the admin prefix. + const generatedFilePath = `${docPath}/${filename.toLowerCase().replace('admin.', '')}.html`; return fs.exists(generatedFilePath).then(exists => { if (exists) { // Store in a lookup table for link fixing. lowerToUpperLookup[ - `${filename.toLowerCase()}.html` + `${filename.toLowerCase().replace('admin.', '')}.html` ] = `${filename}.html`; return fs.rename(generatedFilePath, tocFilePath); } else { @@ -167,6 +184,7 @@ function checkForMissingFilesAndFixFilenameCase() { } }); }); + return Promise.all(fileCheckPromises).then(() => filenames); } @@ -253,15 +271,16 @@ function fixAllLinks(htmlFiles) { * Updates the auto-generated Firestore API references page, by appending * the specified HTML content block. * + * @param {string} htmlPath Path of the HTML file to update. * @param {string} contentBlock The HTML content block to be added to the Firestore docs. */ -function updateFirestoreHtml(contentBlock) { - const dom = new jsdom.JSDOM(fs.readFileSync(firestoreHtmlPath)); +function updateHtml(htmlPath, contentBlock) { + const dom = new jsdom.JSDOM(fs.readFileSync(htmlPath)); const contentNode = dom.window.document.body.querySelector('.col-12'); const newSection = new jsdom.JSDOM(contentBlock); contentNode.appendChild(newSection.window.document.body.firstChild); - fs.writeFileSync(firestoreHtmlPath, dom.window.document.documentElement.outerHTML); + fs.writeFileSync(htmlPath, dom.window.document.documentElement.outerHTML); } /** @@ -272,7 +291,7 @@ function updateFirestoreHtml(contentBlock) { */ function addFirestoreTypeAliases() { return new Promise((resolve, reject) => { - const fileStream = fs.createReadStream(`${repoPath}/src/index.d.ts`); + const fileStream = fs.createReadStream(`${repoPath}/lib/firestore/index.d.ts`); fileStream.on('error', (err) => { reject(err); }); @@ -297,7 +316,49 @@ function addFirestoreTypeAliases() { lineReader.on('close', () => { try { contentBlock += firestoreFooter; - updateFirestoreHtml(contentBlock); + updateHtml(firestoreHtmlPath, contentBlock); + resolve(); + } catch (err) { + reject(err); + } + }); + }); +} + +/** + * Adds RTDB type aliases to the auto-generated API docs. These are the + * types that are imported from the @firebase/database package, and + * then re-exported from the admin.database namespace. Typedoc currently + * does not handle these correctly, so we need this solution instead. + */ +function addDatabaseTypeAliases() { + return new Promise((resolve, reject) => { + const fileStream = fs.createReadStream(`${repoPath}/lib/database/index.d.ts`); + fileStream.on('error', (err) => { + reject(err); + }); + const lineReader = readline.createInterface({ + input: fileStream, + }); + + let contentBlock = databaseHeader; + lineReader.on('line', (line) => { + line = line.trim(); + if (line.startsWith('export import') && line.indexOf('rtdb.') >= 0) { + const typeName = line.split(' ')[2]; + if (databaseExcludes.indexOf(typeName) === -1) { + contentBlock += ` +
  • + ${typeName} +
  • `; + } + } + }); + + lineReader.on('close', () => { + try { + contentBlock += databaseFooter; + updateHtml(databaseHtmlPath, contentBlock); resolve(); } catch (err) { reject(err); @@ -349,6 +410,8 @@ Promise.all([ moveFilesToRoot('enums'), ]); }) + // Rename the globals file to be the top-level admin doc. + .then(() => fs.rename(`${docPath}/globals.html`, `${docPath}/admin.html`)) // Check for files listed in TOC that are missing and warn if so. // Not blocking. .then(checkForMissingFilesAndFixFilenameCase) @@ -366,6 +429,7 @@ Promise.all([ // Add local variable include line to index.html (to access current SDK // version number). .then(addFirestoreTypeAliases) + .then(addDatabaseTypeAliases) .then(() => { fs.readFile(`${docPath}/index.html`, 'utf8').then(data => { // String to include devsite local variables. diff --git a/gulpfile.js b/gulpfile.js index 4e585f3f79..20f59df0c2 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -19,17 +19,14 @@ /**************/ /* REQUIRES */ /**************/ -var fs = require('fs'); var _ = require('lodash'); var gulp = require('gulp'); var pkg = require('./package.json'); // File I/O -var fs = require('fs'); var ts = require('gulp-typescript'); var del = require('del'); var header = require('gulp-header'); -var replace = require('gulp-replace'); var filter = require('gulp-filter'); @@ -46,42 +43,15 @@ var paths = { '!test/integration/typescript/src/example*.ts', ], - databaseSrc: [ - 'src/**/*.js' - ], - build: 'lib/', - - curatedTypings: [ - 'src/*.d.ts', - '!src/credential.d.ts', - '!src/database.d.ts', - '!src/instance-id.d.ts', - '!src/messaging.d.ts', - '!src/project-management.d.ts', - '!src/remote-config.d.ts', - '!src/security-rules.d.ts', - '!src/storage.d.ts', - ], }; -const TEMPORARY_TYPING_EXCLUDES = [ - '!lib/default-namespace.d.ts', - '!lib/firebase-namespace.d.ts', - '!lib/firebase-app.d.ts', - '!lib/firebase-service.d.ts', - '!lib/auth/*.d.ts', - '!lib/machine-learning/*.d.ts', - '!lib/utils/*.d.ts', -]; - // Create a separate project for buildProject that overrides the rootDir. // This ensures that the generated production files are in their own root // rather than including both src and test in the lib dir. Declaration // is used by TypeScript to determine if auto-generated typings should be // emitted. -const declaration = process.env.TYPE_GENERATION_MODE === 'auto'; -var buildProject = ts.createProject('tsconfig.json', { rootDir: 'src', declaration }); +var buildProject = ts.createProject('tsconfig.json', { rootDir: 'src' }); var buildTest = ts.createProject('tsconfig.json'); @@ -111,19 +81,14 @@ gulp.task('compile', function() { // Add header .pipe(header(banner)); - // Exclude typings that are unintended (currently excludes all auto-generated - // typings, but as services are refactored to auto-generate typings this will - // change). Moreover, all *-internal.d.ts typings should not be exposed to - // developers as it denotes internally used types. - if (declaration) { - const configuration = [ - 'lib/**/*.js', - 'lib/**/*.d.ts', - '!lib/**/*-internal.d.ts', - ].concat(TEMPORARY_TYPING_EXCLUDES); + const configuration = [ + 'lib/**/*.js', + 'lib/**/index.d.ts', + 'lib/firebase-namespace-api.d.ts', + '!lib/utils/index.d.ts', + ]; - workflow = workflow.pipe(filter(configuration)); - } + workflow = workflow.pipe(filter(configuration)); // Write to build directory return workflow.pipe(gulp.dest(paths.build)) @@ -139,30 +104,14 @@ gulp.task('compile_test', function() { .pipe(buildTest()) }); -gulp.task('copyDatabase', function() { - return gulp.src(paths.databaseSrc) - // Add headers - .pipe(header(fs.readFileSync('third_party/database-license.txt', 'utf8'))) - .pipe(header(banner)) - - // Write to build directory - .pipe(gulp.dest(paths.build)) -}); - gulp.task('copyTypings', function() { - let workflow = gulp.src('src/*.d.ts') + return gulp.src(['src/index.d.ts', 'src/firebase-namespace.d.ts']) // Add header - .pipe(header(banner)); - - if (declaration) { - workflow = workflow.pipe(filter(paths.curatedTypings)); - } - - // Write to build directory - return workflow.pipe(gulp.dest(paths.build)) + .pipe(header(banner)) + .pipe(gulp.dest(paths.build)) }); -gulp.task('compile_all', gulp.series('compile', 'copyDatabase', 'copyTypings', 'compile_test')); +gulp.task('compile_all', gulp.series('compile', 'copyTypings', 'compile_test')); // Regenerates js every time a source file changes gulp.task('watch', function() { @@ -170,7 +119,7 @@ gulp.task('watch', function() { }); // Build task -gulp.task('build', gulp.series('cleanup', 'compile', 'copyDatabase', 'copyTypings')); +gulp.task('build', gulp.series('cleanup', 'compile', 'copyTypings')); // Default task gulp.task('default', gulp.series('build')); diff --git a/src/auth/action-code-settings-builder.ts b/src/auth/action-code-settings-builder.ts index f2fc01d605..14c212b6fe 100644 --- a/src/auth/action-code-settings-builder.ts +++ b/src/auth/action-code-settings-builder.ts @@ -16,21 +16,9 @@ import * as validator from '../utils/validator'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { auth } from './index'; -/** Defines the ActionCodeSettings interface. */ -export interface ActionCodeSettings { - url: string; - handleCodeInApp?: boolean; - iOS?: { - bundleId: string; - }; - android?: { - packageName: string; - installApp?: boolean; - minimumVersion?: string; - }; - dynamicLinkDomain?: string; -} +import ActionCodeSettings = auth.ActionCodeSettings; /** Defines the email action code server request. */ interface EmailActionCodeRequest { diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 81789d17e2..f57fdc3fcc 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -18,29 +18,42 @@ import * as validator from '../utils/validator'; import { deepCopy, deepExtend } from '../utils/deep-copy'; import { - UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, - isProviderIdentifier, UidIdentifier, EmailIdentifier, PhoneIdentifier, - ProviderIdentifier, + isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier } from './identifier'; import { FirebaseApp } from '../firebase-app'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError, } from '../utils/api-request'; -import { CreateRequest, UpdateRequest } from './user-record'; import { - UserImportBuilder, UserImportOptions, UserImportRecord, - UserImportResult, AuthFactorInfo, convertMultiFactorInfoToServerFormat, + UserImportBuilder, AuthFactorInfo, convertMultiFactorInfoToServerFormat, } from './user-import-builder'; import * as utils from '../utils/index'; -import { ActionCodeSettings, ActionCodeSettingsBuilder } from './action-code-settings-builder'; +import { ActionCodeSettingsBuilder } from './action-code-settings-builder'; import { SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, - OIDCConfigServerRequest, SAMLConfigServerRequest, AuthProviderConfig, - OIDCUpdateAuthProviderRequest, SAMLUpdateAuthProviderRequest, + OIDCConfigServerRequest, SAMLConfigServerRequest, } from './auth-config'; -import { Tenant, TenantOptions, TenantServerResponse } from './tenant'; - +import { Tenant, TenantServerResponse } from './tenant'; +import { auth } from './index'; + +import CreateRequest = auth.CreateRequest; +import UpdateRequest = auth.UpdateRequest; +import UserIdentifier = auth.UserIdentifier; +import UidIdentifier = auth.UidIdentifier; +import EmailIdentifier = auth.EmailIdentifier; +import PhoneIdentifier = auth.PhoneIdentifier; +import ProviderIdentifier = auth.ProviderIdentifier; +import UserImportOptions = auth.UserImportOptions; +import UserImportRecord = auth.UserImportRecord; +import UserImportResult = auth.UserImportResult; +import ActionCodeSettings = auth.ActionCodeSettings; +import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; +import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; +import OIDCUpdateAuthProviderRequest = auth.OIDCUpdateAuthProviderRequest; +import SAMLUpdateAuthProviderRequest = auth.SAMLUpdateAuthProviderRequest; +import CreateTenantRequest = auth.CreateTenantRequest; +import UpdateTenantRequest = auth.UpdateTenantRequest; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { @@ -132,7 +145,7 @@ class AuthResourceUrlBuilder { constructor(protected app: FirebaseApp, protected version: string = 'v1') { const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST; if (emulatorHost) { - this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, { + this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, { host: emulatorHost }); } else { @@ -199,7 +212,7 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { super(app, version); const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST if (emulatorHost) { - this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, { + this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, { host: emulatorHost }); } else { @@ -253,7 +266,7 @@ function validateAuthFactorInfo(request: AuthFactorInfo, writeOperationType: Wri !validator.isNonEmptyString(request.mfaEnrollmentId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_UID, - `The second factor "uid" must be a valid non-empty string.`, + 'The second factor "uid" must be a valid non-empty string.', ); } if (typeof request.displayName !== 'undefined' && @@ -269,7 +282,7 @@ function validateAuthFactorInfo(request: AuthFactorInfo, writeOperationType: Wri throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLMENT_TIME, `The second factor "enrollmentTime" for "${authFactorInfoIdentifier}" must be a valid ` + - `UTC date string.`); + 'UTC date string.'); } // Validate required fields depending on second factor type. if (typeof request.phoneInfo !== 'undefined') { @@ -278,14 +291,14 @@ function validateAuthFactorInfo(request: AuthFactorInfo, writeOperationType: Wri throw new FirebaseAuthError( AuthClientErrorCode.INVALID_PHONE_NUMBER, `The second factor "phoneNumber" for "${authFactorInfoIdentifier}" must be a non-empty ` + - `E.164 standard compliant identifier string.`); + 'E.164 standard compliant identifier string.'); } } else { // Invalid second factor. For example, a phone second factor may have been provided without // a phone number. A TOTP based second factor may require a secret key, etc. throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLED_FACTORS, - `MFAInfo object provided is invalid.`); + 'MFAInfo object provided is invalid.'); } } @@ -588,7 +601,7 @@ export const FIREBASE_AUTH_DOWNLOAD_ACCOUNT = new ApiSettings('/accounts:batchGe request.maxResults > MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not exceed ` + + 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE}.`, ); } @@ -729,14 +742,14 @@ export const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings('/accounts', 'POST if (typeof request.customAttributes !== 'undefined') { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `"customAttributes" cannot be set when creating a new user.`, + '"customAttributes" cannot be set when creating a new user.', ); } // signupNewUser does not support validSince. if (typeof request.validSince !== 'undefined') { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `"validSince" cannot be set when creating a new user.`, + '"validSince" cannot be set when creating a new user.', ); } // Throw error when tenantId is passed in POST body. @@ -839,7 +852,7 @@ const LIST_OAUTH_IDP_CONFIGS = new ApiSettings('/oauthIdpConfigs', 'GET') request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not exceed ` + + 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`, ); } @@ -902,7 +915,7 @@ const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET') request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not exceed ` + + 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`, ); } @@ -1546,7 +1559,7 @@ export abstract class AbstractAuthRequestHandler { * @return {Promise} A promise that resolves with the newly created OIDC * configuration. */ - public createOAuthIdpConfig(options: AuthProviderConfig): Promise { + public createOAuthIdpConfig(options: OIDCAuthProviderConfig): Promise { // Construct backend request. let request; try { @@ -1669,7 +1682,7 @@ export abstract class AbstractAuthRequestHandler { * @return {Promise} A promise that resolves with the newly created SAML * configuration. */ - public createInboundSamlConfig(options: AuthProviderConfig): Promise { + public createInboundSamlConfig(options: SAMLAuthProviderConfig): Promise { // Construct backend request. let request; try { @@ -1852,7 +1865,7 @@ const LIST_TENANTS = new ApiSettings('/tenants', 'GET') request.pageSize > MAX_LIST_TENANT_PAGE_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive non-zero number that does not exceed ` + + 'Required "maxResults" must be a positive non-zero number that does not exceed ' + `the allowed ${MAX_LIST_TENANT_PAGE_SIZE}.`, ); } @@ -1979,7 +1992,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { * @param {TenantOptions} tenantOptions The properties to set on the new tenant to be created. * @return {Promise} A promise that resolves with the newly created tenant object. */ - public createTenant(tenantOptions: TenantOptions): Promise { + public createTenant(tenantOptions: CreateTenantRequest): Promise { try { // Construct backend request. const request = Tenant.buildServerRequest(tenantOptions, true); @@ -1999,7 +2012,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { * @param {TenantOptions} tenantOptions The properties to update on the existing tenant. * @return {Promise} A promise that resolves with the modified tenant object. */ - public updateTenant(tenantId: string, tenantOptions: TenantOptions): Promise { + public updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 8c8df58fb4..26db94126e 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -17,40 +17,18 @@ import * as validator from '../utils/validator'; import { deepCopy } from '../utils/deep-copy'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { auth } from './index'; + +import MultiFactorConfigInterface = auth.MultiFactorConfig; +import MultiFactorConfigState = auth.MultiFactorConfigState; +import AuthFactorType = auth.AuthFactorType; +import EmailSignInProviderConfig = auth.EmailSignInProviderConfig; +import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; +import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; /** A maximum of 10 test phone number / code pairs can be configured. */ export const MAXIMUM_TEST_PHONE_NUMBERS = 10; -/** The filter interface used for listing provider configurations. */ -export interface AuthProviderConfigFilter { - type: 'saml' | 'oidc'; - maxResults?: number; - pageToken?: string; -} - -/** The base Auth provider configuration interface. */ -export interface AuthProviderConfig { - providerId: string; - displayName?: string; - enabled: boolean; -} - -/** The OIDC Auth provider configuration interface. */ -export interface OIDCAuthProviderConfig extends AuthProviderConfig { - clientId: string; - issuer: string; -} - -/** The SAML Auth provider configuration interface. */ -export interface SAMLAuthProviderConfig extends AuthProviderConfig { - idpEntityId: string; - ssoURL: string; - x509Certificates: string[]; - rpEntityId: string; - callbackURL?: string; - enableRequestSigning?: boolean; -} - /** The server side SAML configuration request interface. */ export interface SAMLConfigServerRequest { idpConfig?: { @@ -111,107 +89,39 @@ export interface OIDCConfigServerResponse { enabled?: boolean; } -/** The public API response interface for listing provider configs. */ -export interface ListProviderConfigResults { - providerConfigs: AuthProviderConfig[]; - pageToken?: string; -} - -/** The public API request interface for updating a SAML Auth provider. */ -export interface SAMLUpdateAuthProviderRequest { - idpEntityId?: string; - ssoURL?: string; - x509Certificates?: string[]; - rpEntityId?: string; - callbackURL?: string; - enableRequestSigning?: boolean; - enabled?: boolean; - displayName?: string; -} - -/** The generic request interface for updating/creating a SAML Auth provider. */ -export interface SAMLAuthProviderRequest extends SAMLUpdateAuthProviderRequest { - providerId?: string; -} - -/** The public API request interface for updating an OIDC Auth provider. */ -export interface OIDCUpdateAuthProviderRequest { - clientId?: string; - issuer?: string; - enabled?: boolean; - displayName?: string; -} - -/** The generic request interface for updating/creating an OIDC Auth provider. */ -export interface OIDCAuthProviderRequest extends OIDCUpdateAuthProviderRequest { - providerId?: string; -} - -/** The public API request interface for updating a generic Auth provider. */ -export type UpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; - -/** The email provider configuration interface. */ -export interface EmailSignInProviderConfig { - enabled?: boolean; - passwordRequired?: boolean; // In the backend API, default is true if not provided -} - /** The server side email configuration request interface. */ export interface EmailSignInConfigServerRequest { allowPasswordSignup?: boolean; enableEmailLinkSignin?: boolean; } -/** Identifies the public second factor type. */ -export type AuthFactorType = 'phone'; - /** Identifies the server side second factor type. */ -export type AuthFactorServerType = 'PHONE_SMS'; +type AuthFactorServerType = 'PHONE_SMS'; /** Client Auth factor type to server auth factor type mapping. */ -export const AUTH_FACTOR_CLIENT_TO_SERVER_TYPE: {[key: string]: AuthFactorServerType} = { +const AUTH_FACTOR_CLIENT_TO_SERVER_TYPE: {[key: string]: AuthFactorServerType} = { phone: 'PHONE_SMS', }; /** Server Auth factor type to client auth factor type mapping. */ -export const AUTH_FACTOR_SERVER_TO_CLIENT_TYPE: {[key: string]: AuthFactorType} = +const AUTH_FACTOR_SERVER_TO_CLIENT_TYPE: {[key: string]: AuthFactorType} = Object.keys(AUTH_FACTOR_CLIENT_TO_SERVER_TYPE) .reduce((res: {[key: string]: AuthFactorType}, key) => { res[AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[key]] = key as AuthFactorType; return res; }, {}); -/** Identifies a multi-factor configuration state. */ -export type MultiFactorConfigState = 'ENABLED' | 'DISABLED'; - -/** - * Public API interface representing a multi-factor configuration. - */ -export interface MultiFactorConfig { - /** - * The multi-factor config state. - */ - state: MultiFactorConfigState; - - /** - * The list of identifiers for enabled second factors. - * Currently only ‘phone’ is supported. - */ - factorIds?: AuthFactorType[]; -} - /** Server side multi-factor configuration. */ export interface MultiFactorAuthServerConfig { state?: MultiFactorConfigState; enabledProviders?: AuthFactorServerType[]; } - /** * Defines the multi-factor config class used to convert client side MultiFactorConfig * to a format that is understood by the Auth server. */ -export class MultiFactorAuthConfig implements MultiFactorConfig { +export class MultiFactorAuthConfig implements MultiFactorConfigInterface { public readonly state: MultiFactorConfigState; public readonly factorIds: AuthFactorType[]; @@ -222,7 +132,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { * @param options The options object to convert to a server request. * @return The resulting server request. */ - public static buildServerRequest(options: MultiFactorConfig): MultiFactorAuthServerConfig { + public static buildServerRequest(options: MultiFactorConfigInterface): MultiFactorAuthServerConfig { const request: MultiFactorAuthServerConfig = {}; MultiFactorAuthConfig.validate(options); if (Object.prototype.hasOwnProperty.call(options, 'state')) { @@ -248,7 +158,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { * * @param options The options object to validate. */ - private static validate(options: MultiFactorConfig): void { + private static validate(options: MultiFactorConfigInterface): void { const validKeys = { state: true, factorIds: true, @@ -492,7 +402,7 @@ export class SAMLConfig implements SAMLAuthProviderConfig { * @return {?SAMLConfigServerRequest} The resulting server request or null if not valid. */ public static buildServerRequest( - options: SAMLAuthProviderRequest, + options: Partial, ignoreMissingFields = false): SAMLConfigServerRequest | null { const makeRequest = validator.isNonNullObject(options) && (options.providerId || ignoreMissingFields); @@ -509,7 +419,7 @@ export class SAMLConfig implements SAMLAuthProviderConfig { request.idpConfig = { idpEntityId: options.idpEntityId, ssoUrl: options.ssoURL, - signRequest: options.enableRequestSigning, + signRequest: (options as any).enableRequestSigning, idpCertificates: typeof options.x509Certificates === 'undefined' ? undefined : [], }; if (options.x509Certificates) { @@ -557,7 +467,7 @@ export class SAMLConfig implements SAMLAuthProviderConfig { * @param {SAMLAuthProviderRequest} options The options object to validate. * @param {boolean=} ignoreMissingFields Whether to ignore missing fields. */ - public static validate(options: SAMLAuthProviderRequest, ignoreMissingFields = false): void { + public static validate(options: Partial, ignoreMissingFields = false): void { const validKeys = { enabled: true, displayName: true, @@ -643,8 +553,8 @@ export class SAMLConfig implements SAMLAuthProviderConfig { ); } }); - if (typeof options.enableRequestSigning !== 'undefined' && - !validator.isBoolean(options.enableRequestSigning)) { + if (typeof (options as any).enableRequestSigning !== 'undefined' && + !validator.isBoolean((options as any).enableRequestSigning)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.enableRequestSigning" must be a boolean.', @@ -714,8 +624,8 @@ export class SAMLConfig implements SAMLAuthProviderConfig { this.displayName = response.displayName; } - /** @return {SAMLAuthProviderConfig} The plain object representation of the SAMLConfig. */ - public toJSON(): SAMLAuthProviderConfig { + /** @return The plain object representation of the SAMLConfig. */ + public toJSON(): object { return { enabled: this.enabled, displayName: this.displayName, @@ -747,12 +657,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig { * Throws an error if validation fails. If the request is not a OIDCConfig request, * returns null. * - * @param {OIDCAuthProviderRequest} options The options object to convert to a server request. - * @param {boolean=} ignoreMissingFields Whether to ignore missing fields. - * @return {?OIDCConfigServerRequest} The resulting server request or null if not valid. + * @param options The options object to convert to a server request. + * @param ignoreMissingFields Whether to ignore missing fields. + * @return The resulting server request or null if not valid. */ public static buildServerRequest( - options: OIDCAuthProviderRequest, + options: Partial, ignoreMissingFields = false): OIDCConfigServerRequest | null { const makeRequest = validator.isNonNullObject(options) && (options.providerId || ignoreMissingFields); @@ -795,10 +705,10 @@ export class OIDCConfig implements OIDCAuthProviderConfig { /** * Validates the OIDCConfig options object. Throws an error on failure. * - * @param {OIDCAuthProviderRequest} options The options object to validate. - * @param {boolean=} ignoreMissingFields Whether to ignore missing fields. + * @param options The options object to validate. + * @param ignoreMissingFields Whether to ignore missing fields. */ - public static validate(options: OIDCAuthProviderRequest, ignoreMissingFields = false): void { + public static validate(options: Partial, ignoreMissingFields = false): void { const validKeys = { enabled: true, displayName: true, diff --git a/src/auth/auth.ts b/src/auth/auth.ts index b9904eadfe..816333e3b2 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -14,33 +14,49 @@ * limitations under the License. */ -import { UserRecord, CreateRequest, UpdateRequest } from './user-record'; +import { UserRecord } from './user-record'; import { - UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, + isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, } from './identifier'; import { FirebaseApp } from '../firebase-app'; import { FirebaseTokenGenerator, EmulatedSigner, cryptoSignerFromApp } from './token-generator'; import { AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, } from './auth-api-request'; -import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo, FirebaseArrayIndexError } from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { - UserImportOptions, UserImportRecord, UserImportResult, -} from './user-import-builder'; - import * as utils from '../utils/index'; import * as validator from '../utils/validator'; -import { - FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier, ALGORITHM_RS256 +import { auth } from './index'; +import { + FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier, ALGORITHM_RS256 } from './token-verifier'; -import { ActionCodeSettings } from './action-code-settings-builder'; import { - AuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults, UpdateAuthProviderRequest, SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from './auth-config'; import { TenantManager } from './tenant-manager'; +import UserIdentifier = auth.UserIdentifier; +import CreateRequest = auth.CreateRequest; +import UpdateRequest = auth.UpdateRequest; +import ActionCodeSettings = auth.ActionCodeSettings; +import UserImportOptions = auth.UserImportOptions; +import UserImportRecord = auth.UserImportRecord; +import UserImportResult = auth.UserImportResult; +import AuthProviderConfig = auth.AuthProviderConfig; +import AuthProviderConfigFilter = auth.AuthProviderConfigFilter; +import ListProviderConfigResults = auth.ListProviderConfigResults; +import UpdateAuthProviderRequest = auth.UpdateAuthProviderRequest; +import GetUsersResult = auth.GetUsersResult; +import ListUsersResult = auth.ListUsersResult; +import DeleteUsersResult = auth.DeleteUsersResult; +import DecodedIdToken = auth.DecodedIdToken; +import SessionCookieOptions = auth.SessionCookieOptions; +import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; +import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; +import BaseAuthInterface = auth.BaseAuth; +import AuthInterface = auth.Auth; +import TenantAwareAuthInterface = auth.TenantAwareAuth; /** * Internals of an Auth instance. @@ -58,72 +74,10 @@ class AuthInternals implements FirebaseServiceInternalsInterface { } -/** Represents the result of the {@link admin.auth.getUsers()} API. */ -export interface GetUsersResult { - /** - * Set of user records, corresponding to the set of users that were - * requested. Only users that were found are listed here. The result set is - * unordered. - */ - users: UserRecord[]; - - /** Set of identifiers that were requested, but not found. */ - notFound: UserIdentifier[]; -} - - -/** Response object for a listUsers operation. */ -export interface ListUsersResult { - users: UserRecord[]; - pageToken?: string; -} - - -/** Response object for deleteUsers operation. */ -export interface DeleteUsersResult { - failureCount: number; - successCount: number; - errors: FirebaseArrayIndexError[]; -} - - -/** Interface representing a decoded ID token. */ -export interface DecodedIdToken { - aud: string; - auth_time: number; - email?: string; - email_verified?: boolean; - exp: number; - firebase: { - identities: { - [key: string]: any; - }; - sign_in_provider: string; - sign_in_second_factor?: string; - second_factor_identifier?: string; - tenant?: string; - [key: string]: any; - }; - iat: number; - iss: string; - phone_number?: string; - picture?: string; - sub: string; - uid: string; - [key: string]: any; -} - - -/** Interface representing the session cookie options. */ -export interface SessionCookieOptions { - expiresIn: number; -} - - /** * Base Auth class. Mainly used for user management APIs. */ -export class BaseAuth { +export class BaseAuth implements BaseAuthInterface { protected readonly tokenGenerator: FirebaseTokenGenerator; protected readonly idTokenVerifier: FirebaseTokenVerifier; @@ -610,7 +564,7 @@ export class BaseAuth { return Promise.reject( new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `"AuthProviderConfigFilter.type" must be either "saml' or "oidc"`)); + '"AuthProviderConfigFilter.type" must be either "saml" or "oidc"')); } /** @@ -694,12 +648,12 @@ export class BaseAuth { )); } if (OIDCConfig.isProviderId(config.providerId)) { - return this.authRequestHandler.createOAuthIdpConfig(config) + return this.authRequestHandler.createOAuthIdpConfig(config as OIDCAuthProviderConfig) .then((response) => { return new OIDCConfig(response); }); } else if (SAMLConfig.isProviderId(config.providerId)) { - return this.authRequestHandler.createInboundSamlConfig(config) + return this.authRequestHandler.createInboundSamlConfig(config as SAMLAuthProviderConfig) .then((response) => { return new SAMLConfig(response); }); @@ -741,10 +695,10 @@ export class BaseAuth { /** * Enable or disable ID token verification. This is used to safely short-circuit token verification with the * Auth emulator. When disabled ONLY unsigned tokens will pass verification, production tokens will not pass. - * + * * WARNING: This is a dangerous method that will compromise your app's security and break your app in * production. Developers should never call this method, it is for internal testing use only. - * + * * @internal */ // @ts-expect-error: this method appears unused but is used privately. @@ -765,7 +719,10 @@ export class BaseAuth { /** * The tenant aware Auth class. */ -export class TenantAwareAuth extends BaseAuth { +export class TenantAwareAuth + extends BaseAuth + implements TenantAwareAuthInterface { + public readonly tenantId: string; /** @@ -862,7 +819,8 @@ export class TenantAwareAuth extends BaseAuth { * Auth service bound to the provided app. * An Auth instance can have multiple tenants. */ -export class Auth extends BaseAuth implements FirebaseServiceInterface { +export class Auth extends BaseAuth + implements FirebaseServiceInterface, AuthInterface { public INTERNAL: AuthInternals = new AuthInternals(); private readonly tenantManager_: TenantManager; @@ -896,8 +854,8 @@ export class Auth extends BaseAuth implements FirebaseServic /** * When true the SDK should communicate with the Auth Emulator for all API * calls and also produce unsigned tokens. - * - * This alone does NOT short-circuit ID Token verification. + * + * This alone does NOT short-circuit ID Token verification. * For security reasons that must be explicitly disabled through * setJwtVerificationEnabled(false); */ diff --git a/src/auth/identifier.ts b/src/auth/identifier.ts index 46ade5d5cb..b9e93b1fc0 100644 --- a/src/auth/identifier.ts +++ b/src/auth/identifier.ts @@ -14,49 +14,16 @@ * limitations under the License. */ -/** - * Used for looking up an account by uid. - * - * See auth.getUsers() - */ -export interface UidIdentifier { - uid: string; -} - -/** - * Used for looking up an account by email. - * - * See auth.getUsers() - */ -export interface EmailIdentifier { - email: string; -} - -/** - * Used for looking up an account by phone number. - * - * See auth.getUsers() - */ -export interface PhoneIdentifier { - phoneNumber: string; -} - -/** - * Used for looking up an account by federated provider. - * - * See auth.getUsers() - */ -export interface ProviderIdentifier { - providerId: string; - providerUid: string; -} +import { auth } from './index'; -/** - * Identifies a user to be looked up. - */ -export type UserIdentifier = UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; +import UserIdentifier = auth.UserIdentifier; +import UidIdentifier = auth.UidIdentifier; +import EmailIdentifier = auth.EmailIdentifier; +import PhoneIdentifier = auth.PhoneIdentifier; +import ProviderIdentifier = auth.ProviderIdentifier; -/* User defined type guards. See +/* + * User defined type guards. See * https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards */ diff --git a/src/auth.d.ts b/src/auth/index.ts similarity index 86% rename from src/auth.d.ts rename to src/auth/index.ts index 312f2c49f7..8e1242153c 100644 --- a/src/auth.d.ts +++ b/src/auth/index.ts @@ -1,13 +1,50 @@ -import * as _admin from './index.d'; - -/* eslint-disable @typescript-eslint/ban-types */ - -export namespace admin.auth { - +/*! + * Copyright 2020 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, FirebaseArrayIndexError } from '../firebase-namespace-api'; + +/** + * Gets the {@link auth.Auth `Auth`} service for the default app or a + * given app. + * + * `admin.auth()` can be called with no arguments to access the default app's + * {@link auth.Auth `Auth`} service or as `admin.auth(app)` to access the + * {@link auth.Auth `Auth`} service associated with a specific app. + * + * @example + * ```javascript + * // Get the Auth service for the default app + * var defaultAuth = admin.auth(); + * ``` + * + * @example + * ```javascript + * // Get the Auth service for a given app + * var otherAuth = admin.auth(otherApp); + * ``` + * + */ +export declare function auth(app?: app.App): auth.Auth; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace auth { /** - * Interface representing a user's metadata. - */ - interface UserMetadata { + * Interface representing a user's metadata. + */ + export interface UserMetadata { /** * The date the user last signed in, formatted as a UTC string. @@ -24,19 +61,19 @@ export namespace admin.auth { * formatted as a UTC Date string (eg 'Sat, 03 Feb 2001 04:05:06 GMT'). * Returns null if the user was never active. */ - lastRefreshTime?: string|null; + lastRefreshTime?: string | null; /** * @return A JSON-serializable representation of this object. */ - toJSON(): Object; + toJSON(): object; } /** * Interface representing a user's info from a third-party identity provider * such as Google or Facebook. */ - interface UserInfo { + export interface UserInfo { /** * The user identifier for the linked provider. @@ -71,13 +108,13 @@ export namespace admin.auth { /** * @return A JSON-serializable representation of this object. */ - toJSON(): Object; + toJSON(): object; } /** * Interface representing the common properties of a user enrolled second factor. */ - interface MultiFactorInfo { + export interface MultiFactorInfo { /** * The ID of the enrolled second factor. This ID is unique to the user. @@ -102,13 +139,13 @@ export namespace admin.auth { /** * @return A JSON-serializable representation of this object. */ - toJSON(): Object; + toJSON(): object; } /** * Interface representing a phone specific user enrolled second factor. */ - interface PhoneMultiFactorInfo extends MultiFactorInfo { + export interface PhoneMultiFactorInfo extends MultiFactorInfo { /** * The phone number associated with a phone second factor. @@ -119,7 +156,7 @@ export namespace admin.auth { /** * Interface representing a user. */ - interface UserRecord { + export interface UserRecord { /** * The user's `uid`. @@ -160,12 +197,12 @@ export namespace admin.auth { /** * Additional metadata about the user. */ - metadata: admin.auth.UserMetadata; + metadata: UserMetadata; /** * An array of providers (for example, Google, Facebook) linked to the user. */ - providerData: admin.auth.UserInfo[]; + providerData: UserInfo[]; /** * The user's hashed password (base64-encoded), only if Firebase Auth hashing @@ -173,7 +210,7 @@ export namespace admin.auth { * when uploading this user, as is typical when migrating from another Auth * system, this will be an empty string. If no password is set, this is * null. This is only available when the user is obtained from - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listUsers `listUsers()`}. + * {@link auth.Auth.listUsers `listUsers()`}. * */ passwordHash?: string; @@ -184,7 +221,7 @@ export namespace admin.auth { * upload this user, typical when migrating from another Auth system, this will * be an empty string. If no password is set, this is null. This is only * available when the user is obtained from - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listUsers `listUsers()`}. + * {@link auth.Auth.listUsers `listUsers()`}. * */ passwordSalt?: string; @@ -193,14 +230,14 @@ export namespace admin.auth { * The user's custom claims object if available, typically used to define * user roles and propagated to an authenticated user's ID token. * This is set via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#setCustomUserClaims `setCustomUserClaims()`} + * {@link auth.Auth.setCustomUserClaims `setCustomUserClaims()`} */ - customClaims?: {[key: string]: any}; + customClaims?: { [key: string]: any }; /** * The date the user's tokens are valid after, formatted as a UTC string. * This is updated every time the user's refresh token are revoked either - * from the {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#revokeRefreshTokens `revokeRefreshTokens()`} + * from the {@link auth.Auth.revokeRefreshTokens `revokeRefreshTokens()`} * API or from the Firebase Auth backend on big account changes (password * resets, password or email updates, etc). */ @@ -214,59 +251,59 @@ export namespace admin.auth { /** * The multi-factor related properties for the current user, if available. */ - multiFactor?: admin.auth.MultiFactorSettings; + multiFactor?: MultiFactorSettings; /** * @return A JSON-serializable representation of this object. */ - toJSON(): Object; + toJSON(): object; } /** * The multi-factor related user settings. */ - interface MultiFactorSettings { + export interface MultiFactorSettings { /** * List of second factors enrolled with the current user. * Currently only phone second factors are supported. */ - enrolledFactors: admin.auth.MultiFactorInfo[]; + enrolledFactors: MultiFactorInfo[]; /** * @return A JSON-serializable representation of this multi-factor object. */ - toJSON(): Object; + toJSON(): object; } /** * The multi-factor related user settings for create operations. */ - interface MultiFactorCreateSettings { + export interface MultiFactorCreateSettings { /** * The created user's list of enrolled second factors. */ - enrolledFactors: admin.auth.CreateMultiFactorInfoRequest[]; + enrolledFactors: CreateMultiFactorInfoRequest[]; } /** * The multi-factor related user settings for update operations. */ - interface MultiFactorUpdateSettings { + export interface MultiFactorUpdateSettings { /** * The updated list of enrolled second factors. The provided list overwrites the user's * existing list of second factors. * When null is passed, all of the user's existing second factors are removed. */ - enrolledFactors: admin.auth.UpdateMultiFactorInfoRequest[] | null; + enrolledFactors: UpdateMultiFactorInfoRequest[] | null; } /** * Interface representing common properties of a user enrolled second factor * for an `UpdateRequest`. */ - interface UpdateMultiFactorInfoRequest { + export interface UpdateMultiFactorInfoRequest { /** * The ID of the enrolled second factor. This ID is unique to the user. When not provided, @@ -294,7 +331,7 @@ export namespace admin.auth { * Interface representing a phone specific user enrolled second factor * for an `UpdateRequest`. */ - interface UpdatePhoneMultiFactorInfoRequest extends UpdateMultiFactorInfoRequest { + export interface UpdatePhoneMultiFactorInfoRequest extends UpdateMultiFactorInfoRequest { /** * The phone number associated with a phone second factor. @@ -305,7 +342,7 @@ export namespace admin.auth { /** * Interface representing the properties to update on the provided user. */ - interface UpdateRequest { + export interface UpdateRequest { /** * Whether or not the user is disabled: `true` for disabled; @@ -346,14 +383,14 @@ export namespace admin.auth { /** * The user's updated multi-factor related properties. */ - multiFactor?: admin.auth.MultiFactorUpdateSettings; + multiFactor?: MultiFactorUpdateSettings; } /** * Interface representing base properties of a user enrolled second factor for a * `CreateRequest`. */ - interface CreateMultiFactorInfoRequest { + export interface CreateMultiFactorInfoRequest { /** * The optional display name for an enrolled second factor. @@ -370,7 +407,7 @@ export namespace admin.auth { * Interface representing a phone specific user enrolled second factor for a * `CreateRequest`. */ - interface CreatePhoneMultiFactorInfoRequest extends CreateMultiFactorInfoRequest { + export interface CreatePhoneMultiFactorInfoRequest extends CreateMultiFactorInfoRequest { /** * The phone number associated with a phone second factor. @@ -382,7 +419,7 @@ export namespace admin.auth { * Interface representing the properties to set on a new user record to be * created. */ - interface CreateRequest extends UpdateRequest { + export interface CreateRequest extends UpdateRequest { /** * The user's `uid`. @@ -392,19 +429,19 @@ export namespace admin.auth { /** * The user's multi-factor related properties. */ - multiFactor?: admin.auth.MultiFactorCreateSettings; + multiFactor?: MultiFactorCreateSettings; } /** * Interface representing a decoded Firebase ID token, returned from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#verifyidtoken `verifyIdToken()`} method. + * {@link auth.Auth.verifyIdToken `verifyIdToken()`} method. * * Firebase ID tokens are OpenID Connect spec-compliant JSON Web Tokens (JWTs). * See the * [ID Token section of the OpenID Connect spec](http://openid.net/specs/openid-connect-core-1_0.html#IDToken) * for more information about the specific properties below. */ - interface DecodedIdToken { + export interface DecodedIdToken { /** * The audience for which this token is intended. @@ -538,8 +575,8 @@ export namespace admin.auth { [key: string]: any; } - /** Represents the result of the {@link admin.auth.getUsers()} API. */ - interface GetUsersResult { + /** Represents the result of the {@link auth.Auth.getUsers} API. */ + export interface GetUsersResult { /** * Set of user records, corresponding to the set of users that were * requested. Only users that were found are listed here. The result set is @@ -553,16 +590,16 @@ export namespace admin.auth { /** * Interface representing the object returned from a - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listUsers `listUsers()`} operation. Contains the list + * {@link auth.Auth.listUsers `listUsers()`} operation. Contains the list * of users for the current batch and the next page token if available. */ - interface ListUsersResult { + export interface ListUsersResult { /** - * The list of {@link admin.auth.UserRecord `UserRecord`} objects for the + * The list of {@link auth.UserRecord `UserRecord`} objects for the * current downloaded batch. */ - users: admin.auth.UserRecord[]; + users: UserRecord[]; /** * The next page token if available. This is needed for the next batch download. @@ -570,16 +607,16 @@ export namespace admin.auth { pageToken?: string; } - type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | + export type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | 'HMAC_SHA256' | 'HMAC_SHA1' | 'HMAC_MD5' | 'MD5' | 'PBKDF_SHA1' | 'BCRYPT' | 'PBKDF2_SHA256' | 'SHA512' | 'SHA256' | 'SHA1'; /** * Interface representing the user import options needed for - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#importUsers `importUsers()`} method. This is used to + * {@link auth.Auth.importUsers `importUsers()`} method. This is used to * provide the password hashing algorithm information. */ - interface UserImportOptions { + export interface UserImportOptions { /** * The password hashing information. @@ -642,10 +679,10 @@ export namespace admin.auth { /** * Interface representing the response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#importUsers `importUsers()`} method for batch + * {@link auth.Auth.importUsers `importUsers()`} method for batch * importing users to Firebase Auth. */ - interface UserImportResult { + export interface UserImportResult { /** * The number of user records that failed to import to Firebase Auth. @@ -661,15 +698,15 @@ export namespace admin.auth { * An array of errors corresponding to the provided users to import. The * length of this array is equal to [`failureCount`](#failureCount). */ - errors: _admin.FirebaseArrayIndexError[]; + errors: FirebaseArrayIndexError[]; } /** * Represents the result of the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#deleteUsers `deleteUsers()`} + * {@link auth.Auth.deleteUsers `deleteUsers()`} * API. */ - interface DeleteUsersResult { + export interface DeleteUsersResult { /** * The number of user records that failed to be deleted (possibly zero). */ @@ -687,13 +724,13 @@ export namespace admin.auth { * were encountered during the deletion. Length of this list is equal to * the return value of [`failureCount`](#failureCount). */ - errors: _admin.FirebaseArrayIndexError[]; + errors: FirebaseArrayIndexError[]; } /** * User metadata to include when importing a user. */ - interface UserMetadataRequest { + export interface UserMetadataRequest { /** * The date the user last signed in, formatted as a UTC string. @@ -709,7 +746,7 @@ export namespace admin.auth { /** * User provider data to include when importing a user. */ - interface UserProviderRequest { + export interface UserProviderRequest { /** * The user identifier for the linked provider. @@ -744,9 +781,9 @@ export namespace admin.auth { /** * Interface representing a user to import to Firebase Auth via the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#importUsers `importUsers()`} method. + * {@link auth.Auth.importUsers `importUsers()`} method. */ - interface UserImportRecord { + export interface UserImportRecord { /** * The user's `uid`. @@ -787,23 +824,23 @@ export namespace admin.auth { /** * Additional metadata about the user. */ - metadata?: admin.auth.UserMetadataRequest; + metadata?: UserMetadataRequest; /** * An array of providers (for example, Google, Facebook) linked to the user. */ - providerData?: admin.auth.UserProviderRequest[]; + providerData?: UserProviderRequest[]; /** * The user's custom claims object if available, typically used to define * user roles and propagated to an authenticated user's ID token. */ - customClaims?: {[key: string]: any}; + customClaims?: { [key: string]: any }; /** * The buffer of bytes representing the user's hashed password. * When a user is to be imported with a password hash, - * {@link admin.auth.UserImportOptions `UserImportOptions`} are required to be + * {@link auth.UserImportOptions `UserImportOptions`} are required to be * specified to identify the hashing algorithm used to generate this hash. */ passwordHash?: Buffer; @@ -825,14 +862,14 @@ export namespace admin.auth { /** * The user's multi-factor related properties. */ - multiFactor?: admin.auth.MultiFactorUpdateSettings; + multiFactor?: MultiFactorUpdateSettings; } /** * Interface representing the session cookie options needed for the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createSessionCookie `createSessionCookie()`} method. + * {@link auth.Auth.createSessionCookie `createSessionCookie()`} method. */ - interface SessionCookieOptions { + export interface SessionCookieOptions { /** * The session cookie custom expiration in milliseconds. The minimum allowed is @@ -845,7 +882,7 @@ export namespace admin.auth { * This is the interface that defines the required continue/state URL with * optional Android and iOS bundle identifiers. */ - interface ActionCodeSettings { + export interface ActionCodeSettings { /** * Defines the link continue/state URL, which has different meanings in @@ -942,7 +979,7 @@ export namespace admin.auth { * All other settings of a tenant will also be inherited. These will need to be managed * from the Cloud Console UI. */ - interface Tenant { + export interface Tenant { /** * The tenant identifier. @@ -974,51 +1011,67 @@ export namespace admin.auth { /** * The multi-factor auth configuration on the current tenant. */ - multiFactorConfig?: admin.auth.MultiFactorConfig; + multiFactorConfig?: MultiFactorConfig; /** * The map containing the test phone number / code pairs for the tenant. */ - testPhoneNumbers?: {[phoneNumber: string]: string}; + testPhoneNumbers?: { [phoneNumber: string]: string }; /** * @return A JSON-serializable representation of this object. */ - toJSON(): Object; + toJSON(): object; } /** * Identifies a second factor type. */ - type AuthFactorType = 'phone'; + export type AuthFactorType = 'phone'; /** * Identifies a multi-factor configuration state. */ - type MultiFactorConfigState = 'ENABLED' | 'DISABLED'; + export type MultiFactorConfigState = 'ENABLED' | 'DISABLED'; /** * Interface representing a multi-factor configuration. * This can be used to define whether multi-factor authentication is enabled * or disabled and the list of second factor challenges that are supported. */ - interface MultiFactorConfig { + export interface MultiFactorConfig { /** * The multi-factor config state. */ - state: admin.auth.MultiFactorConfigState; + state: MultiFactorConfigState; /** * The list of identifiers for enabled second factors. * Currently only ‘phone’ is supported. */ - factorIds?: admin.auth.AuthFactorType[]; + factorIds?: AuthFactorType[]; + } + + /** + * The email sign in configuration. + */ + export interface EmailSignInProviderConfig { + /** + * Whether email provider is enabled. + */ + enabled: boolean; + + /** + * Whether password is required for email sign-in. When not required, + * email sign-in can be performed with password or via email link sign-in. + */ + passwordRequired?: boolean; // In the backend API, default is true if not provided } /** * Interface representing the properties to update on the provided tenant. */ - interface UpdateTenantRequest { + export interface UpdateTenantRequest { /** * The tenant display name. @@ -1028,49 +1081,37 @@ export namespace admin.auth { /** * The email sign in configuration. */ - emailSignInConfig?: { - - /** - * Whether email provider is enabled. - */ - enabled: boolean; - - /** - * Whether password is required for email sign-in. When not required, - * email sign-in can be performed with password or via email link sign-in. - */ - passwordRequired?: boolean; - }; + emailSignInConfig?: EmailSignInProviderConfig; /** * The multi-factor auth configuration to update on the tenant. */ - multiFactorConfig?: admin.auth.MultiFactorConfig; + multiFactorConfig?: MultiFactorConfig; /** * The updated map containing the test phone number / code pairs for the tenant. * Passing null clears the previously save phone number / code pairs. */ - testPhoneNumbers?: {[phoneNumber: string]: string} | null; + testPhoneNumbers?: { [phoneNumber: string]: string } | null; } /** * Interface representing the properties to set on a new tenant. */ - type CreateTenantRequest = UpdateTenantRequest; + export type CreateTenantRequest = UpdateTenantRequest; /** * Interface representing the object returned from a - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listTenants `listTenants()`} + * {@link auth.TenantManager.listTenants `listTenants()`} * operation. * Contains the list of tenants for the current batch and the next page token if available. */ - interface ListTenantsResult { + export interface ListTenantsResult { /** - * The list of {@link admin.auth.Tenant `Tenant`} objects for the downloaded batch. + * The list of {@link auth.Tenant `Tenant`} objects for the downloaded batch. */ - tenants: admin.auth.Tenant[]; + tenants: Tenant[]; /** * The next page token if available. This is needed for the next batch download. @@ -1081,9 +1122,9 @@ export namespace admin.auth { /** * The filter interface used for listing provider configurations. This is used * when specifying how to list configured identity providers via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listProviderConfigs `listProviderConfigs()`}. + * {@link auth.Auth.listProviderConfigs `listProviderConfigs()`}. */ - interface AuthProviderConfigFilter { + export interface AuthProviderConfigFilter { /** * The Auth provider configuration filter. This can be either `saml` or `oidc`. @@ -1108,7 +1149,7 @@ export namespace admin.auth { /** * The base Auth provider configuration interface. */ - interface AuthProviderConfig { + export interface AuthProviderConfig { /** * The provider ID defined by the developer. @@ -1134,9 +1175,9 @@ export namespace admin.auth { * The * [SAML](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) * Auth provider configuration interface. A SAML provider can be created via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createProviderConfig `createProviderConfig()`}. + * {@link auth.Auth.createProviderConfig `createProviderConfig()`}. */ - interface SAMLAuthProviderConfig extends admin.auth.AuthProviderConfig { + export interface SAMLAuthProviderConfig extends AuthProviderConfig { /** * The SAML IdP entity identifier. @@ -1179,9 +1220,9 @@ export namespace admin.auth { /** * The [OIDC](https://openid.net/specs/openid-connect-core-1_0-final.html) Auth * provider configuration interface. An OIDC provider can be created via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createProviderConfig `createProviderConfig()`}. + * {@link auth.Auth.createProviderConfig `createProviderConfig()`}. */ - interface OIDCAuthProviderConfig extends admin.auth.AuthProviderConfig { + export interface OIDCAuthProviderConfig extends AuthProviderConfig { /** * This is the required client ID used to confirm the audience of an OIDC @@ -1213,9 +1254,9 @@ export namespace admin.auth { /** * The request interface for updating a SAML Auth provider. This is used * when updating a SAML provider's configuration via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#updateProviderConfig `updateProviderConfig()`}. + * {@link auth.Auth.updateProviderConfig `updateProviderConfig()`}. */ - interface SAMLUpdateAuthProviderRequest { + export interface SAMLUpdateAuthProviderRequest { /** * The SAML provider's updated display name. If not provided, the existing @@ -1263,9 +1304,9 @@ export namespace admin.auth { /** * The request interface for updating an OIDC Auth provider. This is used * when updating an OIDC provider's configuration via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#updateProviderConfig `updateProviderConfig()`}. + * {@link auth.Auth.updateProviderConfig `updateProviderConfig()`}. */ - interface OIDCUpdateAuthProviderRequest { + export interface OIDCUpdateAuthProviderRequest { /** * The OIDC provider's updated display name. If not provided, the existing @@ -1295,14 +1336,14 @@ export namespace admin.auth { /** * The response interface for listing provider configs. This is only available * when listing all identity providers' configurations via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listProviderConfigs `listProviderConfigs()`}. + * {@link auth.Auth.listProviderConfigs `listProviderConfigs()`}. */ - interface ListProviderConfigResults { + export interface ListProviderConfigResults { /** * The list of providers for the specified type in the current page. */ - providerConfigs: admin.auth.AuthProviderConfig[]; + providerConfigs: AuthProviderConfig[]; /** * The next page token, if available. @@ -1310,15 +1351,15 @@ export namespace admin.auth { pageToken?: string; } - type UpdateAuthProviderRequest = - admin.auth.SAMLUpdateAuthProviderRequest | admin.auth.OIDCUpdateAuthProviderRequest; + export type UpdateAuthProviderRequest = + SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; /** * Used for looking up an account by uid. * * See auth.getUsers() */ - interface UidIdentifier { + export interface UidIdentifier { uid: string; } @@ -1327,7 +1368,7 @@ export namespace admin.auth { * * See auth.getUsers() */ - interface EmailIdentifier { + export interface EmailIdentifier { email: string; } @@ -1336,7 +1377,7 @@ export namespace admin.auth { * * See auth.getUsers() */ - interface PhoneIdentifier { + export interface PhoneIdentifier { phoneNumber: string; } @@ -1345,7 +1386,7 @@ export namespace admin.auth { * * See auth.getUsers() */ - interface ProviderIdentifier { + export interface ProviderIdentifier { providerId: string; providerUid: string; } @@ -1353,9 +1394,10 @@ export namespace admin.auth { /** * Identifies a user to be looked up. */ - type UserIdentifier = UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; + export type UserIdentifier = + UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; - interface BaseAuth { + export interface BaseAuth { /** * Creates a new Firebase custom token (JWT) that can be sent back to a client @@ -1387,7 +1429,7 @@ export namespace admin.auth { * @return A promise fulfilled with the user * data corresponding to the newly created user. */ - createUser(properties: admin.auth.CreateRequest): Promise; + createUser(properties: CreateRequest): Promise; /** * Deletes an existing user. @@ -1424,7 +1466,7 @@ export namespace admin.auth { * deletions, as well as the array of errors that corresponds to the * failed deletions. */ - deleteUsers(uids: string[]): Promise; + deleteUsers(uids: string[]): Promise; /** * Gets the user data for the user corresponding to a given `uid`. @@ -1437,7 +1479,7 @@ export namespace admin.auth { * @return A promise fulfilled with the user * data corresponding to the provided `uid`. */ - getUser(uid: string): Promise; + getUser(uid: string): Promise; /** * Gets the user data for the user corresponding to a given email. @@ -1451,7 +1493,7 @@ export namespace admin.auth { * @return A promise fulfilled with the user * data corresponding to the provided email. */ - getUserByEmail(email: string): Promise; + getUserByEmail(email: string): Promise; /** * Gets the user data for the user corresponding to a given phone number. The @@ -1466,7 +1508,7 @@ export namespace admin.auth { * @return A promise fulfilled with the user * data corresponding to the provided phone number. */ - getUserByPhoneNumber(phoneNumber: string): Promise; + getUserByPhoneNumber(phoneNumber: string): Promise; /** * Gets the user data corresponding to the specified identifiers. @@ -1483,7 +1525,7 @@ export namespace admin.auth { * @throws FirebaseAuthError If any of the identifiers are invalid or if more than 100 * identifiers are specified. */ - getUsers(identifiers: admin.auth.UserIdentifier[]): Promise; + getUsers(identifiers: UserIdentifier[]): Promise; /** * Retrieves a list of users (single batch only) with a size of `maxResults` @@ -1500,7 +1542,7 @@ export namespace admin.auth { * @return A promise that resolves with * the current batch of downloaded users and the next page token. */ - listUsers(maxResults?: number, pageToken?: string): Promise; + listUsers(maxResults?: number, pageToken?: string): Promise; /** * Updates an existing user. @@ -1515,7 +1557,7 @@ export namespace admin.auth { * @return A promise fulfilled with the * updated user data. */ - updateUser(uid: string, properties: admin.auth.UpdateRequest): Promise; + updateUser(uid: string, properties: UpdateRequest): Promise; /** * Verifies a Firebase ID token (JWT). If the token is valid, the promise is @@ -1537,7 +1579,7 @@ export namespace admin.auth { * token's decoded claims if the ID token is valid; otherwise, a rejected * promise. */ - verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; + verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; /** * Sets additional developer claims on an existing user identified by the @@ -1568,7 +1610,7 @@ export namespace admin.auth { * Revokes all refresh tokens for an existing user. * * This API will update the user's - * {@link admin.auth.UserRecord#tokensValidAfterTime `tokensValidAfterTime`} to + * {@link auth.UserRecord.tokensValidAfterTime `tokensValidAfterTime`} to * the current UTC. It is important that the server on which this is called has * its clock set correctly and synchronized. * @@ -1576,7 +1618,7 @@ export namespace admin.auth { * new ID tokens for existing sessions from getting minted, existing ID tokens * may remain active until their natural expiration (one hour). To verify that * ID tokens are revoked, use - * {@link admin.auth.Auth#verifyIdToken `verifyIdToken(idToken, true)`} + * {@link auth.Auth.verifyIdToken `verifyIdToken(idToken, true)`} * where `checkRevoked` is set to true. * * @param uid The `uid` corresponding to the user whose refresh tokens @@ -1591,7 +1633,7 @@ export namespace admin.auth { * Imports the provided list of users into Firebase Auth. * A maximum of 1000 users are allowed to be imported one at a time. * When importing users with passwords, - * {@link admin.auth.UserImportOptions `UserImportOptions`} are required to be + * {@link auth.UserImportOptions `UserImportOptions`} are required to be * specified. * This operation is optimized for bulk imports and will ignore checks on `uid`, * `email` and other identifier uniqueness which could result in duplications. @@ -1605,9 +1647,9 @@ export namespace admin.auth { * corresponding errors. */ importUsers( - users: admin.auth.UserImportRecord[], - options?: admin.auth.UserImportOptions, - ): Promise; + users: UserImportRecord[], + options?: UserImportOptions, + ): Promise; /** * Creates a new Firebase session cookie with the specified options. The created @@ -1628,7 +1670,7 @@ export namespace admin.auth { */ createSessionCookie( idToken: string, - sessionCookieOptions: admin.auth.SessionCookieOptions, + sessionCookieOptions: SessionCookieOptions, ): Promise; /** @@ -1655,12 +1697,12 @@ export namespace admin.auth { verifySessionCookie( sessionCookie: string, checkForRevocation?: boolean, - ): Promise; + ): Promise; /** * Generates the out of band email action link to reset a user's password. * The link is generated for the user with the specified email address. The - * optional {@link admin.auth.ActionCodeSettings `ActionCodeSettings`} object + * optional {@link auth.ActionCodeSettings `ActionCodeSettings`} object * defines whether the link is to be handled by a mobile app or browser and the * additional state information to be passed in the deep link, etc. * @@ -1708,13 +1750,13 @@ export namespace admin.auth { */ generatePasswordResetLink( email: string, - actionCodeSettings?: admin.auth.ActionCodeSettings, + actionCodeSettings?: ActionCodeSettings, ): Promise; /** * Generates the out of band email action link to verify the user's ownership * of the specified email. The - * {@link admin.auth.ActionCodeSettings `ActionCodeSettings`} object provided + * {@link auth.ActionCodeSettings `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. @@ -1762,13 +1804,13 @@ export namespace admin.auth { */ generateEmailVerificationLink( email: string, - actionCodeSettings?: admin.auth.ActionCodeSettings, + actionCodeSettings?: ActionCodeSettings, ): Promise; /** * Generates the out of band email action link to sign in or sign up the owner * of the specified email. The - * {@link admin.auth.ActionCodeSettings `ActionCodeSettings`} object provided + * {@link auth.ActionCodeSettings `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. @@ -1816,7 +1858,7 @@ export namespace admin.auth { */ generateSignInWithEmailLink( email: string, - actionCodeSettings: admin.auth.ActionCodeSettings, + actionCodeSettings: ActionCodeSettings, ): Promise; /** @@ -1832,8 +1874,8 @@ export namespace admin.auth { * filter requirements. */ listProviderConfigs( - options: admin.auth.AuthProviderConfigFilter - ): Promise; + options: AuthProviderConfigFilter + ): Promise; /** * Looks up an Auth provider configuration by the provided ID. @@ -1850,7 +1892,7 @@ export namespace admin.auth { * @return A promise that resolves * with the configuration corresponding to the provided ID. */ - getProviderConfig(providerId: string): Promise; + getProviderConfig(providerId: string): Promise; /** * Deletes the provider configuration corresponding to the provider ID passed. @@ -1883,8 +1925,8 @@ export namespace admin.auth { * @return A promise that resolves with the updated provider configuration. */ updateProviderConfig( - providerId: string, updatedConfig: admin.auth.UpdateAuthProviderRequest - ): Promise; + providerId: string, updatedConfig: UpdateAuthProviderRequest + ): Promise; /** * Returns a promise that resolves with the newly created `AuthProviderConfig` @@ -1898,8 +1940,8 @@ export namespace admin.auth { * @return A promise that resolves with the created provider configuration. */ createProviderConfig( - config: admin.auth.AuthProviderConfig - ): Promise; + config: AuthProviderConfig + ): Promise; } /** @@ -1919,7 +1961,7 @@ export namespace admin.auth { * `TenantAwareAuth` instances for a specific `tenantId` can be instantiated by calling * `auth.tenantManager().authForTenant(tenantId)`. */ - interface TenantAwareAuth extends BaseAuth { + export interface TenantAwareAuth extends BaseAuth { /** * The tenant identifier corresponding to this `TenantAwareAuth` instance. @@ -1929,13 +1971,13 @@ export namespace admin.auth { tenantId: string; } - interface Auth extends admin.auth.BaseAuth { - app: _admin.app.App; + export interface Auth extends BaseAuth { + app: app.App; /** * @return The tenant manager instance associated with the current project. */ - tenantManager(): admin.auth.TenantManager; + tenantManager(): TenantManager; } /** @@ -1949,13 +1991,13 @@ export namespace admin.auth { * email link generation, etc) in the context of a specified tenant. * */ - interface TenantManager { + export interface TenantManager { /** * @param tenantId The tenant ID whose `TenantAwareAuth` instance is to be returned. * * @return The `TenantAwareAuth` instance corresponding to this tenant identifier. */ - authForTenant(tenantId: string): admin.auth.TenantAwareAuth; + authForTenant(tenantId: string): TenantAwareAuth; /** * Gets the tenant configuration for the tenant corresponding to a given `tenantId`. @@ -1964,7 +2006,7 @@ export namespace admin.auth { * * @return A promise fulfilled with the tenant configuration to the provided `tenantId`. */ - getTenant(tenantId: string): Promise; + getTenant(tenantId: string): Promise; /** * Retrieves a list of tenants (single batch only) with a size of `maxResults` @@ -1979,7 +2021,7 @@ export namespace admin.auth { * @return A promise that resolves with * a batch of downloaded tenants and the next page token. */ - listTenants(maxResults?: number, pageToken?: string): Promise; + listTenants(maxResults?: number, pageToken?: string): Promise; /** * Deletes an existing tenant. @@ -2000,7 +2042,7 @@ export namespace admin.auth { * @return A promise fulfilled with the tenant configuration corresponding to the newly * created tenant. */ - createTenant(tenantOptions: admin.auth.CreateTenantRequest): Promise; + createTenant(tenantOptions: CreateTenantRequest): Promise; /** * Updates an existing tenant configuration. @@ -2010,6 +2052,6 @@ export namespace admin.auth { * * @return A promise fulfilled with the update tenant data. */ - updateTenant(tenantId: string, tenantOptions: admin.auth.UpdateTenantRequest): Promise; + updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise; } } diff --git a/src/auth/tenant-manager.ts b/src/auth/tenant-manager.ts index 4f0b1c39cb..f97f0aaefc 100644 --- a/src/auth/tenant-manager.ts +++ b/src/auth/tenant-manager.ts @@ -17,11 +17,15 @@ import { AuthRequestHandler } from './auth-api-request'; import { FirebaseApp } from '../firebase-app'; import { TenantAwareAuth } from './auth'; -import { - Tenant, TenantServerResponse, ListTenantsResult, TenantOptions, -} from './tenant'; +import { Tenant, TenantServerResponse } from './tenant'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import * as validator from '../utils/validator'; +import { auth } from './index'; + +import ListTenantsResult = auth.ListTenantsResult; +import TenantManagerInterface = auth.TenantManager; +import CreateTenantRequest = auth.CreateTenantRequest; +import UpdateTenantRequest = auth.UpdateTenantRequest; /** * Data structure used to help manage tenant related operations. @@ -30,7 +34,7 @@ import * as validator from '../utils/validator'; * - Getting a TenantAwareAuth instance for running Auth related operations (user mgmt, provider config mgmt, etc) * in the context of a specified tenant. */ -export class TenantManager { +export class TenantManager implements TenantManagerInterface { private readonly authRequestHandler: AuthRequestHandler; private readonly tenantsMap: {[key: string]: TenantAwareAuth}; @@ -126,7 +130,7 @@ export class TenantManager { * @param tenantOptions The properties to set on the new tenant to be created. * @return A promise that resolves with the newly created tenant. */ - public createTenant(tenantOptions: TenantOptions): Promise { + public createTenant(tenantOptions: CreateTenantRequest): Promise { return this.authRequestHandler.createTenant(tenantOptions) .then((response: TenantServerResponse) => { return new Tenant(response); @@ -140,7 +144,7 @@ export class TenantManager { * @param tenantOptions The properties to update on the existing tenant. * @return A promise that resolves with the modified tenant. */ - public updateTenant(tenantId: string, tenantOptions: TenantOptions): Promise { + public updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise { return this.authRequestHandler.updateTenant(tenantId, tenantOptions) .then((response: TenantServerResponse) => { return new Tenant(response); diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index ce3bb36ecb..a7b1d188f4 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -18,18 +18,13 @@ import * as validator from '../utils/validator'; import { deepCopy } from '../utils/deep-copy'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { - EmailSignInConfig, EmailSignInConfigServerRequest, EmailSignInProviderConfig, - MultiFactorConfig, MultiFactorAuthServerConfig, MultiFactorAuthConfig, - validateTestPhoneNumbers, + EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, + MultiFactorAuthConfig, validateTestPhoneNumbers, } from './auth-config'; +import { auth } from './index'; -/** The TenantOptions interface used for create/read/update tenant operations. */ -export interface TenantOptions { - displayName?: string; - emailSignInConfig?: EmailSignInProviderConfig; - multiFactorConfig?: MultiFactorConfig; - testPhoneNumbers?: {[phoneNumber: string]: string} | null; -} +import TenantInterface = auth.Tenant; +import UpdateTenantRequest = auth.UpdateTenantRequest; /** The corresponding server side representation of a TenantOptions object. */ export interface TenantOptionsServerRequest extends EmailSignInConfigServerRequest { @@ -48,17 +43,10 @@ export interface TenantServerResponse { testPhoneNumbers?: {[key: string]: string}; } -/** The interface representing the listTenant API response. */ -export interface ListTenantsResult { - tenants: Tenant[]; - pageToken?: string; -} - - /** * Tenant class that defines a Firebase Auth tenant. */ -export class Tenant { +export class Tenant implements TenantInterface { public readonly tenantId: string; public readonly displayName?: string; public readonly emailSignInConfig?: EmailSignInConfig; @@ -73,7 +61,7 @@ export class Tenant { * @return {object} The equivalent server request. */ public static buildServerRequest( - tenantOptions: TenantOptions, createRequest: boolean): TenantOptionsServerRequest { + tenantOptions: UpdateTenantRequest, createRequest: boolean): TenantOptionsServerRequest { Tenant.validate(tenantOptions, createRequest); let request: TenantOptionsServerRequest = {}; if (typeof tenantOptions.emailSignInConfig !== 'undefined') { diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 8f6850f6c6..994d1bf25f 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -91,7 +91,7 @@ interface JWTBody { * sign data. Performs all operations locally, and does not make any RPC calls. */ export class ServiceAccountSigner implements CryptoSigner { - + algorithm = ALGORITHM_RS256; /** @@ -220,8 +220,8 @@ export class IAMSigner implements CryptoSigner { }).catch((err) => { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CREDENTIAL, - `Failed to determine service account. Make sure to initialize ` + - `the SDK with a service account credential. Alternatively specify a service ` + + 'Failed to determine service account. Make sure to initialize ' + + 'the SDK with a service account credential. Alternatively specify a service ' + `account with iam.serviceAccounts.signBlob permission. Original error: ${err}`, ); }); @@ -239,7 +239,8 @@ export class EmulatedSigner implements CryptoSigner { /** * @inheritDoc */ - public sign(_: Buffer): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public sign(buffer: Buffer): Promise { return Promise.resolve(Buffer.from('')); } diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index 768cc5bb7b..c39aa84fc7 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -15,13 +15,14 @@ */ import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; - import * as util from '../utils/index'; import * as validator from '../utils/validator'; import * as jwt from 'jsonwebtoken'; import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; -import { DecodedIdToken } from './auth'; import { FirebaseApp } from '../firebase-app'; +import { auth } from './index'; + +import DecodedIdToken = auth.DecodedIdToken; // Audience to use for Firebase Auth Custom tokens const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; @@ -82,47 +83,47 @@ export class FirebaseTokenVerifier { if (!validator.isURL(clientCertUrl)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The provided public client certificate URL is an invalid URL.`, + 'The provided public client certificate URL is an invalid URL.', ); } else if (!validator.isNonEmptyString(algorithm)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The provided JWT algorithm is an empty string.`, + 'The provided JWT algorithm is an empty string.', ); } else if (!validator.isURL(issuer)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The provided JWT issuer is an invalid URL.`, + 'The provided JWT issuer is an invalid URL.', ); } else if (!validator.isNonNullObject(tokenInfo)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The provided JWT information is not an object or null.`, + 'The provided JWT information is not an object or null.', ); } else if (!validator.isURL(tokenInfo.url)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The provided JWT verification documentation URL is invalid.`, + 'The provided JWT verification documentation URL is invalid.', ); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The JWT verify API name must be a non-empty string.`, + 'The JWT verify API name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The JWT public full name must be a non-empty string.`, + 'The JWT public full name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The JWT public short name must be a non-empty string.`, + 'The JWT public short name must be a non-empty string.', ); } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The JWT expiration error code must be a non-null ErrorInfo object.`, + 'The JWT expiration error code must be a non-null ErrorInfo object.', ); } this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; @@ -163,7 +164,7 @@ export class FirebaseTokenVerifier { if (!validator.isNonEmptyString(projectId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CREDENTIAL, - `Must initialize app with a cert credential or set your Firebase project ID as the ` + + 'Must initialize app with a cert credential or set your Firebase project ID as the ' + `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`, ); } @@ -176,7 +177,7 @@ export class FirebaseTokenVerifier { const payload = fullDecodedToken && fullDecodedToken.payload; const projectIdMatchMessage = ` Make sure the ${this.tokenInfo.shortName} comes from the same ` + - `Firebase project as the service account used to authenticate this SDK.`; + 'Firebase project as the service account used to authenticate this SDK.'; const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; @@ -200,16 +201,16 @@ export class FirebaseTokenVerifier { errorMessage += verifyJwtTokenDocsMessage; } else if (header.alg !== this.algorithm) { - errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + `" but got ` + - `"` + header.alg + `".` + verifyJwtTokenDocsMessage; + errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + '" but got ' + + '"' + header.alg + '".' + verifyJwtTokenDocsMessage; } else if (payload.aud !== projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + - projectId + `" but got "` + payload.aud + `".` + projectIdMatchMessage + + projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage; } else if (payload.iss !== this.issuer + projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` + - `"${this.issuer}"` + projectId + `" but got "` + - payload.iss + `".` + projectIdMatchMessage + verifyJwtTokenDocsMessage; + `"${this.issuer}"` + projectId + '" but got "' + + payload.iss + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage; } else if (typeof payload.sub !== 'string') { errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage; } else if (payload.sub === '') { @@ -235,7 +236,7 @@ export class FirebaseTokenVerifier { AuthClientErrorCode.INVALID_ARGUMENT, `${this.tokenInfo.jwtName} has "kid" claim which does not correspond to a known public key. ` + `Most likely the ${this.tokenInfo.shortName} is expired, so get a fresh token from your ` + - `client app and try again.`, + 'client app and try again.', ), ); } else { @@ -256,7 +257,7 @@ export class FirebaseTokenVerifier { const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; return new Promise((resolve, reject) => { - jwt.verify(jwtToken, publicKey || "", { + jwt.verify(jwtToken, publicKey || '', { algorithms: [this.algorithm], }, (error: jwt.VerifyErrors | null, decodedToken: object | undefined) => { if (error) { diff --git a/src/auth/user-import-builder.ts b/src/auth/user-import-builder.ts index 1f1b784a03..92e84cc08c 100644 --- a/src/auth/user-import-builder.ts +++ b/src/auth/user-import-builder.ts @@ -17,69 +17,15 @@ import { deepCopy, deepExtend } from '../utils/deep-copy'; import * as utils from '../utils'; import * as validator from '../utils/validator'; -import { AuthClientErrorCode, FirebaseAuthError, FirebaseArrayIndexError } from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { FirebaseArrayIndexError } from '../firebase-namespace-api'; +import { auth } from './index'; -/** Firebase Auth supported hashing algorithms for import operations. */ -export type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | - 'HMAC_SHA256' | 'HMAC_SHA1' | 'HMAC_MD5' | 'MD5' | 'PBKDF_SHA1' | 'BCRYPT' | - 'PBKDF2_SHA256' | 'SHA512' | 'SHA256' | 'SHA1'; - - -/** User import options for bulk account imports. */ -export interface UserImportOptions { - hash: { - algorithm: HashAlgorithmType; - key?: Buffer; - saltSeparator?: Buffer; - rounds?: number; - memoryCost?: number; - parallelization?: number; - blockSize?: number; - derivedKeyLength?: number; - }; -} - -interface SecondFactor { - uid: string; - phoneNumber: string; - displayName?: string; - enrollmentTime?: string; - factorId: string; -} - -interface UserProviderRequest { - uid: string; - displayName?: string; - email?: string; - phoneNumber?: string; - photoURL?: string; - providerId: string; -} - -interface UserMetadataRequest { - lastSignInTime?: string; - creationTime?: string; -} - -/** User import record as accepted from developer. */ -export interface UserImportRecord { - uid: string; - email?: string; - emailVerified?: boolean; - displayName?: string; - phoneNumber?: string; - photoURL?: string; - disabled?: boolean; - metadata?: UserMetadataRequest; - providerData?: Array; - multiFactor?: { - enrolledFactors: SecondFactor[]; - }; - customClaims?: {[key: string]: any}; - passwordHash?: Buffer; - passwordSalt?: Buffer; - tenantId?: string; -} +import UpdateMultiFactorInfoRequest = auth.UpdateMultiFactorInfoRequest; +import UpdatePhoneMultiFactorInfoRequest = auth.UpdatePhoneMultiFactorInfoRequest; +import UserImportRecord = auth.UserImportRecord; +import UserImportOptions = auth.UserImportOptions; +import UserImportResult = auth.UserImportResult; /** Interface representing an Auth second factor in Auth server format. */ export interface AuthFactorInfo { @@ -138,14 +84,6 @@ export interface UploadAccountRequest extends UploadAccountOptions { } -/** Response object for importUsers operation. */ -export interface UserImportResult { - failureCount: number; - successCount: number; - errors: FirebaseArrayIndexError[]; -} - - /** Callback function to validate an UploadAccountUser object. */ export type ValidatorFunction = (data: UploadAccountUser) => void; @@ -155,7 +93,7 @@ export type ValidatorFunction = (data: UploadAccountUser) => void; * @param multiFactorInfo The client format second factor. * @return The corresponding AuthFactorInfo server request format. */ -export function convertMultiFactorInfoToServerFormat(multiFactorInfo: SecondFactor): AuthFactorInfo { +export function convertMultiFactorInfoToServerFormat(multiFactorInfo: UpdateMultiFactorInfoRequest): AuthFactorInfo { let enrolledAt; if (typeof multiFactorInfo.enrollmentTime !== 'undefined') { if (validator.isUTCDateString(multiFactorInfo.enrollmentTime)) { @@ -165,11 +103,11 @@ export function convertMultiFactorInfoToServerFormat(multiFactorInfo: SecondFact throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLMENT_TIME, `The second factor "enrollmentTime" for "${multiFactorInfo.uid}" must be a valid ` + - `UTC date string.`); + 'UTC date string.'); } } // Currently only phone second factors are supported. - if (multiFactorInfo.factorId === 'phone') { + if (isPhoneFactor(multiFactorInfo)) { // If any required field is missing or invalid, validation will still fail later. const authFactorInfo: AuthFactorInfo = { mfaEnrollmentId: multiFactorInfo.uid, @@ -192,6 +130,10 @@ export function convertMultiFactorInfoToServerFormat(multiFactorInfo: SecondFact } } +function isPhoneFactor(multiFactorInfo: UpdateMultiFactorInfoRequest): + multiFactorInfo is UpdatePhoneMultiFactorInfoRequest { + return multiFactorInfo.factorId === 'phone'; +} /** * @param {any} obj The object to check for number field within. @@ -392,14 +334,14 @@ export class UserImportBuilder { if (!validator.isNonNullObject(options.hash)) { throw new FirebaseAuthError( AuthClientErrorCode.MISSING_HASH_ALGORITHM, - `"hash.algorithm" is missing from the provided "UserImportOptions".`, + '"hash.algorithm" is missing from the provided "UserImportOptions".', ); } if (typeof options.hash.algorithm === 'undefined' || !validator.isNonEmptyString(options.hash.algorithm)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ALGORITHM, - `"hash.algorithm" must be a string matching the list of supported algorithms.`, + '"hash.algorithm" must be a string matching the list of supported algorithms.', ); } @@ -412,7 +354,7 @@ export class UserImportBuilder { if (!validator.isBuffer(options.hash.key)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_KEY, - `A non-empty "hash.key" byte buffer must be provided for ` + + 'A non-empty "hash.key" byte buffer must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -448,7 +390,7 @@ export class UserImportBuilder { if (isNaN(rounds) || rounds < 0 || rounds > 120000) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ROUNDS, - `A valid "hash.rounds" number between 0 and 120000 must be provided for ` + + 'A valid "hash.rounds" number between 0 and 120000 must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -462,7 +404,7 @@ export class UserImportBuilder { if (!validator.isBuffer(options.hash.key)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_KEY, - `A "hash.key" byte buffer must be provided for ` + + 'A "hash.key" byte buffer must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -470,7 +412,7 @@ export class UserImportBuilder { if (isNaN(rounds) || rounds <= 0 || rounds > 8) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ROUNDS, - `A valid "hash.rounds" number between 1 and 8 must be provided for ` + + 'A valid "hash.rounds" number between 1 and 8 must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -478,7 +420,7 @@ export class UserImportBuilder { if (isNaN(memoryCost) || memoryCost <= 0 || memoryCost > 14) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_MEMORY_COST, - `A valid "hash.memoryCost" number between 1 and 14 must be provided for ` + + 'A valid "hash.memoryCost" number between 1 and 14 must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -486,7 +428,7 @@ export class UserImportBuilder { !validator.isBuffer(options.hash.saltSeparator)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_SALT_SEPARATOR, - `"hash.saltSeparator" must be a byte buffer.`, + '"hash.saltSeparator" must be a byte buffer.', ); } populatedOptions = { @@ -509,7 +451,7 @@ export class UserImportBuilder { if (isNaN(cpuMemCost)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_MEMORY_COST, - `A valid "hash.memoryCost" number must be provided for ` + + 'A valid "hash.memoryCost" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -517,7 +459,7 @@ export class UserImportBuilder { if (isNaN(parallelization)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_PARALLELIZATION, - `A valid "hash.parallelization" number must be provided for ` + + 'A valid "hash.parallelization" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -525,7 +467,7 @@ export class UserImportBuilder { if (isNaN(blockSize)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_BLOCK_SIZE, - `A valid "hash.blockSize" number must be provided for ` + + 'A valid "hash.blockSize" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -533,7 +475,7 @@ export class UserImportBuilder { if (isNaN(dkLen)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, - `A valid "hash.derivedKeyLength" number must be provided for ` + + 'A valid "hash.derivedKeyLength" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 8ec5c08a8e..96ca179dd4 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -18,6 +18,14 @@ import { deepCopy } from '../utils/deep-copy'; import { isNonNullObject } from '../utils/validator'; import * as utils from '../utils'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { auth } from './index'; + +import MultiFactorInfoInterface = auth.MultiFactorInfo; +import PhoneMultiFactorInfoInterface = auth.PhoneMultiFactorInfo; +import MultiFactorSettings = auth.MultiFactorSettings; +import UserMetadataInterface = auth.UserMetadata; +import UserInfoInterface = auth.UserInfo; +import UserRecordInterface = auth.UserRecord; /** * 'REDACTED', encoded as a base64 string. @@ -42,64 +50,6 @@ function parseDate(time: any): string | null { return null; } -/** - * Interface representing base properties of a user enrolled second factor for a - * `CreateRequest`. - */ -export interface CreateMultiFactorInfoRequest { - displayName?: string; - factorId: string; -} - -/** - * Interface representing a phone specific user enrolled second factor for a - * `CreateRequest`. - */ -export interface CreatePhoneMultiFactorInfoRequest extends CreateMultiFactorInfoRequest { - phoneNumber: string; -} - -/** - * Interface representing common properties of a user enrolled second factor - * for an `UpdateRequest`. - */ -export interface UpdateMultiFactorInfoRequest { - uid?: string; - displayName?: string; - enrollmentTime?: string; - factorId: string; -} - -/** - * Interface representing a phone specific user enrolled second factor - * for an `UpdateRequest`. - */ -export interface UpdatePhoneMultiFactorInfoRequest extends UpdateMultiFactorInfoRequest { - phoneNumber: string; -} - -/** Parameters for update user operation */ -export interface UpdateRequest { - disabled?: boolean; - displayName?: string | null; - email?: string; - emailVerified?: boolean; - password?: string; - phoneNumber?: string | null; - photoURL?: string | null; - multiFactor?: { - enrolledFactors: UpdateMultiFactorInfoRequest[] | null; - }; -} - -/** Parameters for create user operation */ -export interface CreateRequest extends UpdateRequest { - uid?: string; - multiFactor?: { - enrolledFactors: CreateMultiFactorInfoRequest[]; - }; -} - export interface MultiFactorInfoResponse { mfaEnrollmentId: string; displayName?: string; @@ -138,18 +88,17 @@ export interface GetAccountInfoUserResponse { [key: string]: any; } -/** Enums for multi-factor identifiers. */ -export enum MultiFactorId { +enum MultiFactorId { Phone = 'phone', } /** * Abstract class representing a multi-factor info interface. */ -export abstract class MultiFactorInfo { +export abstract class MultiFactorInfo implements MultiFactorInfoInterface { public readonly uid: string; public readonly displayName?: string; - public readonly factorId: MultiFactorId; + public readonly factorId: string; public readonly enrollmentTime?: string; /** @@ -197,7 +146,7 @@ export abstract class MultiFactorInfo { * @return The multi-factor ID associated with the provided response. If the response is * not associated with any known multi-factor ID, null is returned. */ - protected abstract getFactorId(response: MultiFactorInfoResponse): MultiFactorId | null; + protected abstract getFactorId(response: MultiFactorInfoResponse): string | null; /** * Initializes the MultiFactorInfo object using the provided server response. @@ -228,7 +177,7 @@ export abstract class MultiFactorInfo { } /** Class representing a phone MultiFactorInfo object. */ -export class PhoneMultiFactorInfo extends MultiFactorInfo { +export class PhoneMultiFactorInfo extends MultiFactorInfo implements PhoneMultiFactorInfoInterface { public readonly phoneNumber: string; /** @@ -258,13 +207,13 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { * @return The multi-factor ID associated with the provided response. If the response is * not associated with any known multi-factor ID, null is returned. */ - protected getFactorId(response: MultiFactorInfoResponse): MultiFactorId | null { + protected getFactorId(response: MultiFactorInfoResponse): string | null { return (response && response.phoneInfo) ? MultiFactorId.Phone : null; } } /** Class representing multi-factor related properties of a user. */ -export class MultiFactor { +export class MultiFactor implements MultiFactorSettings { public enrolledFactors: MultiFactorInfo[]; /** @@ -308,7 +257,7 @@ export class MultiFactor { * endpoint. * @constructor */ -export class UserMetadata { +export class UserMetadata implements UserMetadataInterface { public readonly creationTime: string; public readonly lastSignInTime: string; @@ -347,7 +296,7 @@ export class UserMetadata { * endpoint. * @constructor */ -export class UserInfo { +export class UserInfo implements UserInfoInterface { public readonly uid: string; public readonly displayName: string; public readonly email: string; @@ -392,7 +341,7 @@ export class UserInfo { * endpoint. * @constructor */ -export class UserRecord { +export class UserRecord implements UserRecordInterface { public readonly uid: string; public readonly email: string; public readonly emailVerified: boolean; diff --git a/src/credential.d.ts b/src/credential.d.ts deleted file mode 100644 index 1a3b8a97d7..0000000000 --- a/src/credential.d.ts +++ /dev/null @@ -1,149 +0,0 @@ -import * as _admin from './index.d'; -import { Agent } from 'http'; - -export namespace admin.credential { - /** - * Interface that provides Google OAuth2 access tokens used to authenticate - * with Firebase services. - * - * In most cases, you will not need to implement this yourself and can instead - * use the default implementations provided by - * {@link admin.credential `admin.credential`}. - */ - interface Credential { - - /** - * Returns a Google OAuth2 access token object used to authenticate with - * Firebase services. - * - * This object contains the following properties: - * * `access_token` (`string`): The actual Google OAuth2 access token. - * * `expires_in` (`number`): The number of seconds from when the token was - * issued that it expires. - * - * @return A Google OAuth2 access token object. - */ - getAccessToken(): Promise<_admin.GoogleOAuthAccessToken>; - } - - - /** - * Returns a credential created from the - * {@link - * https://developers.google.com/identity/protocols/application-default-credentials - * Google Application Default Credentials} - * that grants admin access to Firebase services. This credential can be used - * in the call to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * Google Application Default Credentials are available on any Google - * infrastructure, such as Google App Engine and Google Compute Engine. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * admin.initializeApp({ - * credential: admin.credential.applicationDefault(), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param {!Object=} httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return {!admin.credential.Credential} A credential authenticated via Google - * Application Default Credentials that can be used to initialize an app. - */ - function applicationDefault(httpAgent?: Agent): admin.credential.Credential; - - /** - * Returns a credential created from the provided service account that grants - * admin access to Firebase services. This credential can be used in the call - * to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * // Providing a path to a service account key JSON file - * var serviceAccount = require("path/to/serviceAccountKey.json"); - * admin.initializeApp({ - * credential: admin.credential.cert(serviceAccount), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @example - * ```javascript - * // Providing a service account object inline - * admin.initializeApp({ - * credential: admin.credential.cert({ - * projectId: "", - * clientEmail: "foo@.iam.gserviceaccount.com", - * privateKey: "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n" - * }), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param serviceAccountPathOrObject The path to a service - * account key JSON file or an object representing a service account key. - * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return A credential authenticated via the - * provided service account that can be used to initialize an app. - */ - function cert( - serviceAccountPathOrObject: string | _admin.ServiceAccount, - httpAgent?: Agent): admin.credential.Credential; - - /** - * Returns a credential created from the provided refresh token that grants - * admin access to Firebase services. This credential can be used in the call - * to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * // Providing a path to a refresh token JSON file - * var refreshToken = require("path/to/refreshToken.json"); - * admin.initializeApp({ - * credential: admin.credential.refreshToken(refreshToken), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param refreshTokenPathOrObject The path to a Google - * OAuth2 refresh token JSON file or an object representing a Google OAuth2 - * refresh token. - * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return A credential authenticated via the - * provided service account that can be used to initialize an app. - */ - function refreshToken(refreshTokenPathOrObject: string | object, httpAgent?: Agent): admin.credential.Credential; -} diff --git a/src/credential/credential-interfaces.ts b/src/credential/credential-interfaces.ts deleted file mode 100644 index d627586020..0000000000 --- a/src/credential/credential-interfaces.ts +++ /dev/null @@ -1,50 +0,0 @@ -/*! - * Copyright 2020 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. - */ - -// TODO: According to the typings this is part of the Firebase Namespace today -// and not credential; it will need to be moved accordingly. -/** - * Interface for Google OAuth 2.0 access tokens. - */ -export interface GoogleOAuthAccessToken { - /* tslint:disable:variable-name */ - access_token: string; - expires_in: number; - /* tslint:enable:variable-name */ -} - -/** - * Interface that provides Google OAuth2 access tokens used to authenticate - * with Firebase services. - * - * In most cases, you will not need to implement this yourself and can instead - * use the default implementations provided by - * {@link admin.credential `admin.credential`}. - */ -export interface Credential { - /** - * Returns a Google OAuth2 access token object used to authenticate with - * Firebase services. - * - * This object contains the following properties: - * * `access_token` (`string`): The actual Google OAuth2 access token. - * * `expires_in` (`number`): The number of seconds from when the token was - * issued that it expires. - * - * @return A Google OAuth2 access token object. - */ - getAccessToken(): Promise; -} diff --git a/src/credential/credential-internal.ts b/src/credential/credential-internal.ts index a1037857af..a266a42ceb 100644 --- a/src/credential/credential-internal.ts +++ b/src/credential/credential-internal.ts @@ -14,17 +14,18 @@ * limitations under the License. */ -// Use untyped import syntax for Node built-ins import fs = require('fs'); import os = require('os'); import path = require('path'); -import { GoogleOAuthAccessToken, Credential } from './credential-interfaces'; +import { Agent } from 'http'; +import { credential, GoogleOAuthAccessToken } from './index'; import { AppErrorCodes, FirebaseAppError } from '../utils/error'; import { HttpClient, HttpRequestConfig, HttpError, HttpResponse } from '../utils/api-request'; -import { Agent } from 'http'; import * as util from '../utils/validator'; +import Credential = credential.Credential; + const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token'; const GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com'; const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token'; diff --git a/src/credential/credential.ts b/src/credential/credential.ts index f05b40676b..fbac0ed36d 100644 --- a/src/credential/credential.ts +++ b/src/credential/credential.ts @@ -14,104 +14,23 @@ * limitations under the License. */ -// Use untyped import syntax for Node built-ins -import { Agent } from 'http'; import { - ServiceAccountCredential, RefreshTokenCredential, getApplicationDefault + ServiceAccountCredential, RefreshTokenCredential, getApplicationDefault } from './credential-internal'; -import { Credential } from './credential-interfaces'; +import { credential } from './index'; -let globalAppDefaultCred: Credential; +let globalAppDefaultCred: credential.Credential; const globalCertCreds: { [key: string]: ServiceAccountCredential } = {}; const globalRefreshTokenCreds: { [key: string]: RefreshTokenCredential } = {}; -/** - * Returns a credential created from the - * {@link - * https://developers.google.com/identity/protocols/application-default-credentials - * Google Application Default Credentials} - * that grants admin access to Firebase services. This credential can be used - * in the call to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * Google Application Default Credentials are available on any Google - * infrastructure, such as Google App Engine and Google Compute Engine. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * admin.initializeApp({ - * credential: admin.credential.applicationDefault(), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param {!Object=} httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return {!admin.credential.Credential} A credential authenticated via Google - * Application Default Credentials that can be used to initialize an app. - */ -export function applicationDefault(httpAgent?: Agent): Credential { +export const applicationDefault: typeof credential.applicationDefault = (httpAgent?) => { if (typeof globalAppDefaultCred === 'undefined') { globalAppDefaultCred = getApplicationDefault(httpAgent); } return globalAppDefaultCred; } -/** - * Returns a credential created from the provided service account that grants - * admin access to Firebase services. This credential can be used in the call - * to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * // Providing a path to a service account key JSON file - * var serviceAccount = require("path/to/serviceAccountKey.json"); - * admin.initializeApp({ - * credential: admin.credential.cert(serviceAccount), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @example - * ```javascript - * // Providing a service account object inline - * admin.initializeApp({ - * credential: admin.credential.cert({ - * projectId: "", - * clientEmail: "foo@.iam.gserviceaccount.com", - * privateKey: "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n" - * }), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param serviceAccountPathOrObject The path to a service - * account key JSON file or an object representing a service account key. - * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return A credential authenticated via the - * provided service account that can be used to initialize an app. - */ -export function cert(serviceAccountPathOrObject: string | object, httpAgent?: Agent): Credential { +export const cert: typeof credential.cert = (serviceAccountPathOrObject, httpAgent?) => { const stringifiedServiceAccount = JSON.stringify(serviceAccountPathOrObject); if (!(stringifiedServiceAccount in globalCertCreds)) { globalCertCreds[stringifiedServiceAccount] = new ServiceAccountCredential(serviceAccountPathOrObject, httpAgent); @@ -119,40 +38,7 @@ export function cert(serviceAccountPathOrObject: string | object, httpAgent?: Ag return globalCertCreds[stringifiedServiceAccount]; } -/** - * Returns a credential created from the provided refresh token that grants - * admin access to Firebase services. This credential can be used in the call - * to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * // Providing a path to a refresh token JSON file - * var refreshToken = require("path/to/refreshToken.json"); - * admin.initializeApp({ - * credential: admin.credential.refreshToken(refreshToken), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param refreshTokenPathOrObject The path to a Google - * OAuth2 refresh token JSON file or an object representing a Google OAuth2 - * refresh token. - * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return A credential authenticated via the - * provided service account that can be used to initialize an app. - */ -export function refreshToken(refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential { +export const refreshToken: typeof credential.refreshToken = (refreshTokenPathOrObject, httpAgent) => { const stringifiedRefreshToken = JSON.stringify(refreshTokenPathOrObject); if (!(stringifiedRefreshToken in globalRefreshTokenCreds)) { globalRefreshTokenCreds[stringifiedRefreshToken] = new RefreshTokenCredential( diff --git a/src/credential/index.ts b/src/credential/index.ts index db1e5258bb..dfd27819ed 100644 --- a/src/credential/index.ts +++ b/src/credential/index.ts @@ -14,23 +14,164 @@ * limitations under the License. */ -import * as credentialApi from './credential'; -import * as credentialInterfacesApi from './credential-interfaces'; +import { Agent } from 'http'; + +export interface ServiceAccount { + projectId?: string; + clientEmail?: string; + privateKey?: string; +} /** - * Temporarily, admin.credential is used as the namespace name because we - * cannot barrel re-export the contents from credential.ts, and we want it to - * match the namespacing in the re-export inside src/index.d.ts + * Interface for Google OAuth 2.0 access tokens. */ +export interface GoogleOAuthAccessToken { + access_token: string; + expires_in: number; +} + /* eslint-disable @typescript-eslint/no-namespace */ -export namespace admin.credential { - // See https://github.com/microsoft/TypeScript/issues/4336 - /* eslint-disable @typescript-eslint/no-unused-vars */ - // See https://github.com/typescript-eslint/typescript-eslint/issues/363 - // Allows for exposing classes as interfaces in typings - /* eslint-disable @typescript-eslint/no-empty-interface */ - export import Credential = credentialInterfacesApi.Credential; - export const applicationDefault = credentialApi.applicationDefault; - export const cert = credentialApi.cert; - export const refreshToken = credentialApi.refreshToken; +export namespace credential { + /** + * Interface that provides Google OAuth2 access tokens used to authenticate + * with Firebase services. + * + * In most cases, you will not need to implement this yourself and can instead + * use the default implementations provided by + * {@link credential `admin.credential`}. + */ + export interface Credential { + /** + * Returns a Google OAuth2 access token object used to authenticate with + * Firebase services. + * + * This object contains the following properties: + * * `access_token` (`string`): The actual Google OAuth2 access token. + * * `expires_in` (`number`): The number of seconds from when the token was + * issued that it expires. + * + * @return A Google OAuth2 access token object. + */ + getAccessToken(): Promise; + } + + /** + * Returns a credential created from the + * {@link + * https://developers.google.com/identity/protocols/application-default-credentials + * Google Application Default Credentials} + * that grants admin access to Firebase services. This credential can be used + * in the call to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * Google Application Default Credentials are available on any Google + * infrastructure, such as Google App Engine and Google Compute Engine. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * admin.initializeApp({ + * credential: admin.credential.applicationDefault(), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param {!Object=} httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return {!admin.credential.Credential} A credential authenticated via Google + * Application Default Credentials that can be used to initialize an app. + */ + export declare function applicationDefault(httpAgent?: Agent): Credential; + + /** + * Returns a credential created from the provided service account that grants + * admin access to Firebase services. This credential can be used in the call + * to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * // Providing a path to a service account key JSON file + * var serviceAccount = require("path/to/serviceAccountKey.json"); + * admin.initializeApp({ + * credential: admin.credential.cert(serviceAccount), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @example + * ```javascript + * // Providing a service account object inline + * admin.initializeApp({ + * credential: admin.credential.cert({ + * projectId: "", + * clientEmail: "foo@.iam.gserviceaccount.com", + * privateKey: "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n" + * }), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param serviceAccountPathOrObject The path to a service + * account key JSON file or an object representing a service account key. + * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return A credential authenticated via the + * provided service account that can be used to initialize an app. + */ + export declare function cert( + serviceAccountPathOrObject: string | ServiceAccount, httpAgent?: Agent): Credential; + + /** + * Returns a credential created from the provided refresh token that grants + * admin access to Firebase services. This credential can be used in the call + * to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * // Providing a path to a refresh token JSON file + * var refreshToken = require("path/to/refreshToken.json"); + * admin.initializeApp({ + * credential: admin.credential.refreshToken(refreshToken), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param refreshTokenPathOrObject The path to a Google + * OAuth2 refresh token JSON file or an object representing a Google OAuth2 + * refresh token. + * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return A credential authenticated via the + * provided service account that can be used to initialize an app. + */ + export declare function refreshToken( + refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential; } diff --git a/src/database.d.ts b/src/database.d.ts deleted file mode 100644 index 40ca71ea73..0000000000 --- a/src/database.d.ts +++ /dev/null @@ -1,1662 +0,0 @@ -import * as _admin from './index.d'; - -/* eslint-disable @typescript-eslint/ban-types */ - -export namespace admin.database { - - /** - * The Firebase Realtime Database service interface. - * - * Do not call this constructor directly. Instead, use - * [`admin.database()`](admin.database#database). - * - * See - * {@link - * https://firebase.google.com/docs/database/admin/start/ - * Introduction to the Admin Database API} - * for a full guide on how to use the Firebase Realtime Database service. - */ - interface Database { - app: _admin.app.App; - - /** - * Disconnects from the server (all Database operations will be completed - * offline). - * - * The client automatically maintains a persistent connection to the Database - * server, which will remain active indefinitely and reconnect when - * disconnected. However, the `goOffline()` and `goOnline()` methods may be used - * to control the client connection in cases where a persistent connection is - * undesirable. - * - * While offline, the client will no longer receive data updates from the - * Database. However, all Database operations performed locally will continue to - * immediately fire events, allowing your application to continue behaving - * normally. Additionally, each operation performed locally will automatically - * be queued and retried upon reconnection to the Database server. - * - * To reconnect to the Database and begin receiving remote events, see - * `goOnline()`. - * - * @example - * ```javascript - * admin.database().goOffline(); - * ``` - */ - goOffline(): void; - - /** - * Reconnects to the server and synchronizes the offline Database state - * with the server state. - * - * This method should be used after disabling the active connection with - * `goOffline()`. Once reconnected, the client will transmit the proper data - * and fire the appropriate events so that your client "catches up" - * automatically. - * - * @example - * ```javascript - * admin.database().goOnline(); - * ``` - */ - goOnline(): void; - - /** - * Returns a `Reference` representing the location in the Database - * corresponding to the provided path. Also can be invoked with an existing - * `Reference` as the argument. In that case returns a new `Reference` - * pointing to the same location. If no path argument is - * provided, returns a `Reference` that represents the root of the Database. - * - * @example - * ```javascript - * // Get a reference to the root of the Database - * var rootRef = admin.database.ref(); - * ``` - * - * @example - * ```javascript - * // Get a reference to the /users/ada node - * var adaRef = admin.database().ref("users/ada"); - * // The above is shorthand for the following operations: - * //var rootRef = admin.database().ref(); - * //var adaRef = rootRef.child("users/ada"); - * ``` - * - * @example - * ```javascript - * var adaRef = admin.database().ref("users/ada"); - * // Get a new reference pointing to the same location. - * var anotherAdaRef = admin.database().ref(adaRef); - * ``` - * - * - * @param path Optional path representing - * the location the returned `Reference` will point. Alternatively, a - * `Reference` object to copy. If not provided, the returned `Reference` will - * point to the root of the Database. - * @return If a path is provided, a `Reference` - * pointing to the provided path. Otherwise, a `Reference` pointing to the - * root of the Database. - */ - ref(path?: string | admin.database.Reference): admin.database.Reference; - - /** - * Returns a `Reference` representing the location in the Database - * corresponding to the provided Firebase URL. - * - * An exception is thrown if the URL is not a valid Firebase Database URL or it - * has a different domain than the current `Database` instance. - * - * Note that all query parameters (`orderBy`, `limitToLast`, etc.) are ignored - * and are not applied to the returned `Reference`. - * - * @example - * ```javascript - * // Get a reference to the root of the Database - * var rootRef = admin.database().ref("https://.firebaseio.com"); - * ``` - * - * @example - * ```javascript - * // Get a reference to the /users/ada node - * var adaRef = admin.database().ref("https://.firebaseio.com/users/ada"); - * ``` - * - * @param url The Firebase URL at which the returned `Reference` will - * point. - * @return A `Reference` pointing to the provided Firebase URL. - */ - refFromURL(url: string): admin.database.Reference; - - /** - * Gets the currently applied security rules as a string. The return value consists of - * the rules source including comments. - * - * @return A promise fulfilled with the rules as a raw string. - */ - getRules(): Promise; - - /** - * Gets the currently applied security rules as a parsed JSON object. Any comments in - * the original source are stripped away. - * - * @return A promise fulfilled with the parsed rules object. - */ - getRulesJSON(): Promise; - - /** - * Sets the specified rules on the Firebase Realtime Database instance. If the rules source is - * specified as a string or a Buffer, it may include comments. - * - * @param source Source of the rules to apply. Must not be `null` or empty. - * @return Resolves when the rules are set on the Realtime Database. - */ - setRules(source: string | Buffer | object): Promise; - } - - /** - * A `DataSnapshot` contains data from a Database location. - * - * Any time you read data from the Database, you receive the data as a - * `DataSnapshot`. A `DataSnapshot` is passed to the event callbacks you attach - * with `on()` or `once()`. You can extract the contents of the snapshot as a - * JavaScript object by calling the `val()` method. Alternatively, you can - * traverse into the snapshot by calling `child()` to return child snapshots - * (which you could then call `val()` on). - * - * A `DataSnapshot` is an efficiently generated, immutable copy of the data at - * a Database location. It cannot be modified and will never change (to modify - * data, you always call the `set()` method on a `Reference` directly). - */ - interface DataSnapshot { - key: string | null; - ref: admin.database.Reference; - - /** - * Gets another `DataSnapshot` for the location at the specified relative path. - * - * Passing a relative path to the `child()` method of a DataSnapshot returns - * another `DataSnapshot` for the location at the specified relative path. The - * relative path can either be a simple child name (for example, "ada") or a - * deeper, slash-separated path (for example, "ada/name/first"). If the child - * location has no data, an empty `DataSnapshot` (that is, a `DataSnapshot` - * whose value is `null`) is returned. - * - * @example - * ```javascript - * // Assume we have the following data in the Database: - * { - * "name": { - * "first": "Ada", - * "last": "Lovelace" - * } - * } - * - * // Test for the existence of certain keys within a DataSnapshot - * var ref = admin.database().ref("users/ada"); - * ref.once("value") - * .then(function(snapshot) { - * var name = snapshot.child("name").val(); // {first:"Ada",last:"Lovelace"} - * var firstName = snapshot.child("name/first").val(); // "Ada" - * var lastName = snapshot.child("name").child("last").val(); // "Lovelace" - * var age = snapshot.child("age").val(); // null - * }); - * ``` - * - * @param path A relative path to the location of child data. - * @return `DataSnapshot` for the location at the specified relative path. - */ - child(path: string): admin.database.DataSnapshot; - - /** - * Returns true if this `DataSnapshot` contains any data. It is slightly more - * efficient than using `snapshot.val() !== null`. - * - * @example - * ```javascript - * // Assume we have the following data in the Database: - * { - * "name": { - * "first": "Ada", - * "last": "Lovelace" - * } - * } - * - * // Test for the existence of certain keys within a DataSnapshot - * var ref = admin.database().ref("users/ada"); - * ref.once("value") - * .then(function(snapshot) { - * var a = snapshot.exists(); // true - * var b = snapshot.child("name").exists(); // true - * var c = snapshot.child("name/first").exists(); // true - * var d = snapshot.child("name/middle").exists(); // false - * }); - * ``` - * - * @return Whether this `DataSnapshot` contains any data. - */ - exists(): boolean; - - /** - * Exports the entire contents of the DataSnapshot as a JavaScript object. - * - * The `exportVal()` method is similar to `val()`, except priority information - * is included (if available), making it suitable for backing up your data. - * - * @return The DataSnapshot's contents as a JavaScript value (Object, - * Array, string, number, boolean, or `null`). - */ - exportVal(): any; - - /** - * Enumerates the top-level children in the `DataSnapshot`. - * - * Because of the way JavaScript objects work, the ordering of data in the - * JavaScript object returned by `val()` is not guaranteed to match the ordering - * on the server nor the ordering of `child_added` events. That is where - * `forEach()` comes in handy. It guarantees the children of a `DataSnapshot` - * will be iterated in their query order. - * - * If no explicit `orderBy*()` method is used, results are returned - * ordered by key (unless priorities are used, in which case, results are - * returned by priority). - * - * @example - * ```javascript - * - * // Assume we have the following data in the Database: - * { - * "users": { - * "ada": { - * "first": "Ada", - * "last": "Lovelace" - * }, - * "alan": { - * "first": "Alan", - * "last": "Turing" - * } - * } - * } - * - * // Loop through users in order with the forEach() method. The callback - * // provided to forEach() will be called synchronously with a DataSnapshot - * // for each child: - * var query = admin.database().ref("users").orderByKey(); - * query.once("value") - * .then(function(snapshot) { - * snapshot.forEach(function(childSnapshot) { - * // key will be "ada" the first time and "alan" the second time - * var key = childSnapshot.key; - * // childData will be the actual contents of the child - * var childData = childSnapshot.val(); - * }); - * }); - * ``` - * - * @example - * ```javascript - * // You can cancel the enumeration at any point by having your callback - * // function return true. For example, the following code sample will only - * // fire the callback function one time: - * var query = admin.database().ref("users").orderByKey(); - * query.once("value") - * .then(function(snapshot) { - * snapshot.forEach(function(childSnapshot) { - * var key = childSnapshot.key; // "ada" - * - * // Cancel enumeration - * return true; - * }); - * }); - * ``` - * - * @param action A function - * that will be called for each child `DataSnapshot`. The callback can return - * true to cancel further enumeration. - * @return True if enumeration was canceled due to your callback - * returning true. - */ - forEach(action: (a: admin.database.DataSnapshot) => boolean | void): boolean; - - /** - * Gets the priority value of the data in this `DataSnapshot`. - * - * Applications need not use priority but can order collections by - * ordinary properties (see - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data}). - * - * @return The the priority value of the data in this `DataSnapshot`. - */ - getPriority(): string | number | null; - - /** - * Returns true if the specified child path has (non-null) data. - * - * @example - * ```javascript - * // Assume we have the following data in the Database: - * { - * "name": { - * "first": "Ada", - * "last": "Lovelace" - * } - * } - * - * // Determine which child keys in DataSnapshot have data. - * var ref = admin.database().ref("users/ada"); - * ref.once("value") - * .then(function(snapshot) { - * var hasName = snapshot.hasChild("name"); // true - * var hasAge = snapshot.hasChild("age"); // false - * }); - * ``` - * - * @param path A relative path to the location of a potential child. - * @return `true` if data exists at the specified child path; else - * `false`. - */ - hasChild(path: string): boolean; - - /** - * Returns whether or not the `DataSnapshot` has any non-`null` child - * properties. - * - * You can use `hasChildren()` to determine if a `DataSnapshot` has any - * children. If it does, you can enumerate them using `forEach()`. If it - * doesn't, then either this snapshot contains a primitive value (which can be - * retrieved with `val()`) or it is empty (in which case, `val()` will return - * `null`). - * - * @example - * ```javascript - * // Assume we have the following data in the Database: - * { - * "name": { - * "first": "Ada", - * "last": "Lovelace" - * } - * } - * - * var ref = admin.database().ref("users/ada"); - * ref.once("value") - * .then(function(snapshot) { - * var a = snapshot.hasChildren(); // true - * var b = snapshot.child("name").hasChildren(); // true - * var c = snapshot.child("name/first").hasChildren(); // false - * }); - * ``` - * - * @return True if this snapshot has any children; else false. - */ - hasChildren(): boolean; - - /** - * Returns the number of child properties of this `DataSnapshot`. - * - * @example - * ```javascript - * // Assume we have the following data in the Database: - * { - * "name": { - * "first": "Ada", - * "last": "Lovelace" - * } - * } - * - * var ref = admin.database().ref("users/ada"); - * ref.once("value") - * .then(function(snapshot) { - * var a = snapshot.numChildren(); // 1 ("name") - * var b = snapshot.child("name").numChildren(); // 2 ("first", "last") - * var c = snapshot.child("name/first").numChildren(); // 0 - * }); - * ``` - * - * @return The number of child properties of this `DataSnapshot`. - */ - numChildren(): number; - - /** - * @return A JSON-serializable representation of this object. - */ - toJSON(): Object | null; - - /** - * Extracts a JavaScript value from a `DataSnapshot`. - * - * Depending on the data in a `DataSnapshot`, the `val()` method may return a - * scalar type (string, number, or boolean), an array, or an object. It may also - * return null, indicating that the `DataSnapshot` is empty (contains no data). - * - * @example - * ```javascript - * // Write and then read back a string from the Database. - * ref.set("hello") - * .then(function() { - * return ref.once("value"); - * }) - * .then(function(snapshot) { - * var data = snapshot.val(); // data === "hello" - * }); - * ``` - * - * @example - * ```javascript - * // Write and then read back a JavaScript object from the Database. - * ref.set({ name: "Ada", age: 36 }) - * .then(function() { - * return ref.once("value"); - * }) - * .then(function(snapshot) { - * var data = snapshot.val(); - * // data is { "name": "Ada", "age": 36 } - * // data.name === "Ada" - * // data.age === 36 - * }); - * ``` - * - * @return The DataSnapshot's contents as a JavaScript value (Object, - * Array, string, number, boolean, or `null`). - */ - val(): any; - } - - /** - * The `onDisconnect` class allows you to write or clear data when your client - * disconnects from the Database server. These updates occur whether your - * client disconnects cleanly or not, so you can rely on them to clean up data - * even if a connection is dropped or a client crashes. - * - * The `onDisconnect` class is most commonly used to manage presence in - * applications where it is useful to detect how many clients are connected and - * when other clients disconnect. See - * {@link - * https://firebase.google.com/docs/database/web/offline-capabilities - * Enabling Offline Capabilities in JavaScript} for more information. - * - * To avoid problems when a connection is dropped before the requests can be - * transferred to the Database server, these functions should be called before - * any data is written. - * - * Note that `onDisconnect` operations are only triggered once. If you want an - * operation to occur each time a disconnect occurs, you'll need to re-establish - * the `onDisconnect` operations each time you reconnect. - */ - interface OnDisconnect { - - /** - * Cancels all previously queued `onDisconnect()` set or update events for this - * location and all children. - * - * If a write has been queued for this location via a `set()` or `update()` at a - * parent location, the write at this location will be canceled, though all - * other siblings will still be written. - * - * @example - * ```javascript - * var ref = admin.database().ref("onlineState"); - * ref.onDisconnect().set(false); - * // ... sometime later - * ref.onDisconnect().cancel(); - * ``` - * - * @param onComplete An optional callback function that is - * called when synchronization to the server has completed. The callback - * will be passed a single parameter: null for success, or an Error object - * indicating a failure. - * @return Resolves when synchronization to the server is complete. - */ - cancel(onComplete?: (a: Error | null) => any): Promise; - - /** - * Ensures the data at this location is deleted when the client is disconnected - * (due to closing the browser, navigating to a new page, or network issues). - * - * @param onComplete An optional callback function that is - * called when synchronization to the server has completed. The callback - * will be passed a single parameter: null for success, or an Error object - * indicating a failure. - * @return Resolves when synchronization to the server is complete. - */ - remove(onComplete?: (a: Error | null) => any): Promise; - - /** - * Ensures the data at this location is set to the specified value when the - * client is disconnected (due to closing the browser, navigating to a new page, - * or network issues). - * - * `set()` is especially useful for implementing "presence" systems, where a - * value should be changed or cleared when a user disconnects so that they - * appear "offline" to other users. See - * {@link - * https://firebase.google.com/docs/database/web/offline-capabilities - * Enabling Offline Capabilities in JavaScript} for more information. - * - * Note that `onDisconnect` operations are only triggered once. If you want an - * operation to occur each time a disconnect occurs, you'll need to re-establish - * the `onDisconnect` operations each time. - * - * @example - * ```javascript - * var ref = admin.database().ref("users/ada/status"); - * ref.onDisconnect().set("I disconnected!"); - * ``` - * - * @param value The value to be written to this location on - * disconnect (can be an object, array, string, number, boolean, or null). - * @param onComplete An optional callback function that - * will be called when synchronization to the database server has completed. - * The callback will be passed a single parameter: null for success, or an - * `Error` object indicating a failure. - * @return A promise that resolves when synchronization to the database is complete. - */ - set(value: any, onComplete?: (a: Error | null) => any): Promise; - - /** - * Ensures the data at this location is set to the specified value and priority - * when the client is disconnected (due to closing the browser, navigating to a - * new page, or network issues). - * - * @param value The value to be written to this location on - * disconnect (can be an object, array, string, number, boolean, or null). - * @param priority - * @param onComplete An optional callback function that is - * called when synchronization to the server has completed. The callback - * will be passed a single parameter: null for success, or an Error object - * indicating a failure. - * @return A promise that resolves when synchronization to the database is complete. - */ - setWithPriority( - value: any, - priority: number | string | null, - onComplete?: (a: Error | null) => any - ): Promise; - - /** - * Writes multiple values at this location when the client is disconnected (due - * to closing the browser, navigating to a new page, or network issues). - * - * The `values` argument contains multiple property-value pairs that will be - * written to the Database together. Each child property can either be a simple - * property (for example, "name") or a relative path (for example, "name/first") - * from the current location to the data to update. - * - * As opposed to the `set()` method, `update()` can be use to selectively update - * only the referenced properties at the current location (instead of replacing - * all the child properties at the current location). - * - * See {@link https://firebase.google.com/docs/reference/admin/node/admin.database.Reference#update} - * for examples of using the connected version of `update`. - * - * @example - * ```javascript - * var ref = admin.database().ref("users/ada"); - * ref.update({ - * onlineState: true, - * status: "I'm online." - * }); - * ref.onDisconnect().update({ - * onlineState: false, - * status: "I'm offline." - * }); - * ``` - * - * @param values object containing multiple values. - * @param onComplete An optional callback function that will - * be called when synchronization to the server has completed. The - * callback will be passed a single parameter: null for success, or an Error - * object indicating a failure. - * @return Resolves when synchronization to the - * Database is complete. - */ - update(values: object, onComplete?: (a: Error | null) => any): Promise; - } - - type EventType = 'value' | 'child_added' | 'child_changed' | 'child_moved' | 'child_removed'; - - /** - * A `Query` sorts and filters the data at a Database location so only a subset - * of the child data is included. This can be used to order a collection of - * data by some attribute (for example, height of dinosaurs) as well as to - * restrict a large list of items (for example, chat messages) down to a number - * suitable for synchronizing to the client. Queries are created by chaining - * together one or more of the filter methods defined here. - * - * Just as with a `Reference`, you can receive data from a `Query` by using the - * `on()` method. You will only receive events and `DataSnapshot`s for the - * subset of the data that matches your query. - * - * See - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data} for more information. - */ - interface Query { - ref: admin.database.Reference; - - /** - * Creates a `Query` with the specified ending point. - * - * Using `startAt()`, `endAt()`, and `equalTo()` allows you to choose arbitrary - * starting and ending points for your queries. - * - * The ending point is inclusive, so children with exactly the specified value - * will be included in the query. The optional key argument can be used to - * further limit the range of the query. If it is specified, then children that - * have exactly the specified value must also have a key name less than or equal - * to the specified key. - * - * You can read more about `endAt()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. - * - * @example - * ```javascript - * // Find all dinosaurs whose names come before Pterodactyl lexicographically. - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByKey().endAt("pterodactyl").on("child_added", function(snapshot) { - * console.log(snapshot.key); - * }); - * ``` - * - * @param value The value to end at. The argument - * type depends on which `orderBy*()` function was used in this query. - * Specify a value that matches the `orderBy*()` type. When used in - * combination with `orderByKey()`, the value must be a string. - * @param key The child key to end at, among the children with the - * previously specified priority. This argument is only allowed if ordering by - * priority. - * @return A new `Query` object. - */ - endAt(value: number | string | boolean | null, key?: string): admin.database.Query; - - /** - * Creates a `Query` that includes children that match the specified value. - * - * Using `startAt()`, `endAt()`, and `equalTo()` allows us to choose arbitrary - * starting and ending points for our queries. - * - * The optional key argument can be used to further limit the range of the - * query. If it is specified, then children that have exactly the specified - * value must also have exactly the specified key as their key name. This can be - * used to filter result sets with many matches for the same value. - * - * You can read more about `equalTo()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. - * - * @example - * // Find all dinosaurs whose height is exactly 25 meters. - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByChild("height").equalTo(25).on("child_added", function(snapshot) { - * console.log(snapshot.key); - * }); - * - * @param value The value to match for. The - * argument type depends on which `orderBy*()` function was used in this - * query. Specify a value that matches the `orderBy*()` type. When used in - * combination with `orderByKey()`, the value must be a string. - * @param key The child key to start at, among the children with the - * previously specified priority. This argument is only allowed if ordering by - * priority. - * @return A new `Query` object. - */ - equalTo(value: number | string | boolean | null, key?: string): admin.database.Query; - - /** - * Returns whether or not the current and provided queries represent the same - * location, have the same query parameters, and are from the same instance of - * `admin.app.App`. - * - * Two `Reference` objects are equivalent if they represent the same location - * and are from the same instance of `admin.app.App`. - * - * Two `Query` objects are equivalent if they represent the same location, have - * the same query parameters, and are from the same instance of `admin.app.App`. - * Equivalent queries share the same sort order, limits, and starting and - * ending points. - * - * @example - * ```javascript - * var rootRef = admin.database().ref(); - * var usersRef = rootRef.child("users"); - * - * usersRef.isEqual(rootRef); // false - * usersRef.isEqual(rootRef.child("users")); // true - * usersRef.parent.isEqual(rootRef); // true - * ``` - * - * @example - * ```javascript - * var rootRef = admin.database().ref(); - * var usersRef = rootRef.child("users"); - * var usersQuery = usersRef.limitToLast(10); - * - * usersQuery.isEqual(usersRef); // false - * usersQuery.isEqual(usersRef.limitToLast(10)); // true - * usersQuery.isEqual(rootRef.limitToLast(10)); // false - * usersQuery.isEqual(usersRef.orderByKey().limitToLast(10)); // false - * ``` - * - * @param other The query to compare against. - * @return Whether or not the current and provided queries are - * equivalent. - */ - isEqual(other: admin.database.Query | null): boolean; - - /** - * Generates a new `Query` limited to the first specific number of children. - * - * The `limitToFirst()` method is used to set a maximum number of children to be - * synced for a given callback. If we set a limit of 100, we will initially only - * receive up to 100 `child_added` events. If we have fewer than 100 messages - * stored in our Database, a `child_added` event will fire for each message. - * However, if we have over 100 messages, we will only receive a `child_added` - * event for the first 100 ordered messages. As items change, we will receive - * `child_removed` events for each item that drops out of the active list so - * that the total number stays at 100. - * - * You can read more about `limitToFirst()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. - * - * @example - * ```javascript - * // Find the two shortest dinosaurs. - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByChild("height").limitToFirst(2).on("child_added", function(snapshot) { - * // This will be called exactly two times (unless there are less than two - * // dinosaurs in the Database). - * - * // It will also get fired again if one of the first two dinosaurs is - * // removed from the data set, as a new dinosaur will now be the second - * // shortest. - * console.log(snapshot.key); - * }); - * ``` - * - * @param limit The maximum number of nodes to include in this query. - * @return A `Query` object. - */ - limitToFirst(limit: number): admin.database.Query; - - /** - * Generates a new `Query` object limited to the last specific number of - * children. - * - * The `limitToLast()` method is used to set a maximum number of children to be - * synced for a given callback. If we set a limit of 100, we will initially only - * receive up to 100 `child_added` events. If we have fewer than 100 messages - * stored in our Database, a `child_added` event will fire for each message. - * However, if we have over 100 messages, we will only receive a `child_added` - * event for the last 100 ordered messages. As items change, we will receive - * `child_removed` events for each item that drops out of the active list so - * that the total number stays at 100. - * - * You can read more about `limitToLast()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. - * - * @example - * ```javascript - * // Find the two heaviest dinosaurs. - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByChild("weight").limitToLast(2).on("child_added", function(snapshot) { - * // This callback will be triggered exactly two times, unless there are - * // fewer than two dinosaurs stored in the Database. It will also get fired - * // for every new, heavier dinosaur that gets added to the data set. - * console.log(snapshot.key); - * }); - * ``` - * - * @param limit The maximum number of nodes to include in this query. - * @return A `Query` object. - */ - limitToLast(limit: number): admin.database.Query; - - /** - * Detaches a callback previously attached with `on()`. - * - * Detach a callback previously attached with `on()`. Note that if `on()` was - * called multiple times with the same eventType and callback, the callback - * will be called multiple times for each event, and `off()` must be called - * multiple times to remove the callback. Calling `off()` on a parent listener - * will not automatically remove listeners registered on child nodes, `off()` - * must also be called on any child listeners to remove the callback. - * - * If a callback is not specified, all callbacks for the specified eventType - * will be removed. Similarly, if no eventType or callback is specified, all - * callbacks for the `Reference` will be removed. - * - * @example - * ```javascript - * var onValueChange = function(dataSnapshot) { ... }; - * ref.on('value', onValueChange); - * ref.child('meta-data').on('child_added', onChildAdded); - * // Sometime later... - * ref.off('value', onValueChange); - * - * // You must also call off() for any child listeners on ref - * // to cancel those callbacks - * ref.child('meta-data').off('child_added', onValueAdded); - * ``` - * - * @example - * ```javascript - * // Or you can save a line of code by using an inline function - * // and on()'s return value. - * var onValueChange = ref.on('value', function(dataSnapshot) { ... }); - * // Sometime later... - * ref.off('value', onValueChange); - * ``` - * - * @param eventType One of the following strings: "value", - * "child_added", "child_changed", "child_removed", or "child_moved." - * @param callback The callback function that was passed to `on()`. - * @param context The context that was passed to `on()`. - */ - off( - eventType?: admin.database.EventType, - callback?: (a: admin.database.DataSnapshot, b?: string | null) => any, - context?: object | null - ): void; - - /** - * Listens for data changes at a particular location. - * - * This is the primary way to read data from a Database. Your callback - * will be triggered for the initial data and again whenever the data changes. - * Use `off( )` to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data - * Retrieve Data on the Web} - * for more details. - * - *

    value event

    - * - * This event will trigger once with the initial data stored at this location, - * and then trigger again each time the data changes. The `DataSnapshot` passed - * to the callback will be for the location at which `on()` was called. It - * won't trigger until the entire contents has been synchronized. If the - * location has no data, it will be triggered with an empty `DataSnapshot` - * (`val()` will return `null`). - * - *

    child_added event

    - * - * This event will be triggered once for each initial child at this location, - * and it will be triggered again every time a new child is added. The - * `DataSnapshot` passed into the callback will reflect the data for the - * relevant child. For ordering purposes, it is passed a second argument which - * is a string containing the key of the previous sibling child by sort order - * (or `null` if it is the first child). - * - *

    child_removed event

    - * - * This event will be triggered once every time a child is removed. The - * `DataSnapshot` passed into the callback will be the old data for the child - * that was removed. A child will get removed when either: - * - * - a client explicitly calls `remove()` on that child or one of its ancestors - * - a client calls `set(null)` on that child or one of its ancestors - * - that child has all of its children removed - * - there is a query in effect which now filters out the child (because it's - * sort order changed or the max limit was hit) - * - *

    child_changed event

    - * - * This event will be triggered when the data stored in a child (or any of its - * descendants) changes. Note that a single `child_changed` event may represent - * multiple changes to the child. The `DataSnapshot` passed to the callback will - * contain the new child contents. For ordering purposes, the callback is also - * passed a second argument which is a string containing the key of the previous - * sibling child by sort order (or `null` if it is the first child). - * - *

    child_moved event

    - * - * This event will be triggered when a child's sort order changes such that its - * position relative to its siblings changes. The `DataSnapshot` passed to the - * callback will be for the data of the child that has moved. It is also passed - * a second argument which is a string containing the key of the previous - * sibling child by sort order (or `null` if it is the first child). - * - * @example - * ```javascript - * // Handle a new value. - * ref.on('value', function(dataSnapshot) { - * ... - * }); - * ``` - * - * @example - * ```javascript - * // Handle a new child. - * ref.on('child_added', function(childSnapshot, prevChildKey) { - * ... - * }); - * ``` - * - * @example - * ```javascript - * // Handle child removal. - * ref.on('child_removed', function(oldChildSnapshot) { - * ... - * }); - * ``` - * - * @example - * ```javascript - * // Handle child data changes. - * ref.on('child_changed', function(childSnapshot, prevChildKey) { - * ... - * }); - * ``` - * - * @example - * ```javascript - * // Handle child ordering changes. - * ref.on('child_moved', function(childSnapshot, prevChildKey) { - * ... - * }); - * ``` - * - * @param eventType One of the following strings: "value", - * "child_added", "child_changed", "child_removed", or "child_moved." - * @param callback A callback that fires when the specified event occurs. The callback is - * passed a DataSnapshot. For ordering purposes, "child_added", - * "child_changed", and "child_moved" will also be passed a string containing - * the key of the previous child, by sort order (or `null` if it is the - * first child). - * @param cancelCallbackOrContext An optional - * callback that will be notified if your event subscription is ever canceled - * because your client does not have permission to read this data (or it had - * permission but has now lost it). This callback will be passed an `Error` - * object indicating why the failure occurred. - * @param context If provided, this object will be used as `this` - * when calling your callback(s). - * @return The provided - * callback function is returned unmodified. This is just for convenience if - * you want to pass an inline function to `on()`, but store the callback - * function for later passing to `off()`. - */ - on( - eventType: admin.database.EventType, - callback: (a: admin.database.DataSnapshot, b?: string | null) => any, - cancelCallbackOrContext?: ((a: Error) => any) | object | null, - context?: object | null - ): (a: admin.database.DataSnapshot | null, b?: string) => any; - - /** - * Listens for exactly one event of the specified event type, and then stops - * listening. - * - * This is equivalent to calling `on()`, and then calling `off()` inside the - * callback function. See `on()` for details on the event types. - * - * @example - * ```javascript - * // Basic usage of .once() to read the data located at ref. - * ref.once('value') - * .then(function(dataSnapshot) { - * // handle read data. - * }); - * ``` - * - * @param eventType One of the following strings: "value", - * "child_added", "child_changed", "child_removed", or "child_moved." - * @param successCallback A callback that fires when the specified event occurs. The callback is - * passed a `DataSnapshot`. For ordering purposes, "child_added", - * "child_changed", and "child_moved" will also be passed a string containing - * the key of the previous child by sort order (or `null` if it is the - * first child). - * @param failureCallbackOrContext An optional - * callback that will be notified if your client does not have permission to - * read the data. This callback will be passed an `Error` object indicating - * why the failure occurred. - * @param context If provided, this object will be used as `this` - * when calling your callback(s). - * @return {!Promise} - */ - once( - eventType: admin.database.EventType, - successCallback?: (a: admin.database.DataSnapshot, b?: string | null ) => any, - failureCallbackOrContext?: ((a: Error) => void) | object | null, - context?: object | null - ): Promise; - - /** - * Generates a new `Query` object ordered by the specified child key. - * - * Queries can only order by one key at a time. Calling `orderByChild()` - * multiple times on the same query is an error. - * - * Firebase queries allow you to order your data by any child key on the fly. - * However, if you know in advance what your indexes will be, you can define - * them via the .indexOn rule in your Security Rules for better performance. See - * the {@link https://firebase.google.com/docs/database/security/indexing-data - * .indexOn} rule for more information. - * - * You can read more about `orderByChild()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data}. - * - * @example - * ```javascript - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByChild("height").on("child_added", function(snapshot) { - * console.log(snapshot.key + " was " + snapshot.val().height + " m tall"); - * }); - * ``` - * - * @param path - * @return A new `Query` object. - */ - orderByChild(path: string): admin.database.Query; - - /** - * Generates a new `Query` object ordered by key. - * - * Sorts the results of a query by their (ascending) key values. - * - * You can read more about `orderByKey()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data}. - * - * @example - * ```javascript - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByKey().on("child_added", function(snapshot) { - * console.log(snapshot.key); - * }); - * ``` - * - * @return A new `Query` object. - */ - orderByKey(): admin.database.Query; - - /** - * Generates a new `Query` object ordered by priority. - * - * Applications need not use priority but can order collections by - * ordinary properties (see - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data} for alternatives to priority. - * - * @return A new `Query` object. - */ - orderByPriority(): admin.database.Query; - - /** - * Generates a new `Query` object ordered by value. - * - * If the children of a query are all scalar values (string, number, or - * boolean), you can order the results by their (ascending) values. - * - * You can read more about `orderByValue()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data}. - * - * @example - * ```javascript - * var scoresRef = admin.database().ref("scores"); - * scoresRef.orderByValue().limitToLast(3).on("value", function(snapshot) { - * snapshot.forEach(function(data) { - * console.log("The " + data.key + " score is " + data.val()); - * }); - * }); - * ``` - * - * @return A new `Query` object. - */ - orderByValue(): admin.database.Query; - - /** - * Creates a `Query` with the specified starting point. - * - * Using `startAt()`, `endAt()`, and `equalTo()` allows you to choose arbitrary - * starting and ending points for your queries. - * - * The starting point is inclusive, so children with exactly the specified value - * will be included in the query. The optional key argument can be used to - * further limit the range of the query. If it is specified, then children that - * have exactly the specified value must also have a key name greater than or - * equal to the specified key. - * - * You can read more about `startAt()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. - * - * @example - * ```javascript - * // Find all dinosaurs that are at least three meters tall. - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByChild("height").startAt(3).on("child_added", function(snapshot) { - * console.log(snapshot.key) - * }); - * ``` - * - * @param value The value to start at. The argument - * type depends on which `orderBy*()` function was used in this query. - * Specify a value that matches the `orderBy*()` type. When used in - * combination with `orderByKey()`, the value must be a string. - * @param key The child key to start at. This argument is allowed if - * ordering by child, value, or priority. - * @return A new `Query` object. - */ - startAt(value: number | string | boolean | null, key?: string): admin.database.Query; - - /** - * @return A JSON-serializable representation of this object. - */ - toJSON(): Object; - - /** - * Gets the absolute URL for this location. - * - * The `toString()` method returns a URL that is ready to be put into a browser, - * curl command, or a `admin.database().refFromURL()` call. Since all of those - * expect the URL to be url-encoded, `toString()` returns an encoded URL. - * - * Append '.json' to the returned URL when typed into a browser to download - * JSON-formatted data. If the location is secured (that is, not publicly - * readable), you will get a permission-denied error. - * - * @example - * ```javascript - * // Calling toString() on a root Firebase reference returns the URL where its - * // data is stored within the Database: - * var rootRef = admin.database().ref(); - * var rootUrl = rootRef.toString(); - * // rootUrl === "https://sample-app.firebaseio.com/". - * - * // Calling toString() at a deeper Firebase reference returns the URL of that - * // deep path within the Database: - * var adaRef = rootRef.child('users/ada'); - * var adaURL = adaRef.toString(); - * // adaURL === "https://sample-app.firebaseio.com/users/ada". - * ``` - * - * @return The absolute URL for this location. - * @override - */ - toString(): string; - } - - /** - * A `Reference` represents a specific location in your Database and can be used - * for reading or writing data to that Database location. - * - * You can reference the root or child location in your Database by calling - * `admin.database().ref()` or `admin.database().ref("child/path")`. - * - * Writing is done with the `set()` method and reading can be done with the - * `on()` method. See - * {@link - * https://firebase.google.com/docs/database/web/read-and-write - * Read and Write Data on the Web} - */ - interface Reference extends admin.database.Query { - - /** - * The last part of the `Reference`'s path. - * - * For example, `"ada"` is the key for - * `https://.firebaseio.com/users/ada`. - * - * The key of a root `Reference` is `null`. - * - * @example - * ```javascript - * // The key of a root reference is null - * var rootRef = admin.database().ref(); - * var key = rootRef.key; // key === null - * ``` - * - * @example - * ```javascript - * // The key of any non-root reference is the last token in the path - * var adaRef = admin.database().ref("users/ada"); - * var key = adaRef.key; // key === "ada" - * key = adaRef.child("name/last").key; // key === "last" - * ``` - */ - key: string | null; - - /** - * The parent location of a `Reference`. - * - * The parent of a root `Reference` is `null`. - * - * @example - * ```javascript - * // The parent of a root reference is null - * var rootRef = admin.database().ref(); - * parent = rootRef.parent; // parent === null - * ``` - * - * @example - * ```javascript - * // The parent of any non-root reference is the parent location - * var usersRef = admin.database().ref("users"); - * var adaRef = admin.database().ref("users/ada"); - * // usersRef and adaRef.parent represent the same location - * ``` - */ - parent: admin.database.Reference | null; - - /** - * The root `Reference` of the Database. - * - * @example - * ```javascript - * // The root of a root reference is itself - * var rootRef = admin.database().ref(); - * // rootRef and rootRef.root represent the same location - * ``` - * - * @example - * ```javascript - * // The root of any non-root reference is the root location - * var adaRef = admin.database().ref("users/ada"); - * // rootRef and adaRef.root represent the same location - * ``` - */ - root: admin.database.Reference; - /** @deprecated Removed in next major release to match Web SDK typings. */ - path: string; - - /** - * Gets a `Reference` for the location at the specified relative path. - * - * The relative path can either be a simple child name (for example, "ada") or - * a deeper slash-separated path (for example, "ada/name/first"). - * - * @example - * ```javascript - * var usersRef = admin.database().ref('users'); - * var adaRef = usersRef.child('ada'); - * var adaFirstNameRef = adaRef.child('name/first'); - * var path = adaFirstNameRef.toString(); - * // path is now 'https://sample-app.firebaseio.com/users/ada/name/first' - * ``` - * - * @param path A relative path from this location to the desired child - * location. - * @return The specified child location. - */ - child(path: string): admin.database.Reference; - - /** - * Returns an `OnDisconnect` object - see - * {@link - * https://firebase.google.com/docs/database/web/offline-capabilities - * Enabling Offline Capabilities in JavaScript} for more information on how - * to use it. - * - * @return An `OnDisconnect` object . - */ - onDisconnect(): admin.database.OnDisconnect; - - /** - * Generates a new child location using a unique key and returns its - * `Reference`. - * - * This is the most common pattern for adding data to a collection of items. - * - * If you provide a value to `push()`, the value will be written to the - * generated location. If you don't pass a value, nothing will be written to the - * Database and the child will remain empty (but you can use the `Reference` - * elsewhere). - * - * The unique key generated by `push()` are ordered by the current time, so the - * resulting list of items will be chronologically sorted. The keys are also - * designed to be unguessable (they contain 72 random bits of entropy). - * - * - * See - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#append_to_a_list_of_data - * Append to a list of data} - *
    See - * {@link - * https://firebase.googleblog.com/2015/02/the-2120-ways-to-ensure-unique_68.html - * The 2^120 Ways to Ensure Unique Identifiers} - * - * @example - * ```javascript - * var messageListRef = admin.database().ref('message_list'); - * var newMessageRef = messageListRef.push(); - * newMessageRef.set({ - * user_id: 'ada', - * text: 'The Analytical Engine weaves algebraical patterns just as the Jacquard loom weaves flowers and leaves.' - * }); - * // We've appended a new message to the message_list location. - * var path = newMessageRef.toString(); - * // path will be something like - * // 'https://sample-app.firebaseio.com/message_list/-IKo28nwJLH0Nc5XeFmj' - * ``` - * - * @param value Optional value to be written at the generated location. - * @param onComplete Callback called when write to server is - * complete. - * @return Combined `Promise` and - * `Reference`; resolves when write is complete, but can be used immediately - * as the `Reference` to the child location. - */ - push(value?: any, onComplete?: (a: Error | null) => any): admin.database.ThenableReference; - - /** - * Removes the data at this Database location. - * - * Any data at child locations will also be deleted. - * - * The effect of the remove will be visible immediately and the corresponding - * event 'value' will be triggered. Synchronization of the remove to the - * Firebase servers will also be started, and the returned Promise will resolve - * when complete. If provided, the onComplete callback will be called - * asynchronously after synchronization has finished. - * - * @example - * ```javascript - * var adaRef = admin.database().ref('users/ada'); - * adaRef.remove() - * .then(function() { - * console.log("Remove succeeded.") - * }) - * .catch(function(error) { - * console.log("Remove failed: " + error.message) - * }); - * ``` - * - * @param onComplete Callback called when write to server is - * complete. - * @return Resolves when remove on server is complete. - */ - remove(onComplete?: (a: Error | null) => any): Promise; - - /** - * Writes data to this Database location. - * - * This will overwrite any data at this location and all child locations. - * - * The effect of the write will be visible immediately, and the corresponding - * events ("value", "child_added", etc.) will be triggered. Synchronization of - * the data to the Firebase servers will also be started, and the returned - * Promise will resolve when complete. If provided, the `onComplete` callback - * will be called asynchronously after synchronization has finished. - * - * Passing `null` for the new value is equivalent to calling `remove()`; namely, - * all data at this location and all child locations will be deleted. - * - * `set()` will remove any priority stored at this location, so if priority is - * meant to be preserved, you need to use `setWithPriority()` instead. - * - * Note that modifying data with `set()` will cancel any pending transactions - * at that location, so extreme care should be taken if mixing `set()` and - * `transaction()` to modify the same data. - * - * A single `set()` will generate a single "value" event at the location where - * the `set()` was performed. - * - * @example - * ```javascript - * var adaNameRef = admin.database().ref('users/ada/name'); - * adaNameRef.child('first').set('Ada'); - * adaNameRef.child('last').set('Lovelace'); - * // We've written 'Ada' to the Database location storing Ada's first name, - * // and 'Lovelace' to the location storing her last name. - * ``` - * - * @example - * ```javascript - * adaNameRef.set({ first: 'Ada', last: 'Lovelace' }); - * // Exact same effect as the previous example, except we've written - * // Ada's first and last name simultaneously. - * ``` - * - * @example - * ```javascript - * adaNameRef.set({ first: 'Ada', last: 'Lovelace' }) - * .then(function() { - * console.log('Synchronization succeeded'); - * }) - * .catch(function(error) { - * console.log('Synchronization failed'); - * }); - * // Same as the previous example, except we will also log a message - * // when the data has finished synchronizing. - * ``` - * - * @param value The value to be written (string, number, boolean, object, - * array, or null). - * @param onComplete Callback called when write to server is - * complete. - * @return Resolves when write to server is complete. - */ - set(value: any, onComplete?: (a: Error | null) => any): Promise; - - /** - * Sets a priority for the data at this Database location. - * - * Applications need not use priority but can order collections by - * ordinary properties (see - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data}). - * - * @param priority - * @param onComplete - * @return - */ - setPriority( - priority: string | number | null, - onComplete: (a: Error | null) => any - ): Promise; - - /** - * Writes data the Database location. Like `set()` but also specifies the - * priority for that data. - * - * Applications need not use priority but can order collections by - * ordinary properties (see - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data}). - * - * @param newVal - * @param newPriority - * @param onComplete - * @return - */ - setWithPriority( - newVal: any, newPriority: string | number | null, - onComplete?: (a: Error | null) => any - ): Promise; - - /** - * Atomically modifies the data at this location. - * - * Atomically modify the data at this location. Unlike a normal `set()`, which - * just overwrites the data regardless of its previous value, `transaction()` is - * used to modify the existing value to a new value, ensuring there are no - * conflicts with other clients writing to the same location at the same time. - * - * To accomplish this, you pass `transaction()` an update function which is used - * to transform the current value into a new value. If another client writes to - * the location before your new value is successfully written, your update - * function will be called again with the new current value, and the write will - * be retried. This will happen repeatedly until your write succeeds without - * conflict or you abort the transaction by not returning a value from your - * update function. - * - * Note: Modifying data with `set()` will cancel any pending transactions at - * that location, so extreme care should be taken if mixing `set()` and - * `transaction()` to update the same data. - * - * Note: When using transactions with Security and Firebase Rules in place, be - * aware that a client needs `.read` access in addition to `.write` access in - * order to perform a transaction. This is because the client-side nature of - * transactions requires the client to read the data in order to transactionally - * update it. - * - * @example - * ```javascript - * // Increment Ada's rank by 1. - * var adaRankRef = admin.database().ref('users/ada/rank'); - * adaRankRef.transaction(function(currentRank) { - * // If users/ada/rank has never been set, currentRank will be `null`. - * return currentRank + 1; - * }); - * ``` - * - * @example - * ```javascript - * // Try to create a user for ada, but only if the user id 'ada' isn't - * // already taken - * var adaRef = admin.database().ref('users/ada'); - * adaRef.transaction(function(currentData) { - * if (currentData === null) { - * return { name: { first: 'Ada', last: 'Lovelace' } }; - * } else { - * console.log('User ada already exists.'); - * return; // Abort the transaction. - * } - * }, function(error, committed, snapshot) { - * if (error) { - * console.log('Transaction failed abnormally!', error); - * } else if (!committed) { - * console.log('We aborted the transaction (because ada already exists).'); - * } else { - * console.log('User ada added!'); - * } - * console.log("Ada's data: ", snapshot.val()); - * }); - * ``` - * - * @param transactionUpdate A developer-supplied function which - * will be passed the current data stored at this location (as a JavaScript - * object). The function should return the new value it would like written (as - * a JavaScript object). If `undefined` is returned (i.e. you return with no - * arguments) the transaction will be aborted and the data at this location - * will not be modified. - * @param onComplete A callback - * function that will be called when the transaction completes. The callback - * is passed three arguments: a possibly-null `Error`, a `boolean` indicating - * whether the transaction was committed, and a `DataSnapshot` indicating the - * final result. If the transaction failed abnormally, the first argument will - * be an `Error` object indicating the failure cause. If the transaction - * finished normally, but no data was committed because no data was returned - * from `transactionUpdate`, then second argument will be false. If the - * transaction completed and committed data to Firebase, the second argument - * will be true. Regardless, the third argument will be a `DataSnapshot` - * containing the resulting data in this location. - * @param applyLocally By default, events are raised each time the - * transaction update function runs. So if it is run multiple times, you may - * see intermediate states. You can set this to false to suppress these - * intermediate states and instead wait until the transaction has completed - * before events are raised. - * @return Returns a Promise that can optionally be used instead of the `onComplete` - * callback to handle success and failure. - */ - transaction( - transactionUpdate: (a: any) => any, - onComplete?: (a: Error | null, b: boolean, c: admin.database.DataSnapshot | null) => any, - applyLocally?: boolean - ): Promise<{ - committed: boolean; - snapshot: admin.database.DataSnapshot | null; - }>; - - /** - * Writes multiple values to the Database at once. - * - * The `values` argument contains multiple property-value pairs that will be - * written to the Database together. Each child property can either be a simple - * property (for example, "name") or a relative path (for example, - * "name/first") from the current location to the data to update. - * - * As opposed to the `set()` method, `update()` can be use to selectively update - * only the referenced properties at the current location (instead of replacing - * all the child properties at the current location). - * - * The effect of the write will be visible immediately, and the corresponding - * events ('value', 'child_added', etc.) will be triggered. Synchronization of - * the data to the Firebase servers will also be started, and the returned - * Promise will resolve when complete. If provided, the `onComplete` callback - * will be called asynchronously after synchronization has finished. - * - * A single `update()` will generate a single "value" event at the location - * where the `update()` was performed, regardless of how many children were - * modified. - * - * Note that modifying data with `update()` will cancel any pending - * transactions at that location, so extreme care should be taken if mixing - * `update()` and `transaction()` to modify the same data. - * - * Passing `null` to `update()` will remove the data at this location. - * - * See - * {@link - * https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html - * Introducing multi-location updates and more}. - * - * @example - * ```javascript - * var adaNameRef = admin.database().ref('users/ada/name'); - * // Modify the 'first' and 'last' properties, but leave other data at - * // adaNameRef unchanged. - * adaNameRef.update({ first: 'Ada', last: 'Lovelace' }); - * ``` - * - * @param values object containing multiple values. - * @param onComplete Callback called when write to server is - * complete. - * @return Resolves when update on server is complete. - */ - update(values: object, onComplete?: (a: Error | null) => any): Promise; - } - - /** - * @extends {Reference} - */ - interface ThenableReference extends admin.database.Reference, Promise { } - - function enableLogging(logger?: boolean | ((message: string) => any), persistent?: boolean): any; -} - -export namespace admin.database.ServerValue { - - /** - * A placeholder value for auto-populating the current timestamp (time - * since the Unix epoch, in milliseconds) as determined by the Firebase - * servers. - * - * @example - * ```javascript - * var sessionsRef = firebase.database().ref("sessions"); - * sessionsRef.push({ - * startedAt: firebase.database.ServerValue.TIMESTAMP - * }); - * ``` - */ - const TIMESTAMP: Object; - - /** - * Returns a placeholder value that can be used to atomically increment the - * current database value by the provided delta. - * - * @param delta the amount to modify the current value atomically. - * @return a placeholder value for modifying data atomically server-side. - */ - function increment(delta: number): Object; -} diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts index fa955a9b7e..b2d9e4e970 100644 --- a/src/database/database-internal.ts +++ b/src/database/database-internal.ts @@ -21,12 +21,14 @@ import { FirebaseApp } from '../firebase-app'; import { FirebaseDatabaseError, AppErrorCodes, FirebaseAppError } from '../utils/error'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { Database as DatabaseImpl } from '@firebase/database'; -import { Database } from './database'; +import { database } from './index'; import * as validator from '../utils/validator'; import { AuthorizedHttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; import { getSdkVersion } from '../utils/index'; +import Database = database.Database; + /** * Internals of a Database instance. */ diff --git a/src/database/database.ts b/src/database/database.ts deleted file mode 100644 index 474426748f..0000000000 --- a/src/database/database.ts +++ /dev/null @@ -1,49 +0,0 @@ -/*! - * Copyright 2020 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. - */ - -// Required to perform module augmentation to FirebaseDatabase interface. -import { FirebaseDatabase } from '@firebase/database-types'; - -declare module '@firebase/database-types' { - interface FirebaseDatabase { - /** - * Gets the currently applied security rules as a string. The return value consists of - * the rules source including comments. - * - * @return A promise fulfilled with the rules as a raw string. - */ - getRules(): Promise; - - /** - * Gets the currently applied security rules as a parsed JSON object. Any comments in - * the original source are stripped away. - * - * @return A promise fulfilled with the parsed rules object. - */ - getRulesJSON(): Promise; - - /** - * Sets the specified rules on the Firebase Realtime Database instance. If the rules source is - * specified as a string or a Buffer, it may include comments. - * - * @param source Source of the rules to apply. Must not be `null` or empty. - * @return Resolves when the rules are set on the Realtime Database. - */ - setRules(source: string | Buffer | object): Promise; - } -} - -export { FirebaseDatabase as Database } diff --git a/src/database/index.ts b/src/database/index.ts index 30c6b1143a..dae13a3654 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -14,39 +14,84 @@ * limitations under the License. */ -import { FirebaseApp } from '../firebase-app'; +import { app } from '../firebase-namespace-api'; import { ServerValue as sv } from '@firebase/database'; -import * as adminDb from './database'; -import * as firebaseDbTypesApi from '@firebase/database-types'; -import * as firebaseAdmin from '../index'; - -export function database(app?: FirebaseApp): adminDb.Database { - if (typeof(app) === 'undefined') { - app = firebaseAdmin.app(); - } - return app.database(); -} +import * as rtdb from '@firebase/database-types'; /** - * We must define a namespace to make the typings work correctly. Otherwise - * `admin.database()` cannot be called like a function. Temporarily, - * admin.database is used as the namespace name because we cannot barrel - * re-export the contents from @firebase/database-types, and we want it to - * match the namespacing in the re-export inside src/index.d.ts + * Gets the {@link database.Database `Database`} service for the default + * app or a given app. + * + * `admin.database()` can be called with no arguments to access the default + * app's {@link database.Database `Database`} service or as + * `admin.database(app)` to access the + * {@link database.Database `Database`} service associated with a specific + * app. + * + * `admin.database` is also a namespace that can be used to access global + * constants and methods associated with the `Database` service. + * + * @example + * ```javascript + * // Get the Database service for the default app + * var defaultDatabase = admin.database(); + * ``` + * + * @example + * ```javascript + * // Get the Database service for a specific app + * var otherDatabase = admin.database(app); + * ``` + * + * @param App whose `Database` service to + * return. If not provided, the default `Database` service will be returned. + * + * @return The default `Database` service if no app + * is provided or the `Database` service associated with the provided app. */ +export declare function database(app?: app.App): database.Database; + /* eslint-disable @typescript-eslint/no-namespace */ -export namespace admin.database { - // See https://github.com/microsoft/TypeScript/issues/4336 +export namespace database { + export interface Database extends rtdb.FirebaseDatabase { + /** + * Gets the currently applied security rules as a string. The return value consists of + * the rules source including comments. + * + * @return A promise fulfilled with the rules as a raw string. + */ + getRules(): Promise; + + /** + * Gets the currently applied security rules as a parsed JSON object. Any comments in + * the original source are stripped away. + * + * @return A promise fulfilled with the parsed rules object. + */ + getRulesJSON(): Promise; + + /** + * Sets the specified rules on the Firebase Realtime Database instance. If the rules source is + * specified as a string or a Buffer, it may include comments. + * + * @param source Source of the rules to apply. Must not be `null` or empty. + * @return Resolves when the rules are set on the Realtime Database. + */ + setRules(source: string | Buffer | object): Promise; + } + /* eslint-disable @typescript-eslint/no-unused-vars */ - // See https://github.com/typescript-eslint/typescript-eslint/issues/363 - export import DataSnapshot = firebaseDbTypesApi.DataSnapshot; - export import Database = adminDb.Database; - export import EventType = firebaseDbTypesApi.EventType; - export import OnDisconnect = firebaseDbTypesApi.OnDisconnect; - export import Query = firebaseDbTypesApi.Query; - export import Reference = firebaseDbTypesApi.Reference; - export import ThenableReference = firebaseDbTypesApi.ThenableReference; - export import enableLogging = firebaseDbTypesApi.enableLogging; + export import DataSnapshot = rtdb.DataSnapshot; + export import EventType = rtdb.EventType; + export import OnDisconnect = rtdb.OnDisconnect; + export import Query = rtdb.Query; + export import Reference = rtdb.Reference; + export import ThenableReference = rtdb.ThenableReference; + export import enableLogging = rtdb.enableLogging; - export const ServerValue: firebaseDbTypesApi.ServerValue = sv; + /** + * [`ServerValue`](https://firebase.google.com/docs/reference/js/firebase.database.ServerValue) + * module from the `@firebase/database` package. + */ + export const ServerValue: rtdb.ServerValue = sv; } diff --git a/src/firebase-app.ts b/src/firebase-app.ts index b432fa8e4a..f18edf51ef 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Credential, GoogleOAuthAccessToken } from './credential/credential-interfaces'; +import { AppOptions, app } from './firebase-namespace-api'; +import { credential, GoogleOAuthAccessToken } from './credential/index'; import { getApplicationDefault } from './credential/credential-internal'; import * as validator from './utils/validator'; import { deepCopy, deepExtend } from './utils/deep-copy'; @@ -26,35 +27,22 @@ import { Auth } from './auth/auth'; import { MachineLearning } from './machine-learning/machine-learning'; import { Messaging } from './messaging/messaging'; import { Storage } from './storage/storage'; -import { Database } from './database/database'; +import { database } from './database/index'; import { DatabaseService } from './database/database-internal'; import { Firestore } from '@google-cloud/firestore'; import { FirestoreService } from './firestore/firestore-internal'; import { InstanceId } from './instance-id/instance-id'; - import { ProjectManagement } from './project-management/project-management'; import { SecurityRules } from './security-rules/security-rules'; import { RemoteConfig } from './remote-config/remote-config'; -import { Agent } from 'http'; +import Credential = credential.Credential; +import Database = database.Database; /** * Type representing a callback which is called every time an app lifecycle event occurs. */ -export type AppHook = (event: string, app: FirebaseApp) => void; - -/** - * Type representing the options object passed into initializeApp(). - */ -export interface FirebaseAppOptions { - credential?: Credential; - databaseAuthVariableOverride?: object | null; - databaseURL?: string; - serviceAccountId?: string; - storageBucket?: string; - projectId?: string; - httpAgent?: Agent; -} +export type AppHook = (event: string, app: app.App) => void; /** * Type representing a Firebase OAuth access token (derived from a Google OAuth2 access token) which @@ -241,27 +229,25 @@ export class FirebaseAppInternals { } } - - /** * Global context object for a collection of services using a shared authentication state. */ -export class FirebaseApp { +export class FirebaseApp implements app.App { public INTERNAL: FirebaseAppInternals; private name_: string; - private options_: FirebaseAppOptions; + private options_: AppOptions; private services_: {[name: string]: FirebaseServiceInterface} = {}; private isDeleted_ = false; - constructor(options: FirebaseAppOptions, name: string, private firebaseInternals_: FirebaseNamespaceInternals) { + constructor(options: AppOptions, name: string, private firebaseInternals_: FirebaseNamespaceInternals) { this.name_ = name; - this.options_ = deepCopy(options) as FirebaseAppOptions; + this.options_ = deepCopy(options); if (!validator.isNonNullObject(this.options_)) { throw new FirebaseAppError( AppErrorCodes.INVALID_APP_OPTIONS, - `Invalid Firebase app options passed as the first argument to initializeApp() for the ` + + 'Invalid Firebase app options passed as the first argument to initializeApp() for the ' + `app named "${this.name_}". Options must be a non-null object.`, ); } @@ -275,9 +261,9 @@ export class FirebaseApp { if (typeof credential !== 'object' || credential === null || typeof credential.getAccessToken !== 'function') { throw new FirebaseAppError( AppErrorCodes.INVALID_APP_OPTIONS, - `Invalid Firebase app options passed as the first argument to initializeApp() for the ` + + 'Invalid Firebase app options passed as the first argument to initializeApp() for the ' + `app named "${this.name_}". The "credential" property must be an object which implements ` + - `the Credential interface.`, + 'the Credential interface.', ); } @@ -292,7 +278,7 @@ export class FirebaseApp { /** * Returns the Auth service instance associated with this app. * - * @return {Auth} The Auth service instance of this app. + * @return The Auth service instance of this app. */ public auth(): Auth { return this.ensureService_('auth', () => { @@ -304,7 +290,7 @@ export class FirebaseApp { /** * Returns the Database service for the specified URL, and the current app. * - * @return {Database} The Database service instance of this app. + * @return The Database service instance of this app. */ public database(url?: string): Database { const service: DatabaseService = this.ensureService_('database', () => { @@ -317,7 +303,7 @@ export class FirebaseApp { /** * Returns the Messaging service instance associated with this app. * - * @return {Messaging} The Messaging service instance of this app. + * @return The Messaging service instance of this app. */ public messaging(): Messaging { return this.ensureService_('messaging', () => { @@ -329,7 +315,7 @@ export class FirebaseApp { /** * Returns the Storage service instance associated with this app. * - * @return {Storage} The Storage service instance of this app. + * @return The Storage service instance of this app. */ public storage(): Storage { return this.ensureService_('storage', () => { @@ -349,7 +335,7 @@ export class FirebaseApp { /** * Returns the InstanceId service instance associated with this app. * - * @return {InstanceId} The InstanceId service instance of this app. + * @return The InstanceId service instance of this app. */ public instanceId(): InstanceId { return this.ensureService_('iid', () => { @@ -361,7 +347,7 @@ export class FirebaseApp { /** * Returns the MachineLearning service instance associated with this app. * - * @return {MachineLearning} The Machine Learning service instance of this app + * @return The Machine Learning service instance of this app */ public machineLearning(): MachineLearning { return this.ensureService_('machine-learning', () => { @@ -374,7 +360,7 @@ export class FirebaseApp { /** * Returns the ProjectManagement service instance associated with this app. * - * @return {ProjectManagement} The ProjectManagement service instance of this app. + * @return The ProjectManagement service instance of this app. */ public projectManagement(): ProjectManagement { return this.ensureService_('project-management', () => { @@ -387,7 +373,7 @@ export class FirebaseApp { /** * Returns the SecurityRules service instance associated with this app. * - * @return {SecurityRules} The SecurityRules service instance of this app. + * @return The SecurityRules service instance of this app. */ public securityRules(): SecurityRules { return this.ensureService_('security-rules', () => { @@ -400,7 +386,7 @@ export class FirebaseApp { /** * Returns the RemoteConfig service instance associated with this app. * - * @return {RemoteConfig} The RemoteConfig service instance of this app. + * @return The RemoteConfig service instance of this app. */ public remoteConfig(): RemoteConfig { return this.ensureService_('remoteConfig', () => { @@ -412,7 +398,7 @@ export class FirebaseApp { /** * Returns the name of the FirebaseApp instance. * - * @return {string} The name of the FirebaseApp instance. + * @return The name of the FirebaseApp instance. */ get name(): string { this.checkDestroyed_(); @@ -422,17 +408,17 @@ export class FirebaseApp { /** * Returns the options for the FirebaseApp instance. * - * @return {FirebaseAppOptions} The options for the FirebaseApp instance. + * @return The options for the FirebaseApp instance. */ - get options(): FirebaseAppOptions { + get options(): AppOptions { this.checkDestroyed_(); - return deepCopy(this.options_) as FirebaseAppOptions; + return deepCopy(this.options_); } /** * Deletes the FirebaseApp instance. * - * @return {Promise} An empty Promise fulfilled once the FirebaseApp instance is deleted. + * @return An empty Promise fulfilled once the FirebaseApp instance is deleted. */ public delete(): Promise { this.checkDestroyed_(); @@ -465,8 +451,8 @@ export class FirebaseApp { * Returns the service instance associated with this FirebaseApp instance (creating it on demand * if needed). This is used for looking up monkeypatched service instances. * - * @param {string} serviceName The name of the service instance to return. - * @return {FirebaseServiceInterface} The service instance with the provided name. + * @param serviceName The name of the service instance to return. + * @return The service instance with the provided name. */ private getService_(serviceName: string): FirebaseServiceInterface { this.checkDestroyed_(); diff --git a/src/firebase-namespace-api.ts b/src/firebase-namespace-api.ts new file mode 100644 index 0000000000..36df1b778d --- /dev/null +++ b/src/firebase-namespace-api.ts @@ -0,0 +1,266 @@ +/*! + * Copyright 2020 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 { Agent } from 'http'; +import { auth } from './auth/index'; +import { credential } from './credential/index'; +import { database } from './database/index'; +import { firestore } from './firestore/index'; +import { instanceId } from './instance-id/index'; +import { machineLearning } from './machine-learning/index'; +import { messaging } from './messaging/index'; +import { projectManagement } from './project-management/index'; +import { remoteConfig } from './remote-config/index'; +import { securityRules } from './security-rules/index'; +import { storage } from './storage/index'; + +/** + * `FirebaseError` is a subclass of the standard JavaScript `Error` object. In + * addition to a message string and stack trace, it contains a string code. + */ +export interface FirebaseError { + + /** + * Error codes are strings using the following format: `"service/string-code"`. + * Some examples include `"auth/invalid-uid"` and + * `"messaging/invalid-recipient"`. + * + * While the message for a given error can change, the code will remain the same + * between backward-compatible versions of the Firebase SDK. + */ + code: string; + + /** + * An explanatory message for the error that just occurred. + * + * This message is designed to be helpful to you, the developer. Because + * it generally does not convey meaningful information to end users, + * this message should not be displayed in your application. + */ + message: string; + + /** + * A string value containing the execution backtrace when the error originally + * occurred. + * + * This information can be useful to you and can be sent to + * {@link https://firebase.google.com/support/ Firebase Support} to help + * explain the cause of an error. + */ + stack?: string; + + /** + * @return A JSON-serializable representation of this object. + */ + toJSON(): object; +} + +/** + * Composite type which includes both a `FirebaseError` object and an index + * which can be used to get the errored item. + * + * @example + * ```javascript + * var registrationTokens = [token1, token2, token3]; + * admin.messaging().subscribeToTopic(registrationTokens, 'topic-name') + * .then(function(response) { + * if (response.failureCount > 0) { + * console.log("Following devices unsucessfully subscribed to topic:"); + * response.errors.forEach(function(error) { + * var invalidToken = registrationTokens[error.index]; + * console.log(invalidToken, error.error); + * }); + * } else { + * console.log("All devices successfully subscribed to topic:", response); + * } + * }) + * .catch(function(error) { + * console.log("Error subscribing to topic:", error); + * }); + *``` + */ +export interface FirebaseArrayIndexError { + + /** + * The index of the errored item within the original array passed as part of the + * called API method. + */ + index: number; + + /** + * The error object. + */ + error: FirebaseError; +} + +/** + * Available options to pass to [`initializeApp()`](admin#.initializeApp). + */ +export interface AppOptions { + + /** + * A {@link credential.Credential `Credential`} object used to + * authenticate the Admin SDK. + * + * See [Initialize the SDK](/docs/admin/setup#initialize_the_sdk) for detailed + * documentation and code samples. + */ + credential?: credential.Credential; + + /** + * The object to use as the [`auth`](/docs/reference/security/database/#auth) + * variable in your Realtime Database Rules when the Admin SDK reads from or + * writes to the Realtime Database. This allows you to downscope the Admin SDK + * from its default full read and write privileges. + * + * You can pass `null` to act as an unauthenticated client. + * + * See + * [Authenticate with limited privileges](/docs/database/admin/start#authenticate-with-limited-privileges) + * for detailed documentation and code samples. + */ + databaseAuthVariableOverride?: object | null; + + /** + * The URL of the Realtime Database from which to read and write data. + */ + databaseURL?: string; + + /** + * The ID of the service account to be used for signing custom tokens. This + * can be found in the `client_email` field of a service account JSON file. + */ + serviceAccountId?: string; + + /** + * The name of the Google Cloud Storage bucket used for storing application data. + * Use only the bucket name without any prefixes or additions (do *not* prefix + * the name with "gs://"). + */ + storageBucket?: string; + + /** + * The ID of the Google Cloud project associated with the App. + */ + projectId?: string; + + /** + * An [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when making outgoing HTTP calls. This Agent instance is used + * by all services that make REST calls (e.g. `auth`, `messaging`, + * `projectManagement`). + * + * Realtime Database and Firestore use other means of communicating with + * the backend servers, so they do not use this HTTP Agent. `Credential` + * instances also do not use this HTTP Agent, but instead support + * specifying an HTTP Agent in the corresponding factory methods. + */ + httpAgent?: Agent; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace app { + /** + * A Firebase app holds the initialization information for a collection of + * services. + * + * Do not call this constructor directly. Instead, use + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`} + * to create an app. + */ + export interface App { + + /** + * The (read-only) name for this app. + * + * The default app's name is `"[DEFAULT]"`. + * + * @example + * ```javascript + * // The default app's name is "[DEFAULT]" + * admin.initializeApp(defaultAppConfig); + * console.log(admin.app().name); // "[DEFAULT]" + * ``` + * + * @example + * ```javascript + * // A named app's name is what you provide to initializeApp() + * var otherApp = admin.initializeApp(otherAppConfig, "other"); + * console.log(otherApp.name); // "other" + * ``` + */ + name: string; + + /** + * The (read-only) configuration options for this app. These are the original + * parameters given in + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * @example + * ```javascript + * var app = admin.initializeApp(config); + * console.log(app.options.credential === config.credential); // true + * console.log(app.options.databaseURL === config.databaseURL); // true + * ``` + */ + options: AppOptions; + + auth(): auth.Auth; + database(url?: string): database.Database; + firestore(): firestore.Firestore; + instanceId(): instanceId.InstanceId; + machineLearning(): machineLearning.MachineLearning; + messaging(): messaging.Messaging; + projectManagement(): projectManagement.ProjectManagement; + remoteConfig(): remoteConfig.RemoteConfig; + securityRules(): securityRules.SecurityRules; + storage(): storage.Storage; + + /** + * Renders this local `FirebaseApp` unusable and frees the resources of + * all associated services (though it does *not* clean up any backend + * resources). When running the SDK locally, this method + * must be called to ensure graceful termination of the process. + * + * @example + * ```javascript + * app.delete() + * .then(function() { + * console.log("App deleted successfully"); + * }) + * .catch(function(error) { + * console.log("Error deleting app:", error); + * }); + * ``` + */ + delete(): Promise; + } +} + +// Declare other top-level members of the admin namespace below. Unfortunately, there's no +// compile-time mechanism to ensure that the FirebaseNamespace class actually provides these +// signatures. But this part of the API is quite small and stable. It should be easy enough to +// enforce conformance via disciplined coding and good integration tests. + +export declare const SDK_VERSION: string; +export declare const apps: (app.App | null)[]; + +export declare function app(name?: string): app.App; +export declare function initializeApp(options?: AppOptions, name?: string): app.App; diff --git a/src/firebase-namespace.d.ts b/src/firebase-namespace.d.ts new file mode 100644 index 0000000000..3de06b1bbb --- /dev/null +++ b/src/firebase-namespace.d.ts @@ -0,0 +1,28 @@ +/*! + * Copyright 2020 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. + */ + +export * from './credential/index'; +export * from './firebase-namespace-api'; +export * from './auth/index'; +export * from './database/index'; +export * from './firestore/index'; +export * from './instance-id/index'; +export * from './machine-learning/index'; +export * from './messaging/index'; +export * from './project-management/index'; +export * from './remote-config/index'; +export * from './security-rules/index'; +export * from './storage/index'; diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index f4b52b5a6d..4309913921 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -15,29 +15,41 @@ */ import fs = require('fs'); + import { deepExtend } from './utils/deep-copy'; import { AppErrorCodes, FirebaseAppError } from './utils/error'; -import { AppHook, FirebaseApp, FirebaseAppOptions } from './firebase-app'; +import { AppOptions, app } from './firebase-namespace-api'; +import { AppHook, FirebaseApp } from './firebase-app'; import { FirebaseServiceFactory, FirebaseServiceInterface } from './firebase-service'; -import { - cert, refreshToken, applicationDefault -} from './credential/credential'; +import { cert, refreshToken, applicationDefault } from './credential/credential'; import { getApplicationDefault } from './credential/credential-internal'; -import { Auth } from './auth/auth'; -import { MachineLearning } from './machine-learning/machine-learning'; -import { Messaging } from './messaging/messaging'; -import { Storage } from './storage/storage'; -import { Database } from './database/database'; -import { Firestore } from '@google-cloud/firestore'; -import { InstanceId } from './instance-id/instance-id'; -import { ProjectManagement } from './project-management/project-management'; -import { SecurityRules } from './security-rules/security-rules'; -import { RemoteConfig } from './remote-config/remote-config'; +import { auth } from './auth/index'; +import { database } from './database/index'; +import { firestore } from './firestore/index'; +import { instanceId } from './instance-id/index'; +import { machineLearning } from './machine-learning/index'; +import { messaging } from './messaging/index'; +import { projectManagement } from './project-management/index'; +import { remoteConfig } from './remote-config/index'; +import { securityRules } from './security-rules/index'; +import { storage } from './storage/index'; import * as validator from './utils/validator'; import { getSdkVersion } from './utils/index'; +import App = app.App; +import Auth = auth.Auth; +import Database = database.Database; +import Firestore = firestore.Firestore; +import InstanceId = instanceId.InstanceId; +import MachineLearning = machineLearning.MachineLearning; +import Messaging = messaging.Messaging; +import ProjectManagement = projectManagement.ProjectManagement; +import RemoteConfig = remoteConfig.RemoteConfig; +import SecurityRules = securityRules.SecurityRules; +import Storage = storage.Storage; + const DEFAULT_APP_NAME = '[DEFAULT]'; /** @@ -48,35 +60,33 @@ const DEFAULT_APP_NAME = '[DEFAULT]'; export const FIREBASE_CONFIG_VAR = 'FIREBASE_CONFIG'; export interface FirebaseServiceNamespace { - (app?: FirebaseApp): T; + (app?: App): T; [key: string]: any; } - /** * Internals of a FirebaseNamespace instance. */ export class FirebaseNamespaceInternals { public serviceFactories: {[serviceName: string]: FirebaseServiceFactory} = {}; - private apps_: {[appName: string]: FirebaseApp} = {}; + private apps_: {[appName: string]: App} = {}; private appHooks_: {[service: string]: AppHook} = {}; constructor(public firebase_: {[key: string]: any}) {} /** - * Initializes the FirebaseApp instance. + * Initializes the App instance. * - * @param {FirebaseAppOptions} options Optional options for the FirebaseApp instance. If none present - * will try to initialize from the FIREBASE_CONFIG environment variable. - * If the environment variable contains a string that starts with '{' - * it will be parsed as JSON, - * otherwise it will be assumed to be pointing to a file. - * @param {string} [appName] Optional name of the FirebaseApp instance. + * @param options Optional options for the App instance. If none present will try to initialize + * from the FIREBASE_CONFIG environment variable. If the environment variable contains a string + * that starts with '{' it will be parsed as JSON, otherwise it will be assumed to be pointing + * to a file. + * @param appName Optional name of the FirebaseApp instance. * - * @return {FirebaseApp} A new FirebaseApp instance. + * @return A new App instance. */ - public initializeApp(options?: FirebaseAppOptions, appName = DEFAULT_APP_NAME): FirebaseApp { + public initializeApp(options?: AppOptions, appName = DEFAULT_APP_NAME): App { if (typeof options === 'undefined') { options = this.loadOptionsFromEnvVar(); options.credential = getApplicationDefault(); @@ -116,13 +126,13 @@ export class FirebaseNamespaceInternals { } /** - * Returns the FirebaseApp instance with the provided name (or the default FirebaseApp instance + * Returns the App instance with the provided name (or the default App instance * if no name is provided). * - * @param {string} [appName=DEFAULT_APP_NAME] Optional name of the FirebaseApp instance to return. - * @return {FirebaseApp} The FirebaseApp instance which has the provided name. + * @param appName Optional name of the FirebaseApp instance to return. + * @return The App instance which has the provided name. */ - public app(appName = DEFAULT_APP_NAME): FirebaseApp { + public app(appName = DEFAULT_APP_NAME): App { if (typeof appName !== 'string' || appName === '') { throw new FirebaseAppError( AppErrorCodes.INVALID_APP_NAME, @@ -140,25 +150,21 @@ export class FirebaseNamespaceInternals { } /* - * Returns an array of all the non-deleted FirebaseApp instances. - * - * @return {Array} An array of all the non-deleted FirebaseApp instances + * Returns an array of all the non-deleted App instances. */ - public get apps(): FirebaseApp[] { + public get apps(): App[] { // Return a copy so the caller cannot mutate the array return Object.keys(this.apps_).map((appName) => this.apps_[appName]); } /* - * Removes the specified FirebaseApp instance. - * - * @param {string} appName The name of the FirebaseApp instance to remove. + * Removes the specified App instance. */ public removeApp(appName: string): void { if (typeof appName === 'undefined') { throw new FirebaseAppError( AppErrorCodes.INVALID_APP_NAME, - `No Firebase app name provided. App name must be a non-empty string.`, + 'No Firebase app name provided. App name must be a non-empty string.', ); } @@ -167,14 +173,14 @@ export class FirebaseNamespaceInternals { delete this.apps_[appName]; } - /* + /** * Registers a new service on this Firebase namespace. * - * @param {string} serviceName The name of the Firebase service to register. - * @param {FirebaseServiceFactory} createService A factory method to generate an instance of the Firebase service. - * @param {object} [serviceProperties] Optional properties to extend this Firebase namespace with. - * @param {AppHook} [appHook] Optional callback that handles app-related events like app creation and deletion. - * @return {FirebaseServiceNamespace} The Firebase service's namespace. + * @param serviceName The name of the Firebase service to register. + * @param createService A factory method to generate an instance of the Firebase service. + * @param serviceProperties Optional properties to extend this Firebase namespace with. + * @param appHook Optional callback that handles app-related events like app creation and deletion. + * @return The Firebase service's namespace. */ public registerService( serviceName: string, @@ -183,7 +189,7 @@ export class FirebaseNamespaceInternals { appHook?: AppHook): FirebaseServiceNamespace { let errorMessage; if (typeof serviceName === 'undefined') { - errorMessage = `No service name provided. Service name must be a non-empty string.`; + errorMessage = 'No service name provided. Service name must be a non-empty string.'; } else if (typeof serviceName !== 'string' || serviceName === '') { errorMessage = `Invalid service name "${serviceName}" provided. Service name must be a non-empty string.`; } else if (serviceName in this.serviceFactories) { @@ -204,7 +210,7 @@ export class FirebaseNamespaceInternals { // The service namespace is an accessor function which takes a FirebaseApp instance // or uses the default app if no FirebaseApp instance is provided - const serviceNamespace: FirebaseServiceNamespace = (appArg?: FirebaseApp) => { + const serviceNamespace: FirebaseServiceNamespace = (appArg?: App) => { if (typeof appArg === 'undefined') { appArg = this.app(); } @@ -226,12 +232,12 @@ export class FirebaseNamespaceInternals { /** * Calls the app hooks corresponding to the provided event name for each service within the - * provided FirebaseApp instance. + * provided App instance. * - * @param {FirebaseApp} app The FirebaseApp instance whose app hooks to call. - * @param {string} eventName The event name representing which app hooks to call. + * @param app The App instance whose app hooks to call. + * @param eventName The event name representing which app hooks to call. */ - private callAppHooks_(app: FirebaseApp, eventName: string): void { + private callAppHooks_(app: App, eventName: string): void { Object.keys(this.serviceFactories).forEach((serviceName) => { if (this.appHooks_[serviceName]) { this.appHooks_[serviceName](eventName, app); @@ -245,14 +251,14 @@ export class FirebaseNamespaceInternals { * If the environment variable contains a string that starts with '{' it will be parsed as JSON, * otherwise it will be assumed to be pointing to a file. */ - private loadOptionsFromEnvVar(): FirebaseAppOptions { + private loadOptionsFromEnvVar(): AppOptions { const config = process.env[FIREBASE_CONFIG_VAR]; if (!validator.isNonEmptyString(config)) { return {}; } try { const contents = config.startsWith('{') ? config : fs.readFileSync(config, 'utf8'); - return JSON.parse(contents) as FirebaseAppOptions; + return JSON.parse(contents) as AppOptions; } catch (error) { // Throw a nicely formed error message if the file contents cannot be parsed throw new FirebaseAppError( @@ -267,7 +273,6 @@ const firebaseCredential = { cert, refreshToken, applicationDefault }; - /** * Global Firebase context object. */ @@ -296,7 +301,7 @@ export class FirebaseNamespace { * `Auth` service for the default app or an explicitly specified app. */ get auth(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).auth(); }; const auth = require('./auth/auth').Auth; @@ -308,7 +313,7 @@ export class FirebaseNamespace { * `Database` service for the default app or an explicitly specified app. */ get database(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).database(); }; @@ -321,7 +326,7 @@ export class FirebaseNamespace { * `Messaging` service for the default app or an explicitly specified app. */ get messaging(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).messaging(); }; const messaging = require('./messaging/messaging').Messaging; @@ -333,7 +338,7 @@ export class FirebaseNamespace { * `Storage` service for the default app or an explicitly specified app. */ get storage(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).storage(); }; const storage = require('./storage/storage').Storage; @@ -345,7 +350,7 @@ export class FirebaseNamespace { * `Firestore` service for the default app or an explicitly specified app. */ get firestore(): FirebaseServiceNamespace { - let fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + let fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).firestore(); }; @@ -377,7 +382,7 @@ export class FirebaseNamespace { */ get machineLearning(): FirebaseServiceNamespace { const fn: FirebaseServiceNamespace = - (app?: FirebaseApp) => { + (app?: App) => { return this.ensureApp(app).machineLearning(); }; const machineLearning = @@ -390,7 +395,7 @@ export class FirebaseNamespace { * `Instance` service for the default app or an explicitly specified app. */ get instanceId(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).instanceId(); }; const instanceId = require('./instance-id/instance-id').InstanceId; @@ -402,7 +407,7 @@ export class FirebaseNamespace { * `ProjectManagement` service for the default app or an explicitly specified app. */ get projectManagement(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).projectManagement(); }; const projectManagement = require('./project-management/project-management').ProjectManagement; @@ -414,7 +419,7 @@ export class FirebaseNamespace { * `SecurityRules` service for the default app or an explicitly specified app. */ get securityRules(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).securityRules(); }; const securityRules = require('./security-rules/security-rules').SecurityRules; @@ -426,25 +431,27 @@ export class FirebaseNamespace { * `RemoteConfig` service for the default app or an explicitly specified app. */ get remoteConfig(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).remoteConfig(); }; const remoteConfig = require('./remote-config/remote-config').RemoteConfig; return Object.assign(fn, { RemoteConfig: remoteConfig }); } + // TODO: Change the return types to app.App in the following methods. + /** * Initializes the FirebaseApp instance. * - * @param {FirebaseAppOptions} [options] Optional options for the FirebaseApp instance. + * @param options Optional options for the FirebaseApp instance. * If none present will try to initialize from the FIREBASE_CONFIG environment variable. * If the environment variable contains a string that starts with '{' it will be parsed as JSON, * otherwise it will be assumed to be pointing to a file. - * @param {string} [appName] Optional name of the FirebaseApp instance. + * @param appName Optional name of the FirebaseApp instance. * - * @return {FirebaseApp} A new FirebaseApp instance. + * @return A new FirebaseApp instance. */ - public initializeApp(options?: FirebaseAppOptions, appName?: string): FirebaseApp { + public initializeApp(options?: AppOptions, appName?: string): App { return this.INTERNAL.initializeApp(options, appName); } @@ -452,23 +459,21 @@ export class FirebaseNamespace { * Returns the FirebaseApp instance with the provided name (or the default FirebaseApp instance * if no name is provided). * - * @param {string} [appName] Optional name of the FirebaseApp instance to return. - * @return {FirebaseApp} The FirebaseApp instance which has the provided name. + * @param appName Optional name of the FirebaseApp instance to return. + * @return The FirebaseApp instance which has the provided name. */ - public app(appName?: string): FirebaseApp { + public app(appName?: string): App { return this.INTERNAL.app(appName); } /* * Returns an array of all the non-deleted FirebaseApp instances. - * - * @return {Array} An array of all the non-deleted FirebaseApp instances */ - public get apps(): FirebaseApp[] { + public get apps(): App[] { return this.INTERNAL.apps; } - private ensureApp(app?: FirebaseApp): FirebaseApp { + private ensureApp(app?: App): App { if (typeof app === 'undefined') { app = this.app(); } diff --git a/src/firebase-service.ts b/src/firebase-service.ts index 15e3168ee3..f6e4c97393 100644 --- a/src/firebase-service.ts +++ b/src/firebase-service.ts @@ -14,8 +14,10 @@ * limitations under the License. */ +import { app } from './firebase-namespace-api'; import { FirebaseApp } from './firebase-app'; +import App = app.App; /** * Internals of a FirebaseService instance. @@ -28,7 +30,7 @@ export interface FirebaseServiceInternalsInterface { * Services are exposed through instances, each of which is associated with a FirebaseApp. */ export interface FirebaseServiceInterface { - app: FirebaseApp; + app: App; INTERNAL: FirebaseServiceInternalsInterface; } diff --git a/src/firestore/index.ts b/src/firestore/index.ts index fb48b137b8..5204a4f584 100644 --- a/src/firestore/index.ts +++ b/src/firestore/index.ts @@ -14,27 +14,13 @@ * limitations under the License. */ -import { FirebaseApp } from '../firebase-app'; +import { app } from '../firebase-namespace-api'; import * as _firestore from '@google-cloud/firestore'; -import * as firebaseAdmin from '../index'; -export function firestore(app?: FirebaseApp): _firestore.Firestore { - if (typeof (app) === 'undefined') { - app = firebaseAdmin.app(); - } - return app.firestore(); -} +export declare function firestore(app?: app.App): _firestore.Firestore; -/** - * We must define a namespace to make the typings work correctly. Otherwise - * `admin.firestore()` cannot be called like a function. Temporarily, - * admin.firestore is used as the namespace name because we cannot barrel - * re-export the contents from firestore, and we want it to - * match the namespacing in the re-export inside src/index.d.ts - */ /* eslint-disable @typescript-eslint/no-namespace */ -export namespace admin.firestore { - // See https://github.com/microsoft/TypeScript/issues/4336 +export namespace firestore { /* eslint-disable @typescript-eslint/no-unused-vars */ // See https://github.com/typescript-eslint/typescript-eslint/issues/363 export import v1beta1 = _firestore.v1beta1; diff --git a/src/index.d.ts b/src/index.d.ts index 997fcb8779..a748eb21f5 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -14,937 +14,7 @@ * limitations under the License. */ -import * as _firestore from '@google-cloud/firestore'; -import { Agent } from 'http'; - -import * as _auth from './auth'; -import * as _credential from './credential'; -import * as _database from './database'; -import * as _messaging from './messaging'; -import * as _instanceId from './instance-id'; -import * as _projectManagement from './project-management'; -import * as _remoteConfig from './remote-config'; -import * as _securityRules from './security-rules'; -import * as _storage from './storage'; - -/* eslint-disable @typescript-eslint/ban-types */ - -/** - * `admin` is a global namespace from which all Firebase Admin - * services are accessed. - */ -declare namespace admin { - - /** - * `FirebaseError` is a subclass of the standard JavaScript `Error` object. In - * addition to a message string and stack trace, it contains a string code. - */ - interface FirebaseError { - - /** - * Error codes are strings using the following format: `"service/string-code"`. - * Some examples include `"auth/invalid-uid"` and - * `"messaging/invalid-recipient"`. - * - * While the message for a given error can change, the code will remain the same - * between backward-compatible versions of the Firebase SDK. - */ - code: string; - - /** - * An explanatory message for the error that just occurred. - * - * This message is designed to be helpful to you, the developer. Because - * it generally does not convey meaningful information to end users, - * this message should not be displayed in your application. - */ - message: string; - - /** - * A string value containing the execution backtrace when the error originally - * occurred. - * - * This information can be useful to you and can be sent to - * {@link https://firebase.google.com/support/ Firebase Support} to help - * explain the cause of an error. - */ - stack: string; - - /** - * @return A JSON-serializable representation of this object. - */ - toJSON(): Object; - } - - /** - * Composite type which includes both a `FirebaseError` object and an index - * which can be used to get the errored item. - * - * @example - * ```javascript - * var registrationTokens = [token1, token2, token3]; - * admin.messaging().subscribeToTopic(registrationTokens, 'topic-name') - * .then(function(response) { - * if (response.failureCount > 0) { - * console.log("Following devices unsucessfully subscribed to topic:"); - * response.errors.forEach(function(error) { - * var invalidToken = registrationTokens[error.index]; - * console.log(invalidToken, error.error); - * }); - * } else { - * console.log("All devices successfully subscribed to topic:", response); - * } - * }) - * .catch(function(error) { - * console.log("Error subscribing to topic:", error); - * }); - *``` - */ - interface FirebaseArrayIndexError { - - /** - * The index of the errored item within the original array passed as part of the - * called API method. - */ - index: number; - - /** - * The error object. - */ - error: FirebaseError; - } - - interface ServiceAccount { - projectId?: string; - clientEmail?: string; - privateKey?: string; - } - - interface GoogleOAuthAccessToken { - access_token: string; - expires_in: number; - } - - /** - * Available options to pass to [`initializeApp()`](admin#.initializeApp). - */ - interface AppOptions { - - /** - * A {@link admin.credential.Credential `Credential`} object used to - * authenticate the Admin SDK. - * - * See [Initialize the SDK](/docs/admin/setup#initialize_the_sdk) for detailed - * documentation and code samples. - */ - credential?: admin.credential.Credential; - - /** - * The object to use as the [`auth`](/docs/reference/security/database/#auth) - * variable in your Realtime Database Rules when the Admin SDK reads from or - * writes to the Realtime Database. This allows you to downscope the Admin SDK - * from its default full read and write privileges. - * - * You can pass `null` to act as an unauthenticated client. - * - * See - * [Authenticate with limited privileges](/docs/database/admin/start#authenticate-with-limited-privileges) - * for detailed documentation and code samples. - */ - databaseAuthVariableOverride?: object | null; - - /** - * The URL of the Realtime Database from which to read and write data. - */ - databaseURL?: string; - - /** - * The ID of the service account to be used for signing custom tokens. This - * can be found in the `client_email` field of a service account JSON file. - */ - serviceAccountId?: string; - - /** - * The name of the Google Cloud Storage bucket used for storing application data. - * Use only the bucket name without any prefixes or additions (do *not* prefix - * the name with "gs://"). - */ - storageBucket?: string; - - /** - * The ID of the Google Cloud project associated with the App. - */ - projectId?: string; - - /** - * An [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when making outgoing HTTP calls. This Agent instance is used - * by all services that make REST calls (e.g. `auth`, `messaging`, - * `projectManagement`). - * - * Realtime Database and Firestore use other means of communicating with - * the backend servers, so they do not use this HTTP Agent. `Credential` - * instances also do not use this HTTP Agent, but instead support - * specifying an HTTP Agent in the corresponding factory methods. - */ - httpAgent?: Agent; - } - - const SDK_VERSION: string; - const apps: (admin.app.App | null)[]; - - function app(name?: string): admin.app.App; - - /** - * Gets the {@link admin.auth.Auth `Auth`} service for the default app or a - * given app. - * - * `admin.auth()` can be called with no arguments to access the default app's - * {@link admin.auth.Auth `Auth`} service or as `admin.auth(app)` to access the - * {@link admin.auth.Auth `Auth`} service associated with a specific app. - * - * @example - * ```javascript - * // Get the Auth service for the default app - * var defaultAuth = admin.auth(); - * ``` - * - * @example - * ```javascript - * // Get the Auth service for a given app - * var otherAuth = admin.auth(otherApp); - * ``` - * - */ - function auth(app?: admin.app.App): admin.auth.Auth; - - /** - * Gets the {@link admin.database.Database `Database`} service for the default - * app or a given app. - * - * `admin.database()` can be called with no arguments to access the default - * app's {@link admin.database.Database `Database`} service or as - * `admin.database(app)` to access the - * {@link admin.database.Database `Database`} service associated with a specific - * app. - * - * `admin.database` is also a namespace that can be used to access global - * constants and methods associated with the `Database` service. - * - * @example - * ```javascript - * // Get the Database service for the default app - * var defaultDatabase = admin.database(); - * ``` - * - * @example - * ```javascript - * // Get the Database service for a specific app - * var otherDatabase = admin.database(app); - * ``` - * - * @param App whose `Database` service to - * return. If not provided, the default `Database` service will be returned. - * - * @return The default `Database` service if no app - * is provided or the `Database` service associated with the provided app. - */ - function database(app?: admin.app.App): admin.database.Database; - - /** - * Gets the {@link admin.messaging.Messaging `Messaging`} service for the - * default app or a given app. - * - * `admin.messaging()` can be called with no arguments to access the default - * app's {@link admin.messaging.Messaging `Messaging`} service or as - * `admin.messaging(app)` to access the - * {@link admin.messaging.Messaging `Messaging`} service associated with a - * specific app. - * - * @example - * ```javascript - * // Get the Messaging service for the default app - * var defaultMessaging = admin.messaging(); - * ``` - * - * @example - * ```javascript - * // Get the Messaging service for a given app - * var otherMessaging = admin.messaging(otherApp); - * ``` - * - * @param app Optional app whose `Messaging` service to - * return. If not provided, the default `Messaging` service will be returned. - * - * @return The default `Messaging` service if no - * app is provided or the `Messaging` service associated with the provided - * app. - */ - function messaging(app?: admin.app.App): admin.messaging.Messaging; - - /** - * Gets the {@link admin.storage.Storage `Storage`} service for the - * default app or a given app. - * - * `admin.storage()` can be called with no arguments to access the default - * app's {@link admin.storage.Storage `Storage`} service or as - * `admin.storage(app)` to access the - * {@link admin.storage.Storage `Storage`} service associated with a - * specific app. - * - * @example - * ```javascript - * // Get the Storage service for the default app - * var defaultStorage = admin.storage(); - * ``` - * - * @example - * ```javascript - * // Get the Storage service for a given app - * var otherStorage = admin.storage(otherApp); - * ``` - */ - function storage(app?: admin.app.App): admin.storage.Storage; - - /** - * - * @param app A Firebase App instance - * @returns A [Firestore](https://cloud.google.com/nodejs/docs/reference/firestore/latest/Firestore) - * instance as defined in the `@google-cloud/firestore` package. - */ - function firestore(app?: admin.app.App): admin.firestore.Firestore; - - /** - * Gets the {@link admin.instanceId.InstanceId `InstanceId`} service for the - * default app or a given app. - * - * `admin.instanceId()` can be called with no arguments to access the default - * app's {@link admin.instanceId.InstanceId `InstanceId`} service or as - * `admin.instanceId(app)` to access the - * {@link admin.instanceId.InstanceId `InstanceId`} service associated with a - * specific app. - * - * @example - * ```javascript - * // Get the Instance ID service for the default app - * var defaultInstanceId = admin.instanceId(); - * ``` - * - * @example - * ```javascript - * // Get the Instance ID service for a given app - * var otherInstanceId = admin.instanceId(otherApp); - *``` - * - * @param app Optional app whose `InstanceId` service to - * return. If not provided, the default `InstanceId` service will be - * returned. - * - * @return The default `InstanceId` service if - * no app is provided or the `InstanceId` service associated with the - * provided app. - */ - function instanceId(app?: admin.app.App): admin.instanceId.InstanceId; - - /** - * Gets the {@link admin.projectManagement.ProjectManagement - * `ProjectManagement`} service for the default app or a given app. - * - * `admin.projectManagement()` can be called with no arguments to access the - * default app's {@link admin.projectManagement.ProjectManagement - * `ProjectManagement`} service, or as `admin.projectManagement(app)` to access - * the {@link admin.projectManagement.ProjectManagement `ProjectManagement`} - * service associated with a specific app. - * - * @example - * ```javascript - * // Get the ProjectManagement service for the default app - * var defaultProjectManagement = admin.projectManagement(); - * ``` - * - * @example - * ```javascript - * // Get the ProjectManagement service for a given app - * var otherProjectManagement = admin.projectManagement(otherApp); - * ``` - * - * @param app Optional app whose `ProjectManagement` service - * to return. If not provided, the default `ProjectManagement` service will - * be returned. * - * @return The default `ProjectManagement` service if no app is provided or the - * `ProjectManagement` service associated with the provided app. - */ - function projectManagement(app?: admin.app.App): admin.projectManagement.ProjectManagement; - - /** - * Gets the {@link admin.remoteConfig.RemoteConfig `RemoteConfig`} service for the - * default app or a given app. - * - * `admin.remoteConfig()` can be called with no arguments to access the default - * app's {@link admin.remoteConfig.RemoteConfig `RemoteConfig`} service or as - * `admin.remoteConfig(app)` to access the - * {@link admin.remoteConfig.RemoteConfig `RemoteConfig`} service associated with a - * specific app. - * - * @example - * ```javascript - * // Get the `RemoteConfig` service for the default app - * var defaultRemoteConfig = admin.remoteConfig(); - * ``` - * - * @example - * ```javascript - * // Get the `RemoteConfig` service for a given app - * var otherRemoteConfig = admin.remoteConfig(otherApp); - * ``` - * - * @param app Optional app for which to return the `RemoteConfig` service. - * If not provided, the default `RemoteConfig` service is returned. - * - * @return The default `RemoteConfig` service if no - * app is provided, or the `RemoteConfig` service associated with the provided - * app. - */ - function remoteConfig(app?: admin.app.App): admin.remoteConfig.RemoteConfig; - - /** - * Gets the {@link admin.securityRules.SecurityRules - * `SecurityRules`} service for the default app or a given app. - * - * `admin.securityRules()` can be called with no arguments to access the - * default app's {@link admin.securityRules.SecurityRules - * `SecurityRules`} service, or as `admin.securityRules(app)` to access - * the {@link admin.securityRules.SecurityRules `SecurityRules`} - * service associated with a specific app. - * - * @example - * ```javascript - * // Get the SecurityRules service for the default app - * var defaultSecurityRules = admin.securityRules(); - * ``` - * - * @example - * ```javascript - * // Get the SecurityRules service for a given app - * var otherSecurityRules = admin.securityRules(otherApp); - * ``` - * - * @param app Optional app to return the `SecurityRules` service - * for. If not provided, the default `SecurityRules` service - * is returned. - * @return The default `SecurityRules` service if no app is provided, or the - * `SecurityRules` service associated with the provided app. - */ - function securityRules(app?: admin.app.App): admin.securityRules.SecurityRules; - - /** - * Gets the {@link admin.machineLearning.MachineLearning `MachineLearning`} service for the - * default app or a given app. - * - * `admin.machineLearning()` can be called with no arguments to access the - * default app's {@link admin.machineLearning.MachineLearning - * `MachineLearning`} service or as `admin.machineLearning(app)` to access - * the {@link admin.machineLearning.MachineLearning `MachineLearning`} - * service associated with a specific app. - * - * @example - * ```javascript - * // Get the MachineLearning service for the default app - * var defaultMachineLearning = admin.machineLearning(); - * ``` - * - * @example - * ```javascript - * // Get the MachineLearning service for a given app - * var otherMachineLearning = admin.machineLearning(otherApp); - * ``` - * - * @param app Optional app whose `MachineLearning` service to - * return. If not provided, the default `MachineLearning` service - * will be returned. - * - * @return The default `MachineLearning` service if no app is provided or the - * `MachineLearning` service associated with the provided app. - */ - function machineLearning(app?: admin.app.App): admin.machineLearning.MachineLearning; - - function initializeApp(options?: admin.AppOptions, name?: string): admin.app.App; -} - -declare namespace admin.app { - /** - * A Firebase app holds the initialization information for a collection of - * services. - * - * Do not call this constructor directly. Instead, use - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`} - * to create an app. - */ - interface App { - - /** - * The (read-only) name for this app. - * - * The default app's name is `"[DEFAULT]"`. - * - * @example - * ```javascript - * // The default app's name is "[DEFAULT]" - * admin.initializeApp(defaultAppConfig); - * console.log(admin.app().name); // "[DEFAULT]" - * ``` - * - * @example - * ```javascript - * // A named app's name is what you provide to initializeApp() - * var otherApp = admin.initializeApp(otherAppConfig, "other"); - * console.log(otherApp.name); // "other" - * ``` - */ - name: string; - - /** - * The (read-only) configuration options for this app. These are the original - * parameters given in - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * @example - * ```javascript - * var app = admin.initializeApp(config); - * console.log(app.options.credential === config.credential); // true - * console.log(app.options.databaseURL === config.databaseURL); // true - * ``` - */ - options: admin.AppOptions; - - - auth(): admin.auth.Auth; - database(url?: string): admin.database.Database; - firestore(): admin.firestore.Firestore; - instanceId(): admin.instanceId.InstanceId; - machineLearning(): admin.machineLearning.MachineLearning; - messaging(): admin.messaging.Messaging; - projectManagement(): admin.projectManagement.ProjectManagement; - remoteConfig(): admin.remoteConfig.RemoteConfig; - securityRules(): admin.securityRules.SecurityRules; - storage(): admin.storage.Storage; - - /** - * Renders this local `FirebaseApp` unusable and frees the resources of - * all associated services (though it does *not* clean up any backend - * resources). When running the SDK locally, this method - * must be called to ensure graceful termination of the process. - * - * @example - * ```javascript - * app.delete() - * .then(function() { - * console.log("App deleted successfully"); - * }) - * .catch(function(error) { - * console.log("Error deleting app:", error); - * }); - * ``` - */ - delete(): Promise; - } -} - -declare namespace admin.auth { - export import UserMetadata = _auth.admin.auth.UserMetadata; - export import UserInfo = _auth.admin.auth.UserInfo; - export import UserRecord = _auth.admin.auth.UserRecord; - export import UpdateRequest = _auth.admin.auth.UpdateRequest; - export import CreateRequest = _auth.admin.auth.CreateRequest; - export import DecodedIdToken = _auth.admin.auth.DecodedIdToken; - export import ListUsersResult = _auth.admin.auth.ListUsersResult; - export import HashAlgorithmType = _auth.admin.auth.HashAlgorithmType; - export import UserImportOptions = _auth.admin.auth.UserImportOptions; - export import UserImportResult = _auth.admin.auth.UserImportResult; - export import UserImportRecord = _auth.admin.auth.UserImportRecord; - export import SessionCookieOptions = _auth.admin.auth.SessionCookieOptions; - export import ActionCodeSettings = _auth.admin.auth.ActionCodeSettings; - export import Tenant = _auth.admin.auth.Tenant; - export import UpdateTenantRequest = _auth.admin.auth.UpdateTenantRequest; - export import CreateTenantRequest = _auth.admin.auth.CreateTenantRequest; - export import ListTenantsResult = _auth.admin.auth.ListTenantsResult; - export import AuthProviderConfigFilter = _auth.admin.auth.AuthProviderConfigFilter; - export import AuthProviderConfig = _auth.admin.auth.AuthProviderConfig; - export import SAMLAuthProviderConfig = _auth.admin.auth.SAMLAuthProviderConfig; - export import OIDCAuthProviderConfig = _auth.admin.auth.OIDCAuthProviderConfig; - export import SAMLUpdateAuthProviderRequest = _auth.admin.auth.SAMLUpdateAuthProviderRequest; - export import OIDCUpdateAuthProviderRequest = _auth.admin.auth.OIDCUpdateAuthProviderRequest; - export import ListProviderConfigResults = _auth.admin.auth.ListProviderConfigResults; - export import UpdateAuthProviderRequest = _auth.admin.auth.UpdateAuthProviderRequest; - export import BaseAuth = _auth.admin.auth.BaseAuth; - export import TenantAwareAuth = _auth.admin.auth.TenantAwareAuth; - export import Auth = _auth.admin.auth.Auth; - export import TenantManager = _auth.admin.auth.TenantManager; - export import MultiFactorInfo = _auth.admin.auth.MultiFactorInfo; - export import PhoneMultiFactorInfo = _auth.admin.auth.PhoneMultiFactorInfo; - export import CreateMultiFactorInfoRequest = _auth.admin.auth.CreateMultiFactorInfoRequest; - export import CreatePhoneMultiFactorInfoRequest = _auth.admin.auth.CreatePhoneMultiFactorInfoRequest; - export import UpdateMultiFactorInfoRequest = _auth.admin.auth.UpdateMultiFactorInfoRequest; - export import UpdatePhoneMultiFactorInfoRequest = _auth.admin.auth.UpdatePhoneMultiFactorInfoRequest; - export import MultiFactorCreateSettings = _auth.admin.auth.MultiFactorCreateSettings; - export import MultiFactorUpdateSettings = _auth.admin.auth.MultiFactorUpdateSettings; - export import DeleteUsersResult = _auth.admin.auth.DeleteUsersResult; - export import GetUsersResult = _auth.admin.auth.GetUsersResult; -} - -declare namespace admin.credential { - export import Credential = _credential.admin.credential.Credential; - export import applicationDefault = _credential.admin.credential.applicationDefault; - export import cert = _credential.admin.credential.cert; - export import refreshToken = _credential.admin.credential.refreshToken; -} - -declare namespace admin.database { - export import Database = _database.admin.database.Database; - export import DataSnapshot = _database.admin.database.DataSnapshot; - export import OnDisconnect = _database.admin.database.OnDisconnect; - export import EventType = _database.admin.database.EventType; - export import Query = _database.admin.database.Query; - export import Reference = _database.admin.database.Reference; - export import ThenableReference = _database.admin.database.ThenableReference; - export import enableLogging = _database.admin.database.enableLogging; - export import ServerValue = _database.admin.database.ServerValue; -} - -declare namespace admin.messaging { - export import Message = _messaging.admin.messaging.Message; - export import MulticastMessage = _messaging.admin.messaging.MulticastMessage; - export import AndroidConfig = _messaging.admin.messaging.AndroidConfig; - export import AndroidNotification = _messaging.admin.messaging.AndroidNotification; - export import LightSettings = _messaging.admin.messaging.LightSettings; - export import AndroidFcmOptions = _messaging.admin.messaging.AndroidFcmOptions; - export import ApnsConfig = _messaging.admin.messaging.ApnsConfig; - export import ApnsPayload = _messaging.admin.messaging.ApnsPayload; - export import Aps = _messaging.admin.messaging.Aps; - export import ApsAlert = _messaging.admin.messaging.ApsAlert; - export import CriticalSound = _messaging.admin.messaging.CriticalSound; - export import ApnsFcmOptions = _messaging.admin.messaging.ApnsFcmOptions; - export import FcmOptions = _messaging.admin.messaging.FcmOptions; - export import Notification = _messaging.admin.messaging.Notification; - export import WebpushConfig = _messaging.admin.messaging.WebpushConfig; - export import WebpushFcmOptions = _messaging.admin.messaging.WebpushFcmOptions; - export import WebpushNotification = _messaging.admin.messaging.WebpushNotification; - export import MessagingTopicManagementResponse = _messaging.admin.messaging.MessagingTopicManagementResponse; - export import BatchResponse = _messaging.admin.messaging.BatchResponse; - export import SendResponse = _messaging.admin.messaging.SendResponse; - export import Messaging = _messaging.admin.messaging.Messaging; - - // Legacy API types. - export import DataMessagePayload = _messaging.admin.messaging.DataMessagePayload; - export import NotificationMessagePayload = _messaging.admin.messaging.NotificationMessagePayload; - export import MessagingPayload = _messaging.admin.messaging.MessagingPayload; - export import MessagingOptions = _messaging.admin.messaging.MessagingOptions; - export import MessagingDevicesResponse = _messaging.admin.messaging.MessagingDevicesResponse; - export import MessagingDeviceResult = _messaging.admin.messaging.MessagingDeviceResult; - export import MessagingDeviceGroupResponse = _messaging.admin.messaging.MessagingDeviceGroupResponse; - export import MessagingTopicResponse = _messaging.admin.messaging.MessagingTopicResponse; - export import MessagingConditionResponse = _messaging.admin.messaging.MessagingConditionResponse; -} - -declare namespace admin.storage { - export import Storage = _storage.admin.storage.Storage; -} - -declare namespace admin.firestore { - export import v1beta1 = _firestore.v1beta1; - export import v1 = _firestore.v1; - - export import CollectionReference = _firestore.CollectionReference; - export import DocumentData = _firestore.DocumentData; - export import DocumentReference = _firestore.DocumentReference; - export import DocumentSnapshot = _firestore.DocumentSnapshot; - export import FieldPath = _firestore.FieldPath; - export import FieldValue = _firestore.FieldValue; - export import Firestore = _firestore.Firestore; - export import GeoPoint = _firestore.GeoPoint; - export import Query = _firestore.Query; - export import QueryDocumentSnapshot = _firestore.QueryDocumentSnapshot; - export import QuerySnapshot = _firestore.QuerySnapshot; - export import Timestamp = _firestore.Timestamp; - export import Transaction = _firestore.Transaction; - export import WriteBatch = _firestore.WriteBatch; - export import WriteResult = _firestore.WriteResult; - - export import setLogFunction = _firestore.setLogFunction; -} - -declare namespace admin.instanceId { - export import InstanceId = _instanceId.admin.instanceId.InstanceId; -} - -declare namespace admin.projectManagement { - export import ShaCertificate = _projectManagement.admin.projectManagement.ShaCertificate; - export import AppMetadata = _projectManagement.admin.projectManagement.AppMetadata; - export import AppPlatform = _projectManagement.admin.projectManagement.AppPlatform; - export import AndroidAppMetadata = _projectManagement.admin.projectManagement.AndroidAppMetadata; - export import IosAppMetadata = _projectManagement.admin.projectManagement.IosAppMetadata; - export import AndroidApp = _projectManagement.admin.projectManagement.AndroidApp; - export import IosApp = _projectManagement.admin.projectManagement.IosApp; - export import ProjectManagement = _projectManagement.admin.projectManagement.ProjectManagement; -} - -declare namespace admin.remoteConfig { - export import TagColor = _remoteConfig.admin.remoteConfig.TagColor; - export import RemoteConfigTemplate = _remoteConfig.admin.remoteConfig.RemoteConfigTemplate; - export import RemoteConfigParameter = _remoteConfig.admin.remoteConfig.RemoteConfigParameter; - export import RemoteConfigParameterGroup = _remoteConfig.admin.remoteConfig.RemoteConfigParameterGroup; - export import RemoteConfigCondition = _remoteConfig.admin.remoteConfig.RemoteConfigCondition; - export import ExplicitParameterValue = _remoteConfig.admin.remoteConfig.ExplicitParameterValue; - export import InAppDefaultValue = _remoteConfig.admin.remoteConfig.InAppDefaultValue; - export import RemoteConfigParameterValue = _remoteConfig.admin.remoteConfig.RemoteConfigParameterValue; - export import Version = _remoteConfig.admin.remoteConfig.Version; - export import ListVersionsOptions = _remoteConfig.admin.remoteConfig.ListVersionsOptions; - export import ListVersionsResult = _remoteConfig.admin.remoteConfig.ListVersionsResult; - export import RemoteConfigUser = _remoteConfig.admin.remoteConfig.RemoteConfigUser; - export import RemoteConfig = _remoteConfig.admin.remoteConfig.RemoteConfig; -} - -declare namespace admin.securityRules { - export import RulesFile = _securityRules.admin.securityRules.RulesFile; - export import RulesetMetadata = _securityRules.admin.securityRules.RulesetMetadata; - export import Ruleset = _securityRules.admin.securityRules.Ruleset; - export import RulesetMetadataList = _securityRules.admin.securityRules.RulesetMetadataList; - export import SecurityRules = _securityRules.admin.securityRules.SecurityRules; -} - -declare namespace admin.machineLearning { - /** - * Firebase ML Model input objects - */ - interface ModelOptionsBase { - displayName?: string; - tags?: string[]; - } - interface GcsTfliteModelOptions extends ModelOptionsBase { - tfliteModel: { - gcsTfliteUri: string; - }; - } - interface AutoMLTfliteModelOptions extends ModelOptionsBase { - tfliteModel: { - automlModel: string; - }; - } - type ModelOptions = ModelOptionsBase | GcsTfliteModelOptions | AutoMLTfliteModelOptions; - - /** - * A TensorFlow Lite Model output object - * - * One of either the `gcsTfliteUri` or `automlModel` properties will be - * defined. - */ - interface TFLiteModel { - /** The size of the model. */ - readonly sizeBytes: number; - - /** The URI from which the model was originally provided to Firebase. */ - readonly gcsTfliteUri?: string; - /** - * The AutoML model reference from which the model was originally provided - * to Firebase. - */ - readonly automlModel?: string; - } - - /** - * A Firebase ML Model output object - */ - interface Model { - /** The ID of the model. */ - readonly modelId: string; - - /** - * The model's name. This is the name you use from your app to load the - * model. - */ - readonly displayName: string; - - /** - * The model's tags, which can be used to group or filter models in list - * operations. - */ - readonly tags?: string[]; - - /** The timestamp of the model's creation. */ - readonly createTime: string; - - /** The timestamp of the model's most recent update. */ - readonly updateTime: string; - - /** Error message when model validation fails. */ - readonly validationError?: string; - - /** True if the model is published. */ - readonly published: boolean; - - /** - * The ETag identifier of the current version of the model. This value - * changes whenever you update any of the model's properties. - */ - readonly etag: string; - - /** - * The hash of the model's `tflite` file. This value changes only when - * you upload a new TensorFlow Lite model. - */ - readonly modelHash?: string; - - /** - * True if the model is locked by a server-side operation. You can't make - * changes to a locked model. See {@link waitForUnlocked `waitForUnlocked()`}. - */ - readonly locked: boolean; - - /** - * Wait for the model to be unlocked. - * - * @param {number} maxTimeMillis The maximum time in milliseconds to wait. - * If not specified, a default maximum of 2 minutes is used. - * - * @return {Promise} A promise that resolves when the model is unlocked - * or the maximum wait time has passed. - */ - waitForUnlocked(maxTimeMillis?: number): Promise; - - /** - * Return the model as a JSON object. - */ - toJSON(): {[key: string]: any}; - - /** Metadata about the model's TensorFlow Lite model file. */ - readonly tfliteModel?: TFLiteModel; - } - - /** - * Interface representing options for listing Models. - */ - interface ListModelsOptions { - /** - * An expression that specifies how to filter the results. - * - * Examples: - * - * ``` - * display_name = your_model - * display_name : experimental_* - * tags: face_detector AND tags: experimental - * state.published = true - * ``` - * - * See https://firebase.google.com/docs/ml/manage-hosted-models#list_your_projects_models - */ - filter?: string; - - /** The number of results to return in each page. */ - pageSize?: number; - - /** A token that specifies the result page to return. */ - pageToken?: string; - } - - /** Response object for a listModels operation. */ - interface ListModelsResult { - /** A list of models in your project. */ - readonly models: Model[]; - - /** - * A token you can use to retrieve the next page of results. If null, the - * current page is the final page. - */ - readonly pageToken?: string; - } - - - /** - * The Firebase `MachineLearning` service interface. - * - * Do not call this constructor directly. Instead, use - * [`admin.machineLearning()`](admin.machineLearning#machineLearning). - */ - interface MachineLearning { - /** - * The {@link admin.app.App} associated with the current `MachineLearning` - * service instance. - */ - app: admin.app.App; - - /** - * Creates a model in the current Firebase project. - * - * @param {ModelOptions} model The model to create. - * - * @return {Promise} A Promise fulfilled with the created model. - */ - createModel(model: ModelOptions): Promise; - - /** - * Updates a model's metadata or model file. - * - * @param {string} modelId The ID of the model to update. - * @param {ModelOptions} model The model fields to update. - * - * @return {Promise} A Promise fulfilled with the updated model. - */ - updateModel(modelId: string, model: ModelOptions): Promise; - - /** - * Publishes a Firebase ML model. - * - * A published model can be downloaded to client apps. - * - * @param {string} modelId The ID of the model to publish. - * - * @return {Promise} A Promise fulfilled with the published model. - */ - publishModel(modelId: string): Promise; - - /** - * Unpublishes a Firebase ML model. - * - * @param {string} modelId The ID of the model to unpublish. - * - * @return {Promise} A Promise fulfilled with the unpublished model. - */ - unpublishModel(modelId: string): Promise; - - /** - * Gets the model specified by the given ID. - * - * @param {string} modelId The ID of the model to get. - * - * @return {Promise} A Promise fulfilled with the model object. - */ - getModel(modelId: string): Promise; - - /** - * Lists the current project's models. - * - * @param {ListModelsOptions} options The listing options. - * - * @return {Promise} A promise that - * resolves with the current (filtered) list of models and the next page - * token. For the last page, an empty list of models and no page token - * are returned. - */ - listModels(options?: ListModelsOptions): Promise; - - /** - * Deletes a model from the current project. - * - * @param {string} modelId The ID of the model to delete. - */ - deleteModel(modelId: string): Promise; - } -} +import * as admin from './firebase-namespace'; declare module 'firebase-admin' { } diff --git a/src/instance-id.d.ts b/src/instance-id.d.ts deleted file mode 100644 index 34269be5b0..0000000000 --- a/src/instance-id.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as _admin from './index.d'; - -export namespace admin.instanceId { - /** - * Gets the {@link InstanceId `InstanceId`} service for the - * current app. - * - * @example - * ```javascript - * var instanceId = app.instanceId(); - * // The above is shorthand for: - * // var instanceId = admin.instanceId(app); - * ``` - * - * @return The `InstanceId` service for the - * current app. - */ - interface InstanceId { - app: _admin.app.App; - - /** - * Deletes the specified instance ID and the associated data from Firebase. - * - * Note that Google Analytics for Firebase uses its own form of Instance ID to - * keep track of analytics data. Therefore deleting a Firebase Instance ID does - * not delete Analytics data. See - * [Delete an Instance ID](/support/privacy/manage-iids#delete_an_instance_id) - * for more information. - * - * @param instanceId The instance ID to be deleted. - * - * @return A promise fulfilled when the instance ID is deleted. - */ - deleteInstanceId(instanceId: string): Promise; - } -} diff --git a/src/instance-id/index.ts b/src/instance-id/index.ts index 2f6f48fc2b..09cbfe4022 100644 --- a/src/instance-id/index.ts +++ b/src/instance-id/index.ts @@ -14,30 +14,72 @@ * limitations under the License. */ -import { FirebaseApp } from '../firebase-app'; -import * as instanceIdApi from './instance-id'; -import * as firebaseAdmin from '../index'; - -export function instanceId(app?: FirebaseApp): instanceIdApi.InstanceId { - if (typeof(app) === 'undefined') { - app = firebaseAdmin.app(); - } - return app.instanceId(); -} +import { app } from '../firebase-namespace-api'; /** - * We must define a namespace to make the typings work correctly. Otherwise - * `admin.instanceId()` cannot be called like a function. Temporarily, - * admin.instanceId is used as the namespace name because we cannot barrel - * re-export the contents from instance-id, and we want it to - * match the namespacing in the re-export inside src/index.d.ts + * Gets the {@link instanceId.InstanceId `InstanceId`} service for the + * default app or a given app. + * + * `admin.instanceId()` can be called with no arguments to access the default + * app's {@link instanceId.InstanceId `InstanceId`} service or as + * `admin.instanceId(app)` to access the + * {@link instanceId.InstanceId `InstanceId`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the Instance ID service for the default app + * var defaultInstanceId = admin.instanceId(); + * ``` + * + * @example + * ```javascript + * // Get the Instance ID service for a given app + * var otherInstanceId = admin.instanceId(otherApp); + *``` + * + * @param app Optional app whose `InstanceId` service to + * return. If not provided, the default `InstanceId` service will be + * returned. + * + * @return The default `InstanceId` service if + * no app is provided or the `InstanceId` service associated with the + * provided app. */ +export declare function instanceId(app?: app.App): instanceId.InstanceId; + /* eslint-disable @typescript-eslint/no-namespace */ -export namespace admin.instanceId { - // See https://github.com/microsoft/TypeScript/issues/4336 - /* eslint-disable @typescript-eslint/no-unused-vars */ - // See https://github.com/typescript-eslint/typescript-eslint/issues/363 - // Allows for exposing classes as interfaces in typings - /* eslint-disable @typescript-eslint/no-empty-interface */ - export interface InstanceId extends instanceIdApi.InstanceId {} +export namespace instanceId { + /** + * Gets the {@link InstanceId `InstanceId`} service for the + * current app. + * + * @example + * ```javascript + * var instanceId = app.instanceId(); + * // The above is shorthand for: + * // var instanceId = admin.instanceId(app); + * ``` + * + * @return The `InstanceId` service for the + * current app. + */ + export interface InstanceId { + app: app.App; + + /** + * Deletes the specified instance ID and the associated data from Firebase. + * + * Note that Google Analytics for Firebase uses its own form of Instance ID to + * keep track of analytics data. Therefore deleting a Firebase Instance ID does + * not delete Analytics data. See + * [Delete an Instance ID](/support/privacy/manage-iids#delete_an_instance_id) + * for more information. + * + * @param instanceId The instance ID to be deleted. + * + * @return A promise fulfilled when the instance ID is deleted. + */ + deleteInstanceId(instanceId: string): Promise; + } } diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index b17434e9a3..e09c630440 100644 --- a/src/instance-id/instance-id.ts +++ b/src/instance-id/instance-id.ts @@ -18,9 +18,11 @@ import { FirebaseApp } from '../firebase-app'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; import { FirebaseInstanceIdRequestHandler } from './instance-id-request-internal'; - +import { instanceId } from './index'; import * as validator from '../utils/validator'; +import InstanceIdInterface = instanceId.InstanceId; + /** * Internals of an InstanceId service instance. */ @@ -50,7 +52,7 @@ class InstanceIdInternals implements FirebaseServiceInternalsInterface { * @return The `InstanceId` service for the * current app. */ -export class InstanceId implements FirebaseServiceInterface { +export class InstanceId implements FirebaseServiceInterface, InstanceIdInterface { public INTERNAL: InstanceIdInternals = new InstanceIdInternals(); private app_: FirebaseApp; diff --git a/src/machine-learning/index.ts b/src/machine-learning/index.ts new file mode 100644 index 0000000000..7e5a8078cd --- /dev/null +++ b/src/machine-learning/index.ts @@ -0,0 +1,282 @@ +/*! + * Copyright 2020 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 '../firebase-namespace-api'; + +/** + * Gets the {@link machineLearning.MachineLearning `MachineLearning`} service for the + * default app or a given app. + * + * `admin.machineLearning()` can be called with no arguments to access the + * default app's {@link machineLearning.MachineLearning + * `MachineLearning`} service or as `admin.machineLearning(app)` to access + * the {@link machineLearning.MachineLearning `MachineLearning`} + * service associated with a specific app. + * + * @example + * ```javascript + * // Get the MachineLearning service for the default app + * var defaultMachineLearning = admin.machineLearning(); + * ``` + * + * @example + * ```javascript + * // Get the MachineLearning service for a given app + * var otherMachineLearning = admin.machineLearning(otherApp); + * ``` + * + * @param app Optional app whose `MachineLearning` service to + * return. If not provided, the default `MachineLearning` service + * will be returned. + * + * @return The default `MachineLearning` service if no app is provided or the + * `MachineLearning` service associated with the provided app. + */ +export declare function machineLearning(app?: app.App): machineLearning.MachineLearning; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace machineLearning { + /** + * Firebase ML Model input objects + */ + export interface ModelOptionsBase { + displayName?: string; + tags?: string[]; + } + + export interface GcsTfliteModelOptions extends ModelOptionsBase { + tfliteModel: { + gcsTfliteUri: string; + }; + } + + export interface AutoMLTfliteModelOptions extends ModelOptionsBase { + tfliteModel: { + automlModel: string; + }; + } + + export type ModelOptions = ModelOptionsBase | GcsTfliteModelOptions | AutoMLTfliteModelOptions; + + /** + * A TensorFlow Lite Model output object + * + * One of either the `gcsTfliteUri` or `automlModel` properties will be + * defined. + */ + export interface TFLiteModel { + /** The size of the model. */ + readonly sizeBytes: number; + + /** The URI from which the model was originally provided to Firebase. */ + readonly gcsTfliteUri?: string; + /** + * The AutoML model reference from which the model was originally provided + * to Firebase. + */ + readonly automlModel?: string; + } + + /** + * A Firebase ML Model output object + */ + export interface Model { + /** The ID of the model. */ + readonly modelId: string; + + /** + * The model's name. This is the name you use from your app to load the + * model. + */ + readonly displayName: string; + + /** + * The model's tags, which can be used to group or filter models in list + * operations. + */ + readonly tags?: string[]; + + /** The timestamp of the model's creation. */ + readonly createTime: string; + + /** The timestamp of the model's most recent update. */ + readonly updateTime: string; + + /** Error message when model validation fails. */ + readonly validationError?: string; + + /** True if the model is published. */ + readonly published: boolean; + + /** + * The ETag identifier of the current version of the model. This value + * changes whenever you update any of the model's properties. + */ + readonly etag: string; + + /** + * The hash of the model's `tflite` file. This value changes only when + * you upload a new TensorFlow Lite model. + */ + readonly modelHash?: string; + + /** + * True if the model is locked by a server-side operation. You can't make + * changes to a locked model. See {@link waitForUnlocked `waitForUnlocked()`}. + */ + readonly locked: boolean; + + /** + * Wait for the model to be unlocked. + * + * @param {number} maxTimeMillis The maximum time in milliseconds to wait. + * If not specified, a default maximum of 2 minutes is used. + * + * @return {Promise} A promise that resolves when the model is unlocked + * or the maximum wait time has passed. + */ + waitForUnlocked(maxTimeMillis?: number): Promise; + + /** + * Return the model as a JSON object. + */ + toJSON(): {[key: string]: any}; + + /** Metadata about the model's TensorFlow Lite model file. */ + readonly tfliteModel?: TFLiteModel; + } + + /** + * Interface representing options for listing Models. + */ + export interface ListModelsOptions { + /** + * An expression that specifies how to filter the results. + * + * Examples: + * + * ``` + * display_name = your_model + * display_name : experimental_* + * tags: face_detector AND tags: experimental + * state.published = true + * ``` + * + * See https://firebase.google.com/docs/ml/manage-hosted-models#list_your_projects_models + */ + filter?: string; + + /** The number of results to return in each page. */ + pageSize?: number; + + /** A token that specifies the result page to return. */ + pageToken?: string; + } + + /** Response object for a listModels operation. */ + export interface ListModelsResult { + /** A list of models in your project. */ + readonly models: Model[]; + + /** + * A token you can use to retrieve the next page of results. If null, the + * current page is the final page. + */ + readonly pageToken?: string; + } + + + /** + * The Firebase `MachineLearning` service interface. + * + * Do not call this constructor directly. Instead, use + * [`admin.machineLearning()`](admin.machineLearning#machineLearning). + */ + export interface MachineLearning { + /** + * The {@link app.App} associated with the current `MachineLearning` + * service instance. + */ + app: app.App; + + /** + * Creates a model in the current Firebase project. + * + * @param {ModelOptions} model The model to create. + * + * @return {Promise} A Promise fulfilled with the created model. + */ + createModel(model: ModelOptions): Promise; + + /** + * Updates a model's metadata or model file. + * + * @param {string} modelId The ID of the model to update. + * @param {ModelOptions} model The model fields to update. + * + * @return {Promise} A Promise fulfilled with the updated model. + */ + updateModel(modelId: string, model: ModelOptions): Promise; + + /** + * Publishes a Firebase ML model. + * + * A published model can be downloaded to client apps. + * + * @param {string} modelId The ID of the model to publish. + * + * @return {Promise} A Promise fulfilled with the published model. + */ + publishModel(modelId: string): Promise; + + /** + * Unpublishes a Firebase ML model. + * + * @param {string} modelId The ID of the model to unpublish. + * + * @return {Promise} A Promise fulfilled with the unpublished model. + */ + unpublishModel(modelId: string): Promise; + + /** + * Gets the model specified by the given ID. + * + * @param {string} modelId The ID of the model to get. + * + * @return {Promise} A Promise fulfilled with the model object. + */ + getModel(modelId: string): Promise; + + /** + * Lists the current project's models. + * + * @param {ListModelsOptions} options The listing options. + * + * @return {Promise} A promise that + * resolves with the current (filtered) list of models and the next page + * token. For the last page, an empty list of models and no page token + * are returned. + */ + listModels(options?: ListModelsOptions): Promise; + + /** + * Deletes a model from the current project. + * + * @param {string} modelId The ID of the model to delete. + */ + deleteModel(modelId: string): Promise; + } +} diff --git a/src/machine-learning/machine-learning-api-client.ts b/src/machine-learning/machine-learning-api-client.ts index e39c94db9a..2281b84d53 100644 --- a/src/machine-learning/machine-learning-api-client.ts +++ b/src/machine-learning/machine-learning-api-client.ts @@ -14,13 +14,19 @@ * limitations under the License. */ -import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, - ExponentialBackoffPoller } from '../utils/api-request'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, ExponentialBackoffPoller +} from '../utils/api-request'; import { PrefixedFirebaseError } from '../utils/error'; import { FirebaseMachineLearningError, MachineLearningErrorCode } from './machine-learning-utils'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { FirebaseApp } from '../firebase-app'; +import { machineLearning } from './index'; + +import GcsTfliteModelOptions = machineLearning.GcsTfliteModelOptions; +import ListModelsOptions = machineLearning.ListModelsOptions; +import ModelOptions = machineLearning.ModelOptions; const ML_V1BETA2_API = 'https://firebaseml.googleapis.com/v1beta2'; const FIREBASE_VERSION_HEADER = { @@ -37,24 +43,7 @@ export interface StatusErrorResponse { readonly message: string; } -/** - * Firebase ML Model input objects - */ -export interface ModelOptionsBase { - displayName?: string; - tags?: string[]; -} -export interface GcsTfliteModelOptions extends ModelOptionsBase { - tfliteModel: { - gcsTfliteUri: string; - }; -} -export interface AutoMLTfliteModelOptions extends ModelOptionsBase { - tfliteModel: { - automlModel: string; - }; -} -export type ModelOptions = ModelOptionsBase | GcsTfliteModelOptions | AutoMLTfliteModelOptions; + export type ModelUpdateOptions = ModelOptions & { state?: { published?: boolean }}; export function isGcsTfliteModelOptions(options: ModelOptions): options is GcsTfliteModelOptions { @@ -62,13 +51,6 @@ export function isGcsTfliteModelOptions(options: ModelOptions): options is GcsTf return typeof gcsUri !== 'undefined' } -/** Interface representing listModels options. */ -export interface ListModelsOptions { - filter?: string; - pageSize?: number; - pageToken?: string; -} - export interface ModelContent { readonly displayName?: string; readonly tags?: string[]; diff --git a/src/machine-learning/machine-learning.ts b/src/machine-learning/machine-learning.ts index 8430b66cfc..9e9ca50b55 100644 --- a/src/machine-learning/machine-learning.ts +++ b/src/machine-learning/machine-learning.ts @@ -16,14 +16,22 @@ import { FirebaseApp } from '../firebase-app'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { MachineLearningApiClient, ModelResponse, ModelOptions, - ModelUpdateOptions, ListModelsOptions, isGcsTfliteModelOptions } from './machine-learning-api-client'; +import { + MachineLearningApiClient, ModelResponse, ModelUpdateOptions, isGcsTfliteModelOptions +} from './machine-learning-api-client'; import { FirebaseError } from '../utils/error'; - import * as validator from '../utils/validator'; import { FirebaseMachineLearningError } from './machine-learning-utils'; import { deepCopy } from '../utils/deep-copy'; import * as utils from '../utils'; +import { machineLearning } from './index'; + +import ListModelsOptions = machineLearning.ListModelsOptions; +import ListModelsResult = machineLearning.ListModelsResult; +import MachineLearningInterface = machineLearning.MachineLearning; +import ModelInterface = machineLearning.Model; +import ModelOptions = machineLearning.ModelOptions; +import TFLiteModel = machineLearning.TFLiteModel; /** * Internals of an ML instance. @@ -41,16 +49,10 @@ class MachineLearningInternals implements FirebaseServiceInternalsInterface { } } -/** Response object for a listModels operation. */ -export interface ListModelsResult { - models: Model[]; - pageToken?: string; -} - /** * The Firebase Machine Learning class */ -export class MachineLearning implements FirebaseServiceInterface { +export class MachineLearning implements FirebaseServiceInterface, MachineLearningInterface { public readonly INTERNAL = new MachineLearningInternals(); private readonly client: MachineLearningApiClient; @@ -168,7 +170,7 @@ export class MachineLearning implements FirebaseServiceInterface { if (resp.models) { models = resp.models.map((rs) => new Model(rs, this.client)); } - const result: ListModelsResult = { models }; + const result: { models: Model[]; pageToken?: string } = { models }; if (resp.nextPageToken) { result.pageToken = resp.nextPageToken; } @@ -235,7 +237,7 @@ export class MachineLearning implements FirebaseServiceInterface { /** * A Firebase ML Model output object. */ -export class Model { +export class Model implements ModelInterface { private model: ModelResponse; private readonly client?: MachineLearningApiClient; @@ -366,24 +368,13 @@ export class Model { } // Remove '@type' field. We don't need it. - if ((tmpModel as any)["@type"]) { - delete (tmpModel as any)["@type"]; + if ((tmpModel as any)['@type']) { + delete (tmpModel as any)['@type']; } return tmpModel; } } -/** - * A TFLite Model output object - */ -export interface TFLiteModel { - readonly sizeBytes: number; - - // Oneof these two - readonly gcsTfliteUri?: string; - readonly automlModel?: string; -} - function extractModelId(resourceName: string): string { return resourceName.split('/').pop()!; } diff --git a/src/messaging.d.ts b/src/messaging.d.ts deleted file mode 100644 index ff134002ab..0000000000 --- a/src/messaging.d.ts +++ /dev/null @@ -1,1340 +0,0 @@ -import * as _admin from './index.d'; - -type BaseMessage = { - data?: { [key: string]: string }; - notification?: admin.messaging.Notification; - android?: admin.messaging.AndroidConfig; - webpush?: admin.messaging.WebpushConfig; - apns?: admin.messaging.ApnsConfig; - fcmOptions?: admin.messaging.FcmOptions; -}; - -interface TokenMessage extends BaseMessage { - token: string; -} - -interface TopicMessage extends BaseMessage { - topic: string; -} - -interface ConditionMessage extends BaseMessage { - condition: string; -} - -export namespace admin.messaging { - type Message = TokenMessage | TopicMessage | ConditionMessage; - - interface MulticastMessage extends BaseMessage { - tokens: string[]; - } - - /** - * Represents the Android-specific options that can be included in an - * {@link admin.messaging.Message}. - */ - interface AndroidConfig { - - /** - * Collapse key for the message. Collapse key serves as an identifier for a - * group of messages that can be collapsed, so that only the last message gets - * sent when delivery can be resumed. A maximum of four different collapse keys - * may be active at any given time. - */ - collapseKey?: string; - - /** - * Priority of the message. Must be either `normal` or `high`. - */ - priority?: ('high' | 'normal'); - - /** - * Time-to-live duration of the message in milliseconds. - */ - ttl?: number; - - /** - * Package name of the application where the registration tokens must match - * in order to receive the message. - */ - restrictedPackageName?: string; - - /** - * A collection of data fields to be included in the message. All values must - * be strings. When provided, overrides any data fields set on the top-level - * `admin.messaging.Message`.} - */ - data?: { [key: string]: string }; - - /** - * Android notification to be included in the message. - */ - notification?: AndroidNotification; - - /** - * Options for features provided by the FCM SDK for Android. - */ - fcmOptions?: AndroidFcmOptions; - } - - /** - * Represents the Android-specific notification options that can be included in - * {@link admin.messaging.AndroidConfig}. - */ - interface AndroidNotification { - - /** - * Title of the Android notification. When provided, overrides the title set via - * `admin.messaging.Notification`. - */ - title?: string; - - /** - * Body of the Android notification. When provided, overrides the body set via - * `admin.messaging.Notification`. - */ - body?: string; - - /** - * Icon resource for the Android notification. - */ - icon?: string; - - /** - * Notification icon color in `#rrggbb` format. - */ - color?: string; - - /** - * File name of the sound to be played when the device receives the - * notification. - */ - sound?: string; - - /** - * Notification tag. This is an identifier used to replace existing - * notifications in the notification drawer. If not specified, each request - * creates a new notification. - */ - tag?: string; - - /** - * URL of an image to be displayed in the notification. - */ - imageUrl?: string; - - /** - * Action associated with a user click on the notification. If specified, an - * activity with a matching Intent Filter is launched when a user clicks on the - * notification. - */ - clickAction?: string; - - /** - * Key of the body string in the app's string resource to use to localize the - * body text. - * - */ - bodyLocKey?: string; - - /** - * An array of resource keys that will be used in place of the format - * specifiers in `bodyLocKey`. - */ - bodyLocArgs?: string[]; - - /** - * Key of the title string in the app's string resource to use to localize the - * title text. - */ - titleLocKey?: string; - - /** - * An array of resource keys that will be used in place of the format - * specifiers in `titleLocKey`. - */ - titleLocArgs?: string[]; - - /** - * The Android notification channel ID (new in Android O). The app must create - * a channel with this channel ID before any notification with this channel ID - * can be received. If you don't send this channel ID in the request, or if the - * channel ID provided has not yet been created by the app, FCM uses the channel - * ID specified in the app manifest. - */ - channelId?: string; - - /** - * Sets the "ticker" text, which is sent to accessibility services. Prior to - * API level 21 (Lollipop), sets the text that is displayed in the status bar - * when the notification first arrives. - */ - ticker?: string; - - /** - * When set to `false` or unset, the notification is automatically dismissed when - * the user clicks it in the panel. When set to `true`, the notification persists - * even when the user clicks it. - */ - sticky?: boolean; - - /** - * For notifications that inform users about events with an absolute time reference, sets - * the time that the event in the notification occurred. Notifications - * in the panel are sorted by this time. - */ - eventTimestamp?: Date; - - /** - * Sets whether or not this notification is relevant only to the current device. - * Some notifications can be bridged to other devices for remote display, such as - * a Wear OS watch. This hint can be set to recommend this notification not be bridged. - * See [Wear OS guides](https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging) - */ - localOnly?: boolean; - - /** - * Sets the relative priority for this notification. Low-priority notifications - * may be hidden from the user in certain situations. Note this priority differs - * from `AndroidMessagePriority`. This priority is processed by the client after - * the message has been delivered. Whereas `AndroidMessagePriority` is an FCM concept - * that controls when the message is delivered. - */ - priority?: ('min' | 'low' | 'default' | 'high' | 'max'); - - /** - * Sets the vibration pattern to use. Pass in an array of milliseconds to - * turn the vibrator on or off. The first value indicates the duration to wait before - * turning the vibrator on. The next value indicates the duration to keep the - * vibrator on. Subsequent values alternate between duration to turn the vibrator - * off and to turn the vibrator on. If `vibrate_timings` is set and `default_vibrate_timings` - * is set to `true`, the default value is used instead of the user-specified `vibrate_timings`. - */ - vibrateTimingsMillis?: number[]; - - /** - * If set to `true`, use the Android framework's default vibrate pattern for the - * notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). - * If `default_vibrate_timings` is set to `true` and `vibrate_timings` is also set, - * the default value is used instead of the user-specified `vibrate_timings`. - */ - defaultVibrateTimings?: boolean; - - /** - * If set to `true`, use the Android framework's default sound for the notification. - * Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). - */ - defaultSound?: boolean; - - /** - * Settings to control the notification's LED blinking rate and color if LED is - * available on the device. The total blinking time is controlled by the OS. - */ - lightSettings?: LightSettings; - - /** - * If set to `true`, use the Android framework's default LED light settings - * for the notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). - * If `default_light_settings` is set to `true` and `light_settings` is also set, - * the user-specified `light_settings` is used instead of the default value. - */ - defaultLightSettings?: boolean; - - /** - * Sets the visibility of the notification. Must be either `private`, `public`, - * or `secret`. If unspecified, defaults to `private`. - */ - visibility?: ('private' | 'public' | 'secret'); - - /** - * Sets the number of items this notification represents. May be displayed as a - * badge count for Launchers that support badging. See [`NotificationBadge`(https://developer.android.com/training/notify-user/badges). - * For example, this might be useful if you're using just one notification to - * represent multiple new messages but you want the count here to represent - * the number of total new messages. If zero or unspecified, systems - * that support badging use the default, which is to increment a number - * displayed on the long-press menu each time a new notification arrives. - */ - notificationCount?: number; - } - - /** - * Represents settings to control notification LED that can be included in - * {@link admin.messaging.AndroidNotification}. - */ - interface LightSettings { - /** - * Required. Sets color of the LED in `#rrggbb` or `#rrggbbaa` format. - */ - color: string; - - /** - * Required. Along with `light_off_duration`, defines the blink rate of LED flashes. - */ - lightOnDurationMillis: number; - - /** - * Required. Along with `light_on_duration`, defines the blink rate of LED flashes. - */ - lightOffDurationMillis: number; - } - - /** - * Represents options for features provided by the FCM SDK for Android. - */ - interface AndroidFcmOptions { - - /** - * The label associated with the message's analytics data. - */ - analyticsLabel?: string; - } - - /** - * Represents the APNs-specific options that can be included in an - * {@link admin.messaging.Message}. Refer to - * [Apple documentation](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html) - * for various headers and payload fields supported by APNs. - */ - interface ApnsConfig { - - /** - * A collection of APNs headers. Header values must be strings. - */ - headers?: { [key: string]: string }; - - /** - * An APNs payload to be included in the message. - */ - payload?: ApnsPayload; - - /** - * Options for features provided by the FCM SDK for iOS. - */ - fcmOptions?: ApnsFcmOptions; - } - /** - * Represents the payload of an APNs message. Mainly consists of the `aps` - * dictionary. But may also contain other arbitrary custom keys. - */ - interface ApnsPayload { - - /** - * The `aps` dictionary to be included in the message. - */ - aps: Aps; - [customData: string]: object; - } - /** - * Represents the [aps dictionary](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) - * that is part of APNs messages. - */ - interface Aps { - - /** - * Alert to be included in the message. This may be a string or an object of - * type `admin.messaging.ApsAlert`. - */ - alert?: string | ApsAlert; - - /** - * Badge to be displayed with the message. Set to 0 to remove the badge. When - * not specified, the badge will remain unchanged. - */ - badge?: number; - - /** - * Sound to be played with the message. - */ - sound?: string | CriticalSound; - - /** - * Specifies whether to configure a background update notification. - */ - contentAvailable?: boolean; - - /** - * Specifies whether to set the `mutable-content` property on the message - * so the clients can modify the notification via app extensions. - */ - mutableContent?: boolean; - - /** - * Type of the notification. - */ - category?: string; - - /** - * An app-specific identifier for grouping notifications. - */ - threadId?: string; - [customData: string]: any; - } - - interface ApsAlert { - title?: string; - subtitle?: string; - body?: string; - locKey?: string; - locArgs?: string[]; - titleLocKey?: string; - titleLocArgs?: string[]; - subtitleLocKey?: string; - subtitleLocArgs?: string[]; - actionLocKey?: string; - launchImage?: string; - } - - /** - * Represents a critical sound configuration that can be included in the - * `aps` dictionary of an APNs payload. - */ - interface CriticalSound { - - /** - * The critical alert flag. Set to `true` to enable the critical alert. - */ - critical?: boolean; - - /** - * The name of a sound file in the app's main bundle or in the `Library/Sounds` - * folder of the app's container directory. Specify the string "default" to play - * the system sound. - */ - name: string; - - /** - * The volume for the critical alert's sound. Must be a value between 0.0 - * (silent) and 1.0 (full volume). - */ - volume?: number; - } - - /** - * Represents options for features provided by the FCM SDK for iOS. - */ - interface ApnsFcmOptions { - - /** - * The label associated with the message's analytics data. - */ - analyticsLabel?: string; - - /** - * URL of an image to be displayed in the notification. - */ - imageUrl?: string; - } - - /** - * Represents platform-independent options for features provided by the FCM SDKs. - */ - interface FcmOptions { - - /** - * The label associated with the message's analytics data. - */ - analyticsLabel?: string; - } - - - /** - * A notification that can be included in {@link admin.messaging.Message}. - */ - interface Notification { - /** - * The title of the notification. - */ - title?: string; - /** - * The notification body - */ - body?: string; - /** - * URL of an image to be displayed in the notification. - */ - imageUrl?: string; - } - /** - * Represents the WebPush protocol options that can be included in an - * {@link admin.messaging.Message}. - */ - interface WebpushConfig { - - /** - * A collection of WebPush headers. Header values must be strings. - * - * See [WebPush specification](https://tools.ietf.org/html/rfc8030#section-5) - * for supported headers. - */ - headers?: { [key: string]: string }; - - /** - * A collection of data fields. - */ - data?: { [key: string]: string }; - - /** - * A WebPush notification payload to be included in the message. - */ - notification?: WebpushNotification; - - /** - * Options for features provided by the FCM SDK for Web. - */ - fcmOptions?: WebpushFcmOptions; - } - - /** Represents options for features provided by the FCM SDK for Web - * (which are not part of the Webpush standard). - */ - interface WebpushFcmOptions { - - /** - * The link to open when the user clicks on the notification. - * For all URL values, HTTPS is required. - */ - link?: string; - } - - /** - * Represents the WebPush-specific notification options that can be included in - * {@link admin.messaging.WebpushConfig}. This supports most of the standard - * options as defined in the Web Notification - * [specification](https://developer.mozilla.org/en-US/docs/Web/API/notification/Notification). - */ - interface WebpushNotification { - - /** - * Title text of the notification. - */ - title?: string; - - /** - * An array of notification actions representing the actions - * available to the user when the notification is presented. - */ - actions?: Array<{ - - /** - * An action available to the user when the notification is presented - */ - action: string; - - /** - * Optional icon for a notification action. - */ - icon?: string; - - /** - * Title of the notification action. - */ - title: string; - }>; - - /** - * URL of the image used to represent the notification when there is - * not enough space to display the notification itself. - */ - badge?: string; - - /** - * Body text of the notification. - */ - body?: string; - - /** - * Arbitrary data that you want associated with the notification. - * This can be of any data type. - */ - data?: any; - - /** - * The direction in which to display the notification. Must be one - * of `auto`, `ltr` or `rtl`. - */ - dir?: 'auto' | 'ltr' | 'rtl'; - - /** - * URL to the notification icon. - */ - icon?: string; - - /** - * URL of an image to be displayed in the notification. - */ - image?: string; - - /** - * The notification's language as a BCP 47 language tag. - */ - lang?: string; - - /** - * A boolean specifying whether the user should be notified after a - * new notification replaces an old one. Defaults to false. - */ - renotify?: boolean; - - /** - * Indicates that a notification should remain active until the user - * clicks or dismisses it, rather than closing automatically. - * Defaults to false. - */ - requireInteraction?: boolean; - - /** - * A boolean specifying whether the notification should be silent. - * Defaults to false. - */ - silent?: boolean; - - /** - * An identifying tag for the notification. - */ - tag?: string; - - /** - * Timestamp of the notification. Refer to - * https://developer.mozilla.org/en-US/docs/Web/API/notification/timestamp - * for details. - */ - timestamp?: number; - - /** - * A vibration pattern for the device's vibration hardware to emit - * when the notification fires. - */ - vibrate?: number | number[]; - [key: string]: any; - } - /** - * Interface representing an FCM legacy API data message payload. Data - * messages let developers send up to 4KB of custom key-value pairs. The - * keys and values must both be strings. Keys can be any custom string, - * except for the following reserved strings: - * - * * `"from"` - * * Anything starting with `"google."`. - * - * See [Build send requests](/docs/cloud-messaging/send-message) - * for code samples and detailed documentation. - */ - interface DataMessagePayload { - [key: string]: string; - } - - /** - * Interface representing an FCM legacy API notification message payload. - * Notification messages let developers send up to 4KB of predefined - * key-value pairs. Accepted keys are outlined below. - * - * See [Build send requests](/docs/cloud-messaging/send-message) - * for code samples and detailed documentation. - */ - interface NotificationMessagePayload { - - /** - * Identifier used to replace existing notifications in the notification drawer. - * - * If not specified, each request creates a new notification. - * - * If specified and a notification with the same tag is already being shown, - * the new notification replaces the existing one in the notification drawer. - * - * **Platforms:** Android - */ - tag?: string; - - /** - * The notification's body text. - * - * **Platforms:** iOS, Android, Web - */ - body?: string; - - /** - * The notification's icon. - * - * **Android:** Sets the notification icon to `myicon` for drawable resource - * `myicon`. If you don't send this key in the request, FCM displays the - * launcher icon specified in your app manifest. - * - * **Web:** The URL to use for the notification's icon. - * - * **Platforms:** Android, Web - */ - icon?: string; - - /** - * The value of the badge on the home screen app icon. - * - * If not specified, the badge is not changed. - * - * If set to `0`, the badge is removed. - * - * **Platforms:** iOS - */ - badge?: string; - - /** - * The notification icon's color, expressed in `#rrggbb` format. - * - * **Platforms:** Android - */ - color?: string; - - /** - * The sound to be played when the device receives a notification. Supports - * "default" for the default notification sound of the device or the filename of a - * sound resource bundled in the app. - * Sound files must reside in `/res/raw/`. - * - * **Platforms:** Android - */ - sound?: string; - - /** - * The notification's title. - * - * **Platforms:** iOS, Android, Web - */ - title?: string; - - /** - * The key to the body string in the app's string resources to use to localize - * the body text to the user's current localization. - * - * **iOS:** Corresponds to `loc-key` in the APNs payload. See - * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) - * and - * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) - * for more information. - * - * **Android:** See - * [String Resources](http://developer.android.com/guide/topics/resources/string-resource.html) * for more information. - * - * **Platforms:** iOS, Android - */ - bodyLocKey?: string; - - /** - * Variable string values to be used in place of the format specifiers in - * `body_loc_key` to use to localize the body text to the user's current - * localization. - * - * The value should be a stringified JSON array. - * - * **iOS:** Corresponds to `loc-args` in the APNs payload. See - * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) - * and - * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) - * for more information. - * - * **Android:** See - * [Formatting and Styling](http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling) - * for more information. - * - * **Platforms:** iOS, Android - */ - bodyLocArgs?: string; - - /** - * Action associated with a user click on the notification. If specified, an - * activity with a matching Intent Filter is launched when a user clicks on the - * notification. - * - * * **Platforms:** Android - */ - clickAction?: string; - - /** - * The key to the title string in the app's string resources to use to localize - * the title text to the user's current localization. - * - * **iOS:** Corresponds to `title-loc-key` in the APNs payload. See - * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) - * and - * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) - * for more information. - * - * **Android:** See - * [String Resources](http://developer.android.com/guide/topics/resources/string-resource.html) - * for more information. - * - * **Platforms:** iOS, Android - */ - titleLocKey?: string; - - /** - * Variable string values to be used in place of the format specifiers in - * `title_loc_key` to use to localize the title text to the user's current - * localization. - * - * The value should be a stringified JSON array. - * - * **iOS:** Corresponds to `title-loc-args` in the APNs payload. See - * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) - * and - * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) - * for more information. - * - * **Android:** See - * [Formatting and Styling](http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling) - * for more information. - * - * **Platforms:** iOS, Android - */ - titleLocArgs?: string; - [key: string]: string | undefined; - } - - /** - * Interface representing a Firebase Cloud Messaging message payload. One or - * both of the `data` and `notification` keys are required. - * - * See - * [Build send requests](/docs/cloud-messaging/send-message) - * for code samples and detailed documentation. - */ - interface MessagingPayload { - - /** - * The data message payload. - */ - data?: admin.messaging.DataMessagePayload; - - /** - * The notification message payload. - */ - notification?: admin.messaging.NotificationMessagePayload; - } - /** - * Interface representing the options that can be provided when sending a - * message via the FCM legacy APIs. - * - * See [Build send requests](/docs/cloud-messaging/send-message) - * for code samples and detailed documentation. - */ - interface MessagingOptions { - - /** - * Whether or not the message should actually be sent. When set to `true`, - * allows developers to test a request without actually sending a message. When - * set to `false`, the message will be sent. - * - * **Default value:** `false` - */ - dryRun?: boolean; - - /** - * The priority of the message. Valid values are `"normal"` and `"high".` On - * iOS, these correspond to APNs priorities `5` and `10`. - * - * By default, notification messages are sent with high priority, and data - * messages are sent with normal priority. Normal priority optimizes the client - * app's battery consumption and should be used unless immediate delivery is - * required. For messages with normal priority, the app may receive the message - * with unspecified delay. - * - * When a message is sent with high priority, it is sent immediately, and the - * app can wake a sleeping device and open a network connection to your server. - * - * For more information, see - * [Setting the priority of a message](/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message). - * - * **Default value:** `"high"` for notification messages, `"normal"` for data - * messages - */ - priority?: string; - - /** - * How long (in seconds) the message should be kept in FCM storage if the device - * is offline. The maximum time to live supported is four weeks, and the default - * value is also four weeks. For more information, see - * [Setting the lifespan of a message](/docs/cloud-messaging/concept-options#ttl). - * - * **Default value:** `2419200` (representing four weeks, in seconds) - */ - timeToLive?: number; - - /** - * String identifying a group of messages (for example, "Updates Available") - * that can be collapsed, so that only the last message gets sent when delivery - * can be resumed. This is used to avoid sending too many of the same messages - * when the device comes back online or becomes active. - * - * There is no guarantee of the order in which messages get sent. - * - * A maximum of four different collapse keys is allowed at any given time. This - * means FCM server can simultaneously store four different - * send-to-sync messages per client app. If you exceed this number, there is no - * guarantee which four collapse keys the FCM server will keep. - * - * **Default value:** None - */ - collapseKey?: string; - - /** - * On iOS, use this field to represent `mutable-content` in the APNs payload. - * When a notification is sent and this is set to `true`, the content of the - * notification can be modified before it is displayed, using a - * [Notification Service app extension](https://developer.apple.com/reference/usernotifications/unnotificationserviceextension) - * - * On Android and Web, this parameter will be ignored. - * - * **Default value:** `false` - */ - mutableContent?: boolean; - - /** - * On iOS, use this field to represent `content-available` in the APNs payload. - * When a notification or data message is sent and this is set to `true`, an - * inactive client app is awoken. On Android, data messages wake the app by - * default. On Chrome, this flag is currently not supported. - * - * **Default value:** `false` - */ - contentAvailable?: boolean; - - /** - * The package name of the application which the registration tokens must match - * in order to receive the message. - * - * **Default value:** None - */ - restrictedPackageName?: string; - [key: string]: any | undefined; - } - - /** - * Interface representing the status of a message sent to an individual device - * via the FCM legacy APIs. - * - * See - * [Send to individual devices](/docs/cloud-messaging/admin/send-messages#send_to_individual_devices) - * for code samples and detailed documentation. - */ - interface MessagingDeviceResult { - - /** - * The error that occurred when processing the message for the recipient. - */ - error?: _admin.FirebaseError; - - /** - * A unique ID for the successfully processed message. - */ - messageId?: string; - - /** - * The canonical registration token for the client app that the message was - * processed and sent to. You should use this value as the registration token - * for future requests. Otherwise, future messages might be rejected. - */ - canonicalRegistrationToken?: string; - } - - /** - * Interface representing the server response from the legacy - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDevice `sendToDevice()`} method. - * - * See - * [Send to individual devices](/docs/cloud-messaging/admin/send-messages#send_to_individual_devices) - * for code samples and detailed documentation. - */ - interface MessagingDevicesResponse { - - /** - * The number of results that contain a canonical registration token. A - * canonical registration token is the registration token corresponding to the - * last registration requested by the client app. This is the token that you - * should use when sending future messages to the device. - * - * You can access the canonical registration tokens within the - * [`results`](#results) property. - */ - canonicalRegistrationTokenCount: number; - - /** - * The number of messages that could not be processed and resulted in an error. - */ - failureCount: number; - - /** - * The unique ID number identifying this multicast message. - */ - multicastId: number; - - /** - * An array of `MessagingDeviceResult` objects representing the status of the - * processed messages. The objects are listed in the same order as in the - * request. That is, for each registration token in the request, its result has - * the same index in this array. If only a single registration token is - * provided, this array will contain a single object. - */ - results: admin.messaging.MessagingDeviceResult[]; - - /** - * The number of messages that were successfully processed and sent. - */ - successCount: number; - } - /** - * Interface representing the server response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDeviceGroup `sendToDeviceGroup()`} - * method. - * - * See - * [Send messages to device groups](/docs/cloud-messaging/send-message?authuser=0#send_messages_to_device_groups) - * for code samples and detailed documentation. - */ - interface MessagingDeviceGroupResponse { - - /** - * The number of messages that could not be processed and resulted in an error. - */ - successCount: number; - - /** - * The number of messages that could not be processed and resulted in an error. - */ - failureCount: number; - - /** - * An array of registration tokens that failed to receive the message. - */ - failedRegistrationTokens: string[]; - } - - /** - * Interface representing the server response from the legacy - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToTopic `sendToTopic()`} method. - * - * See - * [Send to a topic](/docs/cloud-messaging/admin/send-messages#send_to_a_topic) - * for code samples and detailed documentation. - */ - interface MessagingTopicResponse { - - /** - * The message ID for a successfully received request which FCM will attempt to - * deliver to all subscribed devices. - */ - messageId: number; - } - - /** - * Interface representing the server response from the legacy - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToCondition `sendToCondition()`} method. - * - * See - * [Send to a condition](/docs/cloud-messaging/admin/send-messages#send_to_a_condition) - * for code samples and detailed documentation. - */ - interface MessagingConditionResponse { - - /** - * The message ID for a successfully received request which FCM will attempt to - * deliver to all subscribed devices. - */ - messageId: number; - } - - /** - * Interface representing the server response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#subscribeToTopic `subscribeToTopic()`} and - * {@link - * admin.messaging.Messaging#unsubscribeFromTopic - * `unsubscribeFromTopic()`} - * methods. - * - * See - * [Manage topics from the server](/docs/cloud-messaging/manage-topics) - * for code samples and detailed documentation. - */ - interface MessagingTopicManagementResponse { - - /** - * The number of registration tokens that could not be subscribed to the topic - * and resulted in an error. - */ - failureCount: number; - - /** - * The number of registration tokens that were successfully subscribed to the - * topic. - */ - successCount: number; - - /** - * An array of errors corresponding to the provided registration token(s). The - * length of this array will be equal to [`failureCount`](#failureCount). - */ - errors: _admin.FirebaseArrayIndexError[]; - } - - /** - * Interface representing the server response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendAll `sendAll()`} and - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendMulticast `sendMulticast()`} methods. - */ - interface BatchResponse { - - /** - * An array of responses, each corresponding to a message. - */ - responses: admin.messaging.SendResponse[]; - - /** - * The number of messages that were successfully handed off for sending. - */ - successCount: number; - - /** - * The number of messages that resulted in errors when sending. - */ - failureCount: number; - } - /** - * Interface representing the status of an individual message that was sent as - * part of a batch request. - */ - interface SendResponse { - - /** - * A boolean indicating if the message was successfully handed off to FCM or - * not. When true, the `messageId` attribute is guaranteed to be set. When - * false, the `error` attribute is guaranteed to be set. - */ - success: boolean; - - /** - * A unique message ID string, if the message was handed off to FCM for - * delivery. - * - */ - messageId?: string; - - /** - * An error, if the message was not handed off to FCM successfully. - */ - error?: _admin.FirebaseError; - } - - /** - * Gets the {@link admin.messaging.Messaging `Messaging`} service for the - * current app. - * - * @example - * ```javascript - * var messaging = app.messaging(); - * // The above is shorthand for: - * // var messaging = admin.messaging(app); - * ``` - * - * @return The `Messaging` service for the current app. - */ - interface Messaging { - /** - * The {@link admin.app.App app} associated with the current `Messaging` service - * instance. - * - * @example - * ```javascript - * var app = messaging.app; - * ``` - */ - app: _admin.app.App; - - /** - * Sends the given message via FCM. - * - * @param message The message payload. - * @param dryRun Whether to send the message in the dry-run - * (validation only) mode. - * @return A promise fulfilled with a unique message ID - * string after the message has been successfully handed off to the FCM - * service for delivery. - */ - send(message: admin.messaging.Message, dryRun?: boolean): Promise; - - /** - * Sends all the messages in the given array via Firebase Cloud Messaging. - * Employs batching to send the entire list as a single RPC call. Compared - * to the `send()` method, this method is a significantly more efficient way - * to send multiple messages. - * - * The responses list obtained from the return value - * corresponds to the order of tokens in the `MulticastMessage`. An error - * from this method indicates a total failure -- i.e. none of the messages in - * the list could be sent. Partial failures are indicated by a `BatchResponse` - * return value. - * - * @param messages A non-empty array - * containing up to 500 messages. - * @param dryRun Whether to send the messages in the dry-run - * (validation only) mode. - * @return A Promise fulfilled with an object representing the result of the - * send operation. - */ - sendAll( - messages: Array, - dryRun?: boolean - ): Promise; - - /** - * Sends the given multicast message to all the FCM registration tokens - * specified in it. - * - * This method uses the `sendAll()` API under the hood to send the given - * message to all the target recipients. The responses list obtained from the - * return value corresponds to the order of tokens in the `MulticastMessage`. - * An error from this method indicates a total failure -- i.e. the message was - * not sent to any of the tokens in the list. Partial failures are indicated by - * a `BatchResponse` return value. - * - * @param message A multicast message - * containing up to 500 tokens. - * @param dryRun Whether to send the message in the dry-run - * (validation only) mode. - * @return A Promise fulfilled with an object representing the result of the - * send operation. - */ - sendMulticast( - message: admin.messaging.MulticastMessage, - dryRun?: boolean - ): Promise; - - /** - * Sends an FCM message to a single device corresponding to the provided - * registration token. - * - * See - * [Send to individual devices](/docs/cloud-messaging/admin/legacy-fcm#send_to_individual_devices) - * for code samples and detailed documentation. Takes either a - * `registrationToken` to send to a single device or a - * `registrationTokens` parameter containing an array of tokens to send - * to multiple devices. - * - * @param registrationToken A device registration token or an array of - * device registration tokens to which the message should be sent. - * @param payload The message payload. - * @param options Optional options to - * alter the message. - * - * @return A promise fulfilled with the server's response after the message - * has been sent. - */ - sendToDevice( - registrationToken: string | string[], - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; - - /** - * Sends an FCM message to a device group corresponding to the provided - * notification key. - * - * See - * [Send to a device group](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_device_group) - * for code samples and detailed documentation. - * - * @param notificationKey The notification key for the device group to - * which to send the message. - * @param payload The message payload. - * @param options Optional options to - * alter the message. - * - * @return A promise fulfilled with the server's response after the message - * has been sent. - */ - sendToDeviceGroup( - notificationKey: string, - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; - - /** - * Sends an FCM message to a topic. - * - * See - * [Send to a topic](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_topic) - * for code samples and detailed documentation. - * - * @param topic The topic to which to send the message. - * @param payload The message payload. - * @param options Optional options to - * alter the message. - * - * @return A promise fulfilled with the server's response after the message - * has been sent. - */ - sendToTopic( - topic: string, - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; - - /** - * Sends an FCM message to a condition. - * - * See - * [Send to a condition](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_condition) - * for code samples and detailed documentation. - * - * @param condition The condition determining to which topics to send - * the message. - * @param payload The message payload. - * @param options Optional options to - * alter the message. - * - * @return A promise fulfilled with the server's response after the message - * has been sent. - */ - sendToCondition( - condition: string, - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; - - /** - * Subscribes a device to an FCM topic. - * - * See [Subscribe to a - * topic](/docs/cloud-messaging/manage-topics#suscribe_and_unsubscribe_using_the) - * for code samples and detailed documentation. Optionally, you can provide an - * array of tokens to subscribe multiple devices. - * - * @param registrationTokens A token or array of registration tokens - * for the devices to subscribe to the topic. - * @param topic The topic to which to subscribe. - * - * @return A promise fulfilled with the server's response after the device has been - * subscribed to the topic. - */ - subscribeToTopic( - registrationTokens: string | string[], - topic: string - ): Promise; - - /** - * Unsubscribes a device from an FCM topic. - * - * See [Unsubscribe from a - * topic](/docs/cloud-messaging/admin/manage-topic-subscriptions#unsubscribe_from_a_topic) - * for code samples and detailed documentation. Optionally, you can provide an - * array of tokens to unsubscribe multiple devices. - * - * @param registrationTokens A device registration token or an array of - * device registration tokens to unsubscribe from the topic. - * @param topic The topic from which to unsubscribe. - * - * @return A promise fulfilled with the server's response after the device has been - * unsubscribed from the topic. - */ - unsubscribeFromTopic( - registrationTokens: string | string[], - topic: string - ): Promise; - } -} diff --git a/src/messaging/index.ts b/src/messaging/index.ts index e11697e4a1..8e9657ea47 100644 --- a/src/messaging/index.ts +++ b/src/messaging/index.ts @@ -14,64 +14,1330 @@ * limitations under the License. */ -import { FirebaseApp } from '../firebase-app'; -import * as messagingApi from './messaging'; -import * as messagingTypesApi from './messaging-types'; -import * as firebaseAdmin from '../index'; - -export function messaging(app?: FirebaseApp): messagingApi.Messaging { - if (typeof(app) === 'undefined') { - app = firebaseAdmin.app(); - } - return app.messaging(); -} +import { app, FirebaseError, FirebaseArrayIndexError } from '../firebase-namespace-api'; /** - * We must define a namespace to make the typings work correctly. Otherwise - * `admin.messaging()` cannot be called like a function. Temporarily, - * admin.messaging is used as the namespace name because we cannot barrel - * re-export the contents from messsaging, and we want it to - * match the namespacing in the re-export inside src/index.d.ts + * Gets the {@link messaging.Messaging `Messaging`} service for the + * default app or a given app. + * + * `admin.messaging()` can be called with no arguments to access the default + * app's {@link messaging.Messaging `Messaging`} service or as + * `admin.messaging(app)` to access the + * {@link messaging.Messaging `Messaging`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the Messaging service for the default app + * var defaultMessaging = admin.messaging(); + * ``` + * + * @example + * ```javascript + * // Get the Messaging service for a given app + * var otherMessaging = admin.messaging(otherApp); + * ``` + * + * @param app Optional app whose `Messaging` service to + * return. If not provided, the default `Messaging` service will be returned. + * + * @return The default `Messaging` service if no + * app is provided or the `Messaging` service associated with the provided + * app. */ +export declare function messaging(app?: app.App): messaging.Messaging; + /* eslint-disable @typescript-eslint/no-namespace */ -export namespace admin.messaging { - // See https://github.com/microsoft/TypeScript/issues/4336 - /* eslint-disable @typescript-eslint/no-unused-vars */ - // See https://github.com/typescript-eslint/typescript-eslint/issues/363 - export import AndroidConfig = messagingTypesApi.AndroidConfig; - export import AndroidFcmOptions = messagingTypesApi.AndroidFcmOptions; - export import AndroidNotification = messagingTypesApi.AndroidNotification; - export import ApnsConfig = messagingTypesApi.ApnsConfig; - export import ApnsFcmOptions = messagingTypesApi.ApnsFcmOptions; - export import ApnsPayload = messagingTypesApi.ApnsPayload; - export import Aps = messagingTypesApi.Aps; - export import ApsAlert = messagingTypesApi.ApsAlert; - export import BatchResponse = messagingTypesApi.BatchResponse; - export import CriticalSound = messagingTypesApi.CriticalSound; - export import FcmOptions = messagingTypesApi.FcmOptions; - export import LightSettings = messagingTypesApi.LightSettings; - export import Message = messagingTypesApi.Message; - export import MessagingTopicManagementResponse = messagingTypesApi.MessagingTopicManagementResponse; - export import MulticastMessage = messagingTypesApi.MulticastMessage; - export import Notification = messagingTypesApi.Notification; - export import SendResponse = messagingTypesApi.SendResponse; - export import WebpushConfig = messagingTypesApi.WebpushConfig; - export import WebpushFcmOptions = messagingTypesApi.WebpushFcmOptions; - export import WebpushNotification = messagingTypesApi.WebpushNotification; - - // See https://github.com/microsoft/TypeScript/issues/4336 - // Allows for exposing classes as interfaces in typings - /* eslint-disable @typescript-eslint/no-empty-interface */ - export interface Messaging extends messagingApi.Messaging {} - - // Legacy API types. - export import DataMessagePayload = messagingTypesApi.DataMessagePayload; - export import MessagingConditionResponse = messagingTypesApi.MessagingConditionResponse; - export import MessagingDeviceGroupResponse = messagingTypesApi.MessagingDeviceGroupResponse; - export import MessagingDevicesResponse = messagingTypesApi.MessagingDevicesResponse; - export import MessagingDeviceResult = messagingTypesApi.MessagingDeviceResult; - export import MessagingOptions = messagingTypesApi.MessagingOptions; - export import MessagingPayload = messagingTypesApi.MessagingPayload; - export import MessagingTopicResponse = messagingTypesApi.MessagingTopicResponse; - export import NotificationMessagePayload = messagingTypesApi.NotificationMessagePayload; +export namespace messaging { + interface BaseMessage { + data?: { [key: string]: string }; + notification?: Notification; + android?: AndroidConfig; + webpush?: WebpushConfig; + apns?: ApnsConfig; + fcmOptions?: FcmOptions; + } + + interface TokenMessage extends BaseMessage { + token: string; + } + + interface TopicMessage extends BaseMessage { + topic: string; + } + + interface ConditionMessage extends BaseMessage { + condition: string; + } + + /** + * Payload for the admin.messaging.send() operation. The payload contains all the fields + * in the BaseMessage type, and exactly one of token, topic or condition. + */ + export type Message = TokenMessage | TopicMessage | ConditionMessage; + + /** + * Payload for the admin.messaing.sendMulticase() method. The payload contains all the fields + * in the BaseMessage type, and a list of tokens. + */ + export interface MulticastMessage extends BaseMessage { + tokens: string[]; + } + + /** + * A notification that can be included in {@link messaging.Message}. + */ + export interface Notification { + /** + * The title of the notification. + */ + title?: string; + /** + * The notification body + */ + body?: string; + /** + * URL of an image to be displayed in the notification. + */ + imageUrl?: string; + } + + /** + * Represents platform-independent options for features provided by the FCM SDKs. + */ + export interface FcmOptions { + /** + * The label associated with the message's analytics data. + */ + analyticsLabel?: string; + } + + /** + * Represents the WebPush protocol options that can be included in an + * {@link messaging.Message}. + */ + export interface WebpushConfig { + + /** + * A collection of WebPush headers. Header values must be strings. + * + * See [WebPush specification](https://tools.ietf.org/html/rfc8030#section-5) + * for supported headers. + */ + headers?: { [key: string]: string }; + + /** + * A collection of data fields. + */ + data?: { [key: string]: string }; + + /** + * A WebPush notification payload to be included in the message. + */ + notification?: WebpushNotification; + + /** + * Options for features provided by the FCM SDK for Web. + */ + fcmOptions?: WebpushFcmOptions; + } + + /** Represents options for features provided by the FCM SDK for Web + * (which are not part of the Webpush standard). + */ + export interface WebpushFcmOptions { + + /** + * The link to open when the user clicks on the notification. + * For all URL values, HTTPS is required. + */ + link?: string; + } + + /** + * Represents the WebPush-specific notification options that can be included in + * {@link messaging.WebpushConfig}. This supports most of the standard + * options as defined in the Web Notification + * [specification](https://developer.mozilla.org/en-US/docs/Web/API/notification/Notification). + */ + export interface WebpushNotification { + + /** + * Title text of the notification. + */ + title?: string; + + /** + * An array of notification actions representing the actions + * available to the user when the notification is presented. + */ + actions?: Array<{ + + /** + * An action available to the user when the notification is presented + */ + action: string; + + /** + * Optional icon for a notification action. + */ + icon?: string; + + /** + * Title of the notification action. + */ + title: string; + }>; + + /** + * URL of the image used to represent the notification when there is + * not enough space to display the notification itself. + */ + badge?: string; + + /** + * Body text of the notification. + */ + body?: string; + + /** + * Arbitrary data that you want associated with the notification. + * This can be of any data type. + */ + data?: any; + + /** + * The direction in which to display the notification. Must be one + * of `auto`, `ltr` or `rtl`. + */ + dir?: 'auto' | 'ltr' | 'rtl'; + + /** + * URL to the notification icon. + */ + icon?: string; + + /** + * URL of an image to be displayed in the notification. + */ + image?: string; + + /** + * The notification's language as a BCP 47 language tag. + */ + lang?: string; + + /** + * A boolean specifying whether the user should be notified after a + * new notification replaces an old one. Defaults to false. + */ + renotify?: boolean; + + /** + * Indicates that a notification should remain active until the user + * clicks or dismisses it, rather than closing automatically. + * Defaults to false. + */ + requireInteraction?: boolean; + + /** + * A boolean specifying whether the notification should be silent. + * Defaults to false. + */ + silent?: boolean; + + /** + * An identifying tag for the notification. + */ + tag?: string; + + /** + * Timestamp of the notification. Refer to + * https://developer.mozilla.org/en-US/docs/Web/API/notification/timestamp + * for details. + */ + timestamp?: number; + + /** + * A vibration pattern for the device's vibration hardware to emit + * when the notification fires. + */ + vibrate?: number | number[]; + [key: string]: any; + } + + /** + * Represents the APNs-specific options that can be included in an + * {@link messaging.Message}. Refer to + * [Apple documentation](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html) + * for various headers and payload fields supported by APNs. + */ + export interface ApnsConfig { + /** + * A collection of APNs headers. Header values must be strings. + */ + headers?: { [key: string]: string }; + + /** + * An APNs payload to be included in the message. + */ + payload?: ApnsPayload; + + /** + * Options for features provided by the FCM SDK for iOS. + */ + fcmOptions?: ApnsFcmOptions; + } + + /** + * Represents the payload of an APNs message. Mainly consists of the `aps` + * dictionary. But may also contain other arbitrary custom keys. + */ + export interface ApnsPayload { + + /** + * The `aps` dictionary to be included in the message. + */ + aps: Aps; + [customData: string]: object; + } + + /** + * Represents the [aps dictionary](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) + * that is part of APNs messages. + */ + export interface Aps { + + /** + * Alert to be included in the message. This may be a string or an object of + * type `admin.messaging.ApsAlert`. + */ + alert?: string | ApsAlert; + + /** + * Badge to be displayed with the message. Set to 0 to remove the badge. When + * not specified, the badge will remain unchanged. + */ + badge?: number; + + /** + * Sound to be played with the message. + */ + sound?: string | CriticalSound; + + /** + * Specifies whether to configure a background update notification. + */ + contentAvailable?: boolean; + + /** + * Specifies whether to set the `mutable-content` property on the message + * so the clients can modify the notification via app extensions. + */ + mutableContent?: boolean; + + /** + * Type of the notification. + */ + category?: string; + + /** + * An app-specific identifier for grouping notifications. + */ + threadId?: string; + [customData: string]: any; + } + + export interface ApsAlert { + title?: string; + subtitle?: string; + body?: string; + locKey?: string; + locArgs?: string[]; + titleLocKey?: string; + titleLocArgs?: string[]; + subtitleLocKey?: string; + subtitleLocArgs?: string[]; + actionLocKey?: string; + launchImage?: string; + } + + /** + * Represents a critical sound configuration that can be included in the + * `aps` dictionary of an APNs payload. + */ + export interface CriticalSound { + + /** + * The critical alert flag. Set to `true` to enable the critical alert. + */ + critical?: boolean; + + /** + * The name of a sound file in the app's main bundle or in the `Library/Sounds` + * folder of the app's container directory. Specify the string "default" to play + * the system sound. + */ + name: string; + + /** + * The volume for the critical alert's sound. Must be a value between 0.0 + * (silent) and 1.0 (full volume). + */ + volume?: number; + } + + /** + * Represents options for features provided by the FCM SDK for iOS. + */ + export interface ApnsFcmOptions { + + /** + * The label associated with the message's analytics data. + */ + analyticsLabel?: string; + + /** + * URL of an image to be displayed in the notification. + */ + imageUrl?: string; + } + + + /** + * Represents the Android-specific options that can be included in an + * {@link messaging.Message}. + */ + export interface AndroidConfig { + + /** + * Collapse key for the message. Collapse key serves as an identifier for a + * group of messages that can be collapsed, so that only the last message gets + * sent when delivery can be resumed. A maximum of four different collapse keys + * may be active at any given time. + */ + collapseKey?: string; + + /** + * Priority of the message. Must be either `normal` or `high`. + */ + priority?: ('high' | 'normal'); + + /** + * Time-to-live duration of the message in milliseconds. + */ + ttl?: number; + + /** + * Package name of the application where the registration tokens must match + * in order to receive the message. + */ + restrictedPackageName?: string; + + /** + * A collection of data fields to be included in the message. All values must + * be strings. When provided, overrides any data fields set on the top-level + * `admin.messaging.Message`.} + */ + data?: { [key: string]: string }; + + /** + * Android notification to be included in the message. + */ + notification?: AndroidNotification; + + /** + * Options for features provided by the FCM SDK for Android. + */ + fcmOptions?: AndroidFcmOptions; + } + + /** + * Represents the Android-specific notification options that can be included in + * {@link messaging.AndroidConfig}. + */ + export interface AndroidNotification { + /** + * Title of the Android notification. When provided, overrides the title set via + * `admin.messaging.Notification`. + */ + title?: string; + + /** + * Body of the Android notification. When provided, overrides the body set via + * `admin.messaging.Notification`. + */ + body?: string; + + /** + * Icon resource for the Android notification. + */ + icon?: string; + + /** + * Notification icon color in `#rrggbb` format. + */ + color?: string; + + /** + * File name of the sound to be played when the device receives the + * notification. + */ + sound?: string; + + /** + * Notification tag. This is an identifier used to replace existing + * notifications in the notification drawer. If not specified, each request + * creates a new notification. + */ + tag?: string; + + /** + * URL of an image to be displayed in the notification. + */ + imageUrl?: string; + + /** + * Action associated with a user click on the notification. If specified, an + * activity with a matching Intent Filter is launched when a user clicks on the + * notification. + */ + clickAction?: string; + + /** + * Key of the body string in the app's string resource to use to localize the + * body text. + * + */ + bodyLocKey?: string; + + /** + * An array of resource keys that will be used in place of the format + * specifiers in `bodyLocKey`. + */ + bodyLocArgs?: string[]; + + /** + * Key of the title string in the app's string resource to use to localize the + * title text. + */ + titleLocKey?: string; + + /** + * An array of resource keys that will be used in place of the format + * specifiers in `titleLocKey`. + */ + titleLocArgs?: string[]; + + /** + * The Android notification channel ID (new in Android O). The app must create + * a channel with this channel ID before any notification with this channel ID + * can be received. If you don't send this channel ID in the request, or if the + * channel ID provided has not yet been created by the app, FCM uses the channel + * ID specified in the app manifest. + */ + channelId?: string; + + /** + * Sets the "ticker" text, which is sent to accessibility services. Prior to + * API level 21 (Lollipop), sets the text that is displayed in the status bar + * when the notification first arrives. + */ + ticker?: string; + + /** + * When set to `false` or unset, the notification is automatically dismissed when + * the user clicks it in the panel. When set to `true`, the notification persists + * even when the user clicks it. + */ + sticky?: boolean; + + /** + * For notifications that inform users about events with an absolute time reference, sets + * the time that the event in the notification occurred. Notifications + * in the panel are sorted by this time. + */ + eventTimestamp?: Date; + + /** + * Sets whether or not this notification is relevant only to the current device. + * Some notifications can be bridged to other devices for remote display, such as + * a Wear OS watch. This hint can be set to recommend this notification not be bridged. + * See [Wear OS guides](https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging) + */ + localOnly?: boolean; + + /** + * Sets the relative priority for this notification. Low-priority notifications + * may be hidden from the user in certain situations. Note this priority differs + * from `AndroidMessagePriority`. This priority is processed by the client after + * the message has been delivered. Whereas `AndroidMessagePriority` is an FCM concept + * that controls when the message is delivered. + */ + priority?: ('min' | 'low' | 'default' | 'high' | 'max'); + + /** + * Sets the vibration pattern to use. Pass in an array of milliseconds to + * turn the vibrator on or off. The first value indicates the duration to wait before + * turning the vibrator on. The next value indicates the duration to keep the + * vibrator on. Subsequent values alternate between duration to turn the vibrator + * off and to turn the vibrator on. If `vibrate_timings` is set and `default_vibrate_timings` + * is set to `true`, the default value is used instead of the user-specified `vibrate_timings`. + */ + vibrateTimingsMillis?: number[]; + + /** + * If set to `true`, use the Android framework's default vibrate pattern for the + * notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * If `default_vibrate_timings` is set to `true` and `vibrate_timings` is also set, + * the default value is used instead of the user-specified `vibrate_timings`. + */ + defaultVibrateTimings?: boolean; + + /** + * If set to `true`, use the Android framework's default sound for the notification. + * Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + */ + defaultSound?: boolean; + + /** + * Settings to control the notification's LED blinking rate and color if LED is + * available on the device. The total blinking time is controlled by the OS. + */ + lightSettings?: LightSettings; + + /** + * If set to `true`, use the Android framework's default LED light settings + * for the notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * If `default_light_settings` is set to `true` and `light_settings` is also set, + * the user-specified `light_settings` is used instead of the default value. + */ + defaultLightSettings?: boolean; + + /** + * Sets the visibility of the notification. Must be either `private`, `public`, + * or `secret`. If unspecified, defaults to `private`. + */ + visibility?: ('private' | 'public' | 'secret'); + + /** + * Sets the number of items this notification represents. May be displayed as a + * badge count for Launchers that support badging. See [`NotificationBadge`(https://developer.android.com/training/notify-user/badges). + * For example, this might be useful if you're using just one notification to + * represent multiple new messages but you want the count here to represent + * the number of total new messages. If zero or unspecified, systems + * that support badging use the default, which is to increment a number + * displayed on the long-press menu each time a new notification arrives. + */ + notificationCount?: number; + } + + /** + * Represents settings to control notification LED that can be included in + * {@link messaging.AndroidNotification}. + */ + export interface LightSettings { + /** + * Required. Sets color of the LED in `#rrggbb` or `#rrggbbaa` format. + */ + color: string; + + /** + * Required. Along with `light_off_duration`, defines the blink rate of LED flashes. + */ + lightOnDurationMillis: number; + + /** + * Required. Along with `light_on_duration`, defines the blink rate of LED flashes. + */ + lightOffDurationMillis: number; + } + + /** + * Represents options for features provided by the FCM SDK for Android. + */ + export interface AndroidFcmOptions { + + /** + * The label associated with the message's analytics data. + */ + analyticsLabel?: string; + } + + /** + * Interface representing an FCM legacy API data message payload. Data + * messages let developers send up to 4KB of custom key-value pairs. The + * keys and values must both be strings. Keys can be any custom string, + * except for the following reserved strings: + * + * * `"from"` + * * Anything starting with `"google."`. + * + * See [Build send requests](/docs/cloud-messaging/send-message) + * for code samples and detailed documentation. + */ + export interface DataMessagePayload { + [key: string]: string; + } + + /** + * Interface representing an FCM legacy API notification message payload. + * Notification messages let developers send up to 4KB of predefined + * key-value pairs. Accepted keys are outlined below. + * + * See [Build send requests](/docs/cloud-messaging/send-message) + * for code samples and detailed documentation. + */ + export interface NotificationMessagePayload { + + /** + * Identifier used to replace existing notifications in the notification drawer. + * + * If not specified, each request creates a new notification. + * + * If specified and a notification with the same tag is already being shown, + * the new notification replaces the existing one in the notification drawer. + * + * **Platforms:** Android + */ + tag?: string; + + /** + * The notification's body text. + * + * **Platforms:** iOS, Android, Web + */ + body?: string; + + /** + * The notification's icon. + * + * **Android:** Sets the notification icon to `myicon` for drawable resource + * `myicon`. If you don't send this key in the request, FCM displays the + * launcher icon specified in your app manifest. + * + * **Web:** The URL to use for the notification's icon. + * + * **Platforms:** Android, Web + */ + icon?: string; + + /** + * The value of the badge on the home screen app icon. + * + * If not specified, the badge is not changed. + * + * If set to `0`, the badge is removed. + * + * **Platforms:** iOS + */ + badge?: string; + + /** + * The notification icon's color, expressed in `#rrggbb` format. + * + * **Platforms:** Android + */ + color?: string; + + /** + * The sound to be played when the device receives a notification. Supports + * "default" for the default notification sound of the device or the filename of a + * sound resource bundled in the app. + * Sound files must reside in `/res/raw/`. + * + * **Platforms:** Android + */ + sound?: string; + + /** + * The notification's title. + * + * **Platforms:** iOS, Android, Web + */ + title?: string; + + /** + * The key to the body string in the app's string resources to use to localize + * the body text to the user's current localization. + * + * **iOS:** Corresponds to `loc-key` in the APNs payload. See + * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) + * and + * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) + * for more information. + * + * **Android:** See + * [String Resources](http://developer.android.com/guide/topics/resources/string-resource.html) * for more information. + * + * **Platforms:** iOS, Android + */ + bodyLocKey?: string; + + /** + * Variable string values to be used in place of the format specifiers in + * `body_loc_key` to use to localize the body text to the user's current + * localization. + * + * The value should be a stringified JSON array. + * + * **iOS:** Corresponds to `loc-args` in the APNs payload. See + * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) + * and + * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) + * for more information. + * + * **Android:** See + * [Formatting and Styling](http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling) + * for more information. + * + * **Platforms:** iOS, Android + */ + bodyLocArgs?: string; + + /** + * Action associated with a user click on the notification. If specified, an + * activity with a matching Intent Filter is launched when a user clicks on the + * notification. + * + * * **Platforms:** Android + */ + clickAction?: string; + + /** + * The key to the title string in the app's string resources to use to localize + * the title text to the user's current localization. + * + * **iOS:** Corresponds to `title-loc-key` in the APNs payload. See + * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) + * and + * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) + * for more information. + * + * **Android:** See + * [String Resources](http://developer.android.com/guide/topics/resources/string-resource.html) + * for more information. + * + * **Platforms:** iOS, Android + */ + titleLocKey?: string; + + /** + * Variable string values to be used in place of the format specifiers in + * `title_loc_key` to use to localize the title text to the user's current + * localization. + * + * The value should be a stringified JSON array. + * + * **iOS:** Corresponds to `title-loc-args` in the APNs payload. See + * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) + * and + * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) + * for more information. + * + * **Android:** See + * [Formatting and Styling](http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling) + * for more information. + * + * **Platforms:** iOS, Android + */ + titleLocArgs?: string; + [key: string]: string | undefined; + } + + /** + * Interface representing a Firebase Cloud Messaging message payload. One or + * both of the `data` and `notification` keys are required. + * + * See + * [Build send requests](/docs/cloud-messaging/send-message) + * for code samples and detailed documentation. + */ + export interface MessagingPayload { + + /** + * The data message payload. + */ + data?: DataMessagePayload; + + /** + * The notification message payload. + */ + notification?: NotificationMessagePayload; + } + + /** + * Interface representing the options that can be provided when sending a + * message via the FCM legacy APIs. + * + * See [Build send requests](/docs/cloud-messaging/send-message) + * for code samples and detailed documentation. + */ + export interface MessagingOptions { + + /** + * Whether or not the message should actually be sent. When set to `true`, + * allows developers to test a request without actually sending a message. When + * set to `false`, the message will be sent. + * + * **Default value:** `false` + */ + dryRun?: boolean; + + /** + * The priority of the message. Valid values are `"normal"` and `"high".` On + * iOS, these correspond to APNs priorities `5` and `10`. + * + * By default, notification messages are sent with high priority, and data + * messages are sent with normal priority. Normal priority optimizes the client + * app's battery consumption and should be used unless immediate delivery is + * required. For messages with normal priority, the app may receive the message + * with unspecified delay. + * + * When a message is sent with high priority, it is sent immediately, and the + * app can wake a sleeping device and open a network connection to your server. + * + * For more information, see + * [Setting the priority of a message](/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message). + * + * **Default value:** `"high"` for notification messages, `"normal"` for data + * messages + */ + priority?: string; + + /** + * How long (in seconds) the message should be kept in FCM storage if the device + * is offline. The maximum time to live supported is four weeks, and the default + * value is also four weeks. For more information, see + * [Setting the lifespan of a message](/docs/cloud-messaging/concept-options#ttl). + * + * **Default value:** `2419200` (representing four weeks, in seconds) + */ + timeToLive?: number; + + /** + * String identifying a group of messages (for example, "Updates Available") + * that can be collapsed, so that only the last message gets sent when delivery + * can be resumed. This is used to avoid sending too many of the same messages + * when the device comes back online or becomes active. + * + * There is no guarantee of the order in which messages get sent. + * + * A maximum of four different collapse keys is allowed at any given time. This + * means FCM server can simultaneously store four different + * send-to-sync messages per client app. If you exceed this number, there is no + * guarantee which four collapse keys the FCM server will keep. + * + * **Default value:** None + */ + collapseKey?: string; + + /** + * On iOS, use this field to represent `mutable-content` in the APNs payload. + * When a notification is sent and this is set to `true`, the content of the + * notification can be modified before it is displayed, using a + * [Notification Service app extension](https://developer.apple.com/reference/usernotifications/unnotificationserviceextension) + * + * On Android and Web, this parameter will be ignored. + * + * **Default value:** `false` + */ + mutableContent?: boolean; + + /** + * On iOS, use this field to represent `content-available` in the APNs payload. + * When a notification or data message is sent and this is set to `true`, an + * inactive client app is awoken. On Android, data messages wake the app by + * default. On Chrome, this flag is currently not supported. + * + * **Default value:** `false` + */ + contentAvailable?: boolean; + + /** + * The package name of the application which the registration tokens must match + * in order to receive the message. + * + * **Default value:** None + */ + restrictedPackageName?: string; + [key: string]: any | undefined; + } + + /* Individual status response payload from single devices */ + export interface MessagingDeviceResult { + /** + * The error that occurred when processing the message for the recipient. + */ + error?: FirebaseError; + + /** + * A unique ID for the successfully processed message. + */ + messageId?: string; + + /** + * The canonical registration token for the client app that the message was + * processed and sent to. You should use this value as the registration token + * for future requests. Otherwise, future messages might be rejected. + */ + canonicalRegistrationToken?: string; + } + + /** + * Interface representing the status of a message sent to an individual device + * via the FCM legacy APIs. + * + * See + * [Send to individual devices](/docs/cloud-messaging/admin/send-messages#send_to_individual_devices) + * for code samples and detailed documentation. + */ + export interface MessagingDevicesResponse { + canonicalRegistrationTokenCount: number; + failureCount: number; + multicastId: number; + results: MessagingDeviceResult[]; + successCount: number; + } + + /** + * Interface representing the server response from the + * {@link messaging.Messaging.sendToDeviceGroup `sendToDeviceGroup()`} + * method. + * + * See + * [Send messages to device groups](/docs/cloud-messaging/send-message?authuser=0#send_messages_to_device_groups) + * for code samples and detailed documentation. + */ + export interface MessagingDeviceGroupResponse { + + /** + * The number of messages that could not be processed and resulted in an error. + */ + successCount: number; + + /** + * The number of messages that could not be processed and resulted in an error. + */ + failureCount: number; + + /** + * An array of registration tokens that failed to receive the message. + */ + failedRegistrationTokens: string[]; + } + + /** + * Interface representing the server response from the legacy + * {@link messaging.Messaging.sendToTopic `sendToTopic()`} method. + * + * See + * [Send to a topic](/docs/cloud-messaging/admin/send-messages#send_to_a_topic) + * for code samples and detailed documentation. + */ + export interface MessagingTopicResponse { + /** + * The message ID for a successfully received request which FCM will attempt to + * deliver to all subscribed devices. + */ + messageId: number; + } + + /** + * Interface representing the server response from the legacy + * {@link messaging.Messaging.sendToCondition `sendToCondition()`} method. + * + * See + * [Send to a condition](/docs/cloud-messaging/admin/send-messages#send_to_a_condition) + * for code samples and detailed documentation. + */ + export interface MessagingConditionResponse { + /** + * The message ID for a successfully received request which FCM will attempt to + * deliver to all subscribed devices. + */ + messageId: number; + } + + /** + * Interface representing the server response from the + * {@link messaging.Messaging.subscribeToTopic `subscribeToTopic()`} and + * {@link messaging.Messaging.unsubscribeFromTopic `unsubscribeFromTopic()`} + * methods. + * + * See + * [Manage topics from the server](/docs/cloud-messaging/manage-topics) + * for code samples and detailed documentation. + */ + export interface MessagingTopicManagementResponse { + /** + * The number of registration tokens that could not be subscribed to the topic + * and resulted in an error. + */ + failureCount: number; + + /** + * The number of registration tokens that were successfully subscribed to the + * topic. + */ + successCount: number; + + /** + * An array of errors corresponding to the provided registration token(s). The + * length of this array will be equal to [`failureCount`](#failureCount). + */ + errors: FirebaseArrayIndexError[]; + } + + /** + * Interface representing the server response from the + * {@link messaging.Messaging.sendAll `sendAll()`} and + * {@link messaging.Messaging.sendMulticast `sendMulticast()`} methods. + */ + export interface BatchResponse { + + /** + * An array of responses, each corresponding to a message. + */ + responses: SendResponse[]; + + /** + * The number of messages that were successfully handed off for sending. + */ + successCount: number; + + /** + * The number of messages that resulted in errors when sending. + */ + failureCount: number; + } + + /** + * Interface representing the status of an individual message that was sent as + * part of a batch request. + */ + export interface SendResponse { + /** + * A boolean indicating if the message was successfully handed off to FCM or + * not. When true, the `messageId` attribute is guaranteed to be set. When + * false, the `error` attribute is guaranteed to be set. + */ + success: boolean; + + /** + * A unique message ID string, if the message was handed off to FCM for + * delivery. + * + */ + messageId?: string; + + /** + * An error, if the message was not handed off to FCM successfully. + */ + error?: FirebaseError; + } + + export interface Messaging { + /** + * The {@link app.App app} associated with the current `Messaging` service + * instance. + * + * @example + * ```javascript + * var app = messaging.app; + * ``` + */ + app: app.App; + + /** + * Sends the given message via FCM. + * + * @param message The message payload. + * @param dryRun Whether to send the message in the dry-run + * (validation only) mode. + * @return A promise fulfilled with a unique message ID + * string after the message has been successfully handed off to the FCM + * service for delivery. + */ + send(message: Message, dryRun?: boolean): Promise; + + /** + * Sends all the messages in the given array via Firebase Cloud Messaging. + * Employs batching to send the entire list as a single RPC call. Compared + * to the `send()` method, this method is a significantly more efficient way + * to send multiple messages. + * + * The responses list obtained from the return value + * corresponds to the order of tokens in the `MulticastMessage`. An error + * from this method indicates a total failure -- i.e. none of the messages in + * the list could be sent. Partial failures are indicated by a `BatchResponse` + * return value. + * + * @param messages A non-empty array + * containing up to 500 messages. + * @param dryRun Whether to send the messages in the dry-run + * (validation only) mode. + * @return A Promise fulfilled with an object representing the result of the + * send operation. + */ + sendAll( + messages: Array, + dryRun?: boolean + ): Promise; + + /** + * Sends the given multicast message to all the FCM registration tokens + * specified in it. + * + * This method uses the `sendAll()` API under the hood to send the given + * message to all the target recipients. The responses list obtained from the + * return value corresponds to the order of tokens in the `MulticastMessage`. + * An error from this method indicates a total failure -- i.e. the message was + * not sent to any of the tokens in the list. Partial failures are indicated by + * a `BatchResponse` return value. + * + * @param message A multicast message + * containing up to 500 tokens. + * @param dryRun Whether to send the message in the dry-run + * (validation only) mode. + * @return A Promise fulfilled with an object representing the result of the + * send operation. + */ + sendMulticast( + message: MulticastMessage, + dryRun?: boolean + ): Promise; + + /** + * Sends an FCM message to a single device corresponding to the provided + * registration token. + * + * See + * [Send to individual devices](/docs/cloud-messaging/admin/legacy-fcm#send_to_individual_devices) + * for code samples and detailed documentation. Takes either a + * `registrationToken` to send to a single device or a + * `registrationTokens` parameter containing an array of tokens to send + * to multiple devices. + * + * @param registrationToken A device registration token or an array of + * device registration tokens to which the message should be sent. + * @param payload The message payload. + * @param options Optional options to + * alter the message. + * + * @return A promise fulfilled with the server's response after the message + * has been sent. + */ + sendToDevice( + registrationToken: string | string[], + payload: MessagingPayload, + options?: MessagingOptions + ): Promise; + + /** + * Sends an FCM message to a device group corresponding to the provided + * notification key. + * + * See + * [Send to a device group](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_device_group) + * for code samples and detailed documentation. + * + * @param notificationKey The notification key for the device group to + * which to send the message. + * @param payload The message payload. + * @param options Optional options to + * alter the message. + * + * @return A promise fulfilled with the server's response after the message + * has been sent. + */ + sendToDeviceGroup( + notificationKey: string, + payload: MessagingPayload, + options?: MessagingOptions + ): Promise; + + /** + * Sends an FCM message to a topic. + * + * See + * [Send to a topic](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_topic) + * for code samples and detailed documentation. + * + * @param topic The topic to which to send the message. + * @param payload The message payload. + * @param options Optional options to + * alter the message. + * + * @return A promise fulfilled with the server's response after the message + * has been sent. + */ + sendToTopic( + topic: string, + payload: MessagingPayload, + options?: MessagingOptions + ): Promise; + + /** + * Sends an FCM message to a condition. + * + * See + * [Send to a condition](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_condition) + * for code samples and detailed documentation. + * + * @param condition The condition determining to which topics to send + * the message. + * @param payload The message payload. + * @param options Optional options to + * alter the message. + * + * @return A promise fulfilled with the server's response after the message + * has been sent. + */ + sendToCondition( + condition: string, + payload: MessagingPayload, + options?: MessagingOptions + ): Promise; + + /** + * Subscribes a device to an FCM topic. + * + * See [Subscribe to a + * topic](/docs/cloud-messaging/manage-topics#suscribe_and_unsubscribe_using_the) + * for code samples and detailed documentation. Optionally, you can provide an + * array of tokens to subscribe multiple devices. + * + * @param registrationTokens A token or array of registration tokens + * for the devices to subscribe to the topic. + * @param topic The topic to which to subscribe. + * + * @return A promise fulfilled with the server's response after the device has been + * subscribed to the topic. + */ + subscribeToTopic( + registrationTokens: string | string[], + topic: string + ): Promise; + + /** + * Unsubscribes a device from an FCM topic. + * + * See [Unsubscribe from a + * topic](/docs/cloud-messaging/admin/manage-topic-subscriptions#unsubscribe_from_a_topic) + * for code samples and detailed documentation. Optionally, you can provide an + * array of tokens to unsubscribe multiple devices. + * + * @param registrationTokens A device registration token or an array of + * device registration tokens to unsubscribe from the topic. + * @param topic The topic from which to unsubscribe. + * + * @return A promise fulfilled with the server's response after the device has been + * unsubscribed from the topic. + */ + unsubscribeFromTopic( + registrationTokens: string | string[], + topic: string + ): Promise; + } } diff --git a/src/messaging/messaging-api-request-internal.ts b/src/messaging/messaging-api-request-internal.ts index 99c55cce66..a294f4daa5 100644 --- a/src/messaging/messaging-api-request-internal.ts +++ b/src/messaging/messaging-api-request-internal.ts @@ -20,9 +20,12 @@ import { } from '../utils/api-request'; import { createFirebaseError, getErrorCode } from './messaging-errors-internal'; import { SubRequest, BatchRequestClient } from './batch-request-internal'; -import { SendResponse, BatchResponse } from './messaging-types'; +import { messaging } from './index'; import { getSdkVersion } from '../utils/index'; +import SendResponse = messaging.SendResponse; +import BatchResponse = messaging.BatchResponse; + // FCM backend constants const FIREBASE_MESSAGING_TIMEOUT = 10000; const FIREBASE_MESSAGING_BATCH_URL = 'https://fcm.googleapis.com/batch'; diff --git a/src/messaging/messaging-internal.ts b/src/messaging/messaging-internal.ts index 1979b2bc14..227f894eea 100644 --- a/src/messaging/messaging-internal.ts +++ b/src/messaging/messaging-internal.ts @@ -16,16 +16,24 @@ import { renameProperties } from '../utils/index'; import { MessagingClientErrorCode, FirebaseMessagingError, } from '../utils/error'; -import { - AndroidConfig, AndroidFcmOptions, AndroidNotification, - ApsAlert, ApnsConfig, ApnsFcmOptions, - ApnsPayload, Aps, CriticalSound, - FcmOptions, LightSettings, - Message, Notification, WebpushConfig -} from './messaging-types'; - +import { messaging } from './index'; import * as validator from '../utils/validator'; +import AndroidConfig = messaging.AndroidConfig; +import AndroidFcmOptions = messaging.AndroidFcmOptions; +import AndroidNotification = messaging.AndroidNotification; +import ApsAlert = messaging.ApsAlert; +import ApnsConfig = messaging.ApnsConfig; +import ApnsFcmOptions = messaging.ApnsFcmOptions; +import ApnsPayload = messaging.ApnsPayload; +import Aps = messaging.Aps; +import CriticalSound = messaging.CriticalSound; +import FcmOptions = messaging.FcmOptions; +import LightSettings = messaging.LightSettings; +import Message = messaging.Message; +import Notification = messaging.Notification; +import WebpushConfig = messaging.WebpushConfig; + // Keys which are not allowed in the messaging data payload object. export const BLACKLISTED_DATA_PAYLOAD_KEYS = ['from']; diff --git a/src/messaging/messaging-types.ts b/src/messaging/messaging-types.ts deleted file mode 100644 index 165aaf6e48..0000000000 --- a/src/messaging/messaging-types.ts +++ /dev/null @@ -1,1109 +0,0 @@ -/*! - * Copyright 2019 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 { - FirebaseArrayIndexError, FirebaseError, -} from '../utils/error'; - -interface BaseMessage { - data?: { [key: string]: string }; - notification?: Notification; - android?: AndroidConfig; - webpush?: WebpushConfig; - apns?: ApnsConfig; - fcmOptions?: FcmOptions; -} - -interface TokenMessage extends BaseMessage { - token: string; -} - -interface TopicMessage extends BaseMessage { - topic: string; -} - -interface ConditionMessage extends BaseMessage { - condition: string; -} - -/** - * Payload for the admin.messaging.send() operation. The payload contains all the fields - * in the BaseMessage type, and exactly one of token, topic or condition. - */ -export type Message = TokenMessage | TopicMessage | ConditionMessage; - -/** - * Payload for the admin.messaing.sendMulticase() method. The payload contains all the fields - * in the BaseMessage type, and a list of tokens. - */ -export interface MulticastMessage extends BaseMessage { - tokens: string[]; -} - -/** - * A notification that can be included in {@link admin.messaging.Message}. - */ -export interface Notification { - /** - * The title of the notification. - */ - title?: string; - /** - * The notification body - */ - body?: string; - /** - * URL of an image to be displayed in the notification. - */ - imageUrl?: string; -} - -/** - * Represents platform-independent options for features provided by the FCM SDKs. - */ -export interface FcmOptions { - /** - * The label associated with the message's analytics data. - */ - analyticsLabel?: string; -} - -/** - * Represents the WebPush protocol options that can be included in an - * {@link admin.messaging.Message}. - */ -export interface WebpushConfig { - - /** - * A collection of WebPush headers. Header values must be strings. - * - * See [WebPush specification](https://tools.ietf.org/html/rfc8030#section-5) - * for supported headers. - */ - headers?: { [key: string]: string }; - - /** - * A collection of data fields. - */ - data?: { [key: string]: string }; - - /** - * A WebPush notification payload to be included in the message. - */ - notification?: WebpushNotification; - - /** - * Options for features provided by the FCM SDK for Web. - */ - fcmOptions?: WebpushFcmOptions; -} - -/** Represents options for features provided by the FCM SDK for Web - * (which are not part of the Webpush standard). - */ -export interface WebpushFcmOptions { - - /** - * The link to open when the user clicks on the notification. - * For all URL values, HTTPS is required. - */ - link?: string; -} - -/** - * Represents the WebPush-specific notification options that can be included in - * {@link admin.messaging.WebpushConfig}. This supports most of the standard - * options as defined in the Web Notification - * [specification](https://developer.mozilla.org/en-US/docs/Web/API/notification/Notification). - */ -export interface WebpushNotification { - - /** - * Title text of the notification. - */ - title?: string; - - /** - * An array of notification actions representing the actions - * available to the user when the notification is presented. - */ - actions?: Array<{ - - /** - * An action available to the user when the notification is presented - */ - action: string; - - /** - * Optional icon for a notification action. - */ - icon?: string; - - /** - * Title of the notification action. - */ - title: string; - }>; - - /** - * URL of the image used to represent the notification when there is - * not enough space to display the notification itself. - */ - badge?: string; - - /** - * Body text of the notification. - */ - body?: string; - - /** - * Arbitrary data that you want associated with the notification. - * This can be of any data type. - */ - data?: any; - - /** - * The direction in which to display the notification. Must be one - * of `auto`, `ltr` or `rtl`. - */ - dir?: 'auto' | 'ltr' | 'rtl'; - - /** - * URL to the notification icon. - */ - icon?: string; - - /** - * URL of an image to be displayed in the notification. - */ - image?: string; - - /** - * The notification's language as a BCP 47 language tag. - */ - lang?: string; - - /** - * A boolean specifying whether the user should be notified after a - * new notification replaces an old one. Defaults to false. - */ - renotify?: boolean; - - /** - * Indicates that a notification should remain active until the user - * clicks or dismisses it, rather than closing automatically. - * Defaults to false. - */ - requireInteraction?: boolean; - - /** - * A boolean specifying whether the notification should be silent. - * Defaults to false. - */ - silent?: boolean; - - /** - * An identifying tag for the notification. - */ - tag?: string; - - /** - * Timestamp of the notification. Refer to - * https://developer.mozilla.org/en-US/docs/Web/API/notification/timestamp - * for details. - */ - timestamp?: number; - - /** - * A vibration pattern for the device's vibration hardware to emit - * when the notification fires. - */ - vibrate?: number | number[]; - [key: string]: any; -} - -/** - * Represents the APNs-specific options that can be included in an - * {@link admin.messaging.Message}. Refer to - * [Apple documentation](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html) - * for various headers and payload fields supported by APNs. - */ -export interface ApnsConfig { - /** - * A collection of APNs headers. Header values must be strings. - */ - headers?: { [key: string]: string }; - - /** - * An APNs payload to be included in the message. - */ - payload?: ApnsPayload; - - /** - * Options for features provided by the FCM SDK for iOS. - */ - fcmOptions?: ApnsFcmOptions; -} - -/** - * Represents the payload of an APNs message. Mainly consists of the `aps` - * dictionary. But may also contain other arbitrary custom keys. - */ -export interface ApnsPayload { - - /** - * The `aps` dictionary to be included in the message. - */ - aps: Aps; - [customData: string]: object; -} - -/** - * Represents the [aps dictionary](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) - * that is part of APNs messages. - */ -export interface Aps { - - /** - * Alert to be included in the message. This may be a string or an object of - * type `admin.messaging.ApsAlert`. - */ - alert?: string | ApsAlert; - - /** - * Badge to be displayed with the message. Set to 0 to remove the badge. When - * not specified, the badge will remain unchanged. - */ - badge?: number; - - /** - * Sound to be played with the message. - */ - sound?: string | CriticalSound; - - /** - * Specifies whether to configure a background update notification. - */ - contentAvailable?: boolean; - - /** - * Specifies whether to set the `mutable-content` property on the message - * so the clients can modify the notification via app extensions. - */ - mutableContent?: boolean; - - /** - * Type of the notification. - */ - category?: string; - - /** - * An app-specific identifier for grouping notifications. - */ - threadId?: string; - [customData: string]: any; -} - -export interface ApsAlert { - title?: string; - subtitle?: string; - body?: string; - locKey?: string; - locArgs?: string[]; - titleLocKey?: string; - titleLocArgs?: string[]; - subtitleLocKey?: string; - subtitleLocArgs?: string[]; - actionLocKey?: string; - launchImage?: string; -} - -/** - * Represents a critical sound configuration that can be included in the - * `aps` dictionary of an APNs payload. - */ -export interface CriticalSound { - - /** - * The critical alert flag. Set to `true` to enable the critical alert. - */ - critical?: boolean; - - /** - * The name of a sound file in the app's main bundle or in the `Library/Sounds` - * folder of the app's container directory. Specify the string "default" to play - * the system sound. - */ - name: string; - - /** - * The volume for the critical alert's sound. Must be a value between 0.0 - * (silent) and 1.0 (full volume). - */ - volume?: number; -} - -/** - * Represents options for features provided by the FCM SDK for iOS. - */ -export interface ApnsFcmOptions { - - /** - * The label associated with the message's analytics data. - */ - analyticsLabel?: string; - - /** - * URL of an image to be displayed in the notification. - */ - imageUrl?: string; -} - - -/** - * Represents the Android-specific options that can be included in an - * {@link admin.messaging.Message}. - */ -export interface AndroidConfig { - - /** - * Collapse key for the message. Collapse key serves as an identifier for a - * group of messages that can be collapsed, so that only the last message gets - * sent when delivery can be resumed. A maximum of four different collapse keys - * may be active at any given time. - */ - collapseKey?: string; - - /** - * Priority of the message. Must be either `normal` or `high`. - */ - priority?: ('high' | 'normal'); - - /** - * Time-to-live duration of the message in milliseconds. - */ - ttl?: number; - - /** - * Package name of the application where the registration tokens must match - * in order to receive the message. - */ - restrictedPackageName?: string; - - /** - * A collection of data fields to be included in the message. All values must - * be strings. When provided, overrides any data fields set on the top-level - * `admin.messaging.Message`.} - */ - data?: { [key: string]: string }; - - /** - * Android notification to be included in the message. - */ - notification?: AndroidNotification; - - /** - * Options for features provided by the FCM SDK for Android. - */ - fcmOptions?: AndroidFcmOptions; -} - -/** - * Represents the Android-specific notification options that can be included in - * {@link admin.messaging.AndroidConfig}. - */ -export interface AndroidNotification { - /** - * Title of the Android notification. When provided, overrides the title set via - * `admin.messaging.Notification`. - */ - title?: string; - - /** - * Body of the Android notification. When provided, overrides the body set via - * `admin.messaging.Notification`. - */ - body?: string; - - /** - * Icon resource for the Android notification. - */ - icon?: string; - - /** - * Notification icon color in `#rrggbb` format. - */ - color?: string; - - /** - * File name of the sound to be played when the device receives the - * notification. - */ - sound?: string; - - /** - * Notification tag. This is an identifier used to replace existing - * notifications in the notification drawer. If not specified, each request - * creates a new notification. - */ - tag?: string; - - /** - * URL of an image to be displayed in the notification. - */ - imageUrl?: string; - - /** - * Action associated with a user click on the notification. If specified, an - * activity with a matching Intent Filter is launched when a user clicks on the - * notification. - */ - clickAction?: string; - - /** - * Key of the body string in the app's string resource to use to localize the - * body text. - * - */ - bodyLocKey?: string; - - /** - * An array of resource keys that will be used in place of the format - * specifiers in `bodyLocKey`. - */ - bodyLocArgs?: string[]; - - /** - * Key of the title string in the app's string resource to use to localize the - * title text. - */ - titleLocKey?: string; - - /** - * An array of resource keys that will be used in place of the format - * specifiers in `titleLocKey`. - */ - titleLocArgs?: string[]; - - /** - * The Android notification channel ID (new in Android O). The app must create - * a channel with this channel ID before any notification with this channel ID - * can be received. If you don't send this channel ID in the request, or if the - * channel ID provided has not yet been created by the app, FCM uses the channel - * ID specified in the app manifest. - */ - channelId?: string; - - /** - * Sets the "ticker" text, which is sent to accessibility services. Prior to - * API level 21 (Lollipop), sets the text that is displayed in the status bar - * when the notification first arrives. - */ - ticker?: string; - - /** - * When set to `false` or unset, the notification is automatically dismissed when - * the user clicks it in the panel. When set to `true`, the notification persists - * even when the user clicks it. - */ - sticky?: boolean; - - /** - * For notifications that inform users about events with an absolute time reference, sets - * the time that the event in the notification occurred. Notifications - * in the panel are sorted by this time. - */ - eventTimestamp?: Date; - - /** - * Sets whether or not this notification is relevant only to the current device. - * Some notifications can be bridged to other devices for remote display, such as - * a Wear OS watch. This hint can be set to recommend this notification not be bridged. - * See [Wear OS guides](https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging) - */ - localOnly?: boolean; - - /** - * Sets the relative priority for this notification. Low-priority notifications - * may be hidden from the user in certain situations. Note this priority differs - * from `AndroidMessagePriority`. This priority is processed by the client after - * the message has been delivered. Whereas `AndroidMessagePriority` is an FCM concept - * that controls when the message is delivered. - */ - priority?: ('min' | 'low' | 'default' | 'high' | 'max'); - - /** - * Sets the vibration pattern to use. Pass in an array of milliseconds to - * turn the vibrator on or off. The first value indicates the duration to wait before - * turning the vibrator on. The next value indicates the duration to keep the - * vibrator on. Subsequent values alternate between duration to turn the vibrator - * off and to turn the vibrator on. If `vibrate_timings` is set and `default_vibrate_timings` - * is set to `true`, the default value is used instead of the user-specified `vibrate_timings`. - */ - vibrateTimingsMillis?: number[]; - - /** - * If set to `true`, use the Android framework's default vibrate pattern for the - * notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). - * If `default_vibrate_timings` is set to `true` and `vibrate_timings` is also set, - * the default value is used instead of the user-specified `vibrate_timings`. - */ - defaultVibrateTimings?: boolean; - - /** - * If set to `true`, use the Android framework's default sound for the notification. - * Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). - */ - defaultSound?: boolean; - - /** - * Settings to control the notification's LED blinking rate and color if LED is - * available on the device. The total blinking time is controlled by the OS. - */ - lightSettings?: LightSettings; - - /** - * If set to `true`, use the Android framework's default LED light settings - * for the notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). - * If `default_light_settings` is set to `true` and `light_settings` is also set, - * the user-specified `light_settings` is used instead of the default value. - */ - defaultLightSettings?: boolean; - - /** - * Sets the visibility of the notification. Must be either `private`, `public`, - * or `secret`. If unspecified, defaults to `private`. - */ - visibility?: ('private' | 'public' | 'secret'); - - /** - * Sets the number of items this notification represents. May be displayed as a - * badge count for Launchers that support badging. See [`NotificationBadge`(https://developer.android.com/training/notify-user/badges). - * For example, this might be useful if you're using just one notification to - * represent multiple new messages but you want the count here to represent - * the number of total new messages. If zero or unspecified, systems - * that support badging use the default, which is to increment a number - * displayed on the long-press menu each time a new notification arrives. - */ - notificationCount?: number; -} - -/** - * Represents settings to control notification LED that can be included in - * {@link admin.messaging.AndroidNotification}. - */ -export interface LightSettings { - /** - * Required. Sets color of the LED in `#rrggbb` or `#rrggbbaa` format. - */ - color: string; - - /** - * Required. Along with `light_off_duration`, defines the blink rate of LED flashes. - */ - lightOnDurationMillis: number; - - /** - * Required. Along with `light_on_duration`, defines the blink rate of LED flashes. - */ - lightOffDurationMillis: number; -} - -/** - * Represents options for features provided by the FCM SDK for Android. - */ -export interface AndroidFcmOptions { - - /** - * The label associated with the message's analytics data. - */ - analyticsLabel?: string; -} - -/** - * Interface representing an FCM legacy API data message payload. Data - * messages let developers send up to 4KB of custom key-value pairs. The - * keys and values must both be strings. Keys can be any custom string, - * except for the following reserved strings: - * - * * `"from"` - * * Anything starting with `"google."`. - * - * See [Build send requests](/docs/cloud-messaging/send-message) - * for code samples and detailed documentation. - */ -export interface DataMessagePayload { - [key: string]: string; -} - -/** - * Interface representing an FCM legacy API notification message payload. - * Notification messages let developers send up to 4KB of predefined - * key-value pairs. Accepted keys are outlined below. - * - * See [Build send requests](/docs/cloud-messaging/send-message) - * for code samples and detailed documentation. - */ -export interface NotificationMessagePayload { - - /** - * Identifier used to replace existing notifications in the notification drawer. - * - * If not specified, each request creates a new notification. - * - * If specified and a notification with the same tag is already being shown, - * the new notification replaces the existing one in the notification drawer. - * - * **Platforms:** Android - */ - tag?: string; - - /** - * The notification's body text. - * - * **Platforms:** iOS, Android, Web - */ - body?: string; - - /** - * The notification's icon. - * - * **Android:** Sets the notification icon to `myicon` for drawable resource - * `myicon`. If you don't send this key in the request, FCM displays the - * launcher icon specified in your app manifest. - * - * **Web:** The URL to use for the notification's icon. - * - * **Platforms:** Android, Web - */ - icon?: string; - - /** - * The value of the badge on the home screen app icon. - * - * If not specified, the badge is not changed. - * - * If set to `0`, the badge is removed. - * - * **Platforms:** iOS - */ - badge?: string; - - /** - * The notification icon's color, expressed in `#rrggbb` format. - * - * **Platforms:** Android - */ - color?: string; - - /** - * The sound to be played when the device receives a notification. Supports - * "default" for the default notification sound of the device or the filename of a - * sound resource bundled in the app. - * Sound files must reside in `/res/raw/`. - * - * **Platforms:** Android - */ - sound?: string; - - /** - * The notification's title. - * - * **Platforms:** iOS, Android, Web - */ - title?: string; - - /** - * The key to the body string in the app's string resources to use to localize - * the body text to the user's current localization. - * - * **iOS:** Corresponds to `loc-key` in the APNs payload. See - * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) - * and - * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) - * for more information. - * - * **Android:** See - * [String Resources](http://developer.android.com/guide/topics/resources/string-resource.html) * for more information. - * - * **Platforms:** iOS, Android - */ - bodyLocKey?: string; - - /** - * Variable string values to be used in place of the format specifiers in - * `body_loc_key` to use to localize the body text to the user's current - * localization. - * - * The value should be a stringified JSON array. - * - * **iOS:** Corresponds to `loc-args` in the APNs payload. See - * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) - * and - * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) - * for more information. - * - * **Android:** See - * [Formatting and Styling](http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling) - * for more information. - * - * **Platforms:** iOS, Android - */ - bodyLocArgs?: string; - - /** - * Action associated with a user click on the notification. If specified, an - * activity with a matching Intent Filter is launched when a user clicks on the - * notification. - * - * * **Platforms:** Android - */ - clickAction?: string; - - /** - * The key to the title string in the app's string resources to use to localize - * the title text to the user's current localization. - * - * **iOS:** Corresponds to `title-loc-key` in the APNs payload. See - * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) - * and - * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) - * for more information. - * - * **Android:** See - * [String Resources](http://developer.android.com/guide/topics/resources/string-resource.html) - * for more information. - * - * **Platforms:** iOS, Android - */ - titleLocKey?: string; - - /** - * Variable string values to be used in place of the format specifiers in - * `title_loc_key` to use to localize the title text to the user's current - * localization. - * - * The value should be a stringified JSON array. - * - * **iOS:** Corresponds to `title-loc-args` in the APNs payload. See - * [Payload Key Reference](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) - * and - * [Localizing the Content of Your Remote Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9) - * for more information. - * - * **Android:** See - * [Formatting and Styling](http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling) - * for more information. - * - * **Platforms:** iOS, Android - */ - titleLocArgs?: string; - [key: string]: string | undefined; -} - -/** - * Interface representing a Firebase Cloud Messaging message payload. One or - * both of the `data` and `notification` keys are required. - * - * See - * [Build send requests](/docs/cloud-messaging/send-message) - * for code samples and detailed documentation. - */ -export interface MessagingPayload { - - /** - * The data message payload. - */ - data?: DataMessagePayload; - - /** - * The notification message payload. - */ - notification?: NotificationMessagePayload; -} - -/** - * Interface representing the options that can be provided when sending a - * message via the FCM legacy APIs. - * - * See [Build send requests](/docs/cloud-messaging/send-message) - * for code samples and detailed documentation. - */ -export interface MessagingOptions { - - /** - * Whether or not the message should actually be sent. When set to `true`, - * allows developers to test a request without actually sending a message. When - * set to `false`, the message will be sent. - * - * **Default value:** `false` - */ - dryRun?: boolean; - - /** - * The priority of the message. Valid values are `"normal"` and `"high".` On - * iOS, these correspond to APNs priorities `5` and `10`. - * - * By default, notification messages are sent with high priority, and data - * messages are sent with normal priority. Normal priority optimizes the client - * app's battery consumption and should be used unless immediate delivery is - * required. For messages with normal priority, the app may receive the message - * with unspecified delay. - * - * When a message is sent with high priority, it is sent immediately, and the - * app can wake a sleeping device and open a network connection to your server. - * - * For more information, see - * [Setting the priority of a message](/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message). - * - * **Default value:** `"high"` for notification messages, `"normal"` for data - * messages - */ - priority?: string; - - /** - * How long (in seconds) the message should be kept in FCM storage if the device - * is offline. The maximum time to live supported is four weeks, and the default - * value is also four weeks. For more information, see - * [Setting the lifespan of a message](/docs/cloud-messaging/concept-options#ttl). - * - * **Default value:** `2419200` (representing four weeks, in seconds) - */ - timeToLive?: number; - - /** - * String identifying a group of messages (for example, "Updates Available") - * that can be collapsed, so that only the last message gets sent when delivery - * can be resumed. This is used to avoid sending too many of the same messages - * when the device comes back online or becomes active. - * - * There is no guarantee of the order in which messages get sent. - * - * A maximum of four different collapse keys is allowed at any given time. This - * means FCM server can simultaneously store four different - * send-to-sync messages per client app. If you exceed this number, there is no - * guarantee which four collapse keys the FCM server will keep. - * - * **Default value:** None - */ - collapseKey?: string; - - /** - * On iOS, use this field to represent `mutable-content` in the APNs payload. - * When a notification is sent and this is set to `true`, the content of the - * notification can be modified before it is displayed, using a - * [Notification Service app extension](https://developer.apple.com/reference/usernotifications/unnotificationserviceextension) - * - * On Android and Web, this parameter will be ignored. - * - * **Default value:** `false` - */ - mutableContent?: boolean; - - /** - * On iOS, use this field to represent `content-available` in the APNs payload. - * When a notification or data message is sent and this is set to `true`, an - * inactive client app is awoken. On Android, data messages wake the app by - * default. On Chrome, this flag is currently not supported. - * - * **Default value:** `false` - */ - contentAvailable?: boolean; - - /** - * The package name of the application which the registration tokens must match - * in order to receive the message. - * - * **Default value:** None - */ - restrictedPackageName?: string; - [key: string]: any | undefined; -} - -/* Individual status response payload from single devices */ -export interface MessagingDeviceResult { - /** - * The error that occurred when processing the message for the recipient. - */ - error?: FirebaseError; - - /** - * A unique ID for the successfully processed message. - */ - messageId?: string; - - /** - * The canonical registration token for the client app that the message was - * processed and sent to. You should use this value as the registration token - * for future requests. Otherwise, future messages might be rejected. - */ - canonicalRegistrationToken?: string; -} - -/** - * Interface representing the status of a message sent to an individual device - * via the FCM legacy APIs. - * - * See - * [Send to individual devices](/docs/cloud-messaging/admin/send-messages#send_to_individual_devices) - * for code samples and detailed documentation. - */ -export interface MessagingDevicesResponse { - canonicalRegistrationTokenCount: number; - failureCount: number; - multicastId: number; - results: MessagingDeviceResult[]; - successCount: number; -} - -/** - * Interface representing the server response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDeviceGroup `sendToDeviceGroup()`} - * method. - * - * See - * [Send messages to device groups](/docs/cloud-messaging/send-message?authuser=0#send_messages_to_device_groups) - * for code samples and detailed documentation. - */ -export interface MessagingDeviceGroupResponse { - - /** - * The number of messages that could not be processed and resulted in an error. - */ - successCount: number; - - /** - * The number of messages that could not be processed and resulted in an error. - */ - failureCount: number; - - /** - * An array of registration tokens that failed to receive the message. - */ - failedRegistrationTokens: string[]; -} - -/** - * Interface representing the server response from the legacy - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToTopic `sendToTopic()`} method. - * - * See - * [Send to a topic](/docs/cloud-messaging/admin/send-messages#send_to_a_topic) - * for code samples and detailed documentation. - */ -export interface MessagingTopicResponse { - /** - * The message ID for a successfully received request which FCM will attempt to - * deliver to all subscribed devices. - */ - messageId: number; -} - -/** - * Interface representing the server response from the legacy - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToCondition `sendToCondition()`} method. - * - * See - * [Send to a condition](/docs/cloud-messaging/admin/send-messages#send_to_a_condition) - * for code samples and detailed documentation. - */ -export interface MessagingConditionResponse { - /** - * The message ID for a successfully received request which FCM will attempt to - * deliver to all subscribed devices. - */ - messageId: number; -} - -/** - * Interface representing the server response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#subscribeToTopic `subscribeToTopic()`} and - * {@link - * admin.messaging.Messaging#unsubscribeFromTopic - * `unsubscribeFromTopic()`} - * methods. - * - * See - * [Manage topics from the server](/docs/cloud-messaging/manage-topics) - * for code samples and detailed documentation. - */ -export interface MessagingTopicManagementResponse { - /** - * The number of registration tokens that could not be subscribed to the topic - * and resulted in an error. - */ - failureCount: number; - - /** - * The number of registration tokens that were successfully subscribed to the - * topic. - */ - successCount: number; - - /** - * An array of errors corresponding to the provided registration token(s). The - * length of this array will be equal to [`failureCount`](#failureCount). - */ - errors: FirebaseArrayIndexError[]; -} - -/** - * Interface representing the server response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendAll `sendAll()`} and - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendMulticast `sendMulticast()`} methods. - */ -export interface BatchResponse { - - /** - * An array of responses, each corresponding to a message. - */ - responses: SendResponse[]; - - /** - * The number of messages that were successfully handed off for sending. - */ - successCount: number; - - /** - * The number of messages that resulted in errors when sending. - */ - failureCount: number; -} - -/** - * Interface representing the status of an individual message that was sent as - * part of a batch request. - */ -export interface SendResponse { - /** - * A boolean indicating if the message was successfully handed off to FCM or - * not. When true, the `messageId` attribute is guaranteed to be set. When - * false, the `error` attribute is guaranteed to be set. - */ - success: boolean; - - /** - * A unique message ID string, if the message was handed off to FCM for - * delivery. - * - */ - messageId?: string; - - /** - * An error, if the message was not handed off to FCM successfully. - */ - error?: FirebaseError; -} diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index eec16df661..8c3d7c1ca3 100644 --- a/src/messaging/messaging.ts +++ b/src/messaging/messaging.ts @@ -18,21 +18,29 @@ import { FirebaseApp } from '../firebase-app'; import { deepCopy, deepExtend } from '../utils/deep-copy'; import { SubRequest } from './batch-request-internal'; import { validateMessage, BLACKLISTED_DATA_PAYLOAD_KEYS, BLACKLISTED_OPTIONS_KEYS } from './messaging-internal'; -import { - Message, MessagingDevicesResponse, - MessagingDeviceGroupResponse, MessagingTopicManagementResponse, - MessagingPayload, MessagingOptions, MessagingTopicResponse, - MessagingConditionResponse, BatchResponse, MulticastMessage, DataMessagePayload, NotificationMessagePayload, -} from './messaging-types'; +import { messaging } from './index'; import { FirebaseMessagingRequestHandler } from './messaging-api-request-internal'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; -import { - ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError, -} from '../utils/error'; - +import { ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError } from '../utils/error'; import * as utils from '../utils'; import * as validator from '../utils/validator'; +import MessagingInterface = messaging.Messaging; +import Message = messaging.Message; +import BatchResponse = messaging.BatchResponse; +import MulticastMessage = messaging.MulticastMessage; +import MessagingTopicManagementResponse = messaging.MessagingTopicManagementResponse; + +// Legacy API types +import MessagingDevicesResponse = messaging.MessagingDevicesResponse; +import MessagingDeviceGroupResponse = messaging.MessagingDeviceGroupResponse; +import MessagingPayload = messaging.MessagingPayload; +import MessagingOptions = messaging.MessagingOptions; +import MessagingTopicResponse = messaging.MessagingTopicResponse; +import MessagingConditionResponse = messaging.MessagingConditionResponse; +import DataMessagePayload = messaging.DataMessagePayload; +import NotificationMessagePayload = messaging.NotificationMessagePayload; + /* eslint-disable @typescript-eslint/camelcase */ // FCM endpoints @@ -196,7 +204,7 @@ class MessagingInternals implements FirebaseServiceInternalsInterface { /** * Messaging service bound to the provided app. */ -export class Messaging implements FirebaseServiceInterface { +export class Messaging implements FirebaseServiceInterface, MessagingInterface { public INTERNAL: MessagingInternals = new MessagingInternals(); @@ -205,7 +213,7 @@ export class Messaging implements FirebaseServiceInterface { private readonly messagingRequestHandler: FirebaseMessagingRequestHandler; /** - * Gets the {@link admin.messaging.Messaging `Messaging`} service for the + * Gets the {@link messaging.Messaging `Messaging`} service for the * current app. * * @example @@ -399,7 +407,7 @@ export class Messaging implements FirebaseServiceInterface { registrationTokenOrTokens: string | string[], payload: MessagingPayload, options: MessagingOptions = {}, - ): Promise { + ): Promise { // Validate the input argument types. Since these are common developer errors when getting // started, throw an error instead of returning a rejected promise. this.validateRegistrationTokensType( @@ -437,7 +445,13 @@ export class Messaging implements FirebaseServiceInterface { if ('multicast_id' in response) { return mapRawResponseToDevicesResponse(response); } else { - return mapRawResponseToDeviceGroupResponse(response); + const groupResponse = mapRawResponseToDeviceGroupResponse(response); + return { + ...groupResponse, + canonicalRegistrationTokenCount: -1, + multicastId: -1, + results: [], + } } }); } @@ -463,7 +477,7 @@ export class Messaging implements FirebaseServiceInterface { notificationKey: string, payload: MessagingPayload, options: MessagingOptions = {}, - ): Promise { + ): Promise { if (!validator.isNonEmptyString(notificationKey)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_RECIPIENT, @@ -515,7 +529,11 @@ export class Messaging implements FirebaseServiceInterface { 'Notification key provided to sendToDeviceGroup() is invalid.', ); } else { - return mapRawResponseToDevicesResponse(response); + const devicesResponse = mapRawResponseToDevicesResponse(response); + return { + ...devicesResponse, + failedRegistrationTokens: [], + } } } @@ -806,7 +824,7 @@ export class Messaging implements FirebaseServiceInterface { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, `Messaging payload contains an invalid "${payloadKey}" property. Valid properties are ` + - `"data" and "notification".`, + '"data" and "notification".', ); } else { containsDataOrNotificationKey = true; @@ -827,7 +845,7 @@ export class Messaging implements FirebaseServiceInterface { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, `Messaging payload contains an invalid value for the "${payloadKey}" property. ` + - `Value must be an object.`, + 'Value must be an object.', ); } @@ -837,7 +855,7 @@ export class Messaging implements FirebaseServiceInterface { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, `Messaging payload contains an invalid value for the "${payloadKey}.${subKey}" ` + - `property. Values must be strings.`, + 'property. Values must be strings.', ); } else if (payloadKey === 'data' && /^google\./.test(subKey)) { // Validate the data payload does not contain keys which start with 'google.'. diff --git a/src/project-management.d.ts b/src/project-management.d.ts deleted file mode 100644 index 755f8c9e94..0000000000 --- a/src/project-management.d.ts +++ /dev/null @@ -1,360 +0,0 @@ -import * as _admin from './index.d'; - -export namespace admin.projectManagement { - - /** - * A SHA-1 or SHA-256 certificate. - * - * Do not call this constructor directly. Instead, use - * [`projectManagement.shaCertificate()`](admin.projectManagement.ProjectManagement#shaCertificate). - */ - interface ShaCertificate { - - /** - * The SHA certificate type. - * - * @example - * ```javascript - * var certType = shaCertificate.certType; - * ``` - */ - certType: ('sha1' | 'sha256'); - - /** - * The SHA-1 or SHA-256 hash for this certificate. - * - * @example - * ```javascript - * var shaHash = shaCertificate.shaHash; - * ``` - */ - shaHash: string; - - /** - * The fully-qualified resource name that identifies this sha-key. - * - * This is useful when manually constructing requests for Firebase's public API. - * - * @example - * ```javascript - * var resourceName = shaCertificate.resourceName; - * ``` - */ - resourceName?: string; - } - - /** - * Metadata about a Firebase app. - */ - interface AppMetadata { - - /** - * The globally unique, Firebase-assigned identifier of the app. - * - * @example - * ```javascript - * var appId = appMetadata.appId; - * ``` - */ - appId: string; - - /** - * The optional user-assigned display name of the app. - * - * @example - * ```javascript - * var displayName = appMetadata.displayName; - * ``` - */ - displayName?: string; - - /** - * The development platform of the app. Supporting Android and iOS app platforms. - * - * @example - * ```javascript - * var platform = AppPlatform.ANDROID; - * ``` - */ - platform: AppPlatform; - - /** - * The globally unique, user-assigned ID of the parent project for the app. - * - * @example - * ```javascript - * var projectId = appMetadata.projectId; - * ``` - */ - projectId: string; - - /** - * The fully-qualified resource name that identifies this app. - * - * This is useful when manually constructing requests for Firebase's public API. - * - * @example - * ```javascript - * var resourceName = androidAppMetadata.resourceName; - * ``` - */ - resourceName: string; - } - - /** - * Platforms with which a Firebase App can be associated. - */ - enum AppPlatform { - - /** - * Unknown state. This is only used for distinguishing unset values. - */ - PLATFORM_UNKNOWN = 'PLATFORM_UNKNOWN', - - /** - * The Firebase App is associated with iOS. - */ - IOS = 'IOS', - - /** - * The Firebase App is associated with Android. - */ - ANDROID = 'ANDROID', - } - - /** - * Metadata about a Firebase Android App. - */ - interface AndroidAppMetadata extends AppMetadata { - - platform: AppPlatform.ANDROID; - - /** - * The canonical package name of the Android App, as would appear in the Google Play Developer - * Console. - * - * @example - * ```javascript - * var packageName = androidAppMetadata.packageName; - * ``` - */ - packageName: string; - } - - /** - * Metadata about a Firebase iOS App. - */ - interface IosAppMetadata extends AppMetadata { - platform: AppPlatform.IOS; - - /** - * The canonical bundle ID of the iOS App as it would appear in the iOS App Store. - * - * @example - * ```javascript - * var bundleId = iosAppMetadata.bundleId; - *``` - */ - bundleId: string; - } - - /** - * A reference to a Firebase Android app. - * - * Do not call this constructor directly. Instead, use - * [`projectManagement.androidApp()`](admin.projectManagement.ProjectManagement#androidApp). - */ - interface AndroidApp { - appId: string; - - /** - * Retrieves metadata about this Android app. - * - * @return A promise that resolves to the retrieved metadata about this Android app. - */ - getMetadata(): Promise; - - /** - * Sets the optional user-assigned display name of the app. - * - * @param newDisplayName The new display name to set. - * - * @return A promise that resolves when the display name has been set. - */ - setDisplayName(newDisplayName: string): Promise; - - /** - * Gets the list of SHA certificates associated with this Android app in Firebase. - * - * @return The list of SHA-1 and SHA-256 certificates associated with this Android app in - * Firebase. - */ - getShaCertificates(): Promise; - - /** - * Adds the given SHA certificate to this Android app. - * - * @param certificateToAdd The SHA certificate to add. - * - * @return A promise that resolves when the given certificate - * has been added to the Android app. - */ - addShaCertificate(certificateToAdd: ShaCertificate): Promise; - - /** - * Deletes the specified SHA certificate from this Android app. - * - * @param certificateToDelete The SHA certificate to delete. - * - * @return A promise that resolves when the specified - * certificate has been removed from the Android app. - */ - deleteShaCertificate(certificateToRemove: ShaCertificate): Promise; - - /** - * Gets the configuration artifact associated with this app. - * - * @return A promise that resolves to the Android app's - * Firebase config file, in UTF-8 string format. This string is typically - * intended to be written to a JSON file that gets shipped with your Android - * app. - */ - getConfig(): Promise; - } - - /** - * A reference to a Firebase iOS app. - * - * Do not call this constructor directly. Instead, use - * [`projectManagement.iosApp()`](admin.projectManagement.ProjectManagement#iosApp). - */ - interface IosApp { - appId: string; - - /** - * Retrieves metadata about this iOS app. - * - * @return {!Promise} A promise that - * resolves to the retrieved metadata about this iOS app. - */ - getMetadata(): Promise; - - /** - * Sets the optional user-assigned display name of the app. - * - * @param newDisplayName The new display name to set. - * - * @return A promise that resolves when the display name has - * been set. - */ - setDisplayName(newDisplayName: string): Promise; - - /** - * Gets the configuration artifact associated with this app. - * - * @return A promise that resolves to the iOS app's Firebase - * config file, in UTF-8 string format. This string is typically intended to - * be written to a plist file that gets shipped with your iOS app. - */ - getConfig(): Promise; - } - - /** - * The Firebase ProjectManagement service interface. - * - * Do not call this constructor directly. Instead, use - * [`admin.projectManagement()`](admin.projectManagement#projectManagement). - */ - interface ProjectManagement { - app: _admin.app.App; - - /** - * Lists up to 100 Firebase apps associated with this Firebase project. - * - * @return A promise that resolves to the metadata list of the apps. - */ - listAppMetadata(): Promise; - - /** - * Lists up to 100 Firebase Android apps associated with this Firebase project. - * - * @return The list of Android apps. - */ - listAndroidApps(): Promise; - - /** - * Lists up to 100 Firebase iOS apps associated with this Firebase project. - * - * @return The list of iOS apps. - */ - listIosApps(): Promise; - - /** - * Creates an `AndroidApp` object, referencing the specified Android app within - * this Firebase project. - * - * This method does not perform an RPC. - * - * @param appId The `appId` of the Android app to reference. - * - * @return An `AndroidApp` object that references the specified Firebase Android app. - */ - androidApp(appId: string): admin.projectManagement.AndroidApp; - - /** - * Update the display name of this Firebase project. - * - * @param newDisplayName The new display name to be updated. - * - * @return A promise that resolves when the project display name has been updated. - */ - setDisplayName(newDisplayName: string): Promise; - - /** - * Creates an `iOSApp` object, referencing the specified iOS app within - * this Firebase project. - * - * This method does not perform an RPC. - * - * @param appId The `appId` of the iOS app to reference. - * - * @return An `iOSApp` object that references the specified Firebase iOS app. - */ - iosApp(appId: string): admin.projectManagement.IosApp; - - /** - * Creates a `ShaCertificate` object. - * - * This method does not perform an RPC. - * - * @param shaHash The SHA-1 or SHA-256 hash for this certificate. - * - * @return A `ShaCertificate` object contains the specified SHA hash. - */ - shaCertificate(shaHash: string): admin.projectManagement.ShaCertificate; - - /** - * Creates a new Firebase Android app associated with this Firebase project. - * - * @param packageName The canonical package name of the Android App, - * as would appear in the Google Play Developer Console. - * @param displayName An optional user-assigned display name for this - * new app. - * - * @return A promise that resolves to the newly created Android app. - */ - createAndroidApp( - packageName: string, displayName?: string): Promise; - - /** - * Creates a new Firebase iOS app associated with this Firebase project. - * - * @param bundleId The iOS app bundle ID to use for this new app. - * @param displayName An optional user-assigned display name for this - * new app. - * - * @return A promise that resolves to the newly created iOS app. - */ - createIosApp(bundleId: string, displayName?: string): Promise; - } -} diff --git a/src/project-management/android-app.ts b/src/project-management/android-app.ts index 793d9137f3..8afba57094 100644 --- a/src/project-management/android-app.ts +++ b/src/project-management/android-app.ts @@ -17,9 +17,14 @@ import { FirebaseProjectManagementError } from '../utils/error'; import * as validator from '../utils/validator'; import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; -import { AndroidAppMetadata, AppPlatform } from './app-metadata'; +import { projectManagement } from './index'; -export class AndroidApp { +import AndroidAppInterface = projectManagement.AndroidApp; +import AndroidAppMetadata = projectManagement.AndroidAppMetadata; +import AppPlatform = projectManagement.AppPlatform; +import ShaCertificateInterface = projectManagement.ShaCertificate; + +export class AndroidApp implements AndroidAppInterface { private readonly resourceName: string; constructor( @@ -108,7 +113,7 @@ export class AndroidApp { validator.isNonEmptyString(certificateJson[requiredField]), responseData, `getShaCertificates()'s responseData.certificates[].${requiredField} must be a ` - + `non-empty string.`); + + 'non-empty string.'); }); return new ShaCertificate(certificateJson.shaHash, certificateJson.name); @@ -166,7 +171,7 @@ export class AndroidApp { assertServerResponse( validator.isBase64String(base64ConfigFileContents), responseData, - `getConfig()'s responseData.configFileContents must be a base64 string.`); + 'getConfig()\'s responseData.configFileContents must be a base64 string.'); return Buffer.from(base64ConfigFileContents, 'base64').toString('utf8'); }); @@ -177,9 +182,9 @@ export class AndroidApp { * A SHA-1 or SHA-256 certificate. * * Do not call this constructor directly. Instead, use - * [`projectManagement.shaCertificate()`](admin.projectManagement.ProjectManagement#shaCertificate). + * [`projectManagement.shaCertificate()`](projectManagement.ProjectManagement#shaCertificate). */ -export class ShaCertificate { +export class ShaCertificate implements ShaCertificateInterface { /** * The SHA certificate type. * diff --git a/src/project-management/app-metadata.ts b/src/project-management/app-metadata.ts deleted file mode 100644 index 42be9c6111..0000000000 --- a/src/project-management/app-metadata.ts +++ /dev/null @@ -1,130 +0,0 @@ -/*! - * Copyright 2019 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. - */ - -/** - * Platforms with which a Firebase App can be associated. - */ -export enum AppPlatform { - - /** - * Unknown state. This is only used for distinguishing unset values. - */ - PLATFORM_UNKNOWN = 'PLATFORM_UNKNOWN', - - /** - * The Firebase App is associated with iOS. - */ - IOS = 'IOS', - - /** - * The Firebase App is associated with Android. - */ - ANDROID = 'ANDROID', -} - -/** - * Metadata about a Firebase app. - */ -export interface AppMetadata { - - /** - * The globally unique, Firebase-assigned identifier of the app. - * - * @example - * ```javascript - * var appId = appMetadata.appId; - * ``` - */ - appId: string; - - /** - * The optional user-assigned display name of the app. - * - * @example - * ```javascript - * var displayName = appMetadata.displayName; - * ``` - */ - displayName?: string; - - /** - * The development platform of the app. Supporting Android and iOS app platforms. - * - * @example - * ```javascript - * var platform = AppPlatform.ANDROID; - * ``` - */ - platform: AppPlatform; - - /** - * The globally unique, user-assigned ID of the parent project for the app. - * - * @example - * ```javascript - * var projectId = appMetadata.projectId; - * ``` - */ - projectId: string; - - /** - * The fully-qualified resource name that identifies this app. - * - * This is useful when manually constructing requests for Firebase's public API. - * - * @example - * ```javascript - * var resourceName = androidAppMetadata.resourceName; - * ``` - */ - resourceName: string; -} - -/** - * Metadata about a Firebase iOS App. - */ -export interface IosAppMetadata extends AppMetadata { - platform: AppPlatform.IOS; - - /** - * The canonical bundle ID of the iOS App as it would appear in the iOS App Store. - * - * @example - * ```javascript - * var bundleId = iosAppMetadata.bundleId; - *``` - */ - bundleId: string; -} - -/** - * Metadata about a Firebase Android App. - */ -export interface AndroidAppMetadata extends AppMetadata { - - platform: AppPlatform.ANDROID; - - /** - * The canonical package name of the Android App, as would appear in the Google Play Developer - * Console. - * - * @example - * ```javascript - * var packageName = androidAppMetadata.packageName; - * ``` - */ - packageName: string; -} diff --git a/src/project-management/index.ts b/src/project-management/index.ts index 7f9bb1a3c5..2c905d6c22 100644 --- a/src/project-management/index.ts +++ b/src/project-management/index.ts @@ -14,41 +14,391 @@ * limitations under the License. */ -import { FirebaseApp } from '../firebase-app'; -import * as androidAppApi from './android-app'; -import * as appMetadataApi from './app-metadata'; -import * as iosAppApi from './ios-app'; -import * as projectManagementApi from './project-management'; -import * as firebaseAdmin from '../index'; - -export function projectManagement(app?: FirebaseApp): projectManagementApi.ProjectManagement { - if (typeof(app) === 'undefined') { - app = firebaseAdmin.app(); - } - return app.projectManagement(); -} +import { app } from '../firebase-namespace-api'; /** - * We must define a namespace to make the typings work correctly. Otherwise - * `admin.projectManagement()` cannot be called like a function. Temporarily, - * admin.projectManagement is used as the namespace name because we cannot barrel - * re-export the contents from instance-id, and we want it to - * match the namespacing in the re-export inside src/index.d.ts + * Gets the {@link projectManagement.ProjectManagement + * `ProjectManagement`} service for the default app or a given app. + * + * `admin.projectManagement()` can be called with no arguments to access the + * default app's {@link projectManagement.ProjectManagement + * `ProjectManagement`} service, or as `admin.projectManagement(app)` to access + * the {@link projectManagement.ProjectManagement `ProjectManagement`} + * service associated with a specific app. + * + * @example + * ```javascript + * // Get the ProjectManagement service for the default app + * var defaultProjectManagement = admin.projectManagement(); + * ``` + * + * @example + * ```javascript + * // Get the ProjectManagement service for a given app + * var otherProjectManagement = admin.projectManagement(otherApp); + * ``` + * + * @param app Optional app whose `ProjectManagement` service + * to return. If not provided, the default `ProjectManagement` service will + * be returned. * + * @return The default `ProjectManagement` service if no app is provided or the + * `ProjectManagement` service associated with the provided app. */ +export declare function projectManagement(app?: app.App): projectManagement.ProjectManagement; + /* eslint-disable @typescript-eslint/no-namespace */ -export namespace admin.projectManagement { - // See https://github.com/microsoft/TypeScript/issues/4336 - /* eslint-disable @typescript-eslint/no-unused-vars */ - // See https://github.com/typescript-eslint/typescript-eslint/issues/363 - export import AndroidAppMetadata = appMetadataApi.AndroidAppMetadata - export import AppMetadata = appMetadataApi.AppMetadata - export import AppPlatform = appMetadataApi.AppPlatform - export import IosAppMetadata = appMetadataApi.IosAppMetadata - - // Allows for exposing classes as interfaces in typings - /* eslint-disable @typescript-eslint/no-empty-interface */ - export interface AndroidApp extends androidAppApi.AndroidApp {} - export interface IosApp extends iosAppApi.IosApp {} - export interface ProjectManagement extends projectManagementApi.ProjectManagement {} - export interface ShaCertificate extends androidAppApi.ShaCertificate {} +export namespace projectManagement { + /** + * Metadata about a Firebase Android App. + */ + export interface AndroidAppMetadata extends AppMetadata { + + platform: AppPlatform.ANDROID; + + /** + * The canonical package name of the Android App, as would appear in the Google Play Developer + * Console. + * + * @example + * ```javascript + * var packageName = androidAppMetadata.packageName; + * ``` + */ + packageName: string; + } + + /** + * Metadata about a Firebase app. + */ + export interface AppMetadata { + /** + * The globally unique, Firebase-assigned identifier of the app. + * + * @example + * ```javascript + * var appId = appMetadata.appId; + * ``` + */ + appId: string; + + /** + * The optional user-assigned display name of the app. + * + * @example + * ```javascript + * var displayName = appMetadata.displayName; + * ``` + */ + displayName?: string; + + /** + * The development platform of the app. Supporting Android and iOS app platforms. + * + * @example + * ```javascript + * var platform = AppPlatform.ANDROID; + * ``` + */ + platform: AppPlatform; + + /** + * The globally unique, user-assigned ID of the parent project for the app. + * + * @example + * ```javascript + * var projectId = appMetadata.projectId; + * ``` + */ + projectId: string; + + /** + * The fully-qualified resource name that identifies this app. + * + * This is useful when manually constructing requests for Firebase's public API. + * + * @example + * ```javascript + * var resourceName = androidAppMetadata.resourceName; + * ``` + */ + resourceName: string; + } + + /** + * Platforms with which a Firebase App can be associated. + */ + export enum AppPlatform { + /** + * Unknown state. This is only used for distinguishing unset values. + */ + PLATFORM_UNKNOWN = 'PLATFORM_UNKNOWN', + + /** + * The Firebase App is associated with iOS. + */ + IOS = 'IOS', + + /** + * The Firebase App is associated with Android. + */ + ANDROID = 'ANDROID', + } + + /** + * Metadata about a Firebase iOS App. + */ + export interface IosAppMetadata extends AppMetadata { + platform: AppPlatform.IOS; + + /** + * The canonical bundle ID of the iOS App as it would appear in the iOS App Store. + * + * @example + * ```javascript + * var bundleId = iosAppMetadata.bundleId; + *``` + */ + bundleId: string; + } + + /** + * A reference to a Firebase Android app. + * + * Do not call this constructor directly. Instead, use + * [`projectManagement.androidApp()`](projectManagement.ProjectManagement#androidApp). + */ + export interface AndroidApp { + appId: string; + + /** + * Retrieves metadata about this Android app. + * + * @return A promise that resolves to the retrieved metadata about this Android app. + */ + getMetadata(): Promise; + + /** + * Sets the optional user-assigned display name of the app. + * + * @param newDisplayName The new display name to set. + * + * @return A promise that resolves when the display name has been set. + */ + setDisplayName(newDisplayName: string): Promise; + + /** + * Gets the list of SHA certificates associated with this Android app in Firebase. + * + * @return The list of SHA-1 and SHA-256 certificates associated with this Android app in + * Firebase. + */ + getShaCertificates(): Promise; + + /** + * Adds the given SHA certificate to this Android app. + * + * @param certificateToAdd The SHA certificate to add. + * + * @return A promise that resolves when the given certificate + * has been added to the Android app. + */ + addShaCertificate(certificateToAdd: ShaCertificate): Promise; + + /** + * Deletes the specified SHA certificate from this Android app. + * + * @param certificateToDelete The SHA certificate to delete. + * + * @return A promise that resolves when the specified + * certificate has been removed from the Android app. + */ + deleteShaCertificate(certificateToRemove: ShaCertificate): Promise; + + /** + * Gets the configuration artifact associated with this app. + * + * @return A promise that resolves to the Android app's + * Firebase config file, in UTF-8 string format. This string is typically + * intended to be written to a JSON file that gets shipped with your Android + * app. + */ + getConfig(): Promise; + } + + /** + * A reference to a Firebase iOS app. + * + * Do not call this constructor directly. Instead, use + * [`projectManagement.iosApp()`](projectManagement.ProjectManagement#iosApp). + */ + export interface IosApp { + appId: string; + + /** + * Retrieves metadata about this iOS app. + * + * @return {!Promise} A promise that + * resolves to the retrieved metadata about this iOS app. + */ + getMetadata(): Promise; + + /** + * Sets the optional user-assigned display name of the app. + * + * @param newDisplayName The new display name to set. + * + * @return A promise that resolves when the display name has + * been set. + */ + setDisplayName(newDisplayName: string): Promise; + + /** + * Gets the configuration artifact associated with this app. + * + * @return A promise that resolves to the iOS app's Firebase + * config file, in UTF-8 string format. This string is typically intended to + * be written to a plist file that gets shipped with your iOS app. + */ + getConfig(): Promise; + } + + /** + * A SHA-1 or SHA-256 certificate. + * + * Do not call this constructor directly. Instead, use + * [`projectManagement.shaCertificate()`](projectManagement.ProjectManagement#shaCertificate). + */ + export interface ShaCertificate { + + /** + * The SHA certificate type. + * + * @example + * ```javascript + * var certType = shaCertificate.certType; + * ``` + */ + certType: ('sha1' | 'sha256'); + + /** + * The SHA-1 or SHA-256 hash for this certificate. + * + * @example + * ```javascript + * var shaHash = shaCertificate.shaHash; + * ``` + */ + shaHash: string; + + /** + * The fully-qualified resource name that identifies this sha-key. + * + * This is useful when manually constructing requests for Firebase's public API. + * + * @example + * ```javascript + * var resourceName = shaCertificate.resourceName; + * ``` + */ + resourceName?: string; + } + + /** + * The Firebase ProjectManagement service interface. + * + * Do not call this constructor directly. Instead, use + * [`admin.projectManagement()`](projectManagement#projectManagement). + */ + export interface ProjectManagement { + app: app.App; + + /** + * Lists up to 100 Firebase apps associated with this Firebase project. + * + * @return A promise that resolves to the metadata list of the apps. + */ + listAppMetadata(): Promise; + + /** + * Lists up to 100 Firebase Android apps associated with this Firebase project. + * + * @return The list of Android apps. + */ + listAndroidApps(): Promise; + + /** + * Lists up to 100 Firebase iOS apps associated with this Firebase project. + * + * @return The list of iOS apps. + */ + listIosApps(): Promise; + + /** + * Creates an `AndroidApp` object, referencing the specified Android app within + * this Firebase project. + * + * This method does not perform an RPC. + * + * @param appId The `appId` of the Android app to reference. + * + * @return An `AndroidApp` object that references the specified Firebase Android app. + */ + androidApp(appId: string): AndroidApp; + + /** + * Update the display name of this Firebase project. + * + * @param newDisplayName The new display name to be updated. + * + * @return A promise that resolves when the project display name has been updated. + */ + setDisplayName(newDisplayName: string): Promise; + + /** + * Creates an `iOSApp` object, referencing the specified iOS app within + * this Firebase project. + * + * This method does not perform an RPC. + * + * @param appId The `appId` of the iOS app to reference. + * + * @return An `iOSApp` object that references the specified Firebase iOS app. + */ + iosApp(appId: string): IosApp; + + /** + * Creates a `ShaCertificate` object. + * + * This method does not perform an RPC. + * + * @param shaHash The SHA-1 or SHA-256 hash for this certificate. + * + * @return A `ShaCertificate` object contains the specified SHA hash. + */ + shaCertificate(shaHash: string): ShaCertificate; + + /** + * Creates a new Firebase Android app associated with this Firebase project. + * + * @param packageName The canonical package name of the Android App, + * as would appear in the Google Play Developer Console. + * @param displayName An optional user-assigned display name for this + * new app. + * + * @return A promise that resolves to the newly created Android app. + */ + createAndroidApp( + packageName: string, displayName?: string): Promise; + + /** + * Creates a new Firebase iOS app associated with this Firebase project. + * + * @param bundleId The iOS app bundle ID to use for this new app. + * @param displayName An optional user-assigned display name for this + * new app. + * + * @return A promise that resolves to the newly created iOS app. + */ + createIosApp(bundleId: string, displayName?: string): Promise; + } } diff --git a/src/project-management/ios-app.ts b/src/project-management/ios-app.ts index 0f5955c1a1..b29af6326f 100644 --- a/src/project-management/ios-app.ts +++ b/src/project-management/ios-app.ts @@ -17,9 +17,13 @@ import { FirebaseProjectManagementError } from '../utils/error'; import * as validator from '../utils/validator'; import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; -import { IosAppMetadata, AppPlatform } from './app-metadata'; +import { projectManagement } from './index'; -export class IosApp { +import IosAppInterface = projectManagement.IosApp; +import IosAppMetadata = projectManagement.IosAppMetadata; +import AppPlatform = projectManagement.AppPlatform; + +export class IosApp implements IosAppInterface { private readonly resourceName: string; constructor( @@ -98,7 +102,7 @@ export class IosApp { assertServerResponse( validator.isBase64String(base64ConfigFileContents), responseData, - `getConfig()'s responseData.configFileContents must be a base64 string.`); + 'getConfig()\'s responseData.configFileContents must be a base64 string.'); return Buffer.from(base64ConfigFileContents, 'base64').toString('utf8'); }); diff --git a/src/project-management/project-management-api-request-internal.ts b/src/project-management/project-management-api-request-internal.ts index 3d4fc654ce..445faf83dc 100644 --- a/src/project-management/project-management-api-request-internal.ts +++ b/src/project-management/project-management-api-request-internal.ts @@ -173,11 +173,11 @@ export class ProjectManagementRequestHandler { assertServerResponse( validator.isNonNullObject(responseData), responseData, - `createAndroidApp's responseData must be a non-null object.`); + 'createAndroidApp\'s responseData must be a non-null object.'); assertServerResponse( validator.isNonEmptyString(responseData.name), responseData, - `createAndroidApp's responseData.name must be a non-empty string.`); + 'createAndroidApp\'s responseData.name must be a non-empty string.'); return this.pollRemoteOperationWithExponentialBackoff(responseData.name); }); } @@ -200,11 +200,11 @@ export class ProjectManagementRequestHandler { assertServerResponse( validator.isNonNullObject(responseData), responseData, - `createIosApp's responseData must be a non-null object.`); + 'createIosApp\'s responseData must be a non-null object.'); assertServerResponse( validator.isNonEmptyString(responseData.name), responseData, - `createIosApp's responseData.name must be a non-empty string.`); + 'createIosApp\'s responseData.name must be a non-empty string.'); return this.pollRemoteOperationWithExponentialBackoff(responseData.name); }); } diff --git a/src/project-management/project-management.ts b/src/project-management/project-management.ts index daade63172..0f76912109 100644 --- a/src/project-management/project-management.ts +++ b/src/project-management/project-management.ts @@ -22,7 +22,11 @@ import * as validator from '../utils/validator'; import { AndroidApp, ShaCertificate } from './android-app'; import { IosApp } from './ios-app'; import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; -import { AppMetadata, AppPlatform } from './app-metadata'; +import { projectManagement } from './index'; + +import AppMetadata = projectManagement.AppMetadata; +import AppPlatform = projectManagement.AppPlatform; +import ProjectManagementInterface = projectManagement.ProjectManagement; /** * Internals of a Project Management instance. @@ -43,9 +47,9 @@ class ProjectManagementInternals implements FirebaseServiceInternalsInterface { * The Firebase ProjectManagement service interface. * * Do not call this constructor directly. Instead, use - * [`admin.projectManagement()`](admin.projectManagement#projectManagement). + * [`admin.projectManagement()`](projectManagement#projectManagement). */ -export class ProjectManagement implements FirebaseServiceInterface { +export class ProjectManagement implements FirebaseServiceInterface, ProjectManagementInterface { public readonly INTERNAL: ProjectManagementInternals = new ProjectManagementInternals(); private readonly requestHandler: ProjectManagementRequestHandler; @@ -149,7 +153,7 @@ export class ProjectManagement implements FirebaseServiceInterface { assertServerResponse( validator.isNonEmptyString(responseData.appId), responseData, - `"responseData.appId" field must be present in createAndroidApp()'s response data.`); + '"responseData.appId" field must be present in createAndroidApp()\'s response data.'); return new AndroidApp(responseData.appId, this.requestHandler); }); } @@ -177,7 +181,7 @@ export class ProjectManagement implements FirebaseServiceInterface { assertServerResponse( validator.isNonEmptyString(responseData.appId), responseData, - `"responseData.appId" field must be present in createIosApp()'s response data.`); + '"responseData.appId" field must be present in createIosApp()\'s response data.'); return new IosApp(responseData.appId, this.requestHandler); }); } @@ -225,11 +229,11 @@ export class ProjectManagement implements FirebaseServiceInterface { assertServerResponse( validator.isNonEmptyString(appJson.appId), responseData, - `"apps[].appId" field must be present in the listAppMetadata() response data.`); + '"apps[].appId" field must be present in the listAppMetadata() response data.'); assertServerResponse( validator.isNonEmptyString(appJson.platform), responseData, - `"apps[].platform" field must be present in the listAppMetadata() response data.`); + '"apps[].platform" field must be present in the listAppMetadata() response data.'); const metadata: AppMetadata = { appId: appJson.appId, platform: (AppPlatform as any)[appJson.platform] || AppPlatform.PLATFORM_UNKNOWN, diff --git a/src/remote-config.d.ts b/src/remote-config.d.ts deleted file mode 100644 index 2da1d28336..0000000000 --- a/src/remote-config.d.ts +++ /dev/null @@ -1,349 +0,0 @@ -import * as _admin from './index.d'; - -export namespace admin.remoteConfig { - - /** - * Colors that are associated with conditions for display purposes. - */ - type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | - 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL'; - - /** - * Interface representing a Remote Config template. - */ - interface RemoteConfigTemplate { - /** - * A list of conditions in descending order by priority. - */ - conditions: RemoteConfigCondition[]; - - /** - * Map of parameter keys to their optional default values and optional conditional values. - */ - parameters: { [key: string]: RemoteConfigParameter }; - - /** - * Map of parameter group names to their parameter group objects. - * A group's name is mutable but must be unique among groups in the Remote Config template. - * The name is limited to 256 characters and intended to be human-readable. Any Unicode - * characters are allowed. - */ - parameterGroups: { [key: string]: RemoteConfigParameterGroup }; - - /** - * ETag of the current Remote Config template (readonly). - */ - readonly etag: string; - - /** - * Version information for the current Remote Config template. - */ - version?: Version; - } - - /** - * Interface representing a Remote Config parameter. - * At minimum, a `defaultValue` or a `conditionalValues` entry must be present for the - * parameter to have any effect. - */ - interface RemoteConfigParameter { - - /** - * The value to set the parameter to, when none of the named conditions evaluate to `true`. - */ - defaultValue?: RemoteConfigParameterValue; - - /** - * A `(condition name, value)` map. The condition name of the highest priority - * (the one listed first in the Remote Config template's conditions list) determines the value of - * this parameter. - */ - conditionalValues?: { [key: string]: RemoteConfigParameterValue }; - - /** - * A description for this parameter. Should not be over 100 characters and may contain any - * Unicode characters. - */ - description?: string; - } - - /** - * Interface representing a Remote Config parameter group. - * Grouping parameters is only for management purposes and does not affect client-side - * fetching of parameter values. - */ - export interface RemoteConfigParameterGroup { - /** - * A description for the group. Its length must be less than or equal to 256 characters. - * A description may contain any Unicode characters. - */ - description?: string; - - /** - * Map of parameter keys to their optional default values and optional conditional values for - * parameters that belong to this group. A parameter only appears once per - * Remote Config template. An ungrouped parameter appears at the top level, whereas a - * parameter organized within a group appears within its group's map of parameters. - */ - parameters: { [key: string]: RemoteConfigParameter }; - } - - /** - * Interface representing a Remote Config condition. - * A condition targets a specific group of users. A list of these conditions make up - * part of a Remote Config template. - */ - interface RemoteConfigCondition { - - /** - * A non-empty and unique name of this condition. - */ - name: string; - - /** - * The logic of this condition. - * See the documentation on - * {@link https://firebase.google.com/docs/remote-config/condition-reference condition expressions} - * for the expected syntax of this field. - */ - expression: string; - - /** - * The color associated with this condition for display purposes in the Firebase Console. - * Not specifying this value results in the console picking an arbitrary color to associate - * with the condition. - */ - tagColor?: TagColor; - } - - /** - * Interface representing an explicit parameter value. - */ - interface ExplicitParameterValue { - /** - * The `string` value that the parameter is set to. - */ - value: string; - } - - /** - * Interface representing an in-app-default value. - */ - interface InAppDefaultValue { - /** - * If `true`, the parameter is omitted from the parameter values returned to a client. - */ - useInAppDefault: boolean; - } - - /** - * Type representing a Remote Config parameter value. - * A `RemoteConfigParameterValue` could be either an `ExplicitParameterValue` or - * an `InAppDefaultValue`. - */ - type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; - - /** - * Interface representing a Remote Config template version. - * Output only, except for the version description. Contains metadata about a particular - * version of the Remote Config template. All fields are set at the time the specified Remote - * Config template is published. A version's description field may be specified in - * `publishTemplate` calls. - */ - export interface Version { - /** - * The version number of a Remote Config template. - */ - versionNumber?: string; - - /** - * The timestamp of when this version of the Remote Config template was written to the - * Remote Config backend. - */ - updateTime?: string; - - /** - * The origin of the template update action. - */ - updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | - 'REST_API' | 'ADMIN_SDK_NODE'); - - /** - * The type of the template update action. - */ - updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | - 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); - - /** - * Aggregation of all metadata fields about the account that performed the update. - */ - updateUser?: RemoteConfigUser; - - /** - * The user-provided description of the corresponding Remote Config template. - */ - description?: string; - - /** - * The version number of the Remote Config template that has become the current version - * due to a rollback. Only present if this version is the result of a rollback. - */ - rollbackSource?: string; - - /** - * Indicates whether this Remote Config template was published before version history was - * supported. - */ - isLegacy?: boolean; - } - - /** Interface representing a list of Remote Config template versions. */ - export interface ListVersionsResult { - /** - * A list of version metadata objects, sorted in reverse chronological order. - */ - versions: Version[]; - - /** - * Token to retrieve the next page of results, or empty if there are no more results - * in the list. - */ - nextPageToken?: string; - } - - /** Interface representing options for Remote Config list versions operation. */ - export interface ListVersionsOptions { - /** - * The maximum number of items to return per page. - */ - pageSize?: number; - - /** - * The `nextPageToken` value returned from a previous list versions request, if any. - */ - pageToken?: string; - - /** - * Specifies the newest version number to include in the results. - * If specified, must be greater than zero. Defaults to the newest version. - */ - endVersionNumber?: string | number; - - /** - * Specifies the earliest update time to include in the results. Any entries updated before this - * time are omitted. - */ - startTime?: Date | string; - - /** - * Specifies the latest update time to include in the results. Any entries updated on or after - * this time are omitted. - */ - endTime?: Date | string; - } - - /** Interface representing a Remote Config user.*/ - export interface RemoteConfigUser { - /** - * Email address. Output only. - */ - email: string; - - /** - * Display name. Output only. - */ - name?: string; - - /** - * Image URL. Output only. - */ - imageUrl?: string; - } - - /** - * The Firebase `RemoteConfig` service interface. - * - * Do not call this constructor directly. Instead, use - * [`admin.remoteConfig()`](admin.remoteConfig#remoteConfig). - */ - interface RemoteConfig { - app: _admin.app.App; - - /** - * Gets the current active version of the {@link admin.remoteConfig.RemoteConfigTemplate - * `RemoteConfigTemplate`} of the project. - * - * @return A promise that fulfills with a `RemoteConfigTemplate`. - */ - getTemplate(): Promise; - - /** - * Gets the requested version of the {@link admin.remoteConfig.RemoteConfigTemplate - * `RemoteConfigTemplate`} of the project. - * - * @param versionNumber Version number of the Remote Config template to look up. - * - * @return A promise that fulfills with a `RemoteConfigTemplate`. - */ - getTemplateAtVersion(versionNumber: number | string): Promise; - - /** - * Validates a {@link admin.remoteConfig.RemoteConfigTemplate `RemoteConfigTemplate`}. - * - * @param template The Remote Config template to be validated. - * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. - */ - validateTemplate(template: RemoteConfigTemplate): Promise; - - /** - * Publishes a Remote Config template. - * - * @param template The Remote Config template to be published. - * @param options Optional options object when publishing a Remote Config template: - * - {boolean} `force` Setting this to `true` forces the Remote Config template to - * be updated and circumvent the ETag. This approach is not recommended - * because it risks causing the loss of updates to your Remote Config - * template if multiple clients are updating the Remote Config template. - * See {@link https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates - * ETag usage and forced updates}. - * - * @return A Promise that fulfills with the published `RemoteConfigTemplate`. - */ - publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise; - - /** - * Rolls back a project's published Remote Config template to the specified version. - * A rollback is equivalent to getting a previously published Remote Config - * template and re-publishing it using a force update. - * - * @param versionNumber The version number of the Remote Config template to roll back to. - * The specified version number must be lower than the current version number, and not have - * been deleted due to staleness. Only the last 300 versions are stored. - * All versions that correspond to non-active Remote Config templates (that is, all except the - * template that is being fetched by clients) are also deleted if they are more than 90 days old. - * @return A promise that fulfills with the published `RemoteConfigTemplate`. - */ - rollback(versionNumber: string | number): Promise; - - /** - * Gets a list of Remote Config template versions that have been published, sorted in reverse - * chronological order. Only the last 300 versions are stored. - * All versions that correspond to non-active Remote Config templates (that is, all except the - * template that is being fetched by clients) are also deleted if they are more than 90 days old. - * - * @param options Optional {@link admin.remoteConfig.ListVersionsOptions `ListVersionsOptions`} - * object for getting a list of template versions. - * @return A promise that fulfills with a `ListVersionsResult`. - */ - listVersions(options?: ListVersionsOptions): Promise; - - /** - * Creates and returns a new Remote Config template from a JSON string. - * - * @param json The JSON string to populate a Remote Config template. - * - * @return A new template instance. - */ - createTemplateFromJSON(json: string): RemoteConfigTemplate; - } -} diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts index d9450b83b1..668fee3372 100644 --- a/src/remote-config/index.ts +++ b/src/remote-config/index.ts @@ -14,44 +14,386 @@ * limitations under the License. */ -import { FirebaseApp } from '../firebase-app'; -import * as remoteConfigApi from './remote-config'; -import * as remoteConfigClientApi from './remote-config-api-client'; -import * as firebaseAdmin from '../index'; - -export function remoteConfig(app?: FirebaseApp): remoteConfigApi.RemoteConfig { - if (typeof(app) === 'undefined') { - app = firebaseAdmin.app(); - } - return app.remoteConfig(); -} +import { app } from '../firebase-namespace-api'; /** - * We must define a namespace to make the typings work correctly. Otherwise - * `admin.remoteConfig()` cannot be called like a function. Temporarily, - * admin.remoteConfig is used as the namespace name because we cannot barrel - * re-export the contents from remote-config, and we want it to - * match the namespacing in the re-export inside src/index.d.ts + * Gets the {@link remoteConfig.RemoteConfig `RemoteConfig`} service for the + * default app or a given app. + * + * `admin.remoteConfig()` can be called with no arguments to access the default + * app's {@link remoteConfig.RemoteConfig `RemoteConfig`} service or as + * `admin.remoteConfig(app)` to access the + * {@link remoteConfig.RemoteConfig `RemoteConfig`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the `RemoteConfig` service for the default app + * var defaultRemoteConfig = admin.remoteConfig(); + * ``` + * + * @example + * ```javascript + * // Get the `RemoteConfig` service for a given app + * var otherRemoteConfig = admin.remoteConfig(otherApp); + * ``` + * + * @param app Optional app for which to return the `RemoteConfig` service. + * If not provided, the default `RemoteConfig` service is returned. + * + * @return The default `RemoteConfig` service if no + * app is provided, or the `RemoteConfig` service associated with the provided + * app. */ +export declare function remoteConfig(app?: app.App): remoteConfig.RemoteConfig; + /* eslint-disable @typescript-eslint/no-namespace */ -export namespace admin.remoteConfig { - // See https://github.com/microsoft/TypeScript/issues/4336 - /* eslint-disable @typescript-eslint/no-unused-vars */ - // See https://github.com/typescript-eslint/typescript-eslint/issues/363 - export import ExplicitParameterValue = remoteConfigClientApi.ExplicitParameterValue; - export import ListVersionsOptions = remoteConfigClientApi.ListVersionsOptions; - export import ListVersionsResult = remoteConfigClientApi.ListVersionsResult; - export import InAppDefaultValue = remoteConfigClientApi.InAppDefaultValue; - export import RemoteConfigCondition = remoteConfigClientApi.RemoteConfigCondition; - export import RemoteConfigParameter = remoteConfigClientApi.RemoteConfigParameter; - export import RemoteConfigParameterGroup = remoteConfigClientApi.RemoteConfigParameterGroup; - export import RemoteConfigParameterValue = remoteConfigClientApi.RemoteConfigParameterValue; - export import RemoteConfigTemplate = remoteConfigClientApi.RemoteConfigTemplate; - export import RemoteConfigUser = remoteConfigClientApi.RemoteConfigUser; - export import TagColor = remoteConfigClientApi.TagColor; - export import Version = remoteConfigClientApi.Version; - - // Allows for exposing classes as interfaces in typings - /* eslint-disable @typescript-eslint/no-empty-interface */ - export interface RemoteConfig extends remoteConfigApi.RemoteConfig {} +export namespace remoteConfig { + /** + * Interface representing options for Remote Config list versions operation. + */ + export interface ListVersionsOptions { + /** + * The maximum number of items to return per page. + */ + pageSize?: number; + + /** + * The `nextPageToken` value returned from a previous list versions request, if any. + */ + pageToken?: string; + + /** + * Specifies the newest version number to include in the results. + * If specified, must be greater than zero. Defaults to the newest version. + */ + endVersionNumber?: string | number; + + /** + * Specifies the earliest update time to include in the results. Any entries updated before this + * time are omitted. + */ + startTime?: Date | string; + + /** + * Specifies the latest update time to include in the results. Any entries updated on or after + * this time are omitted. + */ + endTime?: Date | string; + } + + /** + * Interface representing a list of Remote Config template versions. + */ + export interface ListVersionsResult { + /** + * A list of version metadata objects, sorted in reverse chronological order. + */ + versions: Version[]; + + /** + * Token to retrieve the next page of results, or empty if there are no more results + * in the list. + */ + nextPageToken?: string; + } + + /** + * Interface representing a Remote Config condition. + * A condition targets a specific group of users. A list of these conditions make up + * part of a Remote Config template. + */ + export interface RemoteConfigCondition { + + /** + * A non-empty and unique name of this condition. + */ + name: string; + + /** + * The logic of this condition. + * See the documentation on + * {@link https://firebase.google.com/docs/remote-config/condition-reference condition expressions} + * for the expected syntax of this field. + */ + expression: string; + + /** + * The color associated with this condition for display purposes in the Firebase Console. + * Not specifying this value results in the console picking an arbitrary color to associate + * with the condition. + */ + tagColor?: TagColor; + } + + /** + * Interface representing a Remote Config parameter. + * At minimum, a `defaultValue` or a `conditionalValues` entry must be present for the + * parameter to have any effect. + */ + export interface RemoteConfigParameter { + + /** + * The value to set the parameter to, when none of the named conditions evaluate to `true`. + */ + defaultValue?: RemoteConfigParameterValue; + + /** + * A `(condition name, value)` map. The condition name of the highest priority + * (the one listed first in the Remote Config template's conditions list) determines the value of + * this parameter. + */ + conditionalValues?: { [key: string]: RemoteConfigParameterValue }; + + /** + * A description for this parameter. Should not be over 100 characters and may contain any + * Unicode characters. + */ + description?: string; + } + + /** + * Interface representing a Remote Config parameter group. + * Grouping parameters is only for management purposes and does not affect client-side + * fetching of parameter values. + */ + export interface RemoteConfigParameterGroup { + /** + * A description for the group. Its length must be less than or equal to 256 characters. + * A description may contain any Unicode characters. + */ + description?: string; + + /** + * Map of parameter keys to their optional default values and optional conditional values for + * parameters that belong to this group. A parameter only appears once per + * Remote Config template. An ungrouped parameter appears at the top level, whereas a + * parameter organized within a group appears within its group's map of parameters. + */ + parameters: { [key: string]: RemoteConfigParameter }; + } + + /** + * Interface representing an explicit parameter value. + */ + export interface ExplicitParameterValue { + /** + * The `string` value that the parameter is set to. + */ + value: string; + } + + /** + * Interface representing an in-app-default value. + */ + export interface InAppDefaultValue { + /** + * If `true`, the parameter is omitted from the parameter values returned to a client. + */ + useInAppDefault: boolean; + } + + /** + * Type representing a Remote Config parameter value. + * A `RemoteConfigParameterValue` could be either an `ExplicitParameterValue` or + * an `InAppDefaultValue`. + */ + export type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; + + /** + * Interface representing a Remote Config template. + */ + export interface RemoteConfigTemplate { + /** + * A list of conditions in descending order by priority. + */ + conditions: RemoteConfigCondition[]; + + /** + * Map of parameter keys to their optional default values and optional conditional values. + */ + parameters: { [key: string]: RemoteConfigParameter }; + + /** + * Map of parameter group names to their parameter group objects. + * A group's name is mutable but must be unique among groups in the Remote Config template. + * The name is limited to 256 characters and intended to be human-readable. Any Unicode + * characters are allowed. + */ + parameterGroups: { [key: string]: RemoteConfigParameterGroup }; + + /** + * ETag of the current Remote Config template (readonly). + */ + readonly etag: string; + + /** + * Version information for the current Remote Config template. + */ + version?: Version; + } + + /** + * Interface representing a Remote Config user. + */ + export interface RemoteConfigUser { + /** + * Email address. Output only. + */ + email: string; + + /** + * Display name. Output only. + */ + name?: string; + + /** + * Image URL. Output only. + */ + imageUrl?: string; + } + + /** + * Colors that are associated with conditions for display purposes. + */ + export type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | + 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL'; + + /** + * Interface representing a Remote Config template version. + * Output only, except for the version description. Contains metadata about a particular + * version of the Remote Config template. All fields are set at the time the specified Remote + * Config template is published. A version's description field may be specified in + * `publishTemplate` calls. + */ + export interface Version { + /** + * The version number of a Remote Config template. + */ + versionNumber?: string; + + /** + * The timestamp of when this version of the Remote Config template was written to the + * Remote Config backend. + */ + updateTime?: string; + + /** + * The origin of the template update action. + */ + updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | + 'REST_API' | 'ADMIN_SDK_NODE'); + + /** + * The type of the template update action. + */ + updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | + 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); + + /** + * Aggregation of all metadata fields about the account that performed the update. + */ + updateUser?: RemoteConfigUser; + + /** + * The user-provided description of the corresponding Remote Config template. + */ + description?: string; + + /** + * The version number of the Remote Config template that has become the current version + * due to a rollback. Only present if this version is the result of a rollback. + */ + rollbackSource?: string; + + /** + * Indicates whether this Remote Config template was published before version history was + * supported. + */ + isLegacy?: boolean; + } + + /** + * The Firebase `RemoteConfig` service interface. + */ + export interface RemoteConfig { + app: app.App; + + /** + * Gets the current active version of the {@link remoteConfig.RemoteConfigTemplate + * `RemoteConfigTemplate`} of the project. + * + * @return A promise that fulfills with a `RemoteConfigTemplate`. + */ + getTemplate(): Promise; + + /** + * Gets the requested version of the {@link remoteConfig.RemoteConfigTemplate + * `RemoteConfigTemplate`} of the project. + * + * @param versionNumber Version number of the Remote Config template to look up. + * + * @return A promise that fulfills with a `RemoteConfigTemplate`. + */ + getTemplateAtVersion(versionNumber: number | string): Promise; + + /** + * Validates a {@link remoteConfig.RemoteConfigTemplate `RemoteConfigTemplate`}. + * + * @param template The Remote Config template to be validated. + * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. + */ + validateTemplate(template: RemoteConfigTemplate): Promise; + + /** + * Publishes a Remote Config template. + * + * @param template The Remote Config template to be published. + * @param options Optional options object when publishing a Remote Config template: + * - {boolean} `force` Setting this to `true` forces the Remote Config template to + * be updated and circumvent the ETag. This approach is not recommended + * because it risks causing the loss of updates to your Remote Config + * template if multiple clients are updating the Remote Config template. + * See {@link https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates + * ETag usage and forced updates}. + * + * @return A Promise that fulfills with the published `RemoteConfigTemplate`. + */ + publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise; + + /** + * Rolls back a project's published Remote Config template to the specified version. + * A rollback is equivalent to getting a previously published Remote Config + * template and re-publishing it using a force update. + * + * @param versionNumber The version number of the Remote Config template to roll back to. + * The specified version number must be lower than the current version number, and not have + * been deleted due to staleness. Only the last 300 versions are stored. + * All versions that correspond to non-active Remote Config templates (that is, all except the + * template that is being fetched by clients) are also deleted if they are more than 90 days old. + * @return A promise that fulfills with the published `RemoteConfigTemplate`. + */ + rollback(versionNumber: string | number): Promise; + + /** + * Gets a list of Remote Config template versions that have been published, sorted in reverse + * chronological order. Only the last 300 versions are stored. + * All versions that correspond to non-active Remote Config templates (that is, all except the + * template that is being fetched by clients) are also deleted if they are more than 90 days old. + * + * @param options Optional {@link remoteConfig.ListVersionsOptions `ListVersionsOptions`} + * object for getting a list of template versions. + * @return A promise that fulfills with a `ListVersionsResult`. + */ + listVersions(options?: ListVersionsOptions): Promise; + + /** + * Creates and returns a new Remote Config template from a JSON string. + * + * @param json The JSON string to populate a Remote Config template. + * + * @return A new template instance. + */ + createTemplateFromJSON(json: string): RemoteConfigTemplate; + } } diff --git a/src/remote-config/remote-config-api-client-internal.ts b/src/remote-config/remote-config-api-client-internal.ts index 315c9582b4..ac2d071453 100644 --- a/src/remote-config/remote-config-api-client-internal.ts +++ b/src/remote-config/remote-config-api-client-internal.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { RemoteConfigTemplate, ListVersionsOptions, ListVersionsResult } from './remote-config-api-client'; +import { remoteConfig } from './index'; import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, HttpResponse } from '../utils/api-request'; import { PrefixedFirebaseError } from '../utils/error'; import { FirebaseApp } from '../firebase-app'; @@ -22,6 +22,10 @@ import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { deepCopy } from '../utils/deep-copy'; +import RemoteConfigTemplate = remoteConfig.RemoteConfigTemplate; +import ListVersionsOptions = remoteConfig.ListVersionsOptions; +import ListVersionsResult = remoteConfig.ListVersionsResult; + // Remote Config backend constants const FIREBASE_REMOTE_CONFIG_V1_API = 'https://firebaseremoteconfig.googleapis.com/v1'; const FIREBASE_REMOTE_CONFIG_HEADERS = { @@ -95,7 +99,7 @@ export class RemoteConfigApiClient { template = this.validateInputRemoteConfigTemplate(template); return this.sendPutRequest(template, template.etag, true) .then((resp) => { - // validating a template returns an etag with the suffix -0 means that your update + // validating a template returns an etag with the suffix -0 means that your update // was successfully validated. We set the etag back to the original etag of the template // to allow future operations. this.validateEtag(resp.headers['etag']); @@ -260,7 +264,7 @@ export class RemoteConfigApiClient { * Removes output only properties from version metadata. * * @param {RemoteConfigTemplate} template A RemoteConfigTemplate object to be validated. - * + * * @returns {RemoteConfigTemplate} The validated RemoteConfigTemplate object. */ private validateInputRemoteConfigTemplate(template: RemoteConfigTemplate): RemoteConfigTemplate { @@ -303,7 +307,7 @@ export class RemoteConfigApiClient { * If valid, returns the string representation of the provided version number. * * @param {string|number} versionNumber A version number to be validated. - * + * * @returns {string} The validated version number as a string. */ private validateVersionNumber(versionNumber: string | number, propertyName = 'versionNumber'): string { @@ -332,9 +336,9 @@ export class RemoteConfigApiClient { /** * Checks if a given `ListVersionsOptions` object is valid. If successful, creates a copy of the * options object and convert `startTime` and `endTime` to RFC3339 UTC "Zulu" format, if present. - * + * * @param {ListVersionsOptions} options An options object to be validated. - * + * * @return {ListVersionsOptions} A copy of the provided options object with timestamps converted * to UTC Zulu format. */ @@ -406,7 +410,7 @@ interface Error { const ERROR_CODE_MAPPING: { [key: string]: RemoteConfigErrorCode } = { ABORTED: 'aborted', - ALREADY_EXISTS: `already-exists`, + ALREADY_EXISTS: 'already-exists', INVALID_ARGUMENT: 'invalid-argument', INTERNAL: 'internal-error', FAILED_PRECONDITION: 'failed-precondition', diff --git a/src/remote-config/remote-config-api-client.ts b/src/remote-config/remote-config-api-client.ts deleted file mode 100644 index 08621f972b..0000000000 --- a/src/remote-config/remote-config-api-client.ts +++ /dev/null @@ -1,284 +0,0 @@ -/*! - * Copyright 2020 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. - */ - -/** - * Colors that are associated with conditions for display purposes. - */ -export enum TagColor { - BLUE = "Blue", - BROWN = "Brown", - CYAN = "Cyan", - DEEP_ORANGE = "Red Orange", - GREEN = "Green", - INDIGO = "Indigo", - LIME = "Lime", - ORANGE = "Orange", - PINK = "Pink", - PURPLE = "Purple", - TEAL = "Teal", -} - -/** - * Interface representing an explicit parameter value. - */ -export interface ExplicitParameterValue { - /** - * The `string` value that the parameter is set to. - */ - value: string; -} - -/** - * Interface representing an in-app-default value. - */ -export interface InAppDefaultValue { - /** - * If `true`, the parameter is omitted from the parameter values returned to a client. - */ - useInAppDefault: boolean; -} - -/** - * Type representing a Remote Config parameter value. - * A `RemoteConfigParameterValue` could be either an `ExplicitParameterValue` or - * an `InAppDefaultValue`. - */ -export type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; - -/** - * Interface representing a Remote Config parameter. - * At minimum, a `defaultValue` or a `conditionalValues` entry must be present for the - * parameter to have any effect. - */ -export interface RemoteConfigParameter { - - /** - * The value to set the parameter to, when none of the named conditions evaluate to `true`. - */ - defaultValue?: RemoteConfigParameterValue; - - /** - * A `(condition name, value)` map. The condition name of the highest priority - * (the one listed first in the Remote Config template's conditions list) determines the value of - * this parameter. - */ - conditionalValues?: { [key: string]: RemoteConfigParameterValue }; - - /** - * A description for this parameter. Should not be over 100 characters and may contain any - * Unicode characters. - */ - description?: string; -} - -/** - * Interface representing a Remote Config parameter group. - * Grouping parameters is only for management purposes and does not affect client-side - * fetching of parameter values. - */ -export interface RemoteConfigParameterGroup { - /** - * A description for the group. Its length must be less than or equal to 256 characters. - * A description may contain any Unicode characters. - */ - description?: string; - - /** - * Map of parameter keys to their optional default values and optional conditional values for - * parameters that belong to this group. A parameter only appears once per - * Remote Config template. An ungrouped parameter appears at the top level, whereas a - * parameter organized within a group appears within its group's map of parameters. - */ - parameters: { [key: string]: RemoteConfigParameter }; -} - -/** - * Interface representing a Remote Config condition. - * A condition targets a specific group of users. A list of these conditions make up - * part of a Remote Config template. - */ -export interface RemoteConfigCondition { - - /** - * A non-empty and unique name of this condition. - */ - name: string; - - /** - * The logic of this condition. - * See the documentation on - * {@link https://firebase.google.com/docs/remote-config/condition-reference condition expressions} - * for the expected syntax of this field. - */ - expression: string; - - /** - * The color associated with this condition for display purposes in the Firebase Console. - * Not specifying this value results in the console picking an arbitrary color to associate - * with the condition. - */ - tagColor?: TagColor; -} - -/** - * Interface representing a Remote Config template. - */ -export interface RemoteConfigTemplate { - /** - * A list of conditions in descending order by priority. - */ - conditions: RemoteConfigCondition[]; - - /** - * Map of parameter keys to their optional default values and optional conditional values. - */ - parameters: { [key: string]: RemoteConfigParameter }; - - /** - * Map of parameter group names to their parameter group objects. - * A group's name is mutable but must be unique among groups in the Remote Config template. - * The name is limited to 256 characters and intended to be human-readable. Any Unicode - * characters are allowed. - */ - parameterGroups: { [key: string]: RemoteConfigParameterGroup }; - - /** - * ETag of the current Remote Config template (readonly). - */ - readonly etag: string; - - /** - * Version information for the current Remote Config template. - */ - version?: Version; -} - -/** - * Interface representing a Remote Config template version. - * Output only, except for the version description. Contains metadata about a particular - * version of the Remote Config template. All fields are set at the time the specified Remote - * Config template is published. A version's description field may be specified in - * `publishTemplate` calls. - */ -export interface Version { - /** - * The version number of a Remote Config template. - */ - versionNumber?: string; - - /** - * The timestamp of when this version of the Remote Config template was written to the - * Remote Config backend. - */ - updateTime?: string; - - /** - * The origin of the template update action. - */ - updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | - 'REST_API' | 'ADMIN_SDK_NODE'); - - /** - * The type of the template update action. - */ - updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | - 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); - - /** - * Aggregation of all metadata fields about the account that performed the update. - */ - updateUser?: RemoteConfigUser; - - /** - * The user-provided description of the corresponding Remote Config template. - */ - description?: string; - - /** - * The version number of the Remote Config template that has become the current version - * due to a rollback. Only present if this version is the result of a rollback. - */ - rollbackSource?: string; - - /** - * Indicates whether this Remote Config template was published before version history was - * supported. - */ - isLegacy?: boolean; -} - -/** Interface representing a list of Remote Config template versions. */ -export interface ListVersionsResult { - /** - * A list of version metadata objects, sorted in reverse chronological order. - */ - versions: Version[]; - - /** - * Token to retrieve the next page of results, or empty if there are no more results - * in the list. - */ - nextPageToken?: string; -} - -/** Interface representing options for Remote Config list versions operation. */ -export interface ListVersionsOptions { - /** - * The maximum number of items to return per page. - */ - pageSize?: number; - - /** - * The `nextPageToken` value returned from a previous list versions request, if any. - */ - pageToken?: string; - - /** - * Specifies the newest version number to include in the results. - * If specified, must be greater than zero. Defaults to the newest version. - */ - endVersionNumber?: string | number; - - /** - * Specifies the earliest update time to include in the results. Any entries updated before this - * time are omitted. - */ - startTime?: Date | string; - - /** - * Specifies the latest update time to include in the results. Any entries updated on or after - * this time are omitted. - */ - endTime?: Date | string; -} - -/** Interface representing a Remote Config user.*/ -export interface RemoteConfigUser { - /** - * Email address. Output only. - */ - email: string; - - /** - * Display name. Output only. - */ - name?: string; - - /** - * Image URL. Output only. - */ - imageUrl?: string; -} diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 4c67189a9f..d8d194d26e 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -17,18 +17,19 @@ import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { FirebaseApp } from '../firebase-app'; import * as validator from '../utils/validator'; -import { - RemoteConfigTemplate, - RemoteConfigParameter, - RemoteConfigCondition, - RemoteConfigParameterGroup, - ListVersionsOptions, - ListVersionsResult, - RemoteConfigUser, - Version, -} from './remote-config-api-client'; +import { remoteConfig } from './index'; import { FirebaseRemoteConfigError, RemoteConfigApiClient } from './remote-config-api-client-internal'; +import RemoteConfigTemplate = remoteConfig.RemoteConfigTemplate; +import RemoteConfigParameter = remoteConfig.RemoteConfigParameter; +import RemoteConfigCondition = remoteConfig.RemoteConfigCondition; +import RemoteConfigParameterGroup = remoteConfig.RemoteConfigParameterGroup; +import ListVersionsOptions = remoteConfig.ListVersionsOptions; +import ListVersionsResult = remoteConfig.ListVersionsResult; +import RemoteConfigUser = remoteConfig.RemoteConfigUser; +import Version = remoteConfig.Version; +import RemoteConfigInterface = remoteConfig.RemoteConfig; + /** * Internals of an RemoteConfig service instance. */ @@ -47,13 +48,13 @@ class RemoteConfigInternals implements FirebaseServiceInternalsInterface { /** * Remote Config service bound to the provided app. */ -export class RemoteConfig implements FirebaseServiceInterface { +export class RemoteConfig implements FirebaseServiceInterface, RemoteConfigInterface { public readonly INTERNAL: RemoteConfigInternals = new RemoteConfigInternals(); private readonly client: RemoteConfigApiClient; /** - * @param {FirebaseApp} app The app for this RemoteConfig service. + * @param app The app for this RemoteConfig service. * @constructor */ constructor(readonly app: FirebaseApp) { @@ -61,7 +62,7 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Gets the current active version of the {@link admin.remoteConfig.RemoteConfigTemplate + * Gets the current active version of the {@link remoteConfig.RemoteConfigTemplate * `RemoteConfigTemplate`} of the project. * * @return A promise that fulfills with a `RemoteConfigTemplate`. @@ -74,11 +75,11 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Gets the requested version of the {@link admin.remoteConfig.RemoteConfigTemplate + * Gets the requested version of the {@link remoteConfig.RemoteConfigTemplate * `RemoteConfigTemplate`} of the project. - * + * * @param versionNumber Version number of the Remote Config template to look up. - * + * * @return A promise that fulfills with a `RemoteConfigTemplate`. */ public getTemplateAtVersion(versionNumber: number | string): Promise { @@ -89,7 +90,7 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Validates a {@link admin.remoteConfig.RemoteConfigTemplate `RemoteConfigTemplate`}. + * Validates a {@link remoteConfig.RemoteConfigTemplate `RemoteConfigTemplate`}. * * @param template The Remote Config template to be validated. * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. @@ -126,7 +127,7 @@ export class RemoteConfig implements FirebaseServiceInterface { * Rolls back a project's published Remote Config template to the specified version. * A rollback is equivalent to getting a previously published Remote Config * template and re-publishing it using a force update. - * + * * @param versionNumber The version number of the Remote Config template to roll back to. * The specified version number must be lower than the current version number, and not have * been deleted due to staleness. Only the last 300 versions are stored. @@ -142,12 +143,12 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Gets a list of Remote Config template versions that have been published, sorted in reverse + * Gets a list of Remote Config template versions that have been published, sorted in reverse * chronological order. Only the last 300 versions are stored. - * All versions that correspond to non-active Remote Config templates (i.e., all except the + * All versions that correspond to non-active Remote Config templates (i.e., all except the * template that is being fetched by clients) are also deleted if they are older than 90 days. - * - * @param {ListVersionsOptions} options Optional options object for getting a list of versions. + * + * @param options Optional options object for getting a list of versions. * @return A promise that fulfills with a `ListVersionsResult`. */ public listVersions(options?: ListVersionsOptions): Promise { diff --git a/src/security-rules.d.ts b/src/security-rules.d.ts deleted file mode 100644 index f07a737431..0000000000 --- a/src/security-rules.d.ts +++ /dev/null @@ -1,191 +0,0 @@ -import * as _admin from './index.d'; - -export namespace admin.securityRules { - /** - * A source file containing some Firebase security rules. The content includes raw - * source code including text formatting, indentation and comments. Use the - * [`securityRules.createRulesFileFromSource()`](admin.securityRules.SecurityRules#createRulesFileFromSource) - * method to create new instances of this type. - */ - interface RulesFile { - readonly name: string; - readonly content: string; - } - - /** - * Required metadata associated with a ruleset. - */ - interface RulesetMetadata { - /** - * Name of the `Ruleset` as a short string. This can be directly passed into APIs - * like [`securityRules.getRuleset()`](admin.securityRules.SecurityRules#getRuleset) - * and [`securityRules.deleteRuleset()`](admin.securityRules.SecurityRules#deleteRuleset). - */ - readonly name: string; - - /** - * Creation time of the `Ruleset` as a UTC timestamp string. - */ - readonly createTime: string; - } - - /** - * A set of Firebase security rules. - */ - interface Ruleset extends RulesetMetadata { - readonly source: admin.securityRules.RulesFile[]; - } - - interface RulesetMetadataList { - /** - * A batch of ruleset metadata. - */ - readonly rulesets: admin.securityRules.RulesetMetadata[]; - - /** - * The next page token if available. This is needed to retrieve the next batch. - */ - readonly nextPageToken?: string; - } - - /** - * The Firebase `SecurityRules` service interface. - * - * Do not call this constructor directly. Instead, use - * [`admin.securityRules()`](admin.securityRules#securityRules). - */ - interface SecurityRules { - app: _admin.app.App; - - /** - * Creates a {@link admin.securityRules.RulesFile `RuleFile`} with the given name - * and source. Throws an error if any of the arguments are invalid. This is a local - * operation, and does not involve any network API calls. - * - * @example - * ```javascript - * const source = '// Some rules source'; - * const rulesFile = admin.securityRules().createRulesFileFromSource( - * 'firestore.rules', source); - * ``` - * - * @param name Name to assign to the rules file. This is usually a short file name that - * helps identify the file in a ruleset. - * @param source Contents of the rules file. - * @return A new rules file instance. - */ - createRulesFileFromSource(name: string, source: string | Buffer): admin.securityRules.RulesFile; - - /** - * Creates a new {@link admin.securityRules.Ruleset `Ruleset`} from the given - * {@link admin.securityRules.RulesFile `RuleFile`}. - * - * @param file Rules file to include in the new `Ruleset`. - * @returns A promise that fulfills with the newly created `Ruleset`. - */ - createRuleset(file: admin.securityRules.RulesFile): Promise; - - /** - * Gets the {@link admin.securityRules.Ruleset `Ruleset`} identified by the given - * name. The input name should be the short name string without the project ID - * prefix. For example, to retrieve the `projects/project-id/rulesets/my-ruleset`, - * pass the short name "my-ruleset". Rejects with a `not-found` error if the - * specified `Ruleset` cannot be found. - * - * @param name Name of the `Ruleset` to retrieve. - * @return A promise that fulfills with the specified `Ruleset`. - */ - getRuleset(name: string): Promise; - - /** - * Deletes the {@link admin.securityRules.Ruleset `Ruleset`} identified by the given - * name. The input name should be the short name string without the project ID - * prefix. For example, to delete the `projects/project-id/rulesets/my-ruleset`, - * pass the short name "my-ruleset". Rejects with a `not-found` error if the - * specified `Ruleset` cannot be found. - * - * @param name Name of the `Ruleset` to delete. - * @return A promise that fulfills when the `Ruleset` is deleted. - */ - deleteRuleset(name: string): Promise; - - /** - * Retrieves a page of ruleset metadata. - * - * @param pageSize The page size, 100 if undefined. This is also the maximum allowed - * limit. - * @param nextPageToken The next page token. If not specified, returns rulesets - * starting without any offset. - * @return A promise that fulfills with a page of rulesets. - */ - listRulesetMetadata( - pageSize?: number, nextPageToken?: string): Promise; - - /** - * Gets the {@link admin.securityRules.Ruleset `Ruleset`} currently applied to - * Cloud Firestore. Rejects with a `not-found` error if no ruleset is applied - * on Firestore. - * - * @return A promise that fulfills with the Firestore ruleset. - */ - getFirestoreRuleset(): Promise; - - /** - * Creates a new {@link admin.securityRules.Ruleset `Ruleset`} from the given - * source, and applies it to Cloud Firestore. - * - * @param source Rules source to apply. - * @return A promise that fulfills when the ruleset is created and released. - */ - releaseFirestoreRulesetFromSource(source: string | Buffer): Promise; - - /** - * Applies the specified {@link admin.securityRules.Ruleset `Ruleset`} ruleset - * to Cloud Firestore. - * - * @param ruleset Name of the ruleset to apply or a `RulesetMetadata` object - * containing the name. - * @return A promise that fulfills when the ruleset is released. - */ - releaseFirestoreRuleset(ruleset: string | admin.securityRules.RulesetMetadata): Promise; - - /** - * Gets the {@link admin.securityRules.Ruleset `Ruleset`} currently applied to a - * Cloud Storage bucket. Rejects with a `not-found` error if no ruleset is applied - * on the bucket. - * - * @param bucket Optional name of the Cloud Storage bucket to be retrieved. If not - * specified, retrieves the ruleset applied on the default bucket configured via - * `AppOptions`. - * @return A promise that fulfills with the Cloud Storage ruleset. - */ - getStorageRuleset(bucket?: string): Promise; - - /** - * Creates a new {@link admin.securityRules.Ruleset `Ruleset`} from the given - * source, and applies it to a Cloud Storage bucket. - * - * @param source Rules source to apply. - * @param bucket Optional name of the Cloud Storage bucket to apply the rules on. If - * not specified, applies the ruleset on the default bucket configured via - * {@link admin.AppOptions `AppOptions`}. - * @return A promise that fulfills when the ruleset is created and released. - */ - releaseStorageRulesetFromSource( - source: string | Buffer, bucket?: string): Promise; - - /** - * Applies the specified {@link admin.securityRules.Ruleset `Ruleset`} ruleset - * to a Cloud Storage bucket. - * - * @param ruleset Name of the ruleset to apply or a `RulesetMetadata` object - * containing the name. - * @param bucket Optional name of the Cloud Storage bucket to apply the rules on. If - * not specified, applies the ruleset on the default bucket configured via - * {@link admin.AppOptions `AppOptions`}. - * @return A promise that fulfills when the ruleset is released. - */ - releaseStorageRuleset( - ruleset: string | admin.securityRules.RulesetMetadata, bucket?: string): Promise; - } -} diff --git a/src/security-rules/index.ts b/src/security-rules/index.ts index ecdae9d8f5..2871f7873b 100644 --- a/src/security-rules/index.ts +++ b/src/security-rules/index.ts @@ -14,34 +14,223 @@ * limitations under the License. */ -import { FirebaseApp } from '../firebase-app'; -import * as securityRulesApi from './security-rules'; -import * as firebaseAdmin from '../index'; - -export function securityRules(app?: FirebaseApp): securityRulesApi.SecurityRules { - if (typeof(app) === 'undefined') { - app = firebaseAdmin.app(); - } - return app.securityRules(); -} +import { app } from '../firebase-namespace-api'; /** - * We must define a namespace to make the typings work correctly. Otherwise - * `admin.securityRules()` cannot be called like a function. Temporarily, - * admin.securityRules is used as the namespace name because we cannot barrel - * re-export the contents from security-rules, and we want it to - * match the namespacing in the re-export inside src/index.d.ts + * Gets the {@link securityRules.SecurityRules + * `SecurityRules`} service for the default app or a given app. + * + * `admin.securityRules()` can be called with no arguments to access the + * default app's {@link securityRules.SecurityRules + * `SecurityRules`} service, or as `admin.securityRules(app)` to access + * the {@link securityRules.SecurityRules `SecurityRules`} + * service associated with a specific app. + * + * @example + * ```javascript + * // Get the SecurityRules service for the default app + * var defaultSecurityRules = admin.securityRules(); + * ``` + * + * @example + * ```javascript + * // Get the SecurityRules service for a given app + * var otherSecurityRules = admin.securityRules(otherApp); + * ``` + * + * @param app Optional app to return the `SecurityRules` service + * for. If not provided, the default `SecurityRules` service + * is returned. + * @return The default `SecurityRules` service if no app is provided, or the + * `SecurityRules` service associated with the provided app. */ +export declare function securityRules(app?: app.App): securityRules.SecurityRules; + /* eslint-disable @typescript-eslint/no-namespace */ -export namespace admin.securityRules { - // See https://github.com/microsoft/TypeScript/issues/4336 - /* eslint-disable @typescript-eslint/no-unused-vars */ - // See https://github.com/typescript-eslint/typescript-eslint/issues/363 - export import RulesFile = securityRulesApi.RulesFile; - export import RulesetMetadata = securityRulesApi.RulesetMetadata; - export import RulesetMetadataList = securityRulesApi.RulesetMetadataList; - - /* eslint-disable @typescript-eslint/no-empty-interface */ - export interface Ruleset extends securityRulesApi.Ruleset {} - export interface SecurityRules extends securityRulesApi.SecurityRules {} +export namespace securityRules { + /** + * A source file containing some Firebase security rules. The content includes raw + * source code including text formatting, indentation and comments. Use the + * [`securityRules.createRulesFileFromSource()`](securityRules.SecurityRules#createRulesFileFromSource) + * method to create new instances of this type. + */ + export interface RulesFile { + readonly name: string; + readonly content: string; + } + + /** + * Required metadata associated with a ruleset. + */ + export interface RulesetMetadata { + /** + * Name of the `Ruleset` as a short string. This can be directly passed into APIs + * like {@link securityRules.SecurityRules.getRuleset `securityRules.getRuleset()`} + * and {@link securityRules.SecurityRules.deleteRuleset `securityRules.deleteRuleset()`}. + */ + readonly name: string; + /** + * Creation time of the `Ruleset` as a UTC timestamp string. + */ + readonly createTime: string; + } + + /** + * A page of ruleset metadata. + */ + export interface RulesetMetadataList { + /** + * A batch of ruleset metadata. + */ + readonly rulesets: RulesetMetadata[]; + /** + * The next page token if available. This is needed to retrieve the next batch. + */ + readonly nextPageToken?: string; + } + + /** + * A set of Firebase security rules. + */ + export interface Ruleset extends RulesetMetadata { + readonly source: RulesFile[]; + } + + /** + * The Firebase `SecurityRules` service interface. + */ + export interface SecurityRules { + app: app.App; + + /** + * Creates a {@link securityRules.RulesFile `RuleFile`} with the given name + * and source. Throws an error if any of the arguments are invalid. This is a local + * operation, and does not involve any network API calls. + * + * @example + * ```javascript + * const source = '// Some rules source'; + * const rulesFile = admin.securityRules().createRulesFileFromSource( + * 'firestore.rules', source); + * ``` + * + * @param name Name to assign to the rules file. This is usually a short file name that + * helps identify the file in a ruleset. + * @param source Contents of the rules file. + * @return A new rules file instance. + */ + createRulesFileFromSource(name: string, source: string | Buffer): RulesFile; + + /** + * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given + * {@link securityRules.RulesFile `RuleFile`}. + * + * @param file Rules file to include in the new `Ruleset`. + * @returns A promise that fulfills with the newly created `Ruleset`. + */ + createRuleset(file: RulesFile): Promise; + + /** + * Gets the {@link securityRules.Ruleset `Ruleset`} identified by the given + * name. The input name should be the short name string without the project ID + * prefix. For example, to retrieve the `projects/project-id/rulesets/my-ruleset`, + * pass the short name "my-ruleset". Rejects with a `not-found` error if the + * specified `Ruleset` cannot be found. + * + * @param name Name of the `Ruleset` to retrieve. + * @return A promise that fulfills with the specified `Ruleset`. + */ + getRuleset(name: string): Promise; + + /** + * Deletes the {@link securityRules.Ruleset `Ruleset`} identified by the given + * name. The input name should be the short name string without the project ID + * prefix. For example, to delete the `projects/project-id/rulesets/my-ruleset`, + * pass the short name "my-ruleset". Rejects with a `not-found` error if the + * specified `Ruleset` cannot be found. + * + * @param name Name of the `Ruleset` to delete. + * @return A promise that fulfills when the `Ruleset` is deleted. + */ + deleteRuleset(name: string): Promise; + + /** + * Retrieves a page of ruleset metadata. + * + * @param pageSize The page size, 100 if undefined. This is also the maximum allowed + * limit. + * @param nextPageToken The next page token. If not specified, returns rulesets + * starting without any offset. + * @return A promise that fulfills with a page of rulesets. + */ + listRulesetMetadata( + pageSize?: number, nextPageToken?: string): Promise; + + /** + * Gets the {@link securityRules.Ruleset `Ruleset`} currently applied to + * Cloud Firestore. Rejects with a `not-found` error if no ruleset is applied + * on Firestore. + * + * @return A promise that fulfills with the Firestore ruleset. + */ + getFirestoreRuleset(): Promise; + + /** + * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given + * source, and applies it to Cloud Firestore. + * + * @param source Rules source to apply. + * @return A promise that fulfills when the ruleset is created and released. + */ + releaseFirestoreRulesetFromSource(source: string | Buffer): Promise; + + /** + * Applies the specified {@link securityRules.Ruleset `Ruleset`} ruleset + * to Cloud Firestore. + * + * @param ruleset Name of the ruleset to apply or a `RulesetMetadata` object + * containing the name. + * @return A promise that fulfills when the ruleset is released. + */ + releaseFirestoreRuleset(ruleset: string | RulesetMetadata): Promise; + + /** + * Gets the {@link securityRules.Ruleset `Ruleset`} currently applied to a + * Cloud Storage bucket. Rejects with a `not-found` error if no ruleset is applied + * on the bucket. + * + * @param bucket Optional name of the Cloud Storage bucket to be retrieved. If not + * specified, retrieves the ruleset applied on the default bucket configured via + * `AppOptions`. + * @return A promise that fulfills with the Cloud Storage ruleset. + */ + getStorageRuleset(bucket?: string): Promise; + + /** + * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given + * source, and applies it to a Cloud Storage bucket. + * + * @param source Rules source to apply. + * @param bucket Optional name of the Cloud Storage bucket to apply the rules on. If + * not specified, applies the ruleset on the default bucket configured via + * {@link AppOptions `AppOptions`}. + * @return A promise that fulfills when the ruleset is created and released. + */ + releaseStorageRulesetFromSource( + source: string | Buffer, bucket?: string): Promise; + + /** + * Applies the specified {@link securityRules.Ruleset `Ruleset`} ruleset + * to a Cloud Storage bucket. + * + * @param ruleset Name of the ruleset to apply or a `RulesetMetadata` object + * containing the name. + * @param bucket Optional name of the Cloud Storage bucket to apply the rules on. If + * not specified, applies the ruleset on the default bucket configured via + * {@link AppOptions `AppOptions`}. + * @return A promise that fulfills when the ruleset is released. + */ + releaseStorageRuleset( + ruleset: string | RulesetMetadata, bucket?: string): Promise; + } } diff --git a/src/security-rules/security-rules.ts b/src/security-rules/security-rules.ts index 89eda3825f..e6cf7ab1b1 100644 --- a/src/security-rules/security-rules.ts +++ b/src/security-rules/security-rules.ts @@ -21,47 +21,13 @@ import { SecurityRulesApiClient, RulesetResponse, RulesetContent, ListRulesetsResponse, } from './security-rules-api-client-internal'; import { FirebaseSecurityRulesError } from './security-rules-internal'; +import { securityRules } from './index'; -/** - * A source file containing some Firebase security rules. The content includes raw - * source code including text formatting, indentation and comments. Use the - * [`securityRules.createRulesFileFromSource()`](admin.securityRules.SecurityRules#createRulesFileFromSource) - * method to create new instances of this type. - */ -export interface RulesFile { - readonly name: string; - readonly content: string; -} - -/** - * Required metadata associated with a ruleset. - */ -export interface RulesetMetadata { - /** - * Name of the `Ruleset` as a short string. This can be directly passed into APIs - * like [`securityRules.getRuleset()`](admin.securityRules.SecurityRules#getRuleset) - * and [`securityRules.deleteRuleset()`](admin.securityRules.SecurityRules#deleteRuleset). - */ - readonly name: string; - /** - * Creation time of the `Ruleset` as a UTC timestamp string. - */ - readonly createTime: string; -} - -/** - * A page of ruleset metadata. - */ -export interface RulesetMetadataList { - /** - * A batch of ruleset metadata. - */ - readonly rulesets: RulesetMetadata[]; - /** - * The next page token if available. This is needed to retrieve the next batch. - */ - readonly nextPageToken?: string; -} +import RulesFile = securityRules.RulesFile; +import RulesetMetadata = securityRules.RulesetMetadata; +import RulesetMetadataList = securityRules.RulesetMetadataList; +import RulesetInterface = securityRules.Ruleset; +import SecurityRulesInterface = securityRules.SecurityRules; class RulesetMetadataListImpl implements RulesetMetadataList { @@ -91,7 +57,7 @@ class RulesetMetadataListImpl implements RulesetMetadataList { /** * Represents a set of Firebase security rules. */ -export class Ruleset implements RulesetMetadata { +export class Ruleset implements RulesetInterface { public readonly name: string; public readonly createTime: string; @@ -117,9 +83,9 @@ export class Ruleset implements RulesetMetadata { * The Firebase `SecurityRules` service interface. * * Do not call this constructor directly. Instead, use - * [`admin.securityRules()`](admin.securityRules#securityRules). + * [`admin.securityRules()`](securityRules#securityRules). */ -export class SecurityRules implements FirebaseServiceInterface { +export class SecurityRules implements FirebaseServiceInterface, SecurityRulesInterface { private static readonly CLOUD_FIRESTORE = 'cloud.firestore'; private static readonly FIREBASE_STORAGE = 'firebase.storage'; @@ -255,7 +221,7 @@ export class SecurityRules implements FirebaseServiceInterface { } /** - * Creates a {@link admin.securityRules.RulesFile `RuleFile`} with the given name + * Creates a {@link securityRules.RulesFile `RuleFile`} with the given name * and source. Throws an error if any of the arguments are invalid. This is a local * operation, and does not involve any network API calls. * @@ -294,8 +260,8 @@ export class SecurityRules implements FirebaseServiceInterface { } /** - * Creates a new {@link admin.securityRules.Ruleset `Ruleset`} from the given - * {@link admin.securityRules.RulesFile `RuleFile`}. + * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given + * {@link securityRules.RulesFile `RuleFile`}. * * @param file Rules file to include in the new `Ruleset`. * @returns A promise that fulfills with the newly created `Ruleset`. @@ -314,7 +280,7 @@ export class SecurityRules implements FirebaseServiceInterface { } /** - * Deletes the {@link admin.securityRules.Ruleset `Ruleset`} identified by the given + * Deletes the {@link securityRules.Ruleset `Ruleset`} identified by the given * name. The input name should be the short name string without the project ID * prefix. For example, to delete the `projects/project-id/rulesets/my-ruleset`, * pass the short name "my-ruleset". Rejects with a `not-found` error if the diff --git a/src/storage.d.ts b/src/storage.d.ts deleted file mode 100644 index 00f499a890..0000000000 --- a/src/storage.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*! - * Copyright 2020 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 { Bucket } from '@google-cloud/storage'; -import * as _admin from './index.d'; - -export namespace admin.storage { - - /** - * The default `Storage` service if no - * app is provided or the `Storage` service associated with the provided - * app. - */ - export interface Storage { - /** - * Optional app whose `Storage` service to - * return. If not provided, the default `Storage` service will be returned. - */ - app: _admin.app.App; - /** - * @returns A [Bucket](https://cloud.google.com/nodejs/docs/reference/storage/latest/Bucket) - * instance as defined in the `@google-cloud/storage` package. - */ - bucket(name?: string): Bucket; - } -} diff --git a/src/storage/index.ts b/src/storage/index.ts index a85f4b60d1..109f54431d 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -14,19 +14,50 @@ * limitations under the License. */ -import * as _storage from './storage'; -import { FirebaseApp } from '../firebase-app'; -import * as firebaseAdmin from '../index'; +import { Bucket } from '@google-cloud/storage'; +import { app } from '../firebase-namespace-api'; -export function storage(app?: FirebaseApp): _storage.Storage { - if (typeof(app) === 'undefined') { - app = firebaseAdmin.app(); - } - return app.storage(); -} +/** + * Gets the {@link storage.Storage `Storage`} service for the + * default app or a given app. + * + * `admin.storage()` can be called with no arguments to access the default + * app's {@link storage.Storage `Storage`} service or as + * `admin.storage(app)` to access the + * {@link storage.Storage `Storage`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the Storage service for the default app + * var defaultStorage = admin.storage(); + * ``` + * + * @example + * ```javascript + * // Get the Storage service for a given app + * var otherStorage = admin.storage(otherApp); + * ``` + */ +export declare function storage(app?: app.App): storage.Storage; /* eslint-disable @typescript-eslint/no-namespace */ -export namespace admin.storage { - /* eslint-disable @typescript-eslint/no-empty-interface */ - export interface Storage extends _storage.Storage {} +export namespace storage { + /** + * The default `Storage` service if no + * app is provided or the `Storage` service associated with the provided + * app. + */ + export interface Storage { + /** + * Optional app whose `Storage` service to + * return. If not provided, the default `Storage` service will be returned. + */ + app: app.App; + /** + * @returns A [Bucket](https://cloud.google.com/nodejs/docs/reference/storage/latest/Bucket) + * instance as defined in the `@google-cloud/storage` package. + */ + bucket(name?: string): Bucket; + } } diff --git a/src/storage/storage.ts b/src/storage/storage.ts index eb20194843..1ac61f6237 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -19,9 +19,11 @@ import { FirebaseError } from '../utils/error'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential-internal'; import { Bucket, Storage as StorageClient } from '@google-cloud/storage'; - import * as utils from '../utils/index'; import * as validator from '../utils/validator'; +import { storage } from './index'; + +import StorageInterface = storage.Storage; /** * Internals of a Storage instance. @@ -43,7 +45,7 @@ class StorageInternals implements FirebaseServiceInternalsInterface { * app is provided or the `Storage` service associated with the provided * app. */ -export class Storage implements FirebaseServiceInterface { +export class Storage implements FirebaseServiceInterface, StorageInterface { public readonly INTERNAL: StorageInternals = new StorageInternals(); private readonly appInternal: FirebaseApp; diff --git a/src/utils/error.ts b/src/utils/error.ts index a894accc76..c1762db1af 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { FirebaseError as FireabseErrorInterface } from '../firebase-namespace-api'; import { deepCopy } from '../utils/deep-copy'; /** @@ -24,11 +25,6 @@ export interface ErrorInfo { message: string; } -export interface FirebaseArrayIndexError { - index: number; - error: FirebaseError; -} - /** * Defines a type that stores all server to client codes (string enum). */ @@ -42,7 +38,7 @@ interface ServerToClientCode { * @param {ErrorInfo} errorInfo The error information (code and message). * @constructor */ -export class FirebaseError extends Error { +export class FirebaseError extends Error implements FireabseErrorInterface { constructor(private errorInfo: ErrorInfo) { super(errorInfo.message); diff --git a/src/utils/index.ts b/src/utils/index.ts index 057a9eeffa..da0b73d704 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import { FirebaseApp, FirebaseAppOptions } from '../firebase-app'; -import { ServiceAccountCredential, ComputeEngineCredential } from '../credential/credential-internal'; - +import { app as _app } from '../firebase-namespace-api'; +import { + ServiceAccountCredential, ComputeEngineCredential +} from '../credential/credential-internal'; import * as validator from './validator'; let sdkVersion: string; @@ -70,12 +71,12 @@ export function addReadonlyGetter(obj: object, prop: string, value: any): void { * specified in either the Firebase app options, credentials or the local environment. * Otherwise returns null. * - * @param {FirebaseApp} app A Firebase app to get the project ID from. + * @param app A Firebase app to get the project ID from. * - * @return {string} A project ID string or null. + * @return A project ID string or null. */ -export function getExplicitProjectId(app: FirebaseApp): string | null { - const options: FirebaseAppOptions = app.options; +export function getExplicitProjectId(app: _app.App): string | null { + const options = app.options; if (validator.isNonEmptyString(options.projectId)) { return options.projectId; } @@ -99,11 +100,11 @@ export function getExplicitProjectId(app: FirebaseApp): string | null { * configured, but the SDK has been initialized with ComputeEngineCredentials, this * method attempts to discover the project ID from the local metadata service. * - * @param {FirebaseApp} app A Firebase app to get the project ID from. + * @param app A Firebase app to get the project ID from. * - * @return {Promise} A project ID string or null. + * @return A project ID string or null. */ -export function findProjectId(app: FirebaseApp): Promise { +export function findProjectId(app: _app.App): Promise { const projectId = getExplicitProjectId(app); if (projectId) { return Promise.resolve(projectId); diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 805e0393cf..41e278f821 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -27,7 +27,6 @@ import { } from './setup'; import url = require('url'); import * as mocks from '../resources/mocks'; -import { AuthProviderConfig } from '../../src/auth/auth-config'; import { deepExtend, deepCopy } from '../../src/utils/deep-copy'; import { User, FirebaseAuth } from '@firebase/auth-types'; @@ -1277,11 +1276,11 @@ describe('admin.auth', () => { }); it('listProviderConfig() successfully returns the list of SAML providers', () => { - const configs: AuthProviderConfig[] = []; + const configs: admin.auth.AuthProviderConfig[] = []; const listProviders: any = (type: 'saml' | 'oidc', maxResults?: number, pageToken?: string) => { return admin.auth().listProviderConfigs({ type, maxResults, pageToken }) .then((result) => { - result.providerConfigs.forEach((config: AuthProviderConfig) => { + result.providerConfigs.forEach((config: admin.auth.AuthProviderConfig) => { configs.push(config); }); if (result.pageToken) { @@ -1409,11 +1408,11 @@ describe('admin.auth', () => { }); it('listProviderConfig() successfully returns the list of OIDC providers', () => { - const configs: AuthProviderConfig[] = []; + const configs: admin.auth.AuthProviderConfig[] = []; const listProviders: any = (type: 'saml' | 'oidc', maxResults?: number, pageToken?: string) => { return admin.auth().listProviderConfigs({ type, maxResults, pageToken }) .then((result) => { - result.providerConfigs.forEach((config: AuthProviderConfig) => { + result.providerConfigs.forEach((config: admin.auth.AuthProviderConfig) => { configs.push(config); }); if (result.pageToken) { diff --git a/test/integration/machine-learning.spec.ts b/test/integration/machine-learning.spec.ts index f0ed2b93cd..f767b7d0ad 100644 --- a/test/integration/machine-learning.spec.ts +++ b/test/integration/machine-learning.spec.ts @@ -20,12 +20,11 @@ import * as chai from 'chai'; import * as admin from '../../lib/index'; import { projectId } from './setup'; import { Bucket } from '@google-cloud/storage'; -import { GcsTfliteModelOptions, AutoMLTfliteModelOptions } from - '../../src/machine-learning/machine-learning-api-client'; - -const expect = chai.expect; +import AutoMLTfliteModelOptions = admin.machineLearning.AutoMLTfliteModelOptions; +import GcsTfliteModelOptions = admin.machineLearning.GcsTfliteModelOptions; +const expect = chai.expect; describe('admin.machineLearning', () => { @@ -577,13 +576,13 @@ function getAutoMLModelReference(): Promise { } catch (error) { // Returning an empty string will result in skipping the test. - return Promise.resolve(""); + return Promise.resolve(''); } const parent = automl.locationPath(projectId, 'us-central1'); - return automl.listModels({ parent, filter:"displayName=admin_sdk_integ_test1" }) + return automl.listModels({ parent, filter:'displayName=admin_sdk_integ_test1' }) .then(([models]: [any]) => { - let modelRef = ""; + let modelRef = ''; for (const model of models) { modelRef = model.name; } diff --git a/test/integration/remote-config.spec.ts b/test/integration/remote-config.spec.ts index 7c4ef39d4b..f8773c1b3c 100644 --- a/test/integration/remote-config.spec.ts +++ b/test/integration/remote-config.spec.ts @@ -84,7 +84,7 @@ describe('admin.remoteConfig', () => { it('verify that the etag is read-only', () => { expect(() => { - (currentTemplate as any).etag = "new-etag"; + (currentTemplate as any).etag = 'new-etag'; }).to.throw('Cannot set property etag of # which has only a getter'); }); @@ -273,7 +273,7 @@ describe('admin.remoteConfig', () => { const newTemplate = admin.remoteConfig().createTemplateFromJSON(jsonString); expect(newTemplate.etag).to.equal(sourceTemplate.etag); expect(() => { - (currentTemplate as any).etag = "new-etag"; + (currentTemplate as any).etag = 'new-etag'; }).to.throw( 'Cannot set property etag of # which has only a getter' ); diff --git a/test/integration/setup.ts b/test/integration/setup.ts index 8e39226bb3..c70362fa89 100644 --- a/test/integration/setup.ts +++ b/test/integration/setup.ts @@ -19,7 +19,7 @@ import fs = require('fs'); import minimist = require('minimist'); import path = require('path'); import { random } from 'lodash'; -import { Credential, GoogleOAuthAccessToken } from '../../src/credential/credential-interfaces'; +import { GoogleOAuthAccessToken } from '../../src/credential/index'; // eslint-disable-next-line @typescript-eslint/no-var-requires const chalk = require('chalk'); @@ -106,7 +106,7 @@ after(() => { ]); }); -class CertificatelessCredential implements Credential { +class CertificatelessCredential implements admin.credential.Credential { private readonly delegate: admin.credential.Credential; constructor(delegate: admin.credential.Credential) { diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index caf919e192..34e1e5bb45 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -24,10 +24,12 @@ import stream = require('stream'); import * as _ from 'lodash'; import * as jwt from 'jsonwebtoken'; +import { AppOptions } from '../../src/firebase-namespace-api'; import { FirebaseNamespace } from '../../src/firebase-namespace'; import { FirebaseServiceInterface } from '../../src/firebase-service'; -import { FirebaseApp, FirebaseAppOptions } from '../../src/firebase-app'; -import { Credential, GoogleOAuthAccessToken } from '../../src/credential/credential-interfaces'; +import { FirebaseApp } from '../../src/firebase-app'; +import { app as _app } from '../../src/firebase-namespace-api'; +import { credential as _credential, GoogleOAuthAccessToken } from '../../src/credential/index'; import { ServiceAccountCredential } from '../../src/credential/credential-internal'; const ALGORITHM = 'RS256' as const; @@ -52,13 +54,13 @@ export const storageBucket = 'bucketName.appspot.com'; export const credential = new ServiceAccountCredential(path.resolve(__dirname, './mock.key.json')); -export const appOptions: FirebaseAppOptions = { +export const appOptions: AppOptions = { credential, databaseURL, storageBucket, }; -export const appOptionsWithOverride: FirebaseAppOptions = { +export const appOptionsWithOverride: AppOptions = { credential, databaseAuthVariableOverride, databaseURL, @@ -66,20 +68,20 @@ export const appOptionsWithOverride: FirebaseAppOptions = { projectId, }; -export const appOptionsNoAuth: FirebaseAppOptions = { +export const appOptionsNoAuth: AppOptions = { databaseURL, }; -export const appOptionsNoDatabaseUrl: FirebaseAppOptions = { +export const appOptionsNoDatabaseUrl: AppOptions = { credential, }; -export const appOptionsAuthDB: FirebaseAppOptions = { +export const appOptionsAuthDB: AppOptions = { credential, databaseURL, }; -export class MockCredential implements Credential { +export class MockCredential implements _credential.Credential { public getAccessToken(): Promise { return Promise.resolve({ access_token: 'mock-token', // eslint-disable-line @typescript-eslint/camelcase @@ -101,7 +103,7 @@ export function mockCredentialApp(): FirebaseApp { }, appName, new FirebaseNamespace().INTERNAL); } -export function appWithOptions(options: FirebaseAppOptions): FirebaseApp { +export function appWithOptions(options: AppOptions): FirebaseApp { const namespaceInternals = new FirebaseNamespace().INTERNAL; namespaceInternals.removeApp = _.noop; return new FirebaseApp(options, appName, namespaceInternals); @@ -226,8 +228,9 @@ export function generateSessionCookie(overrides?: object, expiresIn?: number): s return jwt.sign(developerClaims, certificateObject.private_key, options); } +/* eslint-disable @typescript-eslint/no-unused-vars */ export function firebaseServiceFactory( - firebaseApp: FirebaseApp, + firebaseApp: _app.App, _extendApp?: (props: object) => void, ): FirebaseServiceInterface { const result = { @@ -236,6 +239,7 @@ export function firebaseServiceFactory( }; return result as FirebaseServiceInterface; } +/* eslint-enable @typescript-eslint/no-unused-vars */ /** Mock socket emitter class. */ export class MockSocketEmitter extends events.EventEmitter { diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 19e1d59f9d..4c621fe215 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -36,18 +36,24 @@ import { RESERVED_CLAIMS, FIREBASE_AUTH_UPLOAD_ACCOUNT, FIREBASE_AUTH_CREATE_SESSION_COOKIE, EMAIL_ACTION_REQUEST_TYPES, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, } from '../../../src/auth/auth-api-request'; -import { UserImportBuilder, UserImportRecord } from '../../../src/auth/user-import-builder'; +import { UserImportBuilder } from '../../../src/auth/user-import-builder'; import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; import { ActionCodeSettingsBuilder } from '../../../src/auth/action-code-settings-builder'; -import { - OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest, - SAMLUpdateAuthProviderRequest, SAMLConfigServerResponse, -} from '../../../src/auth/auth-config'; -import { UserIdentifier } from '../../../src/auth/identifier'; -import { TenantOptions } from '../../../src/auth/tenant'; -import { UpdateRequest, UpdateMultiFactorInfoRequest } from '../../../src/auth/user-record'; +import { SAMLConfigServerResponse } from '../../../src/auth/auth-config'; import { expectUserImportResult } from './user-import-builder.spec'; import { getSdkVersion } from '../../../src/utils/index'; +import { auth } from '../../../src/auth/index'; + +import UserImportRecord = auth.UserImportRecord; +import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; +import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; +import OIDCUpdateAuthProviderRequest = auth.OIDCUpdateAuthProviderRequest; +import SAMLUpdateAuthProviderRequest = auth.SAMLUpdateAuthProviderRequest; +import UserIdentifier = auth.UserIdentifier; +import UpdateRequest = auth.UpdateRequest; +import UpdateMultiFactorInfoRequest = auth.UpdateMultiFactorInfoRequest; +import CreateTenantRequest = auth.CreateTenantRequest; +import UpdateTenantRequest = auth.UpdateTenantRequest; chai.should(); chai.use(sinonChai); @@ -616,7 +622,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { const largeClaims = JSON.stringify({ key: createRandomString(991) }); expect(() => { return requestValidator({ localId: '1234', customAttributes: largeClaims }); - }).to.throw(`Developer claims payload should not exceed 1000 characters.`); + }).to.throw('Developer claims payload should not exceed 1000 characters.'); }); RESERVED_CLAIMS.forEach((invalidClaim) => { it(`should fail with customAttributes containing blacklisted claim: ${invalidClaim}`, () => { @@ -635,7 +641,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { auth_time: 'time', // eslint-disable-line @typescript-eslint/camelcase }; return requestValidator({ localId: '1234', customAttributes: JSON.stringify(claims) }); - }).to.throw(`Developer claims "auth_time", "sub" are reserved and cannot be specified.`); + }).to.throw('Developer claims "auth_time", "sub" are reserved and cannot be specified.'); }); it('should fail with invalid validSince', () => { expect(() => { @@ -1378,7 +1384,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should throw on invalid options without making an underlying API call', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ALGORITHM, - `Unsupported hash algorithm provider "invalid".`, + 'Unsupported hash algorithm provider "invalid".', ); const invalidOptions = { hash: { @@ -1398,7 +1404,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should throw when 1001 UserImportRecords are provided', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, - `A maximum of 1000 users can be imported at once.`, + 'A maximum of 1000 users can be imported at once.', ); const stub = sinon.stub(HttpClient.prototype, 'send'); stubs.push(stub); @@ -1660,7 +1666,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { index: 9, error: new FirebaseAuthError( AuthClientErrorCode.FORBIDDEN_CLAIM, - `Developer claim "aud" is reserved and cannot be specified.`, + 'Developer claim "aud" is reserved and cannot be specified.', ), }, { index: 10, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH) }, @@ -1669,28 +1675,28 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { index: 12, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_UID, - `The provider "uid" for "google.com" must be a valid non-empty string.`, + 'The provider "uid" for "google.com" must be a valid non-empty string.', ), }, { index: 13, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_DISPLAY_NAME, - `The provider "displayName" for "google.com" must be a valid string.`, + 'The provider "displayName" for "google.com" must be a valid string.', ), }, { index: 14, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_EMAIL, - `The provider "email" for "google.com" must be a valid email string.`, + 'The provider "email" for "google.com" must be a valid email string.', ), }, { index: 15, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_PHOTO_URL, - `The provider "photoURL" for "google.com" must be a valid URL string.`, + 'The provider "photoURL" for "google.com" must be a valid URL string.', ), }, { index: 16, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID) }, @@ -1699,29 +1705,29 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { index: 18, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_UID, - `The second factor "uid" must be a valid non-empty string.`, + 'The second factor "uid" must be a valid non-empty string.', ), }, { index: 19, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_DISPLAY_NAME, - `The second factor "displayName" for "mfaUid1" must be a valid string.`, + 'The second factor "displayName" for "mfaUid1" must be a valid string.', ), }, { index: 20, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLMENT_TIME, - `The second factor "enrollmentTime" for "mfaUid2" must be a valid UTC date string.`, + 'The second factor "enrollmentTime" for "mfaUid2" must be a valid UTC date string.', ), }, { index: 21, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_PHONE_NUMBER, - `The second factor "phoneNumber" for "mfaUid3" must be a non-empty ` + - `E.164 standard compliant identifier string.`, + 'The second factor "phoneNumber" for "mfaUid3" must be a non-empty ' + + 'E.164 standard compliant identifier string.', ), }, { @@ -1752,7 +1758,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); const expectedError = new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - `An internal error has occurred. Raw server response: ` + + 'An internal error has occurred. Raw server response: ' + `"${JSON.stringify(expectedServerError.response.data)}"`, ); const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); @@ -1832,8 +1838,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not ` + - `exceed 1000.`, + 'Required "maxResults" must be a positive integer that does not ' + + 'exceed 1000.', ); const requestHandler = handler.init(mockApp); @@ -2221,7 +2227,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor uid', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_UID, - `The second factor "uid" must be a valid non-empty string.`, + 'The second factor "uid" must be a valid non-empty string.', ), secondFactor: { uid: ['enrollmentId'], @@ -2234,7 +2240,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor display name', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_DISPLAY_NAME, - `The second factor "displayName" for "enrolledSecondFactor1" must be a valid string.`, + 'The second factor "displayName" for "enrolledSecondFactor1" must be a valid string.', ), secondFactor: { uid: 'enrolledSecondFactor1', @@ -2247,8 +2253,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor phone number', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_PHONE_NUMBER, - `The second factor "phoneNumber" for "enrolledSecondFactor1" must be a non-empty ` + - `E.164 standard compliant identifier string.`), + 'The second factor "phoneNumber" for "enrolledSecondFactor1" must be a non-empty ' + + 'E.164 standard compliant identifier string.'), secondFactor: { uid: 'enrolledSecondFactor1', phoneNumber: 'invalid', @@ -2260,8 +2266,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor enrollment time', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLMENT_TIME, - `The second factor "enrollmentTime" for "enrolledSecondFactor1" must be a valid ` + - `UTC date string.`), + 'The second factor "enrollmentTime" for "enrolledSecondFactor1" must be a valid ' + + 'UTC date string.'), secondFactor: { uid: 'enrolledSecondFactor1', phoneNumber: '+16505557348', @@ -2439,7 +2445,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { // Expected error when invalid claims are provided. const expectedError = new FirebaseAuthError( AuthClientErrorCode.FORBIDDEN_CLAIM, - `Developer claim "aud" is reserved and cannot be specified.`, + 'Developer claim "aud" is reserved and cannot be specified.', ); const requestHandler = handler.init(mockApp); const blacklistedClaims = { admin: true, aud: 'bla' }; @@ -2718,7 +2724,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor display name', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_DISPLAY_NAME, - `The second factor "displayName" for "+16505557348" must be a valid string.`, + 'The second factor "displayName" for "+16505557348" must be a valid string.', ), secondFactor: { phoneNumber: '+16505557348', @@ -2730,8 +2736,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor phone number', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_PHONE_NUMBER, - `The second factor "phoneNumber" for "invalid" must be a non-empty ` + - `E.164 standard compliant identifier string.`), + 'The second factor "phoneNumber" for "invalid" must be a non-empty ' + + 'E.164 standard compliant identifier string.'), secondFactor: { phoneNumber: 'invalid', displayName: 'Spouse\'s phone number', @@ -3100,7 +3106,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const invalidRequestType = 'invalid'; const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `"invalid" is not a supported email action request type.`, + '"invalid" is not a supported email action request type.', ); const requestHandler = handler.init(mockApp); @@ -3307,8 +3313,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not ` + - `exceed 100.`, + 'Required "maxResults" must be a positive integer that does not ' + + 'exceed 100.', ); const requestHandler = handler.init(mockApp); @@ -3796,8 +3802,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not ` + - `exceed 100.`, + 'Required "maxResults" must be a positive integer that does not ' + + 'exceed 100.', ); const requestHandler = handler.init(mockApp); @@ -4339,8 +4345,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive non-zero number that does not ` + - `exceed the allowed 1000.`, + 'Required "maxResults" must be a positive non-zero number that does not ' + + 'exceed the allowed 1000.', ); const requestHandler = handler.init(mockApp) as AuthRequestHandler; @@ -4448,7 +4454,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { describe('createTenant', () => { const path = '/v2/projects/project_id/tenants'; const postMethod = 'POST'; - const tenantOptions: TenantOptions = { + const tenantOptions: CreateTenantRequest = { displayName: 'TENANT-DISPLAY-NAME', emailSignInConfig: { enabled: true, @@ -4555,7 +4561,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); const expectedError = new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - `An internal error has occurred. Raw server response: ` + + 'An internal error has occurred. Raw server response: ' + `"${JSON.stringify(expectedServerError.response.data)}"`, ); const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); @@ -4576,7 +4582,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const path = '/v2/projects/project_id/tenants/tenant-id'; const patchMethod = 'PATCH'; const tenantId = 'tenant-id'; - const tenantOptions: TenantOptions = { + const tenantOptions: UpdateTenantRequest = { displayName: 'TENANT-DISPLAY-NAME', emailSignInConfig: { enabled: true, @@ -4749,7 +4755,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); const expectedError = new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - `An internal error has occurred. Raw server response: ` + + 'An internal error has occurred. Raw server response: ' + `"${JSON.stringify(expectedServerError.response.data)}"`, ); const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts index 03b52ed539..ad81c5e62c 100644 --- a/test/unit/auth/auth-config.spec.ts +++ b/test/unit/auth/auth-config.spec.ts @@ -23,12 +23,16 @@ import { deepCopy } from '../../../src/utils/deep-copy'; import { OIDCConfig, SAMLConfig, SAMLConfigServerRequest, SAMLConfigServerResponse, OIDCConfigServerRequest, - OIDCConfigServerResponse, SAMLUpdateAuthProviderRequest, - OIDCUpdateAuthProviderRequest, SAMLAuthProviderConfig, OIDCAuthProviderConfig, + OIDCConfigServerResponse, EmailSignInConfig, MultiFactorAuthConfig, validateTestPhoneNumbers, MAXIMUM_TEST_PHONE_NUMBERS, } from '../../../src/auth/auth-config'; +import { auth } from '../../../src/auth/index'; +import SAMLUpdateAuthProviderRequest = auth.SAMLUpdateAuthProviderRequest; +import OIDCUpdateAuthProviderRequest = auth.OIDCUpdateAuthProviderRequest; +import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; +import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; chai.should(); chai.use(sinonChai); @@ -406,7 +410,6 @@ describe('SAMLConfig', () => { x509Certificates: ['CERT1', 'CERT2'], rpEntityId: 'RP_ENTITY_ID', callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', - enableRequestSigning: true, enabled: true, displayName: 'samlProviderName', }; @@ -525,7 +528,9 @@ describe('SAMLConfig', () => { describe('buildServerRequest()', () => { it('should return expected server request on valid input', () => { - expect(SAMLConfig.buildServerRequest(clientRequest)).to.deep.equal(serverRequest); + const request = deepCopy(clientRequest); + (request as any).enableRequestSigning = true; + expect(SAMLConfig.buildServerRequest(request)).to.deep.equal(serverRequest); }); it('should ignore missing fields if not required', () => { @@ -620,7 +625,7 @@ describe('SAMLConfig', () => { const invalidClientRequest = deepCopy(clientRequest) as any; invalidClientRequest.unsupported = 'value'; expect(() => SAMLConfig.validate(invalidClientRequest)) - .to.throw(`"unsupported" is not a valid SAML config parameter.`); + .to.throw('"unsupported" is not a valid SAML config parameter.'); }); const invalidProviderIds = [ @@ -899,7 +904,7 @@ describe('OIDCConfig', () => { const invalidClientRequest = deepCopy(clientRequest) as any; invalidClientRequest.unsupported = 'value'; expect(() => OIDCConfig.validate(invalidClientRequest)) - .to.throw(`"unsupported" is not a valid OIDC config parameter.`); + .to.throw('"unsupported" is not a valid OIDC config parameter.'); }); const invalidProviderIds = [ diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index e51d0da2c8..27d797abba 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -26,8 +26,8 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import { Auth, TenantAwareAuth, BaseAuth, DecodedIdToken } from '../../../src/auth/auth'; -import { UserRecord, UpdateRequest } from '../../../src/auth/user-record'; +import { Auth, TenantAwareAuth, BaseAuth } from '../../../src/auth/auth'; +import { UserRecord } from '../../../src/auth/user-record'; import { FirebaseApp } from '../../../src/firebase-app'; import { AuthRequestHandler, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, @@ -37,13 +37,17 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error import * as validator from '../../../src/utils/validator'; import { FirebaseTokenVerifier } from '../../../src/auth/token-verifier'; import { - AuthProviderConfigFilter, OIDCConfig, SAMLConfig, - OIDCConfigServerResponse, SAMLConfigServerResponse, + OIDCConfig, SAMLConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from '../../../src/auth/auth-config'; import { deepCopy } from '../../../src/utils/deep-copy'; import { TenantManager } from '../../../src/auth/tenant-manager'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { HttpClient } from '../../../src/utils/api-request'; +import { auth } from '../../../src/auth/index'; + +import DecodedIdToken = auth.DecodedIdToken; +import UpdateRequest = auth.UpdateRequest; +import AuthProviderConfigFilter = auth.AuthProviderConfigFilter; chai.should(); chai.use(sinonChai); @@ -3222,7 +3226,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const unsignedToken = mocks.generateIdToken({ algorithm: 'none' }); - + await expect(mockAuth.verifyIdToken(unsignedToken)) .to.be.rejectedWith('Firebase ID token has incorrect algorithm. Expected "RS256"'); }); @@ -3232,7 +3236,7 @@ AUTH_CONFIGS.forEach((testConfig) => { let claims = {}; if (testConfig.Auth === TenantAwareAuth) { - claims = { + claims = { firebase: { tenant: TENANT_ID } @@ -3242,7 +3246,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const unsignedToken = mocks.generateIdToken({ algorithm: 'none' }, claims); - + const decoded = await mockAuth.verifyIdToken(unsignedToken); expect(decoded).to.be.ok; }); diff --git a/test/unit/auth/tenant-manager.spec.ts b/test/unit/auth/tenant-manager.spec.ts index 6a578323d9..9c7ef80be1 100644 --- a/test/unit/auth/tenant-manager.spec.ts +++ b/test/unit/auth/tenant-manager.spec.ts @@ -25,9 +25,14 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; -import { Tenant, TenantOptions, TenantServerResponse, ListTenantsResult } from '../../../src/auth/tenant'; +import { Tenant, TenantServerResponse } from '../../../src/auth/tenant'; import { TenantManager } from '../../../src/auth/tenant-manager'; import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { auth } from '../../../src/auth/index'; + +import CreateTenantRequest = auth.CreateTenantRequest; +import UpdateTenantRequest = auth.UpdateTenantRequest; +import ListTenantsResult = auth.ListTenantsResult; chai.should(); chai.use(sinonChai); @@ -377,7 +382,7 @@ describe('TenantManager', () => { }); describe('createTenant()', () => { - const tenantOptions: TenantOptions = { + const tenantOptions: CreateTenantRequest = { displayName: 'TENANT-DISPLAY-NAME', emailSignInConfig: { enabled: true, @@ -469,7 +474,7 @@ describe('TenantManager', () => { describe('updateTenant()', () => { const tenantId = 'tenant-id'; - const tenantOptions: TenantOptions = { + const tenantOptions: UpdateTenantRequest = { displayName: 'TENANT-DISPLAY-NAME', emailSignInConfig: { enabled: true, @@ -509,7 +514,7 @@ describe('TenantManager', () => { }); it('should be rejected given invalid TenantOptions', () => { - return tenantManager.updateTenant(tenantId, null as unknown as TenantOptions) + return tenantManager.updateTenant(tenantId, null as unknown as UpdateTenantRequest) .then(() => { throw new Error('Unexpected success'); }) diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 128c6840f5..db090b3279 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -20,13 +20,13 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import { deepCopy } from '../../../src/utils/deep-copy'; -import { - EmailSignInConfig, EmailSignInProviderConfig, MultiFactorAuthConfig, -} from '../../../src/auth/auth-config'; -import { - Tenant, TenantOptions, TenantServerResponse, -} from '../../../src/auth/tenant'; +import { EmailSignInConfig, MultiFactorAuthConfig } from '../../../src/auth/auth-config'; +import { Tenant, TenantServerResponse } from '../../../src/auth/tenant'; +import { auth } from '../../../src/auth/index'; +import EmailSignInProviderConfig = auth.EmailSignInProviderConfig; +import CreateTenantRequest = auth.CreateTenantRequest; +import UpdateTenantRequest = auth.UpdateTenantRequest; chai.should(); chai.use(sinonChai); @@ -50,7 +50,7 @@ describe('Tenant', () => { }, }; - const clientRequest: TenantOptions = { + const clientRequest: UpdateTenantRequest = { displayName: 'TENANT-DISPLAY-NAME', emailSignInConfig: { enabled: true, @@ -73,7 +73,7 @@ describe('Tenant', () => { enableEmailLinkSignin: true, }; - const clientRequestWithoutMfa: TenantOptions = { + const clientRequestWithoutMfa: UpdateTenantRequest = { displayName: 'TENANT-DISPLAY-NAME', emailSignInConfig: { enabled: true, @@ -164,7 +164,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.unsupported = 'value'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw(`"unsupported" is not a valid UpdateTenantRequest parameter.`); + }).to.throw('"unsupported" is not a valid UpdateTenantRequest parameter.'); }); const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; @@ -181,7 +181,7 @@ describe('Tenant', () => { describe('for a create request', () => { it('should return the expected server request without multi-factor and phone config', () => { - const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequestWithoutMfa); + const tenantOptionsClientRequest: CreateTenantRequest = deepCopy(clientRequestWithoutMfa); const tenantOptionsServerRequest: TenantServerResponse = deepCopy(serverRequestWithoutMfa); delete tenantOptionsServerRequest.name; @@ -190,7 +190,7 @@ describe('Tenant', () => { }); it('should return the expected server request with multi-factor and phone config', () => { - const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); + const tenantOptionsClientRequest: CreateTenantRequest = deepCopy(clientRequest); const tenantOptionsServerRequest: TenantServerResponse = deepCopy(serverRequest); delete tenantOptionsServerRequest.name; @@ -199,7 +199,7 @@ describe('Tenant', () => { }); it('should throw on invalid EmailSignInConfig', () => { - const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); + const tenantOptionsClientRequest: CreateTenantRequest = deepCopy(clientRequest); tenantOptionsClientRequest.emailSignInConfig = null as unknown as EmailSignInProviderConfig; expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest)) @@ -211,7 +211,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.multiFactorConfig.factorIds = ['invalid']; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw(`"invalid" is not a valid "AuthFactorType".`,); + }).to.throw('"invalid" is not a valid "AuthFactorType".',); }); it('should throw on invalid testPhoneNumbers attribute', () => { @@ -219,7 +219,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.testPhoneNumbers = { 'invalid': '123456' }; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw(`"invalid" is not a valid E.164 standard compliant phone number.`); + }).to.throw('"invalid" is not a valid E.164 standard compliant phone number.'); }); it('should throw on null testPhoneNumbers attribute', () => { @@ -231,7 +231,7 @@ describe('Tenant', () => { expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw(`"CreateTenantRequest.testPhoneNumbers" must be a non-null object.`); + }).to.throw('"CreateTenantRequest.testPhoneNumbers" must be a non-null object.'); }); const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; @@ -248,7 +248,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.unsupported = 'value'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw(`"unsupported" is not a valid CreateTenantRequest parameter.`); + }).to.throw('"unsupported" is not a valid CreateTenantRequest parameter.'); }); const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index 4164d008be..3b196be9f7 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -131,7 +131,7 @@ describe('CryptoSigner', () => { const input = Buffer.from('input'); const signRequest = { method: 'POST', - url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-service-account:signBlob`, + url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-service-account:signBlob', headers: { Authorization: `Bearer ${mockAccessToken}` }, data: { payload: input.toString('base64') }, }; @@ -182,12 +182,12 @@ describe('CryptoSigner', () => { const response = { signedBlob: Buffer.from('testsignature').toString('base64') }; const metadataRequest = { method: 'GET', - url: `http://metadata/computeMetadata/v1/instance/service-accounts/default/email`, + url: 'http://metadata/computeMetadata/v1/instance/service-accounts/default/email', headers: { 'Metadata-Flavor': 'Google' }, }; const signRequest = { method: 'POST', - url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/discovered-service-account:signBlob`, + url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/discovered-service-account:signBlob', headers: { Authorization: `Bearer ${mockAccessToken}` }, data: { payload: input.toString('base64') }, }; diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 7aae3255b8..6b370a8bb4 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -109,7 +109,7 @@ function createTokenVerifier( app: FirebaseApp, options: { algorithm?: Algorithm } = {} ): verifier.FirebaseTokenVerifier { - const algorithm = options.algorithm || "RS256"; + const algorithm = options.algorithm || 'RS256'; return new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', algorithm, diff --git a/test/unit/auth/user-import-builder.spec.ts b/test/unit/auth/user-import-builder.spec.ts index ab1eab477e..14deeaeb64 100644 --- a/test/unit/auth/user-import-builder.spec.ts +++ b/test/unit/auth/user-import-builder.spec.ts @@ -20,12 +20,15 @@ import * as chaiAsPromised from 'chai-as-promised'; import { deepCopy } from '../../../src/utils/deep-copy'; import { - UserImportBuilder, ValidatorFunction, UserImportResult, UserImportRecord, - UploadAccountRequest, + UserImportBuilder, ValidatorFunction, UploadAccountRequest, } from '../../../src/auth/user-import-builder'; import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; import { toWebSafeBase64 } from '../../../src/utils'; +import { auth } from '../../../src/auth/index'; +import UpdatePhoneMultiFactorInfoRequest = auth.UpdatePhoneMultiFactorInfoRequest; +import UserImportResult = auth.UserImportResult; +import UserImportRecord = auth.UserImportRecord; chai.should(); chai.use(sinonChai); @@ -197,7 +200,7 @@ describe('UserImportBuilder', () => { it('should throw when an invalid hash algorithm is provided', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ALGORITHM, - `Unsupported hash algorithm provider "invalid".`, + 'Unsupported hash algorithm provider "invalid".', ); const invalidOptions = { hash: { @@ -226,7 +229,7 @@ describe('UserImportBuilder', () => { it(`should throw when non-Buffer ${JSON.stringify(key)} hash key is provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_KEY, - `A non-empty "hash.key" byte buffer must be provided for ` + + 'A non-empty "hash.key" byte buffer must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -334,7 +337,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(key)} key provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_KEY, - `A "hash.key" byte buffer must be provided for ` + + 'A "hash.key" byte buffer must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -355,7 +358,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(rounds)} rounds provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ROUNDS, - `A valid "hash.rounds" number between 1 and 8 must be provided for ` + + 'A valid "hash.rounds" number between 1 and 8 must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -376,7 +379,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(memoryCost)} memoryCost provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_MEMORY_COST, - `A valid "hash.memoryCost" number between 1 and 14 must be provided for ` + + 'A valid "hash.memoryCost" number between 1 and 14 must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -397,7 +400,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(saltSeparator)} saltSeparator provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_SALT_SEPARATOR, - `"hash.saltSeparator" must be a byte buffer.`, + '"hash.saltSeparator" must be a byte buffer.', ); const invalidOptions = { hash: { @@ -465,7 +468,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(memoryCost)} memoryCost provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_MEMORY_COST, - `A valid "hash.memoryCost" number must be provided for ` + + 'A valid "hash.memoryCost" number must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -487,7 +490,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(parallelization)} parallelization provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_MEMORY_COST, - `A valid "hash.parallelization" number must be provided for ` + + 'A valid "hash.parallelization" number must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -509,7 +512,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(blockSize)} blockSize provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_BLOCK_SIZE, - `A valid "hash.blockSize" number must be provided for ` + + 'A valid "hash.blockSize" number must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -531,7 +534,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(derivedKeyLength)} dkLen provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, - `A valid "hash.derivedKeyLength" number must be provided for ` + + 'A valid "hash.derivedKeyLength" number must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -652,19 +655,18 @@ describe('UserImportBuilder', () => { }); it('should ignore users with invalid second factor enrollment time', () => { + const phoneFactor: UpdatePhoneMultiFactorInfoRequest = { + uid: 'enrolledSecondFactor1', + phoneNumber: '+16505557348', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + enrollmentTime: 'invalid', + }; const invalidMultiFactorUsers: UserImportRecord[] = [ { uid: '1234', multiFactor: { - enrolledFactors: [ - { - uid: 'enrolledSecondFactor1', - phoneNumber: '+16505557348', - displayName: 'Spouse\'s phone number', - factorId: 'phone', - enrollmentTime: 'invalid', - }, - ], + enrolledFactors: [ phoneFactor ], }, }, { uid: '5678', phoneNumber: '+16505550102' }, @@ -862,8 +864,8 @@ describe('UserImportBuilder', () => { index: 8, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLMENT_TIME, - `The second factor "enrollmentTime" for "enrollmentId1" must be a valid ` + - `UTC date string.`), + 'The second factor "enrollmentTime" for "enrollmentId1" must be a valid ' + + 'UTC date string.'), }, { index: 9, diff --git a/test/unit/credential/credential.spec.ts b/test/unit/credential/credential.spec.ts index 42f965dbd1..c51894cc84 100644 --- a/test/unit/credential/credential.spec.ts +++ b/test/unit/credential/credential.spec.ts @@ -31,8 +31,8 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { - GoogleOAuthAccessToken, Credential -} from '../../../src/credential/credential-interfaces'; + GoogleOAuthAccessToken, credential +} from '../../../src/credential/index'; import { RefreshTokenCredential, ServiceAccountCredential, ComputeEngineCredential, getApplicationDefault, isApplicationDefault @@ -555,7 +555,7 @@ describe('Credential', () => { }); it('should return false for custom credential', () => { - const c: Credential = { + const c: credential.Credential = { getAccessToken: () => { throw new Error(); }, diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 415ce4a94a..c5bdd0a4fc 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -23,10 +23,12 @@ import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; import { DatabaseService } from '../../../src/database/database-internal'; -import { Database } from '../../../src/database/database'; +import { database } from '../../../src/database/index'; import * as utils from '../utils'; import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; +import Database = database.Database; + describe('Database', () => { let mockApp: FirebaseApp; let database: DatabaseService; diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index af923ef423..b0cc477429 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -25,23 +25,34 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from './utils'; import * as mocks from '../resources/mocks'; -import { GoogleOAuthAccessToken } from '../../src/credential/credential-interfaces'; +import { GoogleOAuthAccessToken } from '../../src/credential/index'; import { ServiceAccountCredential } from '../../src/credential/credential-internal'; import { FirebaseServiceInterface } from '../../src/firebase-service'; import { FirebaseApp, FirebaseAccessToken } from '../../src/firebase-app'; import { FirebaseNamespace, FirebaseNamespaceInternals, FIREBASE_CONFIG_VAR } from '../../src/firebase-namespace'; -import { Auth } from '../../src/auth/auth'; -import { Messaging } from '../../src/messaging/messaging'; -import { MachineLearning } from '../../src/machine-learning/machine-learning'; -import { Storage } from '../../src/storage/storage'; -import { Firestore } from '@google-cloud/firestore'; -import { Database } from '../../src/database/database'; -import { InstanceId } from '../../src/instance-id/instance-id'; -import { ProjectManagement } from '../../src/project-management/project-management'; -import { SecurityRules } from '../../src/security-rules/security-rules'; +import { auth } from '../../src/auth/index'; +import { messaging } from '../../src/messaging/index'; +import { machineLearning } from '../../src/machine-learning/index'; +import { storage } from '../../src/storage/index'; +import { firestore } from '../../src/firestore/index'; +import { database } from '../../src/database/index'; +import { instanceId } from '../../src/instance-id/index'; +import { projectManagement } from '../../src/project-management/index'; +import { securityRules } from '../../src/security-rules/index'; +import { remoteConfig } from '../../src/remote-config/index'; import { FirebaseAppError, AppErrorCodes } from '../../src/utils/error'; -import { RemoteConfig } from '../../src/remote-config/remote-config'; + +import Auth = auth.Auth; +import Database = database.Database; +import Messaging = messaging.Messaging; +import MachineLearning = machineLearning.MachineLearning; +import Storage = storage.Storage; +import Firestore = firestore.Firestore; +import InstanceId = instanceId.InstanceId; +import ProjectManagement = projectManagement.ProjectManagement; +import SecurityRules = securityRules.SecurityRules; +import RemoteConfig = remoteConfig.RemoteConfig; chai.should(); chai.use(sinonChai); @@ -133,7 +144,7 @@ describe('FirebaseApp', () => { it('should be read-only', () => { expect(() => { (mockApp as any).name = 'foo'; - }).to.throw(`Cannot set property name of # which has only a getter`); + }).to.throw('Cannot set property name of # which has only a getter'); }); }); @@ -153,7 +164,7 @@ describe('FirebaseApp', () => { it('should be read-only', () => { expect(() => { (mockApp as any).options = {}; - }).to.throw(`Cannot set property options of # which has only a getter`); + }).to.throw('Cannot set property options of # which has only a getter'); }); it('should not return an object which can mutate the underlying options', () => { @@ -175,28 +186,28 @@ describe('FirebaseApp', () => { process.env[FIREBASE_CONFIG_VAR] = './test/resources/non_existant.json'; expect(() => { firebaseNamespace.initializeApp(); - }).to.throw(`Failed to parse app options file: Error: ENOENT: no such file or directory`); + }).to.throw('Failed to parse app options file: Error: ENOENT: no such file or directory'); }); it('should throw when the environment variable contains bad json', () => { process.env[FIREBASE_CONFIG_VAR] = '{,,'; expect(() => { firebaseNamespace.initializeApp(); - }).to.throw(`Failed to parse app options file: SyntaxError: Unexpected token ,`); + }).to.throw('Failed to parse app options file: SyntaxError: Unexpected token ,'); }); it('should throw when the environment variable points to an empty file', () => { process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config_empty.json'; expect(() => { firebaseNamespace.initializeApp(); - }).to.throw(`Failed to parse app options file`); + }).to.throw('Failed to parse app options file'); }); it('should throw when the environment variable points to bad json', () => { process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config_bad.json'; expect(() => { firebaseNamespace.initializeApp(); - }).to.throw(`Failed to parse app options file`); + }).to.throw('Failed to parse app options file'); }); it('should ignore a bad config key in the config file', () => { diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 4cbc98df5e..d993d2871c 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -25,23 +25,17 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../resources/mocks'; import { FirebaseNamespace } from '../../src/firebase-namespace'; -import { FirebaseApp } from '../../src/firebase-app'; -import { Auth } from '../../src/auth/auth'; import { enableLogging, - Database, + Database as DatabaseImpl, DataSnapshot, OnDisconnect, Query, Reference, ServerValue, } from '@firebase/database'; -import { Database as FirebaseDatabase } from '../../src/database/database'; -import { Messaging } from '../../src/messaging/messaging'; -import { MachineLearning } from '../../src/machine-learning/machine-learning'; -import { Storage } from '../../src/storage/storage'; + import { - Firestore, FieldPath, FieldValue, GeoPoint, @@ -49,12 +43,41 @@ import { v1beta1, setLogFunction, } from '@google-cloud/firestore'; -import { InstanceId } from '../../src/instance-id/instance-id'; -import { ProjectManagement } from '../../src/project-management/project-management'; -import { SecurityRules } from '../../src/security-rules/security-rules'; -import { RemoteConfig } from '../../src/remote-config/remote-config'; import { getSdkVersion } from '../../src/utils/index'; +import { app } from '../../src/firebase-namespace-api'; +import { auth } from '../../src/auth/index'; +import { messaging } from '../../src/messaging/index'; +import { machineLearning } from '../../src/machine-learning/index'; +import { storage } from '../../src/storage/index'; +import { firestore } from '../../src/firestore/index'; +import { database } from '../../src/database/index'; +import { instanceId } from '../../src/instance-id/index'; +import { projectManagement } from '../../src/project-management/index'; +import { securityRules } from '../../src/security-rules/index'; +import { remoteConfig } from '../../src/remote-config/index'; + +import { Auth as AuthImpl } from '../../src/auth/auth'; +import { InstanceId as InstanceIdImpl } from '../../src/instance-id/instance-id'; +import { MachineLearning as MachineLearningImpl } from '../../src/machine-learning/machine-learning'; +import { Messaging as MessagingImpl } from '../../src/messaging/messaging'; +import { ProjectManagement as ProjectManagementImpl } from '../../src/project-management/project-management'; +import { RemoteConfig as RemoteConfigImpl } from '../../src/remote-config/remote-config'; +import { SecurityRules as SecurityRulesImpl } from '../../src/security-rules/security-rules'; +import { Storage as StorageImpl } from '../../src/storage/storage'; + +import App = app.App; +import Auth = auth.Auth; +import Database = database.Database; +import Firestore = firestore.Firestore; +import InstanceId = instanceId.InstanceId; +import MachineLearning = machineLearning.MachineLearning; +import Messaging = messaging.Messaging; +import ProjectManagement = projectManagement.ProjectManagement; +import RemoteConfig = remoteConfig.RemoteConfig; +import SecurityRules = securityRules.SecurityRules; +import Storage = storage.Storage; + chai.should(); chai.use(sinonChai); chai.use(chaiAsPromised); @@ -110,7 +133,7 @@ describe('FirebaseNamespace', () => { it('should be read-only', () => { expect(() => { (firebaseNamespace as any).apps = 'foo'; - }).to.throw(`Cannot set property apps of # which has only a getter`); + }).to.throw('Cannot set property apps of # which has only a getter'); }); }); @@ -127,7 +150,7 @@ describe('FirebaseNamespace', () => { it('should throw given empty string app name', () => { expect(() => { return firebaseNamespace.app(''); - }).to.throw(`Invalid Firebase app name "" provided. App name must be a non-empty string.`); + }).to.throw('Invalid Firebase app name "" provided. App name must be a non-empty string.'); }); it('should throw given an app name which does not correspond to an existing app', () => { @@ -180,7 +203,7 @@ describe('FirebaseNamespace', () => { it('should throw given empty string app name', () => { expect(() => { firebaseNamespace.initializeApp(mocks.appOptions, ''); - }).to.throw(`Invalid Firebase app name "" provided. App name must be a non-empty string.`); + }).to.throw('Invalid Firebase app name "" provided. App name must be a non-empty string.'); }); it('should throw given a name corresponding to an existing app', () => { @@ -259,7 +282,7 @@ describe('FirebaseNamespace', () => { it('should throw given empty string app name', () => { expect(() => { firebaseNamespace.INTERNAL.removeApp(''); - }).to.throw(`Invalid Firebase app name "" provided. App name must be a non-empty string.`); + }).to.throw('Invalid Firebase app name "" provided. App name must be a non-empty string.'); }); it('should throw given an app name which does not correspond to an existing app', () => { @@ -271,14 +294,14 @@ describe('FirebaseNamespace', () => { it('should throw given no app name if the default app does not exist', () => { expect(() => { (firebaseNamespace as any).INTERNAL.removeApp(); - }).to.throw(`No Firebase app name provided. App name must be a non-empty string.`); + }).to.throw('No Firebase app name provided. App name must be a non-empty string.'); }); it('should throw given no app name even if the default app exists', () => { firebaseNamespace.initializeApp(mocks.appOptions); expect(() => { (firebaseNamespace as any).INTERNAL.removeApp(); - }).to.throw(`No Firebase app name provided. App name must be a non-empty string.`); + }).to.throw('No Firebase app name provided. App name must be a non-empty string.'); }); it('should remove the app corresponding to the provided app name from the namespace\'s app list', () => { @@ -325,7 +348,7 @@ describe('FirebaseNamespace', () => { it('should throw given no service name', () => { expect(() => { firebaseNamespace.INTERNAL.registerService(undefined as unknown as string, mocks.firebaseServiceFactory); - }).to.throw(`No service name provided. Service name must be a non-empty string.`); + }).to.throw('No service name provided. Service name must be a non-empty string.'); }); const invalidServiceNames = [null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop]; @@ -340,7 +363,7 @@ describe('FirebaseNamespace', () => { it('should throw given an empty string service name', () => { expect(() => { firebaseNamespace.INTERNAL.registerService('', mocks.firebaseServiceFactory); - }).to.throw(`Invalid service name "" provided. Service name must be a non-empty string.`); + }).to.throw('Invalid service name "" provided. Service name must be a non-empty string.'); }); it('should throw given a service name which has already been registered', () => { @@ -373,19 +396,19 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const auth: Auth = firebaseNamespace.auth(); expect(auth.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const auth: Auth = firebaseNamespace.auth(app); expect(auth.app).to.be.deep.equal(app); }); it('should return a reference to Auth type', () => { - expect(firebaseNamespace.auth.Auth).to.be.deep.equal(Auth); + expect(firebaseNamespace.auth.Auth).to.be.deep.equal(AuthImpl); }); }); @@ -404,21 +427,21 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); - const db: FirebaseDatabase = firebaseNamespace.database(); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); + const db: Database = firebaseNamespace.database(); expect(db.app).to.be.deep.equal(app); return app.delete(); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); - const db: FirebaseDatabase = firebaseNamespace.database(app); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const db: Database = firebaseNamespace.database(app); expect(db.app).to.be.deep.equal(app); return app.delete(); }); it('should return a reference to Database type', () => { - expect(firebaseNamespace.database.Database).to.be.deep.equal(Database); + expect(firebaseNamespace.database.Database).to.be.deep.equal(DatabaseImpl); }); it('should return a reference to DataSnapshot type', () => { @@ -461,19 +484,19 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const fcm: Messaging = firebaseNamespace.messaging(); expect(fcm.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const fcm: Messaging = firebaseNamespace.messaging(app); expect(fcm.app).to.be.deep.equal(app); }); it('should return a reference to Messaging type', () => { - expect(firebaseNamespace.messaging.Messaging).to.be.deep.equal(Messaging); + expect(firebaseNamespace.messaging.Messaging).to.be.deep.equal(MessagingImpl); }); }); @@ -492,20 +515,20 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const ml: MachineLearning = firebaseNamespace.machineLearning(); expect(ml.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const ml: MachineLearning = firebaseNamespace.machineLearning(app); expect(ml.app).to.be.deep.equal(app); }); it('should return a reference to Machine Learning type', () => { expect(firebaseNamespace.machineLearning.MachineLearning) - .to.be.deep.equal(MachineLearning); + .to.be.deep.equal(MachineLearningImpl); }); }); @@ -524,19 +547,19 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const gcs: Storage = firebaseNamespace.storage(); expect(gcs.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const gcs: Storage = firebaseNamespace.storage(app); expect(gcs.app).to.be.deep.equal(app); }); it('should return a reference to Storage type', () => { - expect(firebaseNamespace.storage.Storage).to.be.deep.equal(Storage); + expect(firebaseNamespace.storage.Storage).to.be.deep.equal(StorageImpl); }); }); @@ -561,7 +584,7 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const fs: Firestore = firebaseNamespace.firestore(app); expect(fs).to.not.be.null; }); @@ -610,21 +633,21 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const iid: InstanceId = firebaseNamespace.instanceId(); expect(iid).to.not.be.null; expect(iid.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const iid: InstanceId = firebaseNamespace.instanceId(app); expect(iid).to.not.be.null; expect(iid.app).to.be.deep.equal(app); }); it('should return a reference to InstanceId type', () => { - expect(firebaseNamespace.instanceId.InstanceId).to.be.deep.equal(InstanceId); + expect(firebaseNamespace.instanceId.InstanceId).to.be.deep.equal(InstanceIdImpl); }); }); @@ -643,14 +666,14 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const projectManagement: ProjectManagement = firebaseNamespace.projectManagement(); expect(projectManagement).to.not.be.null; expect(projectManagement.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const projectManagement: ProjectManagement = firebaseNamespace.projectManagement(app); expect(projectManagement).to.not.be.null; expect(projectManagement.app).to.be.deep.equal(app); @@ -658,7 +681,7 @@ describe('FirebaseNamespace', () => { it('should return a reference to ProjectManagement type', () => { expect(firebaseNamespace.projectManagement.ProjectManagement) - .to.be.deep.equal(ProjectManagement); + .to.be.deep.equal(ProjectManagementImpl); }); }); @@ -677,14 +700,14 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const securityRules: SecurityRules = firebaseNamespace.securityRules(); expect(securityRules).to.not.be.null; expect(securityRules.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const securityRules: SecurityRules = firebaseNamespace.securityRules(app); expect(securityRules).to.not.be.null; expect(securityRules.app).to.be.deep.equal(app); @@ -692,7 +715,7 @@ describe('FirebaseNamespace', () => { it('should return a reference to SecurityRules type', () => { expect(firebaseNamespace.securityRules.SecurityRules) - .to.be.deep.equal(SecurityRules); + .to.be.deep.equal(SecurityRulesImpl); }); }); @@ -711,19 +734,19 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const rc: RemoteConfig = firebaseNamespace.remoteConfig(); expect(rc.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const rc: RemoteConfig = firebaseNamespace.remoteConfig(app); expect(rc.app).to.be.deep.equal(app); }); it('should return a reference to RemoteConfig type', () => { - expect(firebaseNamespace.remoteConfig.RemoteConfig).to.be.deep.equal(RemoteConfig); + expect(firebaseNamespace.remoteConfig.RemoteConfig).to.be.deep.equal(RemoteConfigImpl); }); }); }); diff --git a/test/unit/firebase.spec.ts b/test/unit/firebase.spec.ts index 98ca68d5f8..71eb5d7297 100644 --- a/test/unit/firebase.spec.ts +++ b/test/unit/firebase.spec.ts @@ -27,6 +27,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../resources/mocks'; import * as firebaseAdmin from '../../src/index'; +import { FirebaseApp, FirebaseAppInternals } from '../../src/firebase-app'; import { RefreshTokenCredential, ServiceAccountCredential, isApplicationDefault } from '../../src/credential/credential-internal'; @@ -115,7 +116,7 @@ describe('Firebase', () => { }); expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.false; - return firebaseAdmin.app().INTERNAL.getToken() + return getAppInternals().getToken() .should.eventually.have.keys(['accessToken', 'expirationTime']); }); @@ -126,7 +127,7 @@ describe('Firebase', () => { }); expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.false; - return firebaseAdmin.app().INTERNAL.getToken() + return getAppInternals().getToken() .should.eventually.have.keys(['accessToken', 'expirationTime']); }); @@ -138,7 +139,7 @@ describe('Firebase', () => { }); expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.true; - return firebaseAdmin.app().INTERNAL.getToken().then((token) => { + return getAppInternals().getToken().then((token) => { if (typeof credPath === 'undefined') { delete process.env.GOOGLE_APPLICATION_CREDENTIALS; } else { @@ -160,7 +161,7 @@ describe('Firebase', () => { }); expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.false; - return firebaseAdmin.app().INTERNAL.getToken() + return getAppInternals().getToken() .should.eventually.have.keys(['accessToken', 'expirationTime']); }); }); @@ -247,4 +248,8 @@ describe('Firebase', () => { }).not.to.throw(); }); }); + + function getAppInternals(): FirebaseAppInternals { + return (firebaseAdmin.app() as FirebaseApp).INTERNAL; + } }); diff --git a/test/unit/machine-learning/machine-learning-api-client.spec.ts b/test/unit/machine-learning/machine-learning-api-client.spec.ts index ae5fe16244..3399dd0f52 100644 --- a/test/unit/machine-learning/machine-learning-api-client.spec.ts +++ b/test/unit/machine-learning/machine-learning-api-client.spec.ts @@ -19,8 +19,6 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { MachineLearningApiClient, ListModelsOptions, - ModelOptions } from '../../../src/machine-learning/machine-learning-api-client'; import { FirebaseMachineLearningError } from '../../../src/machine-learning/machine-learning-utils'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; @@ -28,6 +26,11 @@ import * as mocks from '../../resources/mocks'; import { FirebaseAppError } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; import { getSdkVersion } from '../../../src/utils/index'; +import { MachineLearningApiClient } from '../../../src/machine-learning/machine-learning-api-client'; +import { machineLearning } from '../../../src/machine-learning/index'; + +import ListModelsOptions = machineLearning.ListModelsOptions; +import ModelOptions = machineLearning.ModelOptions; const expect = chai.expect; @@ -313,9 +316,9 @@ describe('MachineLearningApiClient', () => { const GCS_MASK_LIST = ['displayName', 'tfliteModel.gcsTfliteUri']; const AUTOML_MASK_LIST = ['displayName', 'tfliteModel.automlModel']; - const NAME_ONLY_UPDATE_MASK_STRING = "updateMask=displayName"; - const GCS_UPDATE_MASK_STRING = "updateMask=displayName,tfliteModel.gcsTfliteUri"; - const AUTOML_UPDATE_MASK_STRING = "updateMask=displayName,tfliteModel.automlModel"; + const NAME_ONLY_UPDATE_MASK_STRING = 'updateMask=displayName'; + const GCS_UPDATE_MASK_STRING = 'updateMask=displayName,tfliteModel.gcsTfliteUri'; + const AUTOML_UPDATE_MASK_STRING = 'updateMask=displayName,tfliteModel.automlModel'; const invalidOptions: any[] = [null, undefined]; invalidOptions.forEach((option) => { @@ -459,13 +462,13 @@ describe('MachineLearningApiClient', () => { }); }); - it(`should reject when called with prefixed name`, () => { + it('should reject when called with prefixed name', () => { return apiClient.getModel('projects/foo/models/bar') .should.eventually.be.rejected.and.have.property( 'message', 'Model ID must not contain any "/" characters.'); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.getModel(MODEL_ID) .should.eventually.be.rejectedWith(noProjectId); }); @@ -818,13 +821,13 @@ describe('MachineLearningApiClient', () => { }); }); - it(`should reject when called with prefixed name`, () => { + it('should reject when called with prefixed name', () => { return apiClient.deleteModel('projects/foo/rulesets/bar') .should.eventually.be.rejected.and.have.property( 'message', 'Model ID must not contain any "/" characters.'); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.deleteModel(MODEL_ID) .should.eventually.be.rejectedWith(noProjectId); }); diff --git a/test/unit/machine-learning/machine-learning.spec.ts b/test/unit/machine-learning/machine-learning.spec.ts index 8d32619dff..96493d298b 100644 --- a/test/unit/machine-learning/machine-learning.spec.ts +++ b/test/unit/machine-learning/machine-learning.spec.ts @@ -19,13 +19,20 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { MachineLearning, Model } from '../../../src/machine-learning/machine-learning'; import { FirebaseApp } from '../../../src/firebase-app'; import * as mocks from '../../resources/mocks'; -import { MachineLearningApiClient, StatusErrorResponse, - ModelOptions, ModelResponse, OperationResponse } from '../../../src/machine-learning/machine-learning-api-client'; +import { MachineLearning, Model } from '../../../src/machine-learning/machine-learning'; +import { + MachineLearningApiClient, + StatusErrorResponse, + ModelResponse, + OperationResponse +} from '../../../src/machine-learning/machine-learning-api-client'; import { FirebaseMachineLearningError } from '../../../src/machine-learning/machine-learning-utils'; import { deepCopy } from '../../../src/utils/deep-copy'; +import { machineLearning } from '../../../src/machine-learning/index'; + +import ModelOptions = machineLearning.ModelOptions; const expect = chai.expect; diff --git a/test/unit/messaging/batch-requests.spec.ts b/test/unit/messaging/batch-requests.spec.ts index 982b16420f..4303c5aedd 100644 --- a/test/unit/messaging/batch-requests.spec.ts +++ b/test/unit/messaging/batch-requests.spec.ts @@ -51,15 +51,15 @@ function createMultipartResponse(success: object[], failures: object[] = []): Ht const multipart: Buffer[] = []; success.forEach((part) => { let payload = ''; - payload += `HTTP/1.1 200 OK\r\n`; - payload += `Content-type: application/json\r\n\r\n`; + payload += 'HTTP/1.1 200 OK\r\n'; + payload += 'Content-type: application/json\r\n\r\n'; payload += `${JSON.stringify(part)}\r\n`; multipart.push(Buffer.from(payload, 'utf-8')); }); failures.forEach((part) => { let payload = ''; - payload += `HTTP/1.1 500 Internal Server Error\r\n`; - payload += `Content-type: application/json\r\n\r\n`; + payload += 'HTTP/1.1 500 Internal Server Error\r\n'; + payload += 'Content-type: application/json\r\n\r\n'; payload += `${JSON.stringify(part)}\r\n`; multipart.push(Buffer.from(payload, 'utf-8')); }); diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index fc89ad5ec3..d1c0d96831 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -27,10 +27,7 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; -import { - Message, MessagingOptions, MessagingPayload, MessagingDevicesResponse, MessagingDeviceGroupResponse, - MessagingTopicManagementResponse, BatchResponse, SendResponse, MulticastMessage, -} from '../../../src/messaging/messaging-types'; +import { messaging } from '../../../src/messaging/index'; import { Messaging } from '../../../src/messaging/messaging'; import { BLACKLISTED_OPTIONS_KEYS, BLACKLISTED_DATA_PAYLOAD_KEYS } from '../../../src/messaging/messaging-internal'; import { HttpClient } from '../../../src/utils/api-request'; @@ -42,6 +39,16 @@ chai.use(chaiAsPromised); const expect = chai.expect; +import Message = messaging.Message; +import MessagingOptions = messaging.MessagingOptions; +import MessagingPayload = messaging.MessagingPayload; +import MessagingDevicesResponse = messaging.MessagingDevicesResponse; +import MessagingDeviceGroupResponse = messaging.MessagingDeviceGroupResponse +import MessagingTopicManagementResponse = messaging.MessagingTopicManagementResponse; +import BatchResponse = messaging.BatchResponse; +import SendResponse = messaging.SendResponse; +import MulticastMessage = messaging.MulticastMessage; + // FCM endpoints const FCM_SEND_HOST = 'fcm.googleapis.com'; const FCM_SEND_PATH = '/fcm/send'; @@ -103,15 +110,15 @@ function createMultipartPayloadWithErrors( success.forEach((part) => { payload += `--${boundary}\r\n`; payload += 'Content-type: application/http\r\n\r\n'; - payload += `HTTP/1.1 200 OK\r\n`; - payload += `Content-type: application/json\r\n\r\n`; + payload += 'HTTP/1.1 200 OK\r\n'; + payload += 'Content-type: application/json\r\n\r\n'; payload += `${JSON.stringify(part)}\r\n`; }); failures.forEach((part) => { payload += `--${boundary}\r\n`; payload += 'Content-type: application/http\r\n\r\n'; - payload += `HTTP/1.1 500 Internal Server Error\r\n`; - payload += `Content-type: application/json\r\n\r\n`; + payload += 'HTTP/1.1 500 Internal Server Error\r\n'; + payload += 'Content-type: application/json\r\n\r\n'; payload += `${JSON.stringify(part)}\r\n`; }); payload += `--${boundary}--\r\n`; @@ -1417,6 +1424,9 @@ describe('Messaging', () => { mocks.messaging.registrationToken + '0', mocks.messaging.registrationToken + '1', ], + canonicalRegistrationTokenCount: -1, + multicastId: -1, + results: [], }); }); @@ -1667,6 +1677,7 @@ describe('Messaging', () => { results: [ { messageId: `0:${ mocks.messaging.messageId }` }, ], + failedRegistrationTokens: [], }); }); @@ -2438,7 +2449,7 @@ describe('Messaging', () => { }); }); - it(`should throw given an empty vibrateTimingsMillis array`, () => { + it('should throw given an empty vibrateTimingsMillis array', () => { const message: Message = { condition: 'topic-name', android: { @@ -2472,7 +2483,7 @@ describe('Messaging', () => { }); }); - it(`should throw given a negative light on duration`, () => { + it('should throw given a negative light on duration', () => { const message: Message = { condition: 'topic-name', android: { @@ -2491,7 +2502,7 @@ describe('Messaging', () => { 'android.notification.lightSettings.lightOnDurationMillis must be a non-negative duration in milliseconds'); }); - it(`should throw given a negative light off duration`, () => { + it('should throw given a negative light off duration', () => { const message: Message = { condition: 'topic-name', android: { @@ -2638,7 +2649,7 @@ describe('Messaging', () => { }); invalidImages.forEach((imageUrl) => { - it(`should throw given invalid URL string for imageUrl`, () => { + it('should throw given invalid URL string for imageUrl', () => { expect(() => { messaging.send({ apns: { fcmOptions: { imageUrl } }, topic: 'test' }); }).to.throw('imageUrl must be a valid URL string'); @@ -2677,7 +2688,7 @@ describe('Messaging', () => { }).to.throw('apns.payload.aps must be a non-null object'); }); }); - it(`should throw given APNS payload with duplicate fields`, () => { + it('should throw given APNS payload with duplicate fields', () => { expect(() => { messaging.send({ apns: { diff --git a/test/unit/project-management/android-app.spec.ts b/test/unit/project-management/android-app.spec.ts index edc2d464d3..5feff560a9 100644 --- a/test/unit/project-management/android-app.spec.ts +++ b/test/unit/project-management/android-app.spec.ts @@ -27,7 +27,10 @@ import { import { deepCopy } from '../../../src/utils/deep-copy'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; -import { AndroidAppMetadata, AppPlatform } from '../../../src/project-management/app-metadata'; +import { projectManagement } from '../../../src/project-management/index'; + +import AndroidAppMetadata = projectManagement.AndroidAppMetadata; +import AppPlatform = projectManagement.AppPlatform; const expect = chai.expect; @@ -359,7 +362,7 @@ describe('AndroidApp', () => { .should.eventually.be.rejected .and.have.property( 'message', - `getConfig()'s responseData.configFileContents must be a base64 string. ` + 'getConfig()\'s responseData.configFileContents must be a base64 string. ' + `Response data: ${JSON.stringify(apiResponse, null, 2)}`); }); diff --git a/test/unit/project-management/ios-app.spec.ts b/test/unit/project-management/ios-app.spec.ts index 9f00aaff34..e0163d893f 100644 --- a/test/unit/project-management/ios-app.spec.ts +++ b/test/unit/project-management/ios-app.spec.ts @@ -27,7 +27,10 @@ import { import { deepCopy } from '../../../src/utils/deep-copy'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; -import { IosAppMetadata, AppPlatform } from '../../../src/project-management/app-metadata'; +import { projectManagement } from '../../../src/project-management/index'; + +import IosAppMetadata = projectManagement.IosAppMetadata; +import AppPlatform = projectManagement.AppPlatform; const expect = chai.expect; @@ -206,7 +209,7 @@ describe('IosApp', () => { .should.eventually.be.rejected .and.have.property( 'message', - `getConfig()'s responseData.configFileContents must be a base64 string. ` + 'getConfig()\'s responseData.configFileContents must be a base64 string. ' + `Response data: ${JSON.stringify(apiResponse, null, 2)}`); }); diff --git a/test/unit/project-management/project-management-api-request.spec.ts b/test/unit/project-management/project-management-api-request.spec.ts index 23a5153ff5..f4088e0a1f 100644 --- a/test/unit/project-management/project-management-api-request.spec.ts +++ b/test/unit/project-management/project-management-api-request.spec.ts @@ -30,7 +30,9 @@ import * as mocks from '../../resources/mocks'; import * as utils from '../utils'; import { getSdkVersion } from '../../../src/utils/index'; import { ShaCertificate } from '../../../src/project-management/android-app'; -import { AppPlatform } from '../../../src/project-management/app-metadata'; +import { projectManagement } from '../../../src/project-management/index'; + +import AppPlatform = projectManagement.AppPlatform; chai.should(); chai.use(sinonChai); diff --git a/test/unit/project-management/project-management.spec.ts b/test/unit/project-management/project-management.spec.ts index 0e109a6d9b..f7837389c7 100644 --- a/test/unit/project-management/project-management.spec.ts +++ b/test/unit/project-management/project-management.spec.ts @@ -27,8 +27,11 @@ import { } from '../../../src/project-management/project-management-api-request-internal'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; -import { IosApp } from '../../../src/project-management/ios-app'; -import { AppPlatform, AppMetadata } from '../../../src/project-management/app-metadata'; +import { projectManagement } from '../../../src/project-management/index'; + +import AppMetadata = projectManagement.AppMetadata; +import AppPlatform = projectManagement.AppPlatform; +import IosApp = projectManagement.IosApp; const expect = chai.expect; diff --git a/test/unit/remote-config/remote-config-api-client.spec.ts b/test/unit/remote-config/remote-config-api-client.spec.ts index 4a01433f30..c416715dcb 100644 --- a/test/unit/remote-config/remote-config-api-client.spec.ts +++ b/test/unit/remote-config/remote-config-api-client.spec.ts @@ -19,12 +19,7 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { - RemoteConfigTemplate, - TagColor, - ListVersionsResult, - Version, -} from '../../../src/remote-config/remote-config-api-client'; +import { remoteConfig } from '../../../src/remote-config/index'; import { FirebaseRemoteConfigError, RemoteConfigApiClient @@ -37,6 +32,10 @@ import { FirebaseApp } from '../../../src/firebase-app'; import { deepCopy } from '../../../src/utils/deep-copy'; import { getSdkVersion } from '../../../src/utils/index'; +import RemoteConfigTemplate = remoteConfig.RemoteConfigTemplate; +import Version = remoteConfig.Version; +import ListVersionsResult = remoteConfig.ListVersionsResult; + const expect = chai.expect; describe('RemoteConfigApiClient', () => { @@ -50,9 +49,9 @@ describe('RemoteConfigApiClient', () => { }; const VALIDATION_ERROR_MESSAGES = [ - "[VALIDATION_ERROR]: [foo] are not valid condition names. All keys in all conditional value" + - " maps must be valid condition names.", - "[VERSION_MISMATCH]: Expected version 6, found 8 for project: 123456789012" + '[VALIDATION_ERROR]: [foo] are not valid condition names. All keys in all conditional value' + + ' maps must be valid condition names.', + '[VERSION_MISMATCH]: Expected version 6, found 8 for project: 123456789012' ]; const EXPECTED_HEADERS = { @@ -124,7 +123,7 @@ describe('RemoteConfigApiClient', () => { { name: 'ios', expression: 'device.os == \'ios\'', - tagColor: TagColor.PINK, + tagColor: 'PINK', }, ], parameters: { @@ -181,7 +180,7 @@ describe('RemoteConfigApiClient', () => { }); describe('getTemplate', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.getTemplate() .should.eventually.be.rejectedWith(noProjectId); }); @@ -212,7 +211,7 @@ describe('RemoteConfigApiClient', () => { }); describe('getTemplateAtVersion', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.getTemplateAtVersion(65) .should.eventually.be.rejectedWith(noProjectId); }); @@ -263,7 +262,7 @@ describe('RemoteConfigApiClient', () => { }); describe('validateTemplate', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.validateTemplate(REMOTE_CONFIG_TEMPLATE) .should.eventually.be.rejectedWith(noProjectId); }); @@ -342,7 +341,7 @@ describe('RemoteConfigApiClient', () => { error: { code: 400, message: message, - status: "FAILED_PRECONDITION" + status: 'FAILED_PRECONDITION' } }, 400)); stubs.push(stub); @@ -354,7 +353,7 @@ describe('RemoteConfigApiClient', () => { }); describe('publishTemplate', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.publishTemplate(REMOTE_CONFIG_TEMPLATE) .should.eventually.be.rejectedWith(noProjectId); }); @@ -437,7 +436,7 @@ describe('RemoteConfigApiClient', () => { error: { code: 400, message: message, - status: "FAILED_PRECONDITION" + status: 'FAILED_PRECONDITION' } }, 400)); stubs.push(stub); @@ -449,7 +448,7 @@ describe('RemoteConfigApiClient', () => { }); describe('rollback', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.rollback('60') .should.eventually.be.rejectedWith(noProjectId); }); @@ -504,7 +503,7 @@ describe('RemoteConfigApiClient', () => { }); describe('listVersions', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.listVersions() .should.eventually.be.rejectedWith(noProjectId); }); diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index a6512cf3fa..d884a515cf 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -22,18 +22,18 @@ import * as sinon from 'sinon'; import { RemoteConfig } from '../../../src/remote-config/remote-config'; import { FirebaseApp } from '../../../src/firebase-app'; import * as mocks from '../../resources/mocks'; -import { - RemoteConfigTemplate, - RemoteConfigCondition, - TagColor, - ListVersionsResult, -} from '../../../src/remote-config/remote-config-api-client'; +import { remoteConfig } from '../../../src/remote-config/index'; import { FirebaseRemoteConfigError, RemoteConfigApiClient } from '../../../src/remote-config/remote-config-api-client-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; +import RemoteConfigTemplate = remoteConfig.RemoteConfigTemplate; +import RemoteConfigCondition = remoteConfig.RemoteConfigCondition; +import TagColor = remoteConfig.TagColor; +import ListVersionsResult = remoteConfig.ListVersionsResult; + const expect = chai.expect; describe('RemoteConfig', () => { @@ -82,7 +82,7 @@ describe('RemoteConfig', () => { { name: 'ios', expression: 'device.os == \'ios\'', - tagColor: TagColor.BLUE, + tagColor: 'BLUE', }, ], parameters: { @@ -102,7 +102,7 @@ describe('RemoteConfig', () => { conditions: [{ name: 'ios', expression: 'device.os == \'ios\'', - tagColor: TagColor.PINK, + tagColor: 'PINK', }], parameters: { // eslint-disable-next-line @typescript-eslint/camelcase @@ -484,12 +484,12 @@ describe('RemoteConfig', () => { expect(newTemplate.conditions.length).to.equal(1); expect(newTemplate.conditions[0].name).to.equal('ios'); expect(newTemplate.conditions[0].expression).to.equal('device.os == \'ios\''); - expect(newTemplate.conditions[0].tagColor).to.equal(TagColor.BLUE); + expect(newTemplate.conditions[0].tagColor).to.equal('BLUE'); // verify that the etag is unchanged expect(newTemplate.etag).to.equal('etag-123456789012-5'); // verify that the etag is read-only expect(() => { - (newTemplate as any).etag = "new-etag"; + (newTemplate as any).etag = 'new-etag'; }).to.throw( 'Cannot set property etag of # which has only a getter'); @@ -506,7 +506,7 @@ describe('RemoteConfig', () => { const cond = c as RemoteConfigCondition; expect(cond.name).to.equal('ios'); expect(cond.expression).to.equal('device.os == \'ios\''); - expect(cond.tagColor).to.equal(TagColor.BLUE); + expect(cond.tagColor).to.equal('BLUE'); }); }); @@ -552,7 +552,7 @@ describe('RemoteConfig', () => { stubs.push(stub); return rcOperation() .should.eventually.be.rejected.and.have.property( - 'message', `Remote Config parameters must be a non-null object`); + 'message', 'Remote Config parameters must be a non-null object'); }); it('should reject when API response does not contain valid parameter groups', () => { @@ -564,7 +564,7 @@ describe('RemoteConfig', () => { stubs.push(stub); return rcOperation() .should.eventually.be.rejected.and.have.property( - 'message', `Remote Config parameter groups must be a non-null object`); + 'message', 'Remote Config parameter groups must be a non-null object'); }); it('should reject when API response does not contain valid conditions', () => { @@ -576,7 +576,7 @@ describe('RemoteConfig', () => { stubs.push(stub); return rcOperation() .should.eventually.be.rejected.and.have.property( - 'message', `Remote Config conditions must be an array`); + 'message', 'Remote Config conditions must be an array'); }); } @@ -639,11 +639,11 @@ describe('RemoteConfig', () => { expect(template.conditions.length).to.equal(1); expect(template.conditions[0].name).to.equal('ios'); expect(template.conditions[0].expression).to.equal('device.os == \'ios\''); - expect(template.conditions[0].tagColor).to.equal(TagColor.BLUE); + expect(template.conditions[0].tagColor).to.equal('BLUE'); expect(template.etag).to.equal('etag-123456789012-5'); // verify that etag is read-only expect(() => { - (template as any).etag = "new-etag"; + (template as any).etag = 'new-etag'; }).to.throw( 'Cannot set property etag of # which has only a getter'); @@ -670,7 +670,7 @@ describe('RemoteConfig', () => { const cond = c as RemoteConfigCondition; expect(cond.name).to.equal('ios'); expect(cond.expression).to.equal('device.os == \'ios\''); - expect(cond.tagColor).to.equal(TagColor.BLUE); + expect(cond.tagColor).to.equal('BLUE'); const parsed = JSON.parse(JSON.stringify(template)); const expectedTemplate = deepCopy(REMOTE_CONFIG_RESPONSE); diff --git a/test/unit/security-rules/security-rules-api-client.spec.ts b/test/unit/security-rules/security-rules-api-client.spec.ts index 3a3c37a892..f0cd1c4185 100644 --- a/test/unit/security-rules/security-rules-api-client.spec.ts +++ b/test/unit/security-rules/security-rules-api-client.spec.ts @@ -90,13 +90,13 @@ describe('SecurityRulesApiClient', () => { }); }); - it(`should reject when called with prefixed name`, () => { + it('should reject when called with prefixed name', () => { return apiClient.getRuleset('projects/foo/rulesets/bar') .should.eventually.be.rejected.and.have.property( 'message', 'Ruleset name must not contain any "/" characters.'); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.getRuleset(RULESET_NAME) .should.eventually.be.rejectedWith(noProjectId); }); @@ -180,7 +180,7 @@ describe('SecurityRulesApiClient', () => { }); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.createRuleset({ source: { files: [RULES_FILE], @@ -328,7 +328,7 @@ describe('SecurityRulesApiClient', () => { }); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.listRulesets() .should.eventually.be.rejectedWith(noProjectId); }); @@ -427,7 +427,7 @@ describe('SecurityRulesApiClient', () => { }); describe('getRelease', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.getRelease(RELEASE_NAME) .should.eventually.be.rejectedWith(noProjectId); }); @@ -491,7 +491,7 @@ describe('SecurityRulesApiClient', () => { }); describe('updateRelease', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.updateRelease(RELEASE_NAME, RULESET_NAME) .should.eventually.be.rejectedWith(noProjectId); }); @@ -570,13 +570,13 @@ describe('SecurityRulesApiClient', () => { }); }); - it(`should reject when called with prefixed name`, () => { + it('should reject when called with prefixed name', () => { return apiClient.deleteRuleset('projects/foo/rulesets/bar') .should.eventually.be.rejected.and.have.property( 'message', 'Ruleset name must not contain any "/" characters.'); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.deleteRuleset(RULESET_NAME) .should.eventually.be.rejectedWith(noProjectId); }); diff --git a/test/unit/utils.ts b/test/unit/utils.ts index fa655dd4e8..aafb573b8b 100644 --- a/test/unit/utils.ts +++ b/test/unit/utils.ts @@ -20,18 +20,19 @@ import * as sinon from 'sinon'; import * as mocks from '../resources/mocks'; import { FirebaseNamespace } from '../../src/firebase-namespace'; -import { FirebaseApp, FirebaseAppOptions, FirebaseAppInternals, FirebaseAccessToken } from '../../src/firebase-app'; +import { AppOptions } from '../../src/firebase-namespace-api'; +import { FirebaseApp, FirebaseAppInternals, FirebaseAccessToken } from '../../src/firebase-app'; import { HttpError, HttpResponse } from '../../src/utils/api-request'; /** * Returns a new FirebaseApp instance with the provided options. * - * @param {object} options The options for the FirebaseApp instance to create. - * @return {FirebaseApp} A new FirebaseApp instance with the provided options. + * @param options The options for the FirebaseApp instance to create. + * @return A new FirebaseApp instance with the provided options. */ export function createAppWithOptions(options: object): FirebaseApp { const mockFirebaseNamespaceInternals = new FirebaseNamespace().INTERNAL; - return new FirebaseApp(options as FirebaseAppOptions, mocks.appName, mockFirebaseNamespaceInternals); + return new FirebaseApp(options as AppOptions, mocks.appName, mockFirebaseNamespaceInternals); } diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 7a742e1b20..850f9c09e1 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -24,7 +24,7 @@ import { toWebSafeBase64, formatString, generateUpdateMask, } from '../../../src/utils/index'; import { isNonEmptyString } from '../../../src/utils/validator'; -import { FirebaseApp, FirebaseAppOptions } from '../../../src/firebase-app'; +import { FirebaseApp } from '../../../src/firebase-app'; import { ComputeEngineCredential } from '../../../src/credential/credential-internal'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; @@ -104,7 +104,7 @@ describe('getExplicitProjectId()', () => { }); it('should return the explicitly specified project ID from app options', () => { - const options: FirebaseAppOptions = { + const options = { credential: new mocks.MockCredential(), projectId: 'explicit-project-id', }; @@ -172,7 +172,7 @@ describe('findProjectId()', () => { }); it('should return the explicitly specified project ID from app options', () => { - const options: FirebaseAppOptions = { + const options = { credential: new mocks.MockCredential(), projectId: 'explicit-project-id', }; @@ -246,7 +246,7 @@ describe('findProjectId()', () => { }); it('should return the explicitly specified project ID from app options', () => { - const options: FirebaseAppOptions = { + const options = { credential: new mocks.MockCredential(), projectId: 'explicit-project-id', }; diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json deleted file mode 100644 index fdb5ad2719..0000000000 --- a/tsconfig.eslint.json +++ /dev/null @@ -1,12 +0,0 @@ -// Used for @typescript-eslint/no-unused-vars-experimental to glob the entire -// source. An alternative is createDefaultProgram: true but it has exponential -// complexity. See: github.com/typescript-eslint/typescript-eslint/issues/967 -{ - "compilerOptions": { - "strict": true, - }, - "include": [ - "src/**/*.ts", - "test/**/*.ts", - ], -} diff --git a/tsconfig.json b/tsconfig.json index e13292b408..f4071b8a8f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "module": "commonjs", "target": "es5", + "declaration": true, "noImplicitAny": true, "noUnusedLocals": true, // TODO(rsgowman): enable `"strict": true,` and remove explicit setting of: noImplicitAny, noImplicitThis, alwaysStrict, strictBindCallApply, strictNullChecks, strictFunctionTypes, strictPropertyInitialization. From b5606aa5285b6bacc5e64e773deeb61b11c97193 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 2 Nov 2020 14:12:43 -0800 Subject: [PATCH 050/160] fix: Adding es2018 libraries to the build (#1079) --- test/integration/typescript/tsconfig.json | 2 +- tsconfig.json | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/integration/typescript/tsconfig.json b/test/integration/typescript/tsconfig.json index 5ced3e692e..6820c83387 100644 --- a/test/integration/typescript/tsconfig.json +++ b/test/integration/typescript/tsconfig.json @@ -4,7 +4,7 @@ "moduleResolution": "node", "target": "es5", "noImplicitAny": false, - "lib": ["es2015"], + "lib": ["es2018"], "outDir": "lib", "typeRoots": [ "node_modules/@types" diff --git a/tsconfig.json b/tsconfig.json index f4071b8a8f..489b90bedf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,10 +12,8 @@ "strictNullChecks": true, "strictFunctionTypes": true, //"strictPropertyInitialization": true, - "lib": ["es2015"], + "lib": ["es2018"], "outDir": "lib", - // We manually craft typings in src/index.d.ts instead of auto-generating them. - // "declaration": true, "rootDir": "." }, "files": [ From 19a7abf3a11d9a420c7a20cbdeb9f36a6d84b6f4 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 3 Nov 2020 11:53:31 -0800 Subject: [PATCH 051/160] feat(firestore): Exposed more types from the admin.firestore namespace (#1080) * feat(firestore): Exposed more types from the admin.firestore namespace * fix: Updated integration test --- package-lock.json | 305 ++++++++++++++++++++++++----- package.json | 2 +- src/firestore/index.ts | 11 ++ test/integration/firestore.spec.ts | 36 ++++ 4 files changed, 302 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 189d92aa98..f85f9fcd6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -317,9 +317,9 @@ } }, "@google-cloud/firestore": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.1.0.tgz", - "integrity": "sha512-odcAxfpgDUHK5air9bBtx6pewrgg0+LGHJ6Kkc7qJJMzjoyVyckQ3POiAxTZyKshr5+7gycIWKlieY0ewcb+pQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.5.0.tgz", + "integrity": "sha512-sExt4E+TlBqyv4l/av6kBZ4YGS99Cc3P5UvLRNj9z41mT9ekPGhIzptbj4K6O7h0KCyDIDOiJdi4gUPH9lip4g==", "optional": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -378,18 +378,222 @@ } }, "@grpc/grpc-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.2.tgz", - "integrity": "sha512-k2u86Bkm/3xrjUaSWeIyzXScBt/cC8uE7BznR0cpueQi11R33W6qfJdMrkrsmSHirp5likR55JSXUrcWG6ybHA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.8.tgz", + "integrity": "sha512-64hg5rmEm6F/NvlWERhHmmgxbWU8nD2TMWE+9TvG7/WcOrFT3fzg/Uu631pXRFwmJ4aWO/kp9vVSlr8FUjBDLA==", "optional": true, "requires": { + "@grpc/proto-loader": "^0.6.0-pre14", + "@types/node": "^12.12.47", + "google-auth-library": "^6.0.0", "semver": "^6.2.0" + }, + "dependencies": { + "@grpc/proto-loader": { + "version": "0.6.0-pre9", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.0-pre9.tgz", + "integrity": "sha512-oM+LjpEjNzW5pNJjt4/hq1HYayNeQT+eGrOPABJnYHv7TyNPDNzkQ76rDYZF86X5swJOa4EujEMzQ9iiTdPgww==", + "optional": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.9.0", + "yargs": "^15.3.1" + } + }, + "@types/node": { + "version": "12.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.3.tgz", + "integrity": "sha512-8Jduo8wvvwDzEVJCOvS/G6sgilOLvvhn1eMmK3TW8/T217O7u1jdrK6ImKLv80tVryaPSVeKu6sjDEiFjd4/eg==", + "optional": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "optional": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "optional": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "optional": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "optional": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "optional": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "optional": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "optional": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "optional": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "optional": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "optional": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "optional": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "optional": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "optional": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "optional": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "optional": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "optional": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "optional": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } } }, "@grpc/proto-loader": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.4.tgz", - "integrity": "sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", + "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", "optional": true, "requires": { "lodash.camelcase": "^4.3.0", @@ -1993,8 +2197,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decode-uri-component": { "version": "0.2.0", @@ -2290,8 +2493,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "end-of-stream": { "version": "1.4.4", @@ -3431,25 +3633,46 @@ } }, "google-gax": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.6.2.tgz", - "integrity": "sha512-Q5IydUQ+7sF072E72Cl1KDxHaQIa1a5CVqG80OHk3NXdT5ZvKAveBFp/f78E3HjVHGTgUDB16R4M2wDt0ia9mQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.9.1.tgz", + "integrity": "sha512-KQ7HiMTB/PAzKv3OU00x6tC1H7MHvSxQfon5BSyW5o+lkMgRA8xoqvlxZCBC1dlW1azOPGF8vScy8QgFmhaQ9Q==", "optional": true, "requires": { "@grpc/grpc-js": "~1.1.1", "@grpc/proto-loader": "^0.5.1", "@types/long": "^4.0.0", "abort-controller": "^3.0.0", - "duplexify": "^3.6.0", + "duplexify": "^4.0.0", "google-auth-library": "^6.0.0", "is-stream-ended": "^0.1.4", - "lodash.at": "^4.6.0", - "lodash.has": "^4.5.2", - "node-fetch": "^2.6.0", + "node-fetch": "^2.6.1", "protobufjs": "^6.9.0", - "retry-request": "^4.0.0", - "semver": "^6.0.0", - "walkdir": "^0.4.0" + "retry-request": "^4.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "google-p12-pem": { @@ -4976,12 +5199,6 @@ "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", "dev": true }, - "lodash.at": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz", - "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g=", - "optional": true - }, "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -5009,12 +5226,6 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, - "lodash.has": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", - "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=", - "optional": true - }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -6564,9 +6775,9 @@ "dev": true }, "protobufjs": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.9.0.tgz", - "integrity": "sha512-LlGVfEWDXoI/STstRDdZZKb/qusoAWUnmLg9R8OLSO473mBLWHowx8clbX5/+mKDEI+v7GzjoK9tRPZMMcoTrg==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", + "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", @@ -6585,9 +6796,9 @@ }, "dependencies": { "@types/node": { - "version": "13.13.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.13.tgz", - "integrity": "sha512-UfvBE9oRCAJVzfR+3eWm/sdLFe/qroAPEXP3GPJ1SehQiEVgZT6NQZWYbPMiJ3UdcKM06v4j+S1lTcdWCmw+3g==", + "version": "13.13.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.30.tgz", + "integrity": "sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA==", "optional": true } } @@ -6966,8 +7177,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "1.0.1", @@ -7127,8 +7337,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { "version": "2.0.1", @@ -8550,12 +8759,6 @@ "xml-name-validator": "^3.0.0" } }, - "walkdir": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", - "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", - "optional": true - }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/package.json b/package.json index 1dea780c1f..d60b464327 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "node-forge": "^0.10.0" }, "optionalDependencies": { - "@google-cloud/firestore": "^4.0.0", + "@google-cloud/firestore": "^4.5.0", "@google-cloud/storage": "^5.3.0" }, "devDependencies": { diff --git a/src/firestore/index.ts b/src/firestore/index.ts index 5204a4f584..4efe99e053 100644 --- a/src/firestore/index.ts +++ b/src/firestore/index.ts @@ -26,19 +26,30 @@ export namespace firestore { export import v1beta1 = _firestore.v1beta1; export import v1 = _firestore.v1; + export import BulkWriter = _firestore.BulkWriter; + export import BulkWriterOptions = _firestore.BulkWriterOptions; + export import CollectionGroup = _firestore.CollectionGroup; export import CollectionReference = _firestore.CollectionReference; + export import DocumentChangeType = _firestore.DocumentChangeType; export import DocumentData = _firestore.DocumentData; export import DocumentReference = _firestore.DocumentReference; export import DocumentSnapshot = _firestore.DocumentSnapshot; export import FieldPath = _firestore.FieldPath; export import FieldValue = _firestore.FieldValue; export import Firestore = _firestore.Firestore; + export import FirestoreDataConverter = _firestore.FirestoreDataConverter; export import GeoPoint = _firestore.GeoPoint; + export import GrpcStatus = _firestore.GrpcStatus; + export import Precondition = _firestore.Precondition; export import Query = _firestore.Query; export import QueryDocumentSnapshot = _firestore.QueryDocumentSnapshot; + export import QueryPartition = _firestore.QueryPartition; export import QuerySnapshot = _firestore.QuerySnapshot; + export import ReadOptions = _firestore.ReadOptions; + export import Settings = _firestore.Settings; export import Timestamp = _firestore.Timestamp; export import Transaction = _firestore.Transaction; + export import UpdateData = _firestore.UpdateData; export import WriteBatch = _firestore.WriteBatch; export import WriteResult = _firestore.WriteResult; diff --git a/test/integration/firestore.spec.ts b/test/integration/firestore.spec.ts index 3a6dc89fff..13ef9131dd 100644 --- a/test/integration/firestore.spec.ts +++ b/test/integration/firestore.spec.ts @@ -113,6 +113,38 @@ describe('admin.firestore', () => { expect(typeof admin.firestore.WriteResult).to.be.not.undefined; }); + it('admin.firestore.GrpcStatus type is defined', () => { + expect(typeof admin.firestore.GrpcStatus).to.be.not.undefined; + }); + + it('supports operations with custom type converters', () => { + const converter: admin.firestore.FirestoreDataConverter = { + toFirestore: (city: City) => { + return { + name: city.localId, + population: city.people, + }; + }, + fromFirestore: (snap: admin.firestore.QueryDocumentSnapshot) => { + return new City(snap.data().name, snap.data().population); + } + }; + + const expected: City = new City('Sunnyvale', 153185); + const refWithConverter: admin.firestore.DocumentReference = admin.firestore() + .collection('cities') + .doc() + .withConverter(converter); + return refWithConverter.set(expected) + .then(() => { + return refWithConverter.get(); + }) + .then((snapshot: admin.firestore.DocumentSnapshot) => { + expect(snapshot.data()).to.be.instanceOf(City); + return refWithConverter.delete(); + }); + }); + it('supports saving references in documents', () => { const source = admin.firestore().collection('cities').doc(); const target = admin.firestore().collection('cities').doc(); @@ -150,3 +182,7 @@ describe('admin.firestore', () => { }); }); }); + +class City { + constructor(readonly localId: string, readonly people: number) { } +} From 12b02d68f9d89d3e5088a3da73daa0396d67f61d Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 9 Nov 2020 12:36:28 -0800 Subject: [PATCH 052/160] chore: Configuring an API Extractor report for the repo (#1081) * chore: Configuring an API Extractor report for the repo * fix: Setting declarationMap via gulpfile so that tests are not affected --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 3 + .gitignore | 1 + api-extractor.json | 368 ++++++++++++ etc/firebase-admin.api.md | 1068 +++++++++++++++++++++++++++++++++ gulpfile.js | 2 +- package-lock.json | 194 ++++++ package.json | 5 +- tsconfig.json | 1 + 9 files changed, 1641 insertions(+), 2 deletions(-) create mode 100644 api-extractor.json create mode 100644 etc/firebase-admin.api.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7615a6ed62..b5810ed895 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,4 @@ jobs: npm run build npm run build:tests npm test + npm run api-extractor diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b344f22ca..4740665e9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,6 +58,9 @@ jobs: - name: Run unit tests run: npm test + - name: Verify public API + run: npm run api-extractor + - name: Run integration tests run: ./.github/scripts/run_integration_tests.sh env: diff --git a/.gitignore b/.gitignore index 814fb22453..672f8c23a9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ npm-debug.log lib/ .tmp/ +temp/ typings/ coverage/ node_modules/ diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 0000000000..140b645cfe --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,368 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + // We point to the firebase-namespace.d.ts file since index.d.ts uses namespace imports that are + // not supported by API Extractor. See https://github.com/microsoft/rushstack/issues/1029 and + // https://github.com/microsoft/rushstack/issues/2338. + "mainEntryPointFilePath": "/lib/firebase-namespace.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [], + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": true + + /** + * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce + * a full file path. + * + * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + // "reportFileName": ".api.md", + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/etc/" + */ + // "reportFolder": "/etc/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportTempFolder": "/temp/" + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json" + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": false + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + // "untrimmedFilePath": "/dist/.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + // "enabled": true, + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning" + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + }, + + "ae-missing-release-tag": { + "logLevel": "none" + }, + + "ae-unresolved-link": { + "logLevel": "none" + } + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } +} diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md new file mode 100644 index 0000000000..e6b95c8871 --- /dev/null +++ b/etc/firebase-admin.api.md @@ -0,0 +1,1068 @@ +## API Report File for "firebase-admin" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Agent } from 'http'; +import { Bucket } from '@google-cloud/storage'; +import * as _firestore from '@google-cloud/firestore'; +import * as rtdb from '@firebase/database-types'; + +// @public (undocumented) +export function app(name?: string): app.App; + +// @public (undocumented) +export namespace app { + export interface App { + // (undocumented) + auth(): auth.Auth; + // (undocumented) + database(url?: string): database.Database; + delete(): Promise; + // (undocumented) + firestore(): firestore.Firestore; + // (undocumented) + instanceId(): instanceId.InstanceId; + // (undocumented) + machineLearning(): machineLearning.MachineLearning; + // (undocumented) + messaging(): messaging.Messaging; + name: string; + options: AppOptions; + // (undocumented) + projectManagement(): projectManagement.ProjectManagement; + // (undocumented) + remoteConfig(): remoteConfig.RemoteConfig; + // (undocumented) + securityRules(): securityRules.SecurityRules; + // (undocumented) + storage(): storage.Storage; + } +} + +// @public +export interface AppOptions { + credential?: credential.Credential; + databaseAuthVariableOverride?: object | null; + databaseURL?: string; + httpAgent?: Agent; + projectId?: string; + serviceAccountId?: string; + storageBucket?: string; +} + +// @public (undocumented) +export const apps: (app.App | null)[]; + +// @public +export function auth(app?: app.App): auth.Auth; + +// @public (undocumented) +export namespace auth { + export interface ActionCodeSettings { + android?: { + packageName: string; + installApp?: boolean; + minimumVersion?: string; + }; + dynamicLinkDomain?: string; + handleCodeInApp?: boolean; + iOS?: { + bundleId: string; + }; + url: string; + } + // (undocumented) + export interface Auth extends BaseAuth { + // (undocumented) + app: app.App; + tenantManager(): TenantManager; + } + export type AuthFactorType = 'phone'; + export interface AuthProviderConfig { + displayName?: string; + enabled: boolean; + providerId: string; + } + export interface AuthProviderConfigFilter { + maxResults?: number; + pageToken?: string; + type: 'saml' | 'oidc'; + } + // (undocumented) + export interface BaseAuth { + createCustomToken(uid: string, developerClaims?: object): Promise; + createProviderConfig(config: AuthProviderConfig): Promise; + createSessionCookie(idToken: string, sessionCookieOptions: SessionCookieOptions): Promise; + createUser(properties: CreateRequest): Promise; + deleteProviderConfig(providerId: string): Promise; + deleteUser(uid: string): Promise; + deleteUsers(uids: string[]): Promise; + generateEmailVerificationLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise; + generatePasswordResetLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise; + generateSignInWithEmailLink(email: string, actionCodeSettings: ActionCodeSettings): Promise; + getProviderConfig(providerId: string): Promise; + getUser(uid: string): Promise; + getUserByEmail(email: string): Promise; + getUserByPhoneNumber(phoneNumber: string): Promise; + getUsers(identifiers: UserIdentifier[]): Promise; + importUsers(users: UserImportRecord[], options?: UserImportOptions): Promise; + listProviderConfigs(options: AuthProviderConfigFilter): Promise; + listUsers(maxResults?: number, pageToken?: string): Promise; + revokeRefreshTokens(uid: string): Promise; + setCustomUserClaims(uid: string, customUserClaims: object | null): Promise; + updateProviderConfig(providerId: string, updatedConfig: UpdateAuthProviderRequest): Promise; + updateUser(uid: string, properties: UpdateRequest): Promise; + verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; + verifySessionCookie(sessionCookie: string, checkForRevocation?: boolean): Promise; + } + export interface CreateMultiFactorInfoRequest { + displayName?: string; + factorId: string; + } + export interface CreatePhoneMultiFactorInfoRequest extends CreateMultiFactorInfoRequest { + phoneNumber: string; + } + export interface CreateRequest extends UpdateRequest { + multiFactor?: MultiFactorCreateSettings; + uid?: string; + } + export type CreateTenantRequest = UpdateTenantRequest; + export interface DecodedIdToken { + // (undocumented) + [key: string]: any; + aud: string; + auth_time: number; + email?: string; + email_verified?: boolean; + exp: number; + firebase: { + identities: { + [key: string]: any; + }; + sign_in_provider: string; + sign_in_second_factor?: string; + second_factor_identifier?: string; + tenant?: string; + [key: string]: any; + }; + iat: number; + iss: string; + phone_number?: string; + picture?: string; + sub: string; + uid: string; + } + export interface DeleteUsersResult { + errors: FirebaseArrayIndexError[]; + failureCount: number; + successCount: number; + } + export interface EmailIdentifier { + // (undocumented) + email: string; + } + export interface EmailSignInProviderConfig { + enabled: boolean; + passwordRequired?: boolean; + } + export interface GetUsersResult { + notFound: UserIdentifier[]; + users: UserRecord[]; + } + // (undocumented) + export type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | 'HMAC_SHA256' | 'HMAC_SHA1' | 'HMAC_MD5' | 'MD5' | 'PBKDF_SHA1' | 'BCRYPT' | 'PBKDF2_SHA256' | 'SHA512' | 'SHA256' | 'SHA1'; + export interface ListProviderConfigResults { + pageToken?: string; + providerConfigs: AuthProviderConfig[]; + } + export interface ListTenantsResult { + pageToken?: string; + tenants: Tenant[]; + } + export interface ListUsersResult { + pageToken?: string; + users: UserRecord[]; + } + export interface MultiFactorConfig { + factorIds?: AuthFactorType[]; + state: MultiFactorConfigState; + } + export type MultiFactorConfigState = 'ENABLED' | 'DISABLED'; + export interface MultiFactorCreateSettings { + enrolledFactors: CreateMultiFactorInfoRequest[]; + } + export interface MultiFactorInfo { + displayName?: string; + enrollmentTime?: string; + factorId: string; + toJSON(): object; + uid: string; + } + export interface MultiFactorSettings { + enrolledFactors: MultiFactorInfo[]; + toJSON(): object; + } + export interface MultiFactorUpdateSettings { + enrolledFactors: UpdateMultiFactorInfoRequest[] | null; + } + export interface OIDCAuthProviderConfig extends AuthProviderConfig { + clientId: string; + issuer: string; + } + export interface OIDCUpdateAuthProviderRequest { + clientId?: string; + displayName?: string; + enabled?: boolean; + issuer?: string; + } + export interface PhoneIdentifier { + // (undocumented) + phoneNumber: string; + } + export interface PhoneMultiFactorInfo extends MultiFactorInfo { + phoneNumber: string; + } + export interface ProviderIdentifier { + // (undocumented) + providerId: string; + // (undocumented) + providerUid: string; + } + export interface SAMLAuthProviderConfig extends AuthProviderConfig { + callbackURL?: string; + idpEntityId: string; + rpEntityId: string; + ssoURL: string; + x509Certificates: string[]; + } + export interface SAMLUpdateAuthProviderRequest { + callbackURL?: string; + displayName?: string; + enabled?: boolean; + idpEntityId?: string; + rpEntityId?: string; + ssoURL?: string; + x509Certificates?: string[]; + } + export interface SessionCookieOptions { + expiresIn: number; + } + export interface Tenant { + displayName?: string; + emailSignInConfig?: { + enabled: boolean; + passwordRequired?: boolean; + }; + multiFactorConfig?: MultiFactorConfig; + tenantId: string; + testPhoneNumbers?: { + [phoneNumber: string]: string; + }; + toJSON(): object; + } + export interface TenantAwareAuth extends BaseAuth { + tenantId: string; + } + export interface TenantManager { + // (undocumented) + authForTenant(tenantId: string): TenantAwareAuth; + createTenant(tenantOptions: CreateTenantRequest): Promise; + deleteTenant(tenantId: string): Promise; + getTenant(tenantId: string): Promise; + listTenants(maxResults?: number, pageToken?: string): Promise; + updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise; + } + export interface UidIdentifier { + // (undocumented) + uid: string; + } + // (undocumented) + export type UpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; + export interface UpdateMultiFactorInfoRequest { + displayName?: string; + enrollmentTime?: string; + factorId: string; + uid?: string; + } + export interface UpdatePhoneMultiFactorInfoRequest extends UpdateMultiFactorInfoRequest { + phoneNumber: string; + } + export interface UpdateRequest { + disabled?: boolean; + displayName?: string | null; + email?: string; + emailVerified?: boolean; + multiFactor?: MultiFactorUpdateSettings; + password?: string; + phoneNumber?: string | null; + photoURL?: string | null; + } + export interface UpdateTenantRequest { + displayName?: string; + emailSignInConfig?: EmailSignInProviderConfig; + multiFactorConfig?: MultiFactorConfig; + testPhoneNumbers?: { + [phoneNumber: string]: string; + } | null; + } + export type UserIdentifier = UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; + export interface UserImportOptions { + hash: { + algorithm: HashAlgorithmType; + key?: Buffer; + saltSeparator?: Buffer; + rounds?: number; + memoryCost?: number; + parallelization?: number; + blockSize?: number; + derivedKeyLength?: number; + }; + } + export interface UserImportRecord { + customClaims?: { + [key: string]: any; + }; + disabled?: boolean; + displayName?: string; + email?: string; + emailVerified?: boolean; + metadata?: UserMetadataRequest; + multiFactor?: MultiFactorUpdateSettings; + passwordHash?: Buffer; + passwordSalt?: Buffer; + phoneNumber?: string; + photoURL?: string; + providerData?: UserProviderRequest[]; + tenantId?: string; + uid: string; + } + export interface UserImportResult { + errors: FirebaseArrayIndexError[]; + failureCount: number; + successCount: number; + } + export interface UserInfo { + displayName: string; + email: string; + phoneNumber: string; + photoURL: string; + providerId: string; + toJSON(): object; + uid: string; + } + export interface UserMetadata { + creationTime: string; + lastRefreshTime?: string | null; + lastSignInTime: string; + toJSON(): object; + } + export interface UserMetadataRequest { + creationTime?: string; + lastSignInTime?: string; + } + export interface UserProviderRequest { + displayName?: string; + email?: string; + phoneNumber?: string; + photoURL?: string; + providerId: string; + uid: string; + } + export interface UserRecord { + customClaims?: { + [key: string]: any; + }; + disabled: boolean; + displayName?: string; + email?: string; + emailVerified: boolean; + metadata: UserMetadata; + multiFactor?: MultiFactorSettings; + passwordHash?: string; + passwordSalt?: string; + phoneNumber?: string; + photoURL?: string; + providerData: UserInfo[]; + tenantId?: string | null; + toJSON(): object; + tokensValidAfterTime?: string; + uid: string; + } +} + +// @public (undocumented) +export namespace credential { + export function applicationDefault(httpAgent?: Agent): Credential; + export function cert(serviceAccountPathOrObject: string | ServiceAccount, httpAgent?: Agent): Credential; + export interface Credential { + getAccessToken(): Promise; + } + export function refreshToken(refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential; +} + +// @public +export function database(app?: app.App): database.Database; + +// @public (undocumented) +export namespace database { + // (undocumented) + export interface Database extends rtdb.FirebaseDatabase { + getRules(): Promise; + getRulesJSON(): Promise; + setRules(source: string | Buffer | object): Promise; + } + import DataSnapshot = rtdb.DataSnapshot; + import EventType = rtdb.EventType; + import OnDisconnect = rtdb.OnDisconnect; + import Query = rtdb.Query; + import Reference = rtdb.Reference; + import ThenableReference = rtdb.ThenableReference; + import enableLogging = rtdb.enableLogging; + const ServerValue: rtdb.ServerValue; +} + +// @public +export interface FirebaseArrayIndexError { + error: FirebaseError; + index: number; +} + +// @public +export interface FirebaseError { + code: string; + message: string; + stack?: string; + toJSON(): object; +} + +// @public (undocumented) +export function firestore(app?: app.App): _firestore.Firestore; + +// @public (undocumented) +export namespace firestore { + import v1beta1 = _firestore.v1beta1; + import v1 = _firestore.v1; + import BulkWriter = _firestore.BulkWriter; + import BulkWriterOptions = _firestore.BulkWriterOptions; + import CollectionGroup = _firestore.CollectionGroup; + import CollectionReference = _firestore.CollectionReference; + import DocumentChangeType = _firestore.DocumentChangeType; + import DocumentData = _firestore.DocumentData; + import DocumentReference = _firestore.DocumentReference; + import DocumentSnapshot = _firestore.DocumentSnapshot; + import FieldPath = _firestore.FieldPath; + import FieldValue = _firestore.FieldValue; + import Firestore = _firestore.Firestore; + import FirestoreDataConverter = _firestore.FirestoreDataConverter; + import GeoPoint = _firestore.GeoPoint; + import GrpcStatus = _firestore.GrpcStatus; + import Precondition = _firestore.Precondition; + import Query = _firestore.Query; + import QueryDocumentSnapshot = _firestore.QueryDocumentSnapshot; + import QueryPartition = _firestore.QueryPartition; + import QuerySnapshot = _firestore.QuerySnapshot; + import ReadOptions = _firestore.ReadOptions; + import Settings = _firestore.Settings; + import Timestamp = _firestore.Timestamp; + import Transaction = _firestore.Transaction; + import UpdateData = _firestore.UpdateData; + import WriteBatch = _firestore.WriteBatch; + import WriteResult = _firestore.WriteResult; + import setLogFunction = _firestore.setLogFunction; +} + +// @public +export interface GoogleOAuthAccessToken { + // (undocumented) + access_token: string; + // (undocumented) + expires_in: number; +} + +// @public (undocumented) +export function initializeApp(options?: AppOptions, name?: string): app.App; + +// @public +export function instanceId(app?: app.App): instanceId.InstanceId; + +// @public (undocumented) +export namespace instanceId { + export interface InstanceId { + // (undocumented) + app: app.App; + deleteInstanceId(instanceId: string): Promise; + } +} + +// @public +export function machineLearning(app?: app.App): machineLearning.MachineLearning; + +// @public (undocumented) +export namespace machineLearning { + // (undocumented) + export interface AutoMLTfliteModelOptions extends ModelOptionsBase { + // (undocumented) + tfliteModel: { + automlModel: string; + }; + } + // (undocumented) + export interface GcsTfliteModelOptions extends ModelOptionsBase { + // (undocumented) + tfliteModel: { + gcsTfliteUri: string; + }; + } + export interface ListModelsOptions { + filter?: string; + pageSize?: number; + pageToken?: string; + } + export interface ListModelsResult { + readonly models: Model[]; + readonly pageToken?: string; + } + export interface MachineLearning { + app: app.App; + createModel(model: ModelOptions): Promise; + deleteModel(modelId: string): Promise; + getModel(modelId: string): Promise; + listModels(options?: ListModelsOptions): Promise; + publishModel(modelId: string): Promise; + unpublishModel(modelId: string): Promise; + updateModel(modelId: string, model: ModelOptions): Promise; + } + export interface Model { + readonly createTime: string; + readonly displayName: string; + readonly etag: string; + readonly locked: boolean; + readonly modelHash?: string; + readonly modelId: string; + readonly published: boolean; + readonly tags?: string[]; + readonly tfliteModel?: TFLiteModel; + toJSON(): { + [key: string]: any; + }; + readonly updateTime: string; + readonly validationError?: string; + waitForUnlocked(maxTimeMillis?: number): Promise; + } + // (undocumented) + export type ModelOptions = ModelOptionsBase | GcsTfliteModelOptions | AutoMLTfliteModelOptions; + export interface ModelOptionsBase { + // (undocumented) + displayName?: string; + // (undocumented) + tags?: string[]; + } + export interface TFLiteModel { + readonly automlModel?: string; + readonly gcsTfliteUri?: string; + readonly sizeBytes: number; + } +} + +// @public +export function messaging(app?: app.App): messaging.Messaging; + +// @public (undocumented) +export namespace messaging { + export interface AndroidConfig { + collapseKey?: string; + data?: { + [key: string]: string; + }; + fcmOptions?: AndroidFcmOptions; + notification?: AndroidNotification; + priority?: ('high' | 'normal'); + restrictedPackageName?: string; + ttl?: number; + } + export interface AndroidFcmOptions { + analyticsLabel?: string; + } + export interface AndroidNotification { + body?: string; + bodyLocArgs?: string[]; + bodyLocKey?: string; + channelId?: string; + clickAction?: string; + color?: string; + defaultLightSettings?: boolean; + defaultSound?: boolean; + defaultVibrateTimings?: boolean; + eventTimestamp?: Date; + icon?: string; + imageUrl?: string; + lightSettings?: LightSettings; + localOnly?: boolean; + notificationCount?: number; + priority?: ('min' | 'low' | 'default' | 'high' | 'max'); + sound?: string; + sticky?: boolean; + tag?: string; + ticker?: string; + title?: string; + titleLocArgs?: string[]; + titleLocKey?: string; + vibrateTimingsMillis?: number[]; + visibility?: ('private' | 'public' | 'secret'); + } + export interface ApnsConfig { + fcmOptions?: ApnsFcmOptions; + headers?: { + [key: string]: string; + }; + payload?: ApnsPayload; + } + export interface ApnsFcmOptions { + analyticsLabel?: string; + imageUrl?: string; + } + export interface ApnsPayload { + // (undocumented) + [customData: string]: object; + aps: Aps; + } + export interface Aps { + // (undocumented) + [customData: string]: any; + alert?: string | ApsAlert; + badge?: number; + category?: string; + contentAvailable?: boolean; + mutableContent?: boolean; + sound?: string | CriticalSound; + threadId?: string; + } + // (undocumented) + export interface ApsAlert { + // (undocumented) + actionLocKey?: string; + // (undocumented) + body?: string; + // (undocumented) + launchImage?: string; + // (undocumented) + locArgs?: string[]; + // (undocumented) + locKey?: string; + // (undocumented) + subtitle?: string; + // (undocumented) + subtitleLocArgs?: string[]; + // (undocumented) + subtitleLocKey?: string; + // (undocumented) + title?: string; + // (undocumented) + titleLocArgs?: string[]; + // (undocumented) + titleLocKey?: string; + } + // (undocumented) + export interface BaseMessage { + // (undocumented) + android?: AndroidConfig; + // (undocumented) + apns?: ApnsConfig; + // (undocumented) + data?: { + [key: string]: string; + }; + // (undocumented) + fcmOptions?: FcmOptions; + // (undocumented) + notification?: Notification; + // (undocumented) + webpush?: WebpushConfig; + } + export interface BatchResponse { + failureCount: number; + responses: SendResponse[]; + successCount: number; + } + // (undocumented) + export interface ConditionMessage extends BaseMessage { + // (undocumented) + condition: string; + } + export interface CriticalSound { + critical?: boolean; + name: string; + volume?: number; + } + export interface DataMessagePayload { + // (undocumented) + [key: string]: string; + } + export interface FcmOptions { + analyticsLabel?: string; + } + export interface LightSettings { + color: string; + lightOffDurationMillis: number; + lightOnDurationMillis: number; + } + export type Message = TokenMessage | TopicMessage | ConditionMessage; + // (undocumented) + export interface Messaging { + app: app.App; + send(message: Message, dryRun?: boolean): Promise; + sendAll(messages: Array, dryRun?: boolean): Promise; + sendMulticast(message: MulticastMessage, dryRun?: boolean): Promise; + sendToCondition(condition: string, payload: MessagingPayload, options?: MessagingOptions): Promise; + sendToDevice(registrationToken: string | string[], payload: MessagingPayload, options?: MessagingOptions): Promise; + sendToDeviceGroup(notificationKey: string, payload: MessagingPayload, options?: MessagingOptions): Promise; + sendToTopic(topic: string, payload: MessagingPayload, options?: MessagingOptions): Promise; + subscribeToTopic(registrationTokens: string | string[], topic: string): Promise; + unsubscribeFromTopic(registrationTokens: string | string[], topic: string): Promise; + } + export interface MessagingConditionResponse { + messageId: number; + } + export interface MessagingDeviceGroupResponse { + failedRegistrationTokens: string[]; + failureCount: number; + successCount: number; + } + // (undocumented) + export interface MessagingDeviceResult { + canonicalRegistrationToken?: string; + error?: FirebaseError; + messageId?: string; + } + export interface MessagingDevicesResponse { + // (undocumented) + canonicalRegistrationTokenCount: number; + // (undocumented) + failureCount: number; + // (undocumented) + multicastId: number; + // (undocumented) + results: MessagingDeviceResult[]; + // (undocumented) + successCount: number; + } + export interface MessagingOptions { + // (undocumented) + [key: string]: any | undefined; + collapseKey?: string; + contentAvailable?: boolean; + dryRun?: boolean; + mutableContent?: boolean; + priority?: string; + restrictedPackageName?: string; + timeToLive?: number; + } + export interface MessagingPayload { + data?: DataMessagePayload; + notification?: NotificationMessagePayload; + } + export interface MessagingTopicManagementResponse { + errors: FirebaseArrayIndexError[]; + failureCount: number; + successCount: number; + } + export interface MessagingTopicResponse { + messageId: number; + } + export interface MulticastMessage extends BaseMessage { + // (undocumented) + tokens: string[]; + } + export interface Notification { + body?: string; + imageUrl?: string; + title?: string; + } + export interface NotificationMessagePayload { + // (undocumented) + [key: string]: string | undefined; + badge?: string; + body?: string; + bodyLocArgs?: string; + bodyLocKey?: string; + clickAction?: string; + color?: string; + icon?: string; + sound?: string; + tag?: string; + title?: string; + titleLocArgs?: string; + titleLocKey?: string; + } + export interface SendResponse { + error?: FirebaseError; + messageId?: string; + success: boolean; + } + // (undocumented) + export interface TokenMessage extends BaseMessage { + // (undocumented) + token: string; + } + // (undocumented) + export interface TopicMessage extends BaseMessage { + // (undocumented) + topic: string; + } + export interface WebpushConfig { + data?: { + [key: string]: string; + }; + fcmOptions?: WebpushFcmOptions; + headers?: { + [key: string]: string; + }; + notification?: WebpushNotification; + } + export interface WebpushFcmOptions { + link?: string; + } + export interface WebpushNotification { + // (undocumented) + [key: string]: any; + actions?: Array<{ + action: string; + icon?: string; + title: string; + }>; + badge?: string; + body?: string; + data?: any; + dir?: 'auto' | 'ltr' | 'rtl'; + icon?: string; + image?: string; + lang?: string; + renotify?: boolean; + requireInteraction?: boolean; + silent?: boolean; + tag?: string; + timestamp?: number; + title?: string; + vibrate?: number | number[]; + } + {}; +} + +// @public +export function projectManagement(app?: app.App): projectManagement.ProjectManagement; + +// @public (undocumented) +export namespace projectManagement { + export interface AndroidApp { + addShaCertificate(certificateToAdd: ShaCertificate): Promise; + // (undocumented) + appId: string; + deleteShaCertificate(certificateToRemove: ShaCertificate): Promise; + getConfig(): Promise; + getMetadata(): Promise; + getShaCertificates(): Promise; + setDisplayName(newDisplayName: string): Promise; + } + export interface AndroidAppMetadata extends AppMetadata { + packageName: string; + // (undocumented) + platform: AppPlatform.ANDROID; + } + export interface AppMetadata { + appId: string; + displayName?: string; + platform: AppPlatform; + projectId: string; + resourceName: string; + } + export enum AppPlatform { + ANDROID = "ANDROID", + IOS = "IOS", + PLATFORM_UNKNOWN = "PLATFORM_UNKNOWN" + } + export interface IosApp { + // (undocumented) + appId: string; + getConfig(): Promise; + getMetadata(): Promise; + setDisplayName(newDisplayName: string): Promise; + } + export interface IosAppMetadata extends AppMetadata { + bundleId: string; + // (undocumented) + platform: AppPlatform.IOS; + } + export interface ProjectManagement { + androidApp(appId: string): AndroidApp; + // (undocumented) + app: app.App; + createAndroidApp(packageName: string, displayName?: string): Promise; + createIosApp(bundleId: string, displayName?: string): Promise; + iosApp(appId: string): IosApp; + listAndroidApps(): Promise; + listAppMetadata(): Promise; + listIosApps(): Promise; + setDisplayName(newDisplayName: string): Promise; + shaCertificate(shaHash: string): ShaCertificate; + } + export interface ShaCertificate { + certType: ('sha1' | 'sha256'); + resourceName?: string; + shaHash: string; + } +} + +// @public +export function remoteConfig(app?: app.App): remoteConfig.RemoteConfig; + +// @public (undocumented) +export namespace remoteConfig { + export interface ExplicitParameterValue { + value: string; + } + export interface InAppDefaultValue { + useInAppDefault: boolean; + } + export interface ListVersionsOptions { + endTime?: Date | string; + endVersionNumber?: string | number; + pageSize?: number; + pageToken?: string; + startTime?: Date | string; + } + export interface ListVersionsResult { + nextPageToken?: string; + versions: Version[]; + } + export interface RemoteConfig { + // (undocumented) + app: app.App; + createTemplateFromJSON(json: string): RemoteConfigTemplate; + getTemplate(): Promise; + getTemplateAtVersion(versionNumber: number | string): Promise; + listVersions(options?: ListVersionsOptions): Promise; + publishTemplate(template: RemoteConfigTemplate, options?: { + force: boolean; + }): Promise; + rollback(versionNumber: string | number): Promise; + validateTemplate(template: RemoteConfigTemplate): Promise; + } + export interface RemoteConfigCondition { + expression: string; + name: string; + tagColor?: TagColor; + } + export interface RemoteConfigParameter { + conditionalValues?: { + [key: string]: RemoteConfigParameterValue; + }; + defaultValue?: RemoteConfigParameterValue; + description?: string; + } + export interface RemoteConfigParameterGroup { + description?: string; + parameters: { + [key: string]: RemoteConfigParameter; + }; + } + export type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; + export interface RemoteConfigTemplate { + conditions: RemoteConfigCondition[]; + readonly etag: string; + parameterGroups: { + [key: string]: RemoteConfigParameterGroup; + }; + parameters: { + [key: string]: RemoteConfigParameter; + }; + version?: Version; + } + export interface RemoteConfigUser { + email: string; + imageUrl?: string; + name?: string; + } + export type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL'; + export interface Version { + description?: string; + isLegacy?: boolean; + rollbackSource?: string; + updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | 'REST_API' | 'ADMIN_SDK_NODE'); + updateTime?: string; + updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); + updateUser?: RemoteConfigUser; + versionNumber?: string; + } +} + +// @public (undocumented) +export const SDK_VERSION: string; + +// @public +export function securityRules(app?: app.App): securityRules.SecurityRules; + +// @public (undocumented) +export namespace securityRules { + export interface Ruleset extends RulesetMetadata { + // (undocumented) + readonly source: RulesFile[]; + } + export interface RulesetMetadata { + readonly createTime: string; + readonly name: string; + } + export interface RulesetMetadataList { + readonly nextPageToken?: string; + readonly rulesets: RulesetMetadata[]; + } + export interface RulesFile { + // (undocumented) + readonly content: string; + // (undocumented) + readonly name: string; + } + export interface SecurityRules { + // (undocumented) + app: app.App; + createRuleset(file: RulesFile): Promise; + createRulesFileFromSource(name: string, source: string | Buffer): RulesFile; + deleteRuleset(name: string): Promise; + getFirestoreRuleset(): Promise; + getRuleset(name: string): Promise; + getStorageRuleset(bucket?: string): Promise; + listRulesetMetadata(pageSize?: number, nextPageToken?: string): Promise; + releaseFirestoreRuleset(ruleset: string | RulesetMetadata): Promise; + releaseFirestoreRulesetFromSource(source: string | Buffer): Promise; + releaseStorageRuleset(ruleset: string | RulesetMetadata, bucket?: string): Promise; + releaseStorageRulesetFromSource(source: string | Buffer, bucket?: string): Promise; + } +} + +// @public (undocumented) +export interface ServiceAccount { + // (undocumented) + clientEmail?: string; + // (undocumented) + privateKey?: string; + // (undocumented) + projectId?: string; +} + +// @public +export function storage(app?: app.App): storage.Storage; + +// @public (undocumented) +export namespace storage { + export interface Storage { + app: app.App; + // (undocumented) + bucket(name?: string): Bucket; + } +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/gulpfile.js b/gulpfile.js index 20f59df0c2..02c5f136c8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -51,7 +51,7 @@ var paths = { // rather than including both src and test in the lib dir. Declaration // is used by TypeScript to determine if auto-generated typings should be // emitted. -var buildProject = ts.createProject('tsconfig.json', { rootDir: 'src' }); +var buildProject = ts.createProject('tsconfig.json', { rootDir: 'src', declarationMap: true }); var buildTest = ts.createProject('tsconfig.json'); diff --git a/package-lock.json b/package-lock.json index f85f9fcd6c..56ff51790f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -600,6 +600,61 @@ "protobufjs": "^6.8.6" } }, + "@microsoft/api-extractor": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.11.2.tgz", + "integrity": "sha512-iZPv22j9K02cbwIDblOgF1MxZG+KWovp3CQpWCD6UC/+YYO4DfLxX5uZYVNzfgT4vU8fN0rugJmGm85rHX6Ouw==", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.10.8", + "@microsoft/tsdoc": "0.12.19", + "@rushstack/node-core-library": "3.34.7", + "@rushstack/rig-package": "0.2.7", + "@rushstack/ts-command-line": "4.7.6", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.0.5" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "typescript": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz", + "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==", + "dev": true + } + } + }, + "@microsoft/api-extractor-model": { + "version": "7.10.8", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.10.8.tgz", + "integrity": "sha512-9TfiCTPnkUeLaYywZeg9rYbVPX9Tj6AAkO6ThnjSE0tTPLjMcL3RiHkqn0BJ4+aGcl56APwo32zj5+kG+NqxYA==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.12.19", + "@rushstack/node-core-library": "3.34.7" + } + }, + "@microsoft/tsdoc": { + "version": "0.12.19", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.19.tgz", + "integrity": "sha512-IpgPxHrNxZiMNUSXqR1l/gePKPkfAmIKoDRP9hp7OwjU29ZR8WCJsOJ8iBKgw0Qk+pFwR+8Y1cy8ImLY6e9m4A==", + "dev": true + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -664,6 +719,85 @@ "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", "optional": true }, + "@rushstack/node-core-library": { + "version": "3.34.7", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.34.7.tgz", + "integrity": "sha512-7FwJ0jmZsh7bDIZ1IqDNphY9Kc6aAi1D2K8jiq+da4flMyL84HNeq2KxvwFLzjLwu3eMr88X+oBpgxCTD5Y57Q==", + "dev": true, + "requires": { + "@types/node": "10.17.13", + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "timsort": "~0.3.0", + "z-schema": "~3.18.3" + }, + "dependencies": { + "@types/node": { + "version": "10.17.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", + "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", + "dev": true + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "@rushstack/rig-package": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.2.7.tgz", + "integrity": "sha512-hI1L0IIzCHqH/uW64mKqEQ0/MANA/IklVId3jGpj1kt9RJcBdeNUIlzDtHl437LZRAuEA8CyotRHzG6YDgWlTw==", + "dev": true, + "requires": { + "@types/node": "10.17.13", + "resolve": "~1.17.0", + "strip-json-comments": "~3.1.1" + }, + "dependencies": { + "@types/node": { + "version": "10.17.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", + "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + } + } + }, + "@rushstack/ts-command-line": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.7.6.tgz", + "integrity": "sha512-falJVNfpJtsL3gJaY77JXXycfzhzB9VkKhqEfjRWD69/f6ezMUorPR6Nc90MnIaWgePTcdTJPZibxOQrNpu1Uw==", + "dev": true, + "requires": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, "@sinonjs/commons": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", @@ -715,6 +849,12 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "optional": true }, + "@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true + }, "@types/bcrypt": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-2.0.0.tgz", @@ -1908,6 +2048,12 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, + "colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4253,6 +4399,12 @@ "resolve-from": "^4.0.0" } }, + "import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4835,6 +4987,12 @@ "textextensions": "~1.0.0" } }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "dev": true + }, "jquery": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", @@ -5248,6 +5406,12 @@ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, "lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -7803,6 +7967,12 @@ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -8157,6 +8327,12 @@ "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", "dev": true }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -8635,6 +8811,12 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz", + "integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA==", + "dev": true + }, "value-or-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", @@ -9053,6 +9235,18 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", "dev": true + }, + "z-schema": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.18.4.tgz", + "integrity": "sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw==", + "dev": true, + "requires": { + "commander": "^2.7.1", + "lodash.get": "^4.0.0", + "lodash.isequal": "^4.0.0", + "validator": "^8.0.0" + } } } } diff --git a/package.json b/package.json index d60b464327..d4583f19e7 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "test:coverage": "nyc npm run test:unit", "lint:src": "eslint src/ --ext .ts", "lint:test": "eslint test/ --ext .ts", - "apidocs": "node docgen/generate-docs.js --api node" + "apidocs": "node docgen/generate-docs.js --api node", + "api-extractor": "api-extractor run", + "api-extractor:local": "api-extractor run --local" }, "nyc": { "extension": [ @@ -69,6 +71,7 @@ "@firebase/app": "^0.6.9", "@firebase/auth": "^0.14.9", "@firebase/auth-types": "^0.10.1", + "@microsoft/api-extractor": "^7.11.2", "@types/bcrypt": "^2.0.0", "@types/chai": "^4.0.0", "@types/chai-as-promised": "^7.1.0", diff --git a/tsconfig.json b/tsconfig.json index 489b90bedf..67f91761f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "module": "commonjs", "target": "es5", "declaration": true, + "sourceMap": true, "noImplicitAny": true, "noUnusedLocals": true, // TODO(rsgowman): enable `"strict": true,` and remove explicit setting of: noImplicitAny, noImplicitThis, alwaysStrict, strictBindCallApply, strictNullChecks, strictFunctionTypes, strictPropertyInitialization. From fb2c63c0d506fd01f9121166e3bb63aac9af62ff Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Mon, 9 Nov 2020 12:49:15 -0800 Subject: [PATCH 053/160] Handle lookup returning empty array of users. (#1082) * Handle lookup returning empty array of users. * Update tests. * Use single quotes. * Update auth-api-request.spec.ts --- src/auth/auth-api-request.ts | 2 +- test/unit/auth/auth-api-request.spec.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index f57fdc3fcc..753f06e48f 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -629,7 +629,7 @@ export const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings('/accounts:lookup' }) // Set response validator. .setResponseValidator((response: any) => { - if (!response.users) { + if (!response.users || !response.users.length) { throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); } }); diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 4c621fe215..0a86231e53 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -369,17 +369,23 @@ describe('FIREBASE_AUTH_GET_ACCOUNT_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_GET_ACCOUNT_INFO.getResponseValidator(); it('should succeed with users returned', () => { - const validResponse: object = { users: [] }; + const validResponse: object = { users: [{ localId: 'foo' }] }; expect(() => { return responseValidator(validResponse); }).not.to.throw(); }); - it('should fail when users is not returned', () => { + it('should fail when the response object is empty', () => { const invalidResponse = {}; expect(() => { responseValidator(invalidResponse); }).to.throw(); }); + it('should fail when the response object has an empty list of users', () => { + const invalidResponse = { users: [] }; + expect(() => { + responseValidator(invalidResponse); + }).to.throw(); + }); }); }); From cb3d2c3550b0e8842ecdaa54b467bf6320398bac Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Wed, 11 Nov 2020 14:19:23 -0500 Subject: [PATCH 054/160] Use 'owner' token when communicating with Auth emulator (#1085) * Use 'owner' token when communicating with Auth emulator * Unused import * Single quote * Simplify to address code review * Further simplify * Reduce diff * Stray comma * Remove circular import, add unit test * Final review feedback --- src/auth/auth-api-request.ts | 43 ++++++++++++++++--- src/auth/auth.ts | 14 +----- src/utils/api-request.ts | 11 ++++- test/unit/auth/auth-api-request.spec.ts | 57 +++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 22 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 753f06e48f..740d173356 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -143,10 +143,9 @@ class AuthResourceUrlBuilder { * @constructor */ constructor(protected app: FirebaseApp, protected version: string = 'v1') { - const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST; - if (emulatorHost) { + if (useEmulator()) { this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, { - host: emulatorHost + host: emulatorHost() }); } else { this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; @@ -210,10 +209,9 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { */ constructor(protected app: FirebaseApp, protected version: string, protected tenantId: string) { super(app, version); - const emulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST - if (emulatorHost) { + if (useEmulator()) { this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, { - host: emulatorHost + host: emulatorHost() }); } else { this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; @@ -236,6 +234,21 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { } } +/** + * Auth-specific HTTP client which uses the special "owner" token + * when communicating with the Auth Emulator. + */ +class AuthHttpClient extends AuthorizedHttpClient { + + protected getToken(): Promise { + if (useEmulator()) { + return Promise.resolve('owner'); + } + + return super.getToken(); + } + +} /** * Validates an AuthFactorInfo object. All unsupported parameters @@ -991,7 +1004,7 @@ export abstract class AbstractAuthRequestHandler { ); } - this.httpClient = new AuthorizedHttpClient(app); + this.httpClient = new AuthHttpClient(app); } /** @@ -2095,3 +2108,19 @@ export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler { return super.uploadAccount(users, options); } } + +function emulatorHost(): string | undefined { + return process.env.FIREBASE_AUTH_EMULATOR_HOST +} + +/** + * When true the SDK should communicate with the Auth Emulator for all API + * calls and also produce unsigned tokens. + * + * This alone does NOT short-circuit ID Token verification. + * For security reasons that must be explicitly disabled through + * setJwtVerificationEnabled(false); + */ +export function useEmulator(): boolean { + return !!emulatorHost(); +} diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 816333e3b2..03d1c4e666 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -21,7 +21,7 @@ import { import { FirebaseApp } from '../firebase-app'; import { FirebaseTokenGenerator, EmulatedSigner, cryptoSignerFromApp } from './token-generator'; import { - AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, + AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, useEmulator, } from './auth-api-request'; import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; @@ -850,15 +850,3 @@ export class Auth extends BaseAuth return this.tenantManager_; } } - -/** - * When true the SDK should communicate with the Auth Emulator for all API - * calls and also produce unsigned tokens. - * - * This alone does NOT short-circuit ID Token verification. - * For security reasons that must be explicitly disabled through - * setJwtVerificationEnabled(false); - */ -function useEmulator(): boolean { - return !!process.env.FIREBASE_AUTH_EMULATOR_HOST; -} diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index 3b83221d5f..d079b44557 100644 --- a/src/utils/api-request.ts +++ b/src/utils/api-request.ts @@ -814,11 +814,11 @@ export class AuthorizedHttpClient extends HttpClient { } public send(request: HttpRequestConfig): Promise { - return this.app.INTERNAL.getToken().then((accessTokenObj) => { + return this.getToken().then((token) => { const requestCopy = Object.assign({}, request); requestCopy.headers = Object.assign({}, request.headers); const authHeader = 'Authorization'; - requestCopy.headers[authHeader] = `Bearer ${accessTokenObj.accessToken}`; + requestCopy.headers[authHeader] = `Bearer ${token}`; if (!requestCopy.httpAgent && this.app.options.httpAgent) { requestCopy.httpAgent = this.app.options.httpAgent; @@ -826,6 +826,13 @@ export class AuthorizedHttpClient extends HttpClient { return super.send(requestCopy); }); } + + protected getToken(): Promise { + return this.app.INTERNAL.getToken() + .then((accessTokenObj) => { + return accessTokenObj.accessToken; + }); + } } /** diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 0a86231e53..13d5fcfc33 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -865,6 +865,10 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, 'Authorization': 'Bearer ' + mockAccessToken, }; + const expectedHeadersEmulator: {[key: string]: string} = { + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, + 'Authorization': 'Bearer owner', + }; const callParams = (path: string, method: any, data: any): HttpRequestConfig => { return { method, @@ -902,6 +906,59 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + describe('Emulator Support', () => { + const method = 'POST'; + const path = handler.path('v1', '/accounts:lookup', 'project_id'); + const expectedResult = utils.responseFrom({ + users : [ + { localId: 'uid' }, + ], + }); + const data = { localId: ['uid'] }; + + after(() => { + delete process.env.FIREBASE_AUTH_EMULATOR_HOST; + }) + + it('should call a prod URL with a real token when emulator is not running', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + + return requestHandler.getAccountInfoByUid('uid') + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method, + url: `https://${host}${path}`, + data, + headers: expectedHeaders, + timeout, + }); + }); + }); + + it('should call a local URL with a mock token when the emulator is running', () => { + const emulatorHost = 'localhost:9099'; + process.env.FIREBASE_AUTH_EMULATOR_HOST = emulatorHost; + + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByUid('uid') + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method, + url: `http://${emulatorHost}/identitytoolkit.googleapis.com${path}`, + data, + headers: expectedHeadersEmulator, + timeout, + }); + }); + }); + }); + describe('createSessionCookie', () => { const durationInMs = 24 * 60 * 60 * 1000; const path = handler.path('v1', ':createSessionCookie', 'project_id'); From 13afb2049768c9d3fcea0f04bd1b41ef5ce224a2 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 12 Nov 2020 10:33:38 -0800 Subject: [PATCH 055/160] [chore] Release 9.4.0 (#1087) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4583f19e7..987a246e88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.3.0", + "version": "9.4.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 2781eff54c567adfee43e8ddb61cad75775033ad Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Fri, 13 Nov 2020 16:06:48 -0500 Subject: [PATCH 056/160] fix(rc): Fix Version update time parsing failure (#1089) * fix(rc): Fix Version update time parsing failure * Add test cases for 3 and 6 ms places --- src/remote-config/remote-config.ts | 12 ++-- test/unit/remote-config/remote-config.spec.ts | 56 ++++++++++++++++++- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index d8d194d26e..ba25f9c011 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -366,16 +366,12 @@ class VersionImpl implements Version { // as UTC date strings. If a developer uses a previously obtained template with UTC timestamps // we could still validate it below. if (typeof version.updateTime !== 'undefined') { - if (!validator.isISODateString(version.updateTime) && - !validator.isUTCDateString(version.updateTime)) { + if (!this.isValidTimestamp(version.updateTime)) { throw new FirebaseRemoteConfigError( 'invalid-argument', 'Version update time must be a valid date string'); } - if (validator.isISODateString(version.updateTime)) { - // timestamps in output `Version` obtained from the API should be in UTC. - this.updateTime = new Date(version.updateTime).toUTCString(); - } + this.updateTime = new Date(version.updateTime).toUTCString(); } } @@ -394,4 +390,8 @@ class VersionImpl implements Version { updateTime: this.updateTime, } } + + private isValidTimestamp(timestamp: string): boolean { + return validator.isNonEmptyString(timestamp) && (new Date(timestamp)).getTime() > 0; + } } diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index d884a515cf..462d46341e 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -64,7 +64,7 @@ describe('RemoteConfig', () => { email: 'firebase-adminsdk@gserviceaccount.com' }, description: 'production version', - updateTime: '2020-06-15T16:45:03.000Z' + updateTime: '2020-06-15T16:45:03.541527Z' }; const REMOTE_CONFIG_RESPONSE: { @@ -123,7 +123,7 @@ describe('RemoteConfig', () => { versions: [ { versionNumber: '78', - updateTime: '2020-05-07T18:46:09.495Z', + updateTime: '2020-05-07T18:46:09.495234Z', updateUser: { email: 'user@gmail.com', imageUrl: 'https://photo.jpg' @@ -680,5 +680,57 @@ describe('RemoteConfig', () => { expect(parsed).deep.equals(expectedTemplate); }); }); + + it('should resolve with template when Version updateTime contains only 3 ms places', () => { + const response = deepCopy(REMOTE_CONFIG_RESPONSE); + const versionInfo = deepCopy(VERSION_INFO); + versionInfo.updateTime = '2020-11-03T20:24:15.203Z'; + response.version = versionInfo; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response); + stubs.push(stub); + + return rcOperation() + .then((template) => { + expect(template.etag).to.equal('etag-123456789012-5'); + + const version = template.version!; + expect(version.versionNumber).to.equal('86'); + expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE'); + expect(version.updateType).to.equal('INCREMENTAL_UPDATE'); + expect(version.updateUser).to.deep.equal({ + email: 'firebase-adminsdk@gserviceaccount.com' + }); + expect(version.description).to.equal('production version'); + expect(version.updateTime).to.equal('Tue, 03 Nov 2020 20:24:15 GMT'); + }); + }); + + it('should resolve with template when Version updateTime contains 6 ms places', () => { + const response = deepCopy(REMOTE_CONFIG_RESPONSE); + const versionInfo = deepCopy(VERSION_INFO); + versionInfo.updateTime = '2020-11-13T17:01:36.541527Z'; + response.version = versionInfo; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response); + stubs.push(stub); + + return rcOperation() + .then((template) => { + expect(template.etag).to.equal('etag-123456789012-5'); + + const version = template.version!; + expect(version.versionNumber).to.equal('86'); + expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE'); + expect(version.updateType).to.equal('INCREMENTAL_UPDATE'); + expect(version.updateUser).to.deep.equal({ + email: 'firebase-adminsdk@gserviceaccount.com' + }); + expect(version.description).to.equal('production version'); + expect(version.updateTime).to.equal('Fri, 13 Nov 2020 17:01:36 GMT'); + }); + }); } }); From 0f08446676a235596b6cadb67506e6a522f62ac0 Mon Sep 17 00:00:00 2001 From: egilmorez Date: Fri, 13 Nov 2020 14:10:06 -0800 Subject: [PATCH 057/160] Reinstating tag that devsite needs present to supress machine translation. (#1090) --- docgen/theme/layouts/default.hbs | 1 + 1 file changed, 1 insertion(+) diff --git a/docgen/theme/layouts/default.hbs b/docgen/theme/layouts/default.hbs index a5e05fc73e..485d831d71 100644 --- a/docgen/theme/layouts/default.hbs +++ b/docgen/theme/layouts/default.hbs @@ -6,6 +6,7 @@ + {{#ifCond model.name '==' project.name}}{{project.name}}{{else}}{{model.name}} | {{project.name}}{{/ifCond}} From 0d7238040a115099c969021fd9d876788b61d88b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Fri, 13 Nov 2020 17:39:51 -0500 Subject: [PATCH 058/160] [chore] Release 9.4.1 (#1091) Contains a fix for #1088 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 987a246e88..2a3074ad08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.4.0", + "version": "9.4.1", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From a98a3cc275f9fee0474ff1905d2217a600d5f57e Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 24 Nov 2020 12:49:00 -0800 Subject: [PATCH 059/160] fix(fcm): Support arbitrary custom values in the ApnsPayload (#1097) --- etc/firebase-admin.api.md | 2 +- src/messaging/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index e6b95c8871..b5b810a5e6 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -625,7 +625,7 @@ export namespace messaging { } export interface ApnsPayload { // (undocumented) - [customData: string]: object; + [customData: string]: any; aps: Aps; } export interface Aps { diff --git a/src/messaging/index.ts b/src/messaging/index.ts index 8e9657ea47..020ee7c875 100644 --- a/src/messaging/index.ts +++ b/src/messaging/index.ts @@ -299,7 +299,7 @@ export namespace messaging { * The `aps` dictionary to be included in the message. */ aps: Aps; - [customData: string]: object; + [customData: string]: any; } /** From c5ff16eb9c5d98560b2d073415aa2e54ce899023 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 2 Dec 2020 13:19:29 -0800 Subject: [PATCH 060/160] chore: Upgraded JS SDK dependencies (#1104) --- package-lock.json | 80 +++++++++++++++++++---------------------------- package.json | 8 ++--- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56ff51790f..3c71af7ea8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.3.0", + "version": "9.4.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -156,15 +156,15 @@ } }, "@firebase/app": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.9.tgz", - "integrity": "sha512-X2riRgK49IK8LCQ3j7BKLu3zqHDTJSaT6YgcLewtHuOVwtpHfGODiS1cL5VMvKm3ogxP84GA70tN3sdoL/vTog==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.13.tgz", + "integrity": "sha512-xGrJETzvCb89VYbGSHFHCW7O/y067HRxT7MGehUE1xMxdPVBDNayHnxEuKwzfGvXAjVmajXBKFlKxaCWpgSjCQ==", "dev": true, "requires": { "@firebase/app-types": "0.6.1", - "@firebase/component": "0.1.17", + "@firebase/component": "0.1.21", "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.0", + "@firebase/util": "0.3.4", "dom-storage": "2.1.0", "tslib": "^1.11.1", "xmlhttprequest": "1.8.0" @@ -176,20 +176,12 @@ "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==" }, "@firebase/auth": { - "version": "0.14.9", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.14.9.tgz", - "integrity": "sha512-PxYa2r5qUEdheXTvqROFrMstK8W4uPiP7NVfp+2Bec+AjY5PxZapCx/YFDLkU0D7YBI82H74PtZrzdJZw7TJ4w==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.15.2.tgz", + "integrity": "sha512-2n32PBi6x9jVhc0E/ewKLUCYYTzFEXL4PNkvrrlGKbzeTBEkkyzfgUX7OV9UF5wUOG+gurtUthuur1zspZ/9hg==", "dev": true, "requires": { "@firebase/auth-types": "0.10.1" - }, - "dependencies": { - "@firebase/auth-types": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.1.tgz", - "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==", - "dev": true - } } }, "@firebase/auth-interop-types": { @@ -204,47 +196,42 @@ "dev": true }, "@firebase/component": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.17.tgz", - "integrity": "sha512-/tN5iLcFp9rdpTfCJPfQ/o2ziGHlDxOzNx6XD2FoHlu4pG/PPGu+59iRfQXIowBGhxcTGD/l7oJhZEY/PVg0KQ==", + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.21.tgz", + "integrity": "sha512-kd5sVmCLB95EK81Pj+yDTea8pzN2qo/1yr0ua9yVi6UgMzm6zAeih73iVUkaat96MAHy26yosMufkvd3zC4IKg==", "dev": true, "requires": { - "@firebase/util": "0.3.0", + "@firebase/util": "0.3.4", "tslib": "^1.11.1" } }, "@firebase/database": { - "version": "0.6.10", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.10.tgz", - "integrity": "sha512-Hc8zIPAroIbAoRe6xFCI5oFHubcHKoDsbYE3J5G1/BhT6DnEUSoLgx8kJ2npybVSCVyb8BvsD6swh17DGEz+0g==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.8.1.tgz", + "integrity": "sha512-/1HhR4ejpqUaM9Cn3KSeNdQvdlehWIhdfTVWFxS73ZlLYf7ayk9jITwH10H3ZOIm5yNzxF67p/U7Z/0IPhgWaQ==", "requires": { "@firebase/auth-interop-types": "0.1.5", - "@firebase/component": "0.1.17", - "@firebase/database-types": "0.5.2", + "@firebase/component": "0.1.21", + "@firebase/database-types": "0.6.1", "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.0", + "@firebase/util": "0.3.4", "faye-websocket": "0.11.3", "tslib": "^1.11.1" }, "dependencies": { "@firebase/component": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.17.tgz", - "integrity": "sha512-/tN5iLcFp9rdpTfCJPfQ/o2ziGHlDxOzNx6XD2FoHlu4pG/PPGu+59iRfQXIowBGhxcTGD/l7oJhZEY/PVg0KQ==", + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.21.tgz", + "integrity": "sha512-kd5sVmCLB95EK81Pj+yDTea8pzN2qo/1yr0ua9yVi6UgMzm6zAeih73iVUkaat96MAHy26yosMufkvd3zC4IKg==", "requires": { - "@firebase/util": "0.3.0", + "@firebase/util": "0.3.4", "tslib": "^1.11.1" } }, - "@firebase/logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", - "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" - }, "@firebase/util": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.0.tgz", - "integrity": "sha512-GTwC+FSLeCPc44/TXCDReNQ5FPRIS5cb8Gr1XcD1TgiNBOvmyx61Om2YLwHp2GnN++6m6xmwmXARm06HOukATA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.4.tgz", + "integrity": "sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ==", "requires": { "tslib": "^1.11.1" } @@ -252,9 +239,9 @@ } }, "@firebase/database-types": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.2.tgz", - "integrity": "sha512-ap2WQOS3LKmGuVFKUghFft7RxXTyZTDr0Xd8y2aqmWsbJVjgozi0huL/EUMgTjGFrATAjcf2A7aNs8AKKZ2a8g==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.6.1.tgz", + "integrity": "sha512-JtL3FUbWG+bM59iYuphfx9WOu2Mzf0OZNaqWiQ7lJR8wBe7bS9rIm9jlBFtksB7xcya1lZSQPA/GAy2jIlMIkA==", "requires": { "@firebase/app-types": "0.6.1" } @@ -262,13 +249,12 @@ "@firebase/logger": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", - "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==", - "dev": true + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" }, "@firebase/util": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.0.tgz", - "integrity": "sha512-GTwC+FSLeCPc44/TXCDReNQ5FPRIS5cb8Gr1XcD1TgiNBOvmyx61Om2YLwHp2GnN++6m6xmwmXARm06HOukATA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.4.tgz", + "integrity": "sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ==", "dev": true, "requires": { "tslib": "^1.11.1" diff --git a/package.json b/package.json index 2a3074ad08..c2a907a9db 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,8 @@ ], "types": "./lib/index.d.ts", "dependencies": { - "@firebase/database": "^0.6.10", - "@firebase/database-types": "^0.5.2", + "@firebase/database": "^0.8.1", + "@firebase/database-types": "^0.6.1", "@types/node": "^10.10.0", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", @@ -68,8 +68,8 @@ "@google-cloud/storage": "^5.3.0" }, "devDependencies": { - "@firebase/app": "^0.6.9", - "@firebase/auth": "^0.14.9", + "@firebase/app": "^0.6.13", + "@firebase/auth": "^0.15.2", "@firebase/auth-types": "^0.10.1", "@microsoft/api-extractor": "^7.11.2", "@types/bcrypt": "^2.0.0", From 73638aa14b9c7cc8ed7e0aae51ea7e36068dafb7 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 2 Dec 2020 14:37:28 -0800 Subject: [PATCH 061/160] chore: Upgraded mocha, ts-node, typedoc and nock (#1105) --- package-lock.json | 905 ++++++++++++++++++++-------- package.json | 8 +- test/unit/utils/api-request.spec.ts | 2 +- 3 files changed, 652 insertions(+), 263 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c71af7ea8..b549d07128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1063,6 +1063,12 @@ } } }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, "abab": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", @@ -1250,6 +1256,12 @@ "readable-stream": "^2.0.6" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1465,6 +1477,12 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -1500,15 +1518,6 @@ "now-and-later": "^2.0.0" } }, - "backbone": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.0.tgz", - "integrity": "sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==", - "dev": true, - "requires": { - "underscore": ">=1.8.3" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -2053,7 +2062,8 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true + "dev": true, + "optional": true }, "commondir": { "version": "1.0.1", @@ -2346,20 +2356,6 @@ "type-detect": "^4.0.0" } }, - "deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - } - }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2510,9 +2506,9 @@ } }, "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, "doctrine": { @@ -3340,6 +3336,12 @@ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", "dev": true }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, "flat-cache": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", @@ -3430,14 +3432,41 @@ } }, "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", "dev": true, "requires": { + "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + }, + "dependencies": { + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "dev": true + } } }, "fs-minipass": { @@ -4240,15 +4269,15 @@ } }, "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, "highlight.js": { - "version": "9.18.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz", - "integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.4.0.tgz", + "integrity": "sha512-EfrUGcQ63oLJbj0J0RI9ebX6TAITbsDBLbsjr881L/X5fMO9+oadKzEF21C7R3ULKG6Gv3uoab2HiqVJa/4+oA==", "dev": true }, "homedir-polyfill": { @@ -4570,12 +4599,6 @@ } } }, - "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true - }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4734,6 +4757,12 @@ "path-is-inside": "^1.0.1" } }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -4979,12 +5008,6 @@ "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", "dev": true }, - "jquery": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", - "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5440,6 +5463,12 @@ "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", "dev": true }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -5465,6 +5494,66 @@ "lodash._reinterpolate": "^3.0.0" } }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -5481,9 +5570,9 @@ } }, "lunr": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.8.tgz", - "integrity": "sha512-oxMeX/Y35PNFuZoHp+jUj5OSEmLCaIH4KTFJh7a93cHBoFmpw2IoPs22VIz7vyO2YUnx2Tn9dzIwO2P/4quIRg==", + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, "make-dir": { @@ -5526,9 +5615,9 @@ } }, "marked": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz", - "integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-1.2.5.tgz", + "integrity": "sha512-2AlqgYnVPOc9WDyWu7S5DJaEZsfk6dNh/neatQ3IHUW4QLutM/VPSH9lG7bif+XjFWc9K9XR3QvR+fXuECmfdA==", "dev": true }, "matchdep": { @@ -5699,75 +5788,395 @@ } }, "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.2.1.tgz", + "integrity": "sha512-cuLBVfyFfFqbNR0uUKbDGXKGk+UDFe6aR4os78XIrMQpZl/nv7JYHcvP5MFIAb374b2zFXsdgEGwmzMtP0Xg8w==", "dev": true, "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", + "chokidar": "3.4.3", + "debug": "4.2.0", + "diff": "4.0.2", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", "growl": "1.10.5", - "he": "1.1.1", + "he": "1.2.0", + "js-yaml": "3.14.0", + "log-symbols": "4.0.0", "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" + "ms": "2.1.2", + "nanoid": "3.1.12", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "7.2.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.2", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "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 + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "2.1.2" + } + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" } }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "binary-extensions": "^2.0.0" } }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { - "minimist": "0.0.8" + "p-locate": "^5.0.0" } }, - "ms": { + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-main-filename": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.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" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } @@ -5843,6 +6252,12 @@ "dev": true, "optional": true }, + "nanoid": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", + "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==", + "dev": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -5928,37 +6343,15 @@ } }, "nock": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/nock/-/nock-9.6.1.tgz", - "integrity": "sha512-EDgl/WgNQ0C1BZZlASOQkQdE6tAWXJi8QQlugqzN64JJkvZ7ILijZuG24r4vCC7yOfnm6HKpne5AGExLGCeBWg==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.5.tgz", + "integrity": "sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg==", "dev": true, "requires": { - "chai": "^4.1.2", - "debug": "^3.1.0", - "deep-equal": "^1.0.0", + "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.5", - "mkdirp": "^0.5.0", - "propagate": "^1.0.0", - "qs": "^6.5.1", - "semver": "^5.5.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" } }, "node-addon-api": { @@ -6477,16 +6870,6 @@ "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", "dev": true }, - "object-is": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", - "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -6629,7 +7012,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", - "optional": true, "requires": { "p-try": "^2.0.0" } @@ -6818,6 +7200,12 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, "pidtree": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", @@ -6919,9 +7307,9 @@ "dev": true }, "propagate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", - "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true }, "protobufjs": { @@ -7023,6 +7411,15 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -7107,16 +7504,6 @@ "safe-regex": "^1.1.0" } }, - "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - } - }, "regexpp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", @@ -7484,6 +7871,15 @@ "sver-compat": "^1.5.0" } }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -7769,12 +8165,21 @@ } }, "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, "requires": { - "source-map": "^0.5.6" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "source-map-url": { @@ -8428,76 +8833,16 @@ } }, "ts-node": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz", - "integrity": "sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", + "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", "dev": true, "requires": { - "arrify": "^1.0.0", - "chalk": "^2.0.0", - "diff": "^3.1.0", + "arg": "^4.1.0", + "diff": "^4.0.1", "make-error": "^1.1.1", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.0", - "tsconfig": "^6.0.0", - "v8flags": "^3.0.0", - "yn": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "tsconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", - "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", - "dev": true, - "requires": { - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } + "source-map-support": "^0.5.17", + "yn": "3.1.1" } }, "tslib": { @@ -8571,43 +8916,61 @@ } }, "typedoc": { - "version": "0.15.8", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.15.8.tgz", - "integrity": "sha512-a0zypcvfIFsS7Gqpf2MkC1+jNND3K1Om38pbDdy/gYWX01NuJZhC5+O0HkIp0oRIZOo7PWrA5+fC24zkANY28Q==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.19.2.tgz", + "integrity": "sha512-oDEg1BLEzi1qvgdQXc658EYgJ5qJLVSeZ0hQ57Eq4JXy6Vj2VX4RVo18qYxRWz75ifAaYuYNBUCnbhjd37TfOg==", "dev": true, "requires": { - "@types/minimatch": "3.0.3", - "fs-extra": "^8.1.0", - "handlebars": "^4.7.0", - "highlight.js": "^9.17.1", - "lodash": "^4.17.15", - "marked": "^0.8.0", + "fs-extra": "^9.0.1", + "handlebars": "^4.7.6", + "highlight.js": "^10.2.0", + "lodash": "^4.17.20", + "lunr": "^2.3.9", + "marked": "^1.1.1", "minimatch": "^3.0.0", "progress": "^2.0.3", - "shelljs": "^0.8.3", - "typedoc-default-themes": "^0.6.3", - "typescript": "3.7.x" + "semver": "^7.3.2", + "shelljs": "^0.8.4", + "typedoc-default-themes": "^0.11.4" }, "dependencies": { - "typescript": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", - "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true } } }, "typedoc-default-themes": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.6.3.tgz", - "integrity": "sha512-rouf0TcIA4M2nOQFfC7Zp4NEwoYiEX4vX/ZtudJWU9IHA29MPC+PPgSXYLPESkUo7FuB//GxigO3mk9Qe1xp3Q==", - "dev": true, - "requires": { - "backbone": "^1.4.0", - "jquery": "^3.4.1", - "lunr": "^2.3.8", - "underscore": "^1.9.1" - } + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.11.4.tgz", + "integrity": "sha512-Y4Lf+qIb9NTydrexlazAM46SSLrmrQRqWiD52593g53SsmUFioAsMWt8m834J6qsp+7wHRjxCXSZeiiW5cMUdw==", + "dev": true }, "typescript": { "version": "3.9.6", @@ -8616,9 +8979,9 @@ "dev": true }, "uglify-js": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.0.tgz", - "integrity": "sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.1.tgz", + "integrity": "sha512-o8lHP20KjIiQe5b/67Rh68xEGRrc2SRsCuuoYclXXoC74AfSRGblU1HKzJWH3HxPZ+Ort85fWHpSX7KwBUC9CQ==", "dev": true, "optional": true }, @@ -8628,12 +8991,6 @@ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", "dev": true }, - "underscore": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz", - "integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==", - "dev": true - }, "undertaker": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", @@ -9010,6 +9367,12 @@ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, + "workerpool": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.2.tgz", + "integrity": "sha512-DSNyvOpFKrNusaaUwk+ej6cBj1bmhLcBfj80elGk+ZIo5JSkq+unB1dLKEOcNfJDZgjGICfhQ0Q5TbP0PvF4+Q==", + "dev": true + }, "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", @@ -9216,10 +9579,36 @@ "object.assign": "^4.1.0" } }, - "yn": { + "yargs-unparser": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", - "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + } + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true }, "z-schema": { diff --git a/package.json b/package.json index c2a907a9db..3365876dc1 100644 --- a/package.json +++ b/package.json @@ -104,9 +104,9 @@ "jsdom": "^15.0.0", "lodash": "^4.17.15", "minimist": "^1.2.0", - "mocha": "^5.2.0", + "mocha": "^8.0.0", "mz": "^2.7.0", - "nock": "^9.6.0", + "nock": "^13.0.0", "npm-run-all": "^4.1.5", "nyc": "^14.1.0", "request": "^2.75.0", @@ -114,8 +114,8 @@ "run-sequence": "^1.1.5", "sinon": "^9.0.0", "sinon-chai": "^3.0.0", - "ts-node": "^3.3.0", - "typedoc": "^0.15.0", + "ts-node": "^9.0.0", + "typedoc": "^0.19.2", "typescript": "^3.7.3", "yargs": "^16.0.0" } diff --git a/test/unit/utils/api-request.spec.ts b/test/unit/utils/api-request.spec.ts index cab4c4f9ac..32bbf80d87 100644 --- a/test/unit/utils/api-request.spec.ts +++ b/test/unit/utils/api-request.spec.ts @@ -711,7 +711,7 @@ describe('HttpClient', () => { const scope = nock('https://' + mockHost) .get(mockPath) .times(5) - .socketDelay(2000) + .delayConnection(2000) .reply(200, respData, { 'content-type': 'application/json', }); From f624b2963900deebed0fdb5cbe66ce7eb0d244b8 Mon Sep 17 00:00:00 2001 From: Nathan Mosher Date: Thu, 3 Dec 2020 15:13:38 -0800 Subject: [PATCH 062/160] Adds @license JSDoc tag to license comments (#1102) Adds @license JSDoc tag to license comments --- gulpfile.js | 1 + src/auth/auth-api-request.ts | 1 + src/auth/auth.ts | 1 + src/auth/token-generator.ts | 1 + src/auth/user-record.ts | 1 + src/credential/credential.ts | 1 + src/default-namespace.ts | 1 + src/firebase-app.ts | 1 + src/firebase-namespace.ts | 1 + src/firebase-service.ts | 1 + src/firestore/firestore-internal.ts | 1 + src/index.d.ts | 1 + src/index.ts | 1 + src/instance-id/instance-id-request-internal.ts | 1 + src/messaging/messaging-api-request-internal.ts | 1 + src/messaging/messaging.ts | 1 + src/storage/storage.ts | 1 + src/utils/api-request.ts | 1 + src/utils/deep-copy.ts | 1 + src/utils/error.ts | 1 + src/utils/index.ts | 1 + src/utils/validator.ts | 1 + test/integration/typescript/src/example.test.ts | 1 + test/integration/typescript/src/example.ts | 1 + test/resources/mocks.ts | 1 + test/unit/auth/auth-api-request.spec.ts | 5 +++-- test/unit/auth/auth.spec.ts | 1 + test/unit/auth/token-generator.spec.ts | 3 ++- test/unit/auth/user-record.spec.ts | 1 + test/unit/credential/credential.spec.ts | 1 + test/unit/database/database.spec.ts | 1 + test/unit/firebase-app.spec.ts | 1 + test/unit/firebase-namespace.spec.ts | 1 + test/unit/firebase.spec.ts | 1 + test/unit/firestore/firestore.spec.ts | 1 + test/unit/index.spec.ts | 1 + test/unit/instance-id/instance-id-request.spec.ts | 1 + test/unit/instance-id/instance-id.spec.ts | 1 + test/unit/messaging/messaging.spec.ts | 1 + test/unit/storage/storage.spec.ts | 1 + test/unit/utils.ts | 1 + test/unit/utils/api-request.spec.ts | 1 + test/unit/utils/error.spec.ts | 1 + test/unit/utils/index.spec.ts | 1 + test/unit/utils/validator.spec.ts | 1 + 45 files changed, 48 insertions(+), 3 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 02c5f136c8..9ca1aeb941 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 740d173356..0380debeb8 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 03d1c4e666..09c08c6d8e 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 994d1bf25f..a8a76c7b28 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 96ca179dd4..01092a25da 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/credential/credential.ts b/src/credential/credential.ts index fbac0ed36d..a2b4413b73 100644 --- a/src/credential/credential.ts +++ b/src/credential/credential.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/default-namespace.ts b/src/default-namespace.ts index a90d81089b..d15f3cae02 100644 --- a/src/default-namespace.ts +++ b/src/default-namespace.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/firebase-app.ts b/src/firebase-app.ts index f18edf51ef..401c21046b 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 4309913921..28fbbaf290 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/firebase-service.ts b/src/firebase-service.ts index f6e4c97393..321d91efb8 100644 --- a/src/firebase-service.ts +++ b/src/firebase-service.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/firestore/firestore-internal.ts b/src/firestore/firestore-internal.ts index 25ce0f903a..5c2210fdc0 100644 --- a/src/firestore/firestore-internal.ts +++ b/src/firestore/firestore-internal.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/index.d.ts b/src/index.d.ts index a748eb21f5..1807b1e079 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/index.ts b/src/index.ts index 9374b5fd8c..eb121bc023 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/instance-id/instance-id-request-internal.ts b/src/instance-id/instance-id-request-internal.ts index bab069faf1..e0d404d602 100644 --- a/src/instance-id/instance-id-request-internal.ts +++ b/src/instance-id/instance-id-request-internal.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/messaging/messaging-api-request-internal.ts b/src/messaging/messaging-api-request-internal.ts index a294f4daa5..d74f621e0d 100644 --- a/src/messaging/messaging-api-request-internal.ts +++ b/src/messaging/messaging-api-request-internal.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index 8c3d7c1ca3..0501693504 100644 --- a/src/messaging/messaging.ts +++ b/src/messaging/messaging.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 1ac61f6237..c8c314f920 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index d079b44557..a41a2f9b48 100644 --- a/src/utils/api-request.ts +++ b/src/utils/api-request.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/utils/deep-copy.ts b/src/utils/deep-copy.ts index 72a791d30a..8e5bee9d8b 100644 --- a/src/utils/deep-copy.ts +++ b/src/utils/deep-copy.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/utils/error.ts b/src/utils/error.ts index c1762db1af..232bed0981 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/utils/index.ts b/src/utils/index.ts index da0b73d704..b8cfa2faf0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/utils/validator.ts b/src/utils/validator.ts index c2ce758267..bca0c660ab 100644 --- a/src/utils/validator.ts +++ b/src/utils/validator.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/integration/typescript/src/example.test.ts b/test/integration/typescript/src/example.test.ts index 52f865ee58..5aa63c083d 100644 --- a/test/integration/typescript/src/example.test.ts +++ b/test/integration/typescript/src/example.test.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/integration/typescript/src/example.ts b/test/integration/typescript/src/example.ts index 0b254517d1..b44dbcc163 100644 --- a/test/integration/typescript/src/example.ts +++ b/test/integration/typescript/src/example.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index 34e1e5bb45..b90a0c83f8 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 13d5fcfc33..ff154d3b2c 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -934,7 +935,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { data, headers: expectedHeaders, timeout, - }); + }); }); }); @@ -954,7 +955,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { data, headers: expectedHeadersEmulator, timeout, - }); + }); }); }); }); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 27d797abba..5219f55e5d 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index 3b196be9f7..c519c7a3ed 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -311,7 +312,7 @@ describe('FirebaseTokenGenerator', () => { describe('Emulator', () => { const signer = new EmulatedSigner(); const tokenGenerator = new FirebaseTokenGenerator(signer); - + it('should generate a valid unsigned token', async () => { const uid = 'uid123'; const claims = { foo: 'bar' }; diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index 7fe6e058af..0a42de92cb 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/credential/credential.spec.ts b/test/unit/credential/credential.spec.ts index c51894cc84..d34b569bda 100644 --- a/test/unit/credential/credential.spec.ts +++ b/test/unit/credential/credential.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index c5bdd0a4fc..632f35bda9 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index b0cc477429..c3b4c04e3e 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index d993d2871c..d1dbbe7347 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/firebase.spec.ts b/test/unit/firebase.spec.ts index 71eb5d7297..3048121529 100644 --- a/test/unit/firebase.spec.ts +++ b/test/unit/firebase.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/firestore/firestore.spec.ts b/test/unit/firestore/firestore.spec.ts index 0b80a846a7..197e8dfde6 100644 --- a/test/unit/firestore/firestore.spec.ts +++ b/test/unit/firestore/firestore.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index fe22da0aca..efbe059e96 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/instance-id/instance-id-request.spec.ts b/test/unit/instance-id/instance-id-request.spec.ts index f8abeb6bc8..dd28f0f8ef 100644 --- a/test/unit/instance-id/instance-id-request.spec.ts +++ b/test/unit/instance-id/instance-id-request.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index 28e37cb906..b68f8b3ac1 100644 --- a/test/unit/instance-id/instance-id.spec.ts +++ b/test/unit/instance-id/instance-id.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index d1c0d96831..597cadf1ee 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/storage/storage.spec.ts b/test/unit/storage/storage.spec.ts index 0dba8db5a0..997d44846e 100644 --- a/test/unit/storage/storage.spec.ts +++ b/test/unit/storage/storage.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/utils.ts b/test/unit/utils.ts index aafb573b8b..6eb845831a 100644 --- a/test/unit/utils.ts +++ b/test/unit/utils.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/utils/api-request.spec.ts b/test/unit/utils/api-request.spec.ts index 32bbf80d87..f997cfc9dd 100644 --- a/test/unit/utils/api-request.spec.ts +++ b/test/unit/utils/api-request.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/utils/error.spec.ts b/test/unit/utils/error.spec.ts index 3b9e59269d..c2321005ee 100644 --- a/test/unit/utils/error.spec.ts +++ b/test/unit/utils/error.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 850f9c09e1..e63993fb83 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/unit/utils/validator.spec.ts b/test/unit/utils/validator.spec.ts index bcf2a67d3a..15d550c27d 100644 --- a/test/unit/utils/validator.spec.ts +++ b/test/unit/utils/validator.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); From cab6312504de0607d80edd3dc338f62efe2edee9 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Tue, 8 Dec 2020 13:24:50 -0800 Subject: [PATCH 063/160] chore: adds missing provider IDs for Auth (#1106) Adds missing token claims `sign_in_provider` values. --- src/auth/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/auth/index.ts b/src/auth/index.ts index 8e1242153c..1ba9b56af5 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -503,7 +503,13 @@ export namespace auth { /** * The ID of the provider used to sign in the user. * One of `"anonymous"`, `"password"`, `"facebook.com"`, `"github.com"`, - * `"google.com"`, `"twitter.com"`, or `"custom"`. + * `"google.com"`, `"twitter.com"`, `"apple.com"`, `"microsoft.com"`, + * "yahoo.com"`, `"phone"`, `"playgames.google.com"`, `"gc.apple.com"`, + * or `"custom"`. + * + * Additional Identity Platform provider IDs include `"linkedin.com"`, + * OIDC and SAML identity providers prefixed with `"saml."` and `"oidc."` + * respectively. */ sign_in_provider: string; From 9dd4040888d7d6ac7c2970bad26e7646d6a3f384 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 9 Dec 2020 11:12:21 -0800 Subject: [PATCH 064/160] [chore] Release 9.4.2 (#1111) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3365876dc1..37620aa245 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.4.1", + "version": "9.4.2", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From eecbe414e9bea5489db37cd1f39ee8776c5b96a6 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 17 Dec 2020 15:22:45 -0800 Subject: [PATCH 065/160] Adding delayed response message for holidays (#1118) * Adding delayed response message for holidays * Fixed typo --- .github/ISSUE_TEMPLATE/general-bug-report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/general-bug-report.md b/.github/ISSUE_TEMPLATE/general-bug-report.md index 82308572d5..9a59379b75 100644 --- a/.github/ISSUE_TEMPLATE/general-bug-report.md +++ b/.github/ISSUE_TEMPLATE/general-bug-report.md @@ -7,6 +7,8 @@ assignees: '' --- +**Thank you for submitting your issue. We are operating at reduced capacity from Dec 18 2020 to Jan 4 2021. Please expect delayed responses. For more urgent requests please reach us via our support channels https://firebase.google.com/support** + ### [READ] Step 1: Are you in the right place? * For issues related to __the code in this repository__ file a Github issue. From 6f6941e80dfe2e67b53c4d8139262d402fc604ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Jan 2021 10:48:00 -0800 Subject: [PATCH 066/160] build(deps): bump date-and-time from 0.14.1 to 0.14.2 (#1120) Bumps [date-and-time](https://github.com/knowledgecode/date-and-time) from 0.14.1 to 0.14.2. - [Release notes](https://github.com/knowledgecode/date-and-time/releases) - [Commits](https://github.com/knowledgecode/date-and-time/compare/v0.14.1...v0.14.2) 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 b549d07128..0ed6474a4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.4.1", + "version": "9.4.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2317,9 +2317,9 @@ } }, "date-and-time": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.1.tgz", - "integrity": "sha512-M4RggEH5OF2ZuCOxgOU67R6Z9ohjKbxGvAQz48vj53wLmL0bAgumkBvycR32f30pK+Og9pIR+RFDyChbaE4oLA==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.2.tgz", + "integrity": "sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA==", "optional": true }, "dateformat": { From 053de07e3fce133619db78467ee93078ce1bfee8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Jan 2021 11:20:34 -0800 Subject: [PATCH 067/160] build(deps): bump ini from 1.3.5 to 1.3.8 (#1126) Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8) 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 0ed6474a4e..2f6ebfce92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4441,9 +4441,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, "inquirer": { From ce669a326d75de255960df335d265d5afbc23539 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Jan 2021 11:24:06 -0800 Subject: [PATCH 068/160] build(deps): bump highlight.js from 10.4.0 to 10.5.0 (#1127) Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 10.4.0 to 10.5.0. - [Release notes](https://github.com/highlightjs/highlight.js/releases) - [Changelog](https://github.com/highlightjs/highlight.js/blob/master/CHANGES.md) - [Commits](https://github.com/highlightjs/highlight.js/compare/10.4.0...10.5.0) 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 2f6ebfce92..5ec1476a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4275,9 +4275,9 @@ "dev": true }, "highlight.js": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.4.0.tgz", - "integrity": "sha512-EfrUGcQ63oLJbj0J0RI9ebX6TAITbsDBLbsjr881L/X5fMO9+oadKzEF21C7R3ULKG6Gv3uoab2HiqVJa/4+oA==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.5.0.tgz", + "integrity": "sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==", "dev": true }, "homedir-polyfill": { From 8755b2f2507da523e4836e332feb41220a5a7a01 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 5 Jan 2021 16:35:35 -0500 Subject: [PATCH 069/160] Remove delayed response message for holidays (#1125) --- .github/ISSUE_TEMPLATE/general-bug-report.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/general-bug-report.md b/.github/ISSUE_TEMPLATE/general-bug-report.md index 9a59379b75..82308572d5 100644 --- a/.github/ISSUE_TEMPLATE/general-bug-report.md +++ b/.github/ISSUE_TEMPLATE/general-bug-report.md @@ -7,8 +7,6 @@ assignees: '' --- -**Thank you for submitting your issue. We are operating at reduced capacity from Dec 18 2020 to Jan 4 2021. Please expect delayed responses. For more urgent requests please reach us via our support channels https://firebase.google.com/support** - ### [READ] Step 1: Are you in the right place? * For issues related to __the code in this repository__ file a Github issue. From b60191e7ca17f57eb162798ab08ebad5693cdc54 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 6 Jan 2021 11:02:02 -0500 Subject: [PATCH 070/160] chore(rc): Add more unit tests for timestamp validation (#1092) --- src/remote-config/remote-config.ts | 6 ++- test/unit/remote-config/remote-config.spec.ts | 38 ++++++++++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index ba25f9c011..79a261687c 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -362,8 +362,8 @@ class VersionImpl implements Version { this.isLegacy = version.isLegacy; } - // The backend API provides timestamps as ISO date strings. The Admin SDK exposes timestamps - // as UTC date strings. If a developer uses a previously obtained template with UTC timestamps + // The backend API provides timestamps in ISO date strings. The Admin SDK exposes timestamps + // in UTC date strings. If a developer uses a previously obtained template with UTC timestamps // we could still validate it below. if (typeof version.updateTime !== 'undefined') { if (!this.isValidTimestamp(version.updateTime)) { @@ -392,6 +392,8 @@ class VersionImpl implements Version { } private isValidTimestamp(timestamp: string): boolean { + // This validation fails for timestamps earlier than January 1, 1970 and considers strings + // such as "1.2" as valid timestamps. return validator.isNonEmptyString(timestamp) && (new Date(timestamp)).getTime() > 0; } } diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index 462d46341e..6c946f41fa 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -681,10 +681,10 @@ describe('RemoteConfig', () => { }); }); - it('should resolve with template when Version updateTime contains only 3 ms places', () => { + it('should resolve with template when Version updateTime contains 3 digits in fractional seconds', () => { const response = deepCopy(REMOTE_CONFIG_RESPONSE); const versionInfo = deepCopy(VERSION_INFO); - versionInfo.updateTime = '2020-11-03T20:24:15.203Z'; + versionInfo.updateTime = '2020-10-03T17:14:10.203Z'; response.version = versionInfo; const stub = sinon .stub(RemoteConfigApiClient.prototype, operationName) @@ -703,14 +703,14 @@ describe('RemoteConfig', () => { email: 'firebase-adminsdk@gserviceaccount.com' }); expect(version.description).to.equal('production version'); - expect(version.updateTime).to.equal('Tue, 03 Nov 2020 20:24:15 GMT'); + expect(version.updateTime).to.equal('Sat, 03 Oct 2020 17:14:10 GMT'); }); }); - it('should resolve with template when Version updateTime contains 6 ms places', () => { + it('should resolve with template when Version updateTime contains 6 digits in fractional seconds', () => { const response = deepCopy(REMOTE_CONFIG_RESPONSE); const versionInfo = deepCopy(VERSION_INFO); - versionInfo.updateTime = '2020-11-13T17:01:36.541527Z'; + versionInfo.updateTime = '2020-08-14T17:01:36.541527Z'; response.version = versionInfo; const stub = sinon .stub(RemoteConfigApiClient.prototype, operationName) @@ -729,7 +729,33 @@ describe('RemoteConfig', () => { email: 'firebase-adminsdk@gserviceaccount.com' }); expect(version.description).to.equal('production version'); - expect(version.updateTime).to.equal('Fri, 13 Nov 2020 17:01:36 GMT'); + expect(version.updateTime).to.equal('Fri, 14 Aug 2020 17:01:36 GMT'); + }); + }); + + it('should resolve with template when Version updateTime contains 9 digits in fractional seconds', () => { + const response = deepCopy(REMOTE_CONFIG_RESPONSE); + const versionInfo = deepCopy(VERSION_INFO); + versionInfo.updateTime = '2020-11-15T06:57:26.342763941Z'; + response.version = versionInfo; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response); + stubs.push(stub); + + return rcOperation() + .then((template) => { + expect(template.etag).to.equal('etag-123456789012-5'); + + const version = template.version!; + expect(version.versionNumber).to.equal('86'); + expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE'); + expect(version.updateType).to.equal('INCREMENTAL_UPDATE'); + expect(version.updateUser).to.deep.equal({ + email: 'firebase-adminsdk@gserviceaccount.com' + }); + expect(version.description).to.equal('production version'); + expect(version.updateTime).to.equal('Sun, 15 Nov 2020 06:57:26 GMT'); }); }); } From 2f6da8925b953a116f1e44bb6a2b9213db5d2681 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 11 Jan 2021 12:24:22 -0800 Subject: [PATCH 071/160] fix: Removing FirebaseServiceInterface and FirebaseServiceInternalsInterface (#1128) * fix: Removing FirebaseService and FirebaseServiceInterface internal APIs * chore: Added unit tests for service caching behavior * fix: Using equal instead of deep.equal for reference tests --- src/auth/auth.ts | 21 +-- src/database/database-internal.ts | 48 +++---- src/firebase-app.ts | 61 +++------ src/firebase-namespace.ts | 86 +----------- src/firebase-service.ts | 43 ------ src/firestore/firestore-internal.ts | 19 +-- src/instance-id/instance-id.ts | 19 +-- src/machine-learning/machine-learning.ts | 20 +-- src/messaging/messaging.ts | 21 +-- src/project-management/project-management.ts | 19 +-- src/remote-config/remote-config.ts | 19 +-- src/security-rules/security-rules.ts | 11 +- src/storage/storage.ts | 19 +-- test/resources/mocks.ts | 15 --- test/unit/auth/auth.spec.ts | 8 -- test/unit/database/database.spec.ts | 2 +- test/unit/firebase-app.spec.ts | 70 ++-------- test/unit/firebase-namespace.spec.ts | 133 ++++++++++--------- test/unit/messaging/messaging.spec.ts | 6 - 19 files changed, 135 insertions(+), 505 deletions(-) delete mode 100644 src/firebase-service.ts diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 09c08c6d8e..8e35df82c4 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -25,7 +25,6 @@ import { AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, useEmulator, } from './auth-api-request'; import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { auth } from './index'; @@ -59,22 +58,6 @@ import BaseAuthInterface = auth.BaseAuth; import AuthInterface = auth.Auth; import TenantAwareAuthInterface = auth.TenantAwareAuth; -/** - * Internals of an Auth instance. - */ -class AuthInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up - return Promise.resolve(undefined); - } -} - - /** * Base Auth class. Mainly used for user management APIs. */ @@ -820,10 +803,8 @@ export class TenantAwareAuth * Auth service bound to the provided app. * An Auth instance can have multiple tenants. */ -export class Auth extends BaseAuth - implements FirebaseServiceInterface, AuthInterface { +export class Auth extends BaseAuth implements AuthInterface { - public INTERNAL: AuthInternals = new AuthInternals(); private readonly tenantManager_: TenantManager; private readonly app_: FirebaseApp; diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts index b2d9e4e970..a469a41773 100644 --- a/src/database/database-internal.ts +++ b/src/database/database-internal.ts @@ -19,7 +19,6 @@ import * as path from 'path'; import { FirebaseApp } from '../firebase-app'; import { FirebaseDatabaseError, AppErrorCodes, FirebaseAppError } from '../utils/error'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { Database as DatabaseImpl } from '@firebase/database'; import { database } from './index'; @@ -29,35 +28,14 @@ import { getSdkVersion } from '../utils/index'; import Database = database.Database; -/** - * Internals of a Database instance. - */ -class DatabaseInternals implements FirebaseServiceInternalsInterface { +export class DatabaseService { + + private readonly appInternal: FirebaseApp; - public databases: { + private databases: { [dbUrl: string]: Database; } = {}; - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - for (const dbUrl of Object.keys(this.databases)) { - const db: DatabaseImpl = ((this.databases[dbUrl] as any) as DatabaseImpl); - db.INTERNAL.delete(); - } - return Promise.resolve(undefined); - } -} - -export class DatabaseService implements FirebaseServiceInterface { - - public readonly INTERNAL: DatabaseInternals = new DatabaseInternals(); - - private readonly appInternal: FirebaseApp; - constructor(app: FirebaseApp) { if (!validator.isNonNullObject(app) || !('options' in app)) { throw new FirebaseDatabaseError({ @@ -68,6 +46,20 @@ export class DatabaseService implements FirebaseServiceInterface { this.appInternal = app; } + /** + * @internal + */ + public delete(): Promise { + const promises = []; + for (const dbUrl of Object.keys(this.databases)) { + const db: DatabaseImpl = ((this.databases[dbUrl] as any) as DatabaseImpl); + promises.push(db.INTERNAL.delete()); + } + return Promise.all(promises).then(() => { + this.databases = {}; + }); + } + /** * Returns the app associated with this DatabaseService instance. * @@ -86,7 +78,7 @@ export class DatabaseService implements FirebaseServiceInterface { }); } - let db: Database = this.INTERNAL.databases[dbUrl]; + let db: Database = this.databases[dbUrl]; if (typeof db === 'undefined') { const rtdb = require('@firebase/database'); // eslint-disable-line @typescript-eslint/no-var-requires db = rtdb.initStandalone(this.appInternal, dbUrl, getSdkVersion()).instance; @@ -102,7 +94,7 @@ export class DatabaseService implements FirebaseServiceInterface { return rulesClient.setRules(source); }; - this.INTERNAL.databases[dbUrl] = db; + this.databases[dbUrl] = db; } return db; } diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 401c21046b..fb8ad8b0f5 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -19,8 +19,7 @@ import { AppOptions, app } from './firebase-namespace-api'; import { credential, GoogleOAuthAccessToken } from './credential/index'; import { getApplicationDefault } from './credential/credential-internal'; import * as validator from './utils/validator'; -import { deepCopy, deepExtend } from './utils/deep-copy'; -import { FirebaseServiceInterface } from './firebase-service'; +import { deepCopy } from './utils/deep-copy'; import { FirebaseNamespaceInternals } from './firebase-namespace'; import { AppErrorCodes, FirebaseAppError } from './utils/error'; @@ -238,7 +237,7 @@ export class FirebaseApp implements app.App { private name_: string; private options_: AppOptions; - private services_: {[name: string]: FirebaseServiceInterface} = {}; + private services_: {[name: string]: unknown} = {}; private isDeleted_ = false; constructor(options: AppOptions, name: string, private firebaseInternals_: FirebaseNamespaceInternals) { @@ -268,11 +267,6 @@ export class FirebaseApp implements app.App { ); } - Object.keys(firebaseInternals_.serviceFactories).forEach((serviceName) => { - // Defer calling createService() until the service is accessed - (this as {[key: string]: any})[serviceName] = this.getService_.bind(this, serviceName); - }); - this.INTERNAL = new FirebaseAppInternals(credential); } @@ -428,51 +422,24 @@ export class FirebaseApp implements app.App { this.INTERNAL.delete(); return Promise.all(Object.keys(this.services_).map((serviceName) => { - return this.services_[serviceName].INTERNAL.delete(); + const service = this.services_[serviceName]; + if (isStateful(service)) { + return service.delete(); + } + return Promise.resolve(); })).then(() => { this.services_ = {}; this.isDeleted_ = true; }); } - private ensureService_(serviceName: string, initializer: () => T): T { - this.checkDestroyed_(); - - let service: T; - if (serviceName in this.services_) { - service = this.services_[serviceName] as T; - } else { - service = initializer(); - this.services_[serviceName] = service; - } - return service; - } - - /** - * Returns the service instance associated with this FirebaseApp instance (creating it on demand - * if needed). This is used for looking up monkeypatched service instances. - * - * @param serviceName The name of the service instance to return. - * @return The service instance with the provided name. - */ - private getService_(serviceName: string): FirebaseServiceInterface { + private ensureService_(serviceName: string, initializer: () => T): T { this.checkDestroyed_(); - if (!(serviceName in this.services_)) { - this.services_[serviceName] = this.firebaseInternals_.serviceFactories[serviceName]( - this, - this.extendApp_.bind(this), - ); + this.services_[serviceName] = initializer(); } - return this.services_[serviceName]; - } - - /** - * Callback function used to extend an App instance at the time of service instance creation. - */ - private extendApp_(props: {[prop: string]: any}): void { - deepExtend(this, props); + return this.services_[serviceName] as T; } /** @@ -487,3 +454,11 @@ export class FirebaseApp implements app.App { } } } + +interface StatefulFirebaseService { + delete(): Promise; +} + +function isStateful(service: any): service is StatefulFirebaseService { + return typeof service.delete === 'function'; +} diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 28fbbaf290..43b12a92c9 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -17,11 +17,9 @@ import fs = require('fs'); -import { deepExtend } from './utils/deep-copy'; import { AppErrorCodes, FirebaseAppError } from './utils/error'; import { AppOptions, app } from './firebase-namespace-api'; -import { AppHook, FirebaseApp } from './firebase-app'; -import { FirebaseServiceFactory, FirebaseServiceInterface } from './firebase-service'; +import { FirebaseApp } from './firebase-app'; import { cert, refreshToken, applicationDefault } from './credential/credential'; import { getApplicationDefault } from './credential/credential-internal'; @@ -69,11 +67,8 @@ export interface FirebaseServiceNamespace { * Internals of a FirebaseNamespace instance. */ export class FirebaseNamespaceInternals { - public serviceFactories: {[serviceName: string]: FirebaseServiceFactory} = {}; private apps_: {[appName: string]: App} = {}; - private appHooks_: {[service: string]: AppHook} = {}; - constructor(public firebase_: {[key: string]: any}) {} /** @@ -118,11 +113,7 @@ export class FirebaseNamespaceInternals { } const app = new FirebaseApp(options, appName, this); - this.apps_[appName] = app; - - this.callAppHooks_(app, 'create'); - return app; } @@ -170,80 +161,7 @@ export class FirebaseNamespaceInternals { } const appToRemove = this.app(appName); - this.callAppHooks_(appToRemove, 'delete'); - delete this.apps_[appName]; - } - - /** - * Registers a new service on this Firebase namespace. - * - * @param serviceName The name of the Firebase service to register. - * @param createService A factory method to generate an instance of the Firebase service. - * @param serviceProperties Optional properties to extend this Firebase namespace with. - * @param appHook Optional callback that handles app-related events like app creation and deletion. - * @return The Firebase service's namespace. - */ - public registerService( - serviceName: string, - createService: FirebaseServiceFactory, - serviceProperties?: object, - appHook?: AppHook): FirebaseServiceNamespace { - let errorMessage; - if (typeof serviceName === 'undefined') { - errorMessage = 'No service name provided. Service name must be a non-empty string.'; - } else if (typeof serviceName !== 'string' || serviceName === '') { - errorMessage = `Invalid service name "${serviceName}" provided. Service name must be a non-empty string.`; - } else if (serviceName in this.serviceFactories) { - errorMessage = `Firebase service named "${serviceName}" has already been registered.`; - } - - if (typeof errorMessage !== 'undefined') { - throw new FirebaseAppError( - AppErrorCodes.INTERNAL_ERROR, - `INTERNAL ASSERT FAILED: ${errorMessage}`, - ); - } - - this.serviceFactories[serviceName] = createService; - if (appHook) { - this.appHooks_[serviceName] = appHook; - } - - // The service namespace is an accessor function which takes a FirebaseApp instance - // or uses the default app if no FirebaseApp instance is provided - const serviceNamespace: FirebaseServiceNamespace = (appArg?: App) => { - if (typeof appArg === 'undefined') { - appArg = this.app(); - } - - // Forward service instance lookup to the FirebaseApp - return (appArg as any)[serviceName](); - }; - - // ... and a container for service-level properties. - if (serviceProperties !== undefined) { - deepExtend(serviceNamespace, serviceProperties); - } - - // Monkey-patch the service namespace onto the Firebase namespace - this.firebase_[serviceName] = serviceNamespace; - - return serviceNamespace; - } - - /** - * Calls the app hooks corresponding to the provided event name for each service within the - * provided App instance. - * - * @param app The App instance whose app hooks to call. - * @param eventName The event name representing which app hooks to call. - */ - private callAppHooks_(app: App, eventName: string): void { - Object.keys(this.serviceFactories).forEach((serviceName) => { - if (this.appHooks_[serviceName]) { - this.appHooks_[serviceName](eventName, app); - } - }); + delete this.apps_[appToRemove.name]; } /** diff --git a/src/firebase-service.ts b/src/firebase-service.ts deleted file mode 100644 index 321d91efb8..0000000000 --- a/src/firebase-service.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*! - * @license - * Copyright 2017 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { app } from './firebase-namespace-api'; -import { FirebaseApp } from './firebase-app'; - -import App = app.App; - -/** - * Internals of a FirebaseService instance. - */ -export interface FirebaseServiceInternalsInterface { - delete(): Promise; -} - -/** - * Services are exposed through instances, each of which is associated with a FirebaseApp. - */ -export interface FirebaseServiceInterface { - app: App; - INTERNAL: FirebaseServiceInternalsInterface; -} - -/** - * Factory method to create FirebaseService instances given a FirebaseApp instance. Can optionally - * add properties and methods to each FirebaseApp instance via the extendApp() function. - */ -export type FirebaseServiceFactory = - (app: FirebaseApp, extendApp?: (props: object) => void) => FirebaseServiceInterface; diff --git a/src/firestore/firestore-internal.ts b/src/firestore/firestore-internal.ts index 5c2210fdc0..2188ec223a 100644 --- a/src/firestore/firestore-internal.ts +++ b/src/firestore/firestore-internal.ts @@ -17,30 +17,13 @@ import { FirebaseApp } from '../firebase-app'; import { FirebaseFirestoreError } from '../utils/error'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential-internal'; import { Firestore, Settings } from '@google-cloud/firestore'; import * as validator from '../utils/validator'; import * as utils from '../utils/index'; -/** - * Internals of a Firestore instance. - */ -class FirestoreInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up. - return Promise.resolve(); - } -} - -export class FirestoreService implements FirebaseServiceInterface { - public INTERNAL: FirestoreInternals = new FirestoreInternals(); +export class FirestoreService { private appInternal: FirebaseApp; private firestoreClient: Firestore; diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index e09c630440..80afaeae48 100644 --- a/src/instance-id/instance-id.ts +++ b/src/instance-id/instance-id.ts @@ -15,7 +15,6 @@ */ import { FirebaseApp } from '../firebase-app'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; import { FirebaseInstanceIdRequestHandler } from './instance-id-request-internal'; import { instanceId } from './index'; @@ -23,21 +22,6 @@ import * as validator from '../utils/validator'; import InstanceIdInterface = instanceId.InstanceId; -/** - * Internals of an InstanceId service instance. - */ -class InstanceIdInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up - return Promise.resolve(undefined); - } -} - /** * Gets the {@link InstanceId `InstanceId`} service for the * current app. @@ -52,8 +36,7 @@ class InstanceIdInternals implements FirebaseServiceInternalsInterface { * @return The `InstanceId` service for the * current app. */ -export class InstanceId implements FirebaseServiceInterface, InstanceIdInterface { - public INTERNAL: InstanceIdInternals = new InstanceIdInternals(); +export class InstanceId implements InstanceIdInterface { private app_: FirebaseApp; private requestHandler: FirebaseInstanceIdRequestHandler; diff --git a/src/machine-learning/machine-learning.ts b/src/machine-learning/machine-learning.ts index 9e9ca50b55..dcbb72caec 100644 --- a/src/machine-learning/machine-learning.ts +++ b/src/machine-learning/machine-learning.ts @@ -15,7 +15,6 @@ */ import { FirebaseApp } from '../firebase-app'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { MachineLearningApiClient, ModelResponse, ModelUpdateOptions, isGcsTfliteModelOptions } from './machine-learning-api-client'; @@ -33,27 +32,10 @@ import ModelInterface = machineLearning.Model; import ModelOptions = machineLearning.ModelOptions; import TFLiteModel = machineLearning.TFLiteModel; -/** - * Internals of an ML instance. - */ -class MachineLearningInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise} An empty Promise that will be resolved when the - * service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up. - return Promise.resolve(); - } -} - /** * The Firebase Machine Learning class */ -export class MachineLearning implements FirebaseServiceInterface, MachineLearningInterface { - public readonly INTERNAL = new MachineLearningInternals(); +export class MachineLearning implements MachineLearningInterface { private readonly client: MachineLearningApiClient; private readonly appInternal: FirebaseApp; diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index 0501693504..88e66cf565 100644 --- a/src/messaging/messaging.ts +++ b/src/messaging/messaging.ts @@ -21,7 +21,6 @@ import { SubRequest } from './batch-request-internal'; import { validateMessage, BLACKLISTED_DATA_PAYLOAD_KEYS, BLACKLISTED_OPTIONS_KEYS } from './messaging-internal'; import { messaging } from './index'; import { FirebaseMessagingRequestHandler } from './messaging-api-request-internal'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError } from '../utils/error'; import * as utils from '../utils'; import * as validator from '../utils/validator'; @@ -186,28 +185,10 @@ function mapRawResponseToTopicManagementResponse(response: object): MessagingTop } -/** - * Internals of a Messaging instance. - */ -class MessagingInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up. - return Promise.resolve(undefined); - } -} - - /** * Messaging service bound to the provided app. */ -export class Messaging implements FirebaseServiceInterface, MessagingInterface { - - public INTERNAL: MessagingInternals = new MessagingInternals(); +export class Messaging implements MessagingInterface { private urlPath: string; private readonly appInternal: FirebaseApp; diff --git a/src/project-management/project-management.ts b/src/project-management/project-management.ts index 0f76912109..9dc8b29902 100644 --- a/src/project-management/project-management.ts +++ b/src/project-management/project-management.ts @@ -15,7 +15,6 @@ */ import { FirebaseApp } from '../firebase-app'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { FirebaseProjectManagementError } from '../utils/error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; @@ -28,29 +27,13 @@ import AppMetadata = projectManagement.AppMetadata; import AppPlatform = projectManagement.AppPlatform; import ProjectManagementInterface = projectManagement.ProjectManagement; -/** - * Internals of a Project Management instance. - */ -class ProjectManagementInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise} An empty Promise that will be resolved when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up. - return Promise.resolve(); - } -} - /** * The Firebase ProjectManagement service interface. * * Do not call this constructor directly. Instead, use * [`admin.projectManagement()`](projectManagement#projectManagement). */ -export class ProjectManagement implements FirebaseServiceInterface, ProjectManagementInterface { - public readonly INTERNAL: ProjectManagementInternals = new ProjectManagementInternals(); +export class ProjectManagement implements ProjectManagementInterface { private readonly requestHandler: ProjectManagementRequestHandler; private projectId: string; diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 79a261687c..d0b0046832 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { FirebaseApp } from '../firebase-app'; import * as validator from '../utils/validator'; import { remoteConfig } from './index'; @@ -30,26 +29,10 @@ import RemoteConfigUser = remoteConfig.RemoteConfigUser; import Version = remoteConfig.Version; import RemoteConfigInterface = remoteConfig.RemoteConfig; -/** - * Internals of an RemoteConfig service instance. - */ -class RemoteConfigInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up - return Promise.resolve(undefined); - } -} - /** * Remote Config service bound to the provided app. */ -export class RemoteConfig implements FirebaseServiceInterface, RemoteConfigInterface { - public readonly INTERNAL: RemoteConfigInternals = new RemoteConfigInternals(); +export class RemoteConfig implements RemoteConfigInterface { private readonly client: RemoteConfigApiClient; diff --git a/src/security-rules/security-rules.ts b/src/security-rules/security-rules.ts index e6cf7ab1b1..2206410df0 100644 --- a/src/security-rules/security-rules.ts +++ b/src/security-rules/security-rules.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { FirebaseApp } from '../firebase-app'; import * as validator from '../utils/validator'; import { @@ -85,13 +84,11 @@ export class Ruleset implements RulesetInterface { * Do not call this constructor directly. Instead, use * [`admin.securityRules()`](securityRules#securityRules). */ -export class SecurityRules implements FirebaseServiceInterface, SecurityRulesInterface { +export class SecurityRules implements SecurityRulesInterface { private static readonly CLOUD_FIRESTORE = 'cloud.firestore'; private static readonly FIREBASE_STORAGE = 'firebase.storage'; - public readonly INTERNAL = new SecurityRulesInternals(); - private readonly client: SecurityRulesApiClient; /** @@ -352,12 +349,6 @@ export class SecurityRules implements FirebaseServiceInterface, SecurityRulesInt } } -class SecurityRulesInternals implements FirebaseServiceInternalsInterface { - public delete(): Promise { - return Promise.resolve(); - } -} - function stripProjectIdPrefix(name: string): string { return name.split('/').pop()!; } diff --git a/src/storage/storage.ts b/src/storage/storage.ts index c8c314f920..4364658a8c 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -17,7 +17,6 @@ import { FirebaseApp } from '../firebase-app'; import { FirebaseError } from '../utils/error'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential-internal'; import { Bucket, Storage as StorageClient } from '@google-cloud/storage'; import * as utils from '../utils/index'; @@ -26,28 +25,12 @@ import { storage } from './index'; import StorageInterface = storage.Storage; -/** - * Internals of a Storage instance. - */ -class StorageInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up. - return Promise.resolve(); - } -} - /** * The default `Storage` service if no * app is provided or the `Storage` service associated with the provided * app. */ -export class Storage implements FirebaseServiceInterface, StorageInterface { - public readonly INTERNAL: StorageInternals = new StorageInternals(); +export class Storage implements StorageInterface { private readonly appInternal: FirebaseApp; private readonly storageClient: StorageClient; diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index b90a0c83f8..5512756039 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -27,9 +27,7 @@ import * as jwt from 'jsonwebtoken'; import { AppOptions } from '../../src/firebase-namespace-api'; import { FirebaseNamespace } from '../../src/firebase-namespace'; -import { FirebaseServiceInterface } from '../../src/firebase-service'; import { FirebaseApp } from '../../src/firebase-app'; -import { app as _app } from '../../src/firebase-namespace-api'; import { credential as _credential, GoogleOAuthAccessToken } from '../../src/credential/index'; import { ServiceAccountCredential } from '../../src/credential/credential-internal'; @@ -229,19 +227,6 @@ export function generateSessionCookie(overrides?: object, expiresIn?: number): s return jwt.sign(developerClaims, certificateObject.private_key, options); } -/* eslint-disable @typescript-eslint/no-unused-vars */ -export function firebaseServiceFactory( - firebaseApp: _app.App, - _extendApp?: (props: object) => void, -): FirebaseServiceInterface { - const result = { - app: firebaseApp, - INTERNAL: {}, - }; - return result as FirebaseServiceInterface; -} -/* eslint-enable @typescript-eslint/no-unused-vars */ - /** Mock socket emitter class. */ export class MockSocketEmitter extends events.EventEmitter { public setTimeout: (_: number) => void = () => undefined; diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 5219f55e5d..26521b30cd 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -3190,14 +3190,6 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); - if (testConfig.Auth === Auth) { - describe('INTERNAL.delete()', () => { - it('should delete Auth instance', () => { - (auth as Auth).INTERNAL.delete().should.eventually.be.fulfilled; - }); - }); - } - describe('auth emulator support', () => { let mockAuth = testConfig.init(mocks.app()); diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 632f35bda9..4a4f969b1e 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -40,7 +40,7 @@ describe('Database', () => { }); afterEach(() => { - return database.INTERNAL.delete().then(() => { + return database.delete().then(() => { return mockApp.delete(); }); }); diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index c3b4c04e3e..49da6736c5 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -28,7 +28,6 @@ import * as mocks from '../resources/mocks'; import { GoogleOAuthAccessToken } from '../../src/credential/index'; import { ServiceAccountCredential } from '../../src/credential/credential-internal'; -import { FirebaseServiceInterface } from '../../src/firebase-service'; import { FirebaseApp, FirebaseAccessToken } from '../../src/firebase-app'; import { FirebaseNamespace, FirebaseNamespaceInternals, FIREBASE_CONFIG_VAR } from '../../src/firebase-namespace'; @@ -65,13 +64,14 @@ const ONE_HOUR_IN_SECONDS = 60 * 60; const ONE_MINUTE_IN_MILLISECONDS = 60 * 1000; const deleteSpy = sinon.spy(); -function mockServiceFactory(app: FirebaseApp): FirebaseServiceInterface { - return { - app, - INTERNAL: { - delete: deleteSpy.bind(null, app.name), - }, - }; + +class TestService { + public deleted = false; + + public delete(): Promise { + this.deleted = true; + return Promise.resolve(); + } } @@ -342,18 +342,15 @@ describe('FirebaseApp', () => { }); it('should call delete() on each service\'s internals', () => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mockServiceFactory); - firebaseNamespace.INTERNAL.registerService(mocks.serviceName + '2', mockServiceFactory); - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - - (app as {[key: string]: any})[mocks.serviceName](); - (app as {[key: string]: any})[mocks.serviceName + '2'](); + const svc1 = new TestService(); + const svc2 = new TestService(); + (app as any).ensureService_(mocks.serviceName, () => svc1); + (app as any).ensureService_(mocks.serviceName + '2', () => svc2); return app.delete().then(() => { - expect(deleteSpy).to.have.been.calledTwice; - expect(deleteSpy.firstCall.args).to.deep.equal([mocks.appName]); - expect(deleteSpy.secondCall.args).to.deep.equal([mocks.appName]); + expect(svc1.deleted).to.be.true; + expect(svc2.deleted).to.be.true; }); }); }); @@ -675,45 +672,6 @@ describe('FirebaseApp', () => { }); }); - describe('#[service]()', () => { - it('should throw if the app has already been deleted', () => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mockServiceFactory); - - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - - return app.delete().then(() => { - expect(() => { - return (app as {[key: string]: any})[mocks.serviceName](); - }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); - }); - }); - - it('should return the service namespace', () => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mockServiceFactory); - - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - - const serviceNamespace = (app as {[key: string]: any})[mocks.serviceName](); - expect(serviceNamespace).to.have.keys(['app', 'INTERNAL']); - }); - - it('should return a cached version of the service on subsequent calls', () => { - const createServiceSpy = sinon.spy(); - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, createServiceSpy); - - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - - expect(createServiceSpy).to.not.have.been.called; - - const serviceNamespace1 = (app as {[key: string]: any})[mocks.serviceName](); - expect(createServiceSpy).to.have.been.calledOnce; - - const serviceNamespace2 = (app as {[key: string]: any})[mocks.serviceName](); - expect(createServiceSpy).to.have.been.calledOnce; - expect(serviceNamespace1).to.deep.equal(serviceNamespace2); - }); - }); - describe('INTERNAL.getToken()', () => { it('throws a custom credential implementation which returns invalid access tokens', () => { diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index d1dbbe7347..07e5a8ba78 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -19,7 +19,6 @@ 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'; @@ -259,15 +258,6 @@ describe('FirebaseNamespace', () => { const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); expect(firebaseNamespace.app(mocks.appName)).to.deep.equal(app); }); - - it('should call the "create" app hook for the new app', () => { - const appHook = sinon.spy(); - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory, undefined, appHook); - - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - - expect(appHook).to.have.been.calledOnce.and.calledWith('create', app); - }); }); describe('#INTERNAL.removeApp()', () => { @@ -328,58 +318,6 @@ describe('FirebaseNamespace', () => { firebaseNamespace.INTERNAL.removeApp(mocks.appName); }).to.throw(`Firebase app named "${mocks.appName}" does not exist.`); }); - - it('should call the "delete" app hook for the deleted app', () => { - const appHook = sinon.spy(); - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory, undefined, appHook); - - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - - appHook.resetHistory(); - - firebaseNamespace.INTERNAL.removeApp(mocks.appName); - - expect(appHook).to.have.been.calledOnce.and.calledWith('delete', app); - }); - }); - - describe('#INTERNAL.registerService()', () => { - // TODO(jwenger): finish writing tests for regsiterService() to get more code coverage - - it('should throw given no service name', () => { - expect(() => { - firebaseNamespace.INTERNAL.registerService(undefined as unknown as string, mocks.firebaseServiceFactory); - }).to.throw('No service name provided. Service name must be a non-empty string.'); - }); - - const invalidServiceNames = [null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop]; - invalidServiceNames.forEach((invalidServiceName) => { - it('should throw given non-string service name: ' + JSON.stringify(invalidServiceName), () => { - expect(() => { - firebaseNamespace.INTERNAL.registerService(invalidServiceName as any, mocks.firebaseServiceFactory); - }).to.throw(`Invalid service name "${invalidServiceName}" provided. Service name must be a non-empty string.`); - }); - }); - - it('should throw given an empty string service name', () => { - expect(() => { - firebaseNamespace.INTERNAL.registerService('', mocks.firebaseServiceFactory); - }).to.throw('Invalid service name "" provided. Service name must be a non-empty string.'); - }); - - it('should throw given a service name which has already been registered', () => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory); - expect(() => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory); - }).to.throw(`Firebase service named "${mocks.serviceName}" has already been registered.`); - }); - - it('should throw given a service name which has already been registered', () => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory); - expect(() => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory); - }).to.throw(`Firebase service named "${mocks.serviceName}" has already been registered.`); - }); }); describe('#auth()', () => { @@ -411,6 +349,13 @@ describe('FirebaseNamespace', () => { it('should return a reference to Auth type', () => { expect(firebaseNamespace.auth.Auth).to.be.deep.equal(AuthImpl); }); + + it('should return a cached version of Auth on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const serviceNamespace1: Auth = firebaseNamespace.auth(); + const serviceNamespace2: Auth = firebaseNamespace.auth(); + expect(serviceNamespace1).to.equal(serviceNamespace2); + }); }); describe('#database()', () => { @@ -468,6 +413,14 @@ describe('FirebaseNamespace', () => { it('should return a reference to enableLogging function', () => { expect(firebaseNamespace.database.enableLogging).to.be.deep.equal(enableLogging); }); + + it('should return a cached version of Database on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions); + const db1: Database = firebaseNamespace.database(); + const db2: Database = firebaseNamespace.database(); + expect(db1).to.equal(db2); + return app.delete(); + }); }); describe('#messaging()', () => { @@ -499,6 +452,13 @@ describe('FirebaseNamespace', () => { it('should return a reference to Messaging type', () => { expect(firebaseNamespace.messaging.Messaging).to.be.deep.equal(MessagingImpl); }); + + it('should return a cached version of Messaging on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const serviceNamespace1: Messaging = firebaseNamespace.messaging(); + const serviceNamespace2: Messaging = firebaseNamespace.messaging(); + expect(serviceNamespace1).to.equal(serviceNamespace2); + }); }); describe('#machine-learning()', () => { @@ -531,6 +491,13 @@ describe('FirebaseNamespace', () => { expect(firebaseNamespace.machineLearning.MachineLearning) .to.be.deep.equal(MachineLearningImpl); }); + + it('should return a cached version of MachineLearning on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const service1: MachineLearning = firebaseNamespace.machineLearning(); + const service2: MachineLearning = firebaseNamespace.machineLearning(); + expect(service1).to.equal(service2); + }); }); describe('#storage()', () => { @@ -562,6 +529,13 @@ describe('FirebaseNamespace', () => { it('should return a reference to Storage type', () => { expect(firebaseNamespace.storage.Storage).to.be.deep.equal(StorageImpl); }); + + it('should return a cached version of Storage on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const serviceNamespace1: Storage = firebaseNamespace.storage(); + const serviceNamespace2: Storage = firebaseNamespace.storage(); + expect(serviceNamespace1).to.equal(serviceNamespace2); + }); }); describe('#firestore()', () => { @@ -617,6 +591,13 @@ describe('FirebaseNamespace', () => { it('should return a reference to the v1 namespace', () => { expect(firebaseNamespace.firestore.v1).to.be.deep.equal(v1); }); + + it('should return a cached version of Firestore on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const service1: Firestore = firebaseNamespace.firestore(); + const service2: Firestore = firebaseNamespace.firestore(); + expect(service1).to.equal(service2); + }); }); describe('#instanceId()', () => { @@ -650,6 +631,13 @@ describe('FirebaseNamespace', () => { it('should return a reference to InstanceId type', () => { expect(firebaseNamespace.instanceId.InstanceId).to.be.deep.equal(InstanceIdImpl); }); + + it('should return a cached version of InstanceId on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const service1: InstanceId = firebaseNamespace.instanceId(); + const service2: InstanceId = firebaseNamespace.instanceId(); + expect(service1).to.equal(service2); + }); }); describe('#projectManagement()', () => { @@ -684,6 +672,13 @@ describe('FirebaseNamespace', () => { expect(firebaseNamespace.projectManagement.ProjectManagement) .to.be.deep.equal(ProjectManagementImpl); }); + + it('should return a cached version of ProjectManagement on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const service1: ProjectManagement = firebaseNamespace.projectManagement(); + const service2: ProjectManagement = firebaseNamespace.projectManagement(); + expect(service1).to.equal(service2); + }); }); describe('#securityRules()', () => { @@ -718,6 +713,13 @@ describe('FirebaseNamespace', () => { expect(firebaseNamespace.securityRules.SecurityRules) .to.be.deep.equal(SecurityRulesImpl); }); + + it('should return a cached version of SecurityRules on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const service1: SecurityRules = firebaseNamespace.securityRules(); + const service2: SecurityRules = firebaseNamespace.securityRules(); + expect(service1).to.equal(service2); + }); }); describe('#remoteConfig()', () => { @@ -749,5 +751,12 @@ describe('FirebaseNamespace', () => { it('should return a reference to RemoteConfig type', () => { expect(firebaseNamespace.remoteConfig.RemoteConfig).to.be.deep.equal(RemoteConfigImpl); }); + + it('should return a cached version of RemoteConfig on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const service1: RemoteConfig = firebaseNamespace.remoteConfig(); + const service2: RemoteConfig = firebaseNamespace.remoteConfig(); + expect(service1).to.equal(service2); + }); }); }); diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index 597cadf1ee..82f0973f67 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -4007,10 +4007,4 @@ describe('Messaging', () => { describe('unsubscribeFromTopic()', () => { tokenSubscriptionTests('unsubscribeFromTopic'); }); - - describe('INTERNAL.delete()', () => { - it('should delete Messaging instance', () => { - messaging.INTERNAL.delete().should.eventually.be.fulfilled; - }); - }); }); From f3b3caa946336ffe129555735a59ef2b77563f4e Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 12 Jan 2021 15:39:49 -0500 Subject: [PATCH 072/160] chore(core): Automate Daily Integration Tests (#1130) * Automate daily integration tests * Rename to nightly * Change to 6am and 8pm PT & remove tar verification * Fix schedule comment --- .github/workflows/nightly.yml | 68 +++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/nightly.yml diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000000..10221dac24 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,68 @@ +# 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. + +name: Nightly Builds + +on: + # Runs every day at 06:00 AM (PT) and 08:00 PM (PT) / 04:00 AM (UTC) and 02:00 PM (UTC) + schedule: + - cron: "0 4,14 * * *" + +jobs: + nightly: + + runs-on: ubuntu-latest + + steps: + - name: Checkout source for staging + uses: actions/checkout@v2 + with: + ref: ${{ github.event.client_payload.ref || github.ref }} + + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: 10.x + + - name: Install and build + run: | + npm ci + npm run build + npm run build:tests + + - name: Run unit tests + run: npm test + + - name: Verify public API + run: npm run api-extractor + + - name: Run integration tests + run: ./.github/scripts/run_integration_tests.sh + env: + FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }} + FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} + + - name: Package release artifacts + run: | + npm pack + mkdir -p dist + cp *.tgz dist/ + + # Attach the packaged artifacts to the workflow output. These can be manually + # downloaded for later inspection if necessary. + - name: Archive artifacts + uses: actions/upload-artifact@v1 + with: + name: dist + path: dist From 8ae44ce3ba944dde69cf921747650ed0b152d731 Mon Sep 17 00:00:00 2001 From: egilmorez Date: Thu, 14 Jan 2021 15:22:32 -0800 Subject: [PATCH 073/160] Updating Google Cloud naming (#1122) * Reinstating tag that devsite needs present to supress machine translation. * Updating a couple of references to GCP/Google Cloud Platform per new branding guidelines. --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42d573c035..dbb374fa14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,7 +146,7 @@ following credentials from the project: 2. *Web API key*: This is displayed in the "Settings > General" tab of the console. Copy it and save to a new text file at `test/resources/apikey.txt`. -Then set up your Firebase/GCP project as follows: +Then set up your Firebase/Google Cloud project as follows: 1. Enable Firestore: Go to the Firebase Console, and select "Database" from the "Develop" menu. Click on the "Create database" button. You may choose @@ -160,15 +160,15 @@ Then set up your Firebase/GCP project as follows: https://console.developers.google.com/apis/api/firebaseml.googleapis.com/overview) and make sure your project is selected. If the API is not already enabled, click Enable. 4. Enable the IAM API: Go to the - [Google Cloud Platform Console](https://console.cloud.google.com) and make - sure your Firebase/GCP project is selected. Select "APIs & Services > + [Google Cloud Console](https://console.cloud.google.com) and make + sure your Firebase/Google Cloud project is selected. Select "APIs & Services > Dashboard" from the main menu, and click the "ENABLE APIS AND SERVICES" button. Search for and enable the "Identity and Access Management (IAM) API". 5. Grant your service account the 'Firebase Authentication Admin' role. This is required to ensure that exported user records contain the password hashes of the user accounts: - 1. Go to [Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin). + 1. Go to [Google Cloud Console / IAM & admin](https://console.cloud.google.com/iam-admin). 2. Find your service account in the list, and click the 'pencil' icon to edit it's permissions. 3. Click 'ADD ANOTHER ROLE' and choose 'Firebase Authentication Admin'. 4. Click 'SAVE'. From 1862342636e816b61337262254cdfe0cc410c640 Mon Sep 17 00:00:00 2001 From: batuxd <9674241+suchcodemuchwow@users.noreply.github.com> Date: Fri, 22 Jan 2021 20:02:08 +0100 Subject: [PATCH 074/160] update typo in interface name (#1138) FireabseErrorInterface -> FirebaseErrorInterface --- src/utils/error.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/error.ts b/src/utils/error.ts index 232bed0981..5809a294b6 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { FirebaseError as FireabseErrorInterface } from '../firebase-namespace-api'; +import { FirebaseError as FirebaseErrorInterface } from '../firebase-namespace-api'; import { deepCopy } from '../utils/deep-copy'; /** @@ -39,7 +39,7 @@ interface ServerToClientCode { * @param {ErrorInfo} errorInfo The error information (code and message). * @constructor */ -export class FirebaseError extends Error implements FireabseErrorInterface { +export class FirebaseError extends Error implements FirebaseErrorInterface { constructor(private errorInfo: ErrorInfo) { super(errorInfo.message); From 6ce98e2bdd4e43b106238578c404a997fc2aa469 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Wed, 3 Feb 2021 21:20:18 -0800 Subject: [PATCH 075/160] Improve token verification logic with Auth Emulator. (#1148) * Improve token verification logic with Auth Emulator. * Clean up comments. * Fix linting issues. * Address review comments. * Use mock for auth emulator unit test. * Implement session cookies. * Call useEmulator() only once. * Update tests. * Delete unused test helper. * Add unit tests for checking revocation. * Fix typo in test comments. --- package-lock.json | 6 +- package.json | 2 +- src/auth/auth-api-request.ts | 4 - src/auth/auth.ts | 50 +++---- src/auth/token-verifier.ts | 73 ++++++----- test/integration/auth.spec.ts | 179 ++++++++++++++++++++------ test/integration/setup.ts | 78 +++++++---- test/unit/auth/auth.spec.ts | 93 +++++++++---- test/unit/auth/token-verifier.spec.ts | 9 +- 9 files changed, 321 insertions(+), 173 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ec1476a52..f4875def9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -176,9 +176,9 @@ "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==" }, "@firebase/auth": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.15.2.tgz", - "integrity": "sha512-2n32PBi6x9jVhc0E/ewKLUCYYTzFEXL4PNkvrrlGKbzeTBEkkyzfgUX7OV9UF5wUOG+gurtUthuur1zspZ/9hg==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.2.tgz", + "integrity": "sha512-68TlDL0yh3kF8PiCzI8m8RWd/bf/xCLUsdz1NZ2Dwea0sp6e2WAhu0sem1GfhwuEwL+Ns4jCdX7qbe/OQlkVEA==", "dev": true, "requires": { "@firebase/auth-types": "0.10.1" diff --git a/package.json b/package.json index 37620aa245..ee75e5e196 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ }, "devDependencies": { "@firebase/app": "^0.6.13", - "@firebase/auth": "^0.15.2", + "@firebase/auth": "^0.16.2", "@firebase/auth-types": "^0.10.1", "@microsoft/api-extractor": "^7.11.2", "@types/bcrypt": "^2.0.0", diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 0380debeb8..56c06c1423 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -2117,10 +2117,6 @@ function emulatorHost(): string | undefined { /** * When true the SDK should communicate with the Auth Emulator for all API * calls and also produce unsigned tokens. - * - * This alone does NOT short-circuit ID Token verification. - * For security reasons that must be explicitly disabled through - * setJwtVerificationEnabled(false); */ export function useEmulator(): boolean { return !!emulatorHost(); diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 8e35df82c4..56f9ac64b4 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -29,7 +29,7 @@ import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { auth } from './index'; import { - FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier, ALGORITHM_RS256 + FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier } from './token-verifier'; import { SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, @@ -115,15 +115,16 @@ export class BaseAuth implements BaseAuthI * verification. */ public verifyIdToken(idToken: string, checkRevoked = false): Promise { - return this.idTokenVerifier.verifyJWT(idToken) + const isEmulator = useEmulator(); + return this.idTokenVerifier.verifyJWT(idToken, isEmulator) .then((decodedIdToken: DecodedIdToken) => { // Whether to check if the token was revoked. - if (!checkRevoked) { - return decodedIdToken; + if (checkRevoked || isEmulator) { + return this.verifyDecodedJWTNotRevoked( + decodedIdToken, + AuthClientErrorCode.ID_TOKEN_REVOKED); } - return this.verifyDecodedJWTNotRevoked( - decodedIdToken, - AuthClientErrorCode.ID_TOKEN_REVOKED); + return decodedIdToken; }); } @@ -443,15 +444,16 @@ export class BaseAuth implements BaseAuthI */ public verifySessionCookie( sessionCookie: string, checkRevoked = false): Promise { - return this.sessionCookieVerifier.verifyJWT(sessionCookie) + const isEmulator = useEmulator(); + return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator) .then((decodedIdToken: DecodedIdToken) => { // Whether to check if the token was revoked. - if (!checkRevoked) { - return decodedIdToken; + if (checkRevoked || isEmulator) { + return this.verifyDecodedJWTNotRevoked( + decodedIdToken, + AuthClientErrorCode.SESSION_COOKIE_REVOKED); } - return this.verifyDecodedJWTNotRevoked( - decodedIdToken, - AuthClientErrorCode.SESSION_COOKIE_REVOKED); + return decodedIdToken; }); } @@ -675,28 +677,6 @@ export class BaseAuth implements BaseAuthI return decodedIdToken; }); } - - /** - * Enable or disable ID token verification. This is used to safely short-circuit token verification with the - * Auth emulator. When disabled ONLY unsigned tokens will pass verification, production tokens will not pass. - * - * WARNING: This is a dangerous method that will compromise your app's security and break your app in - * production. Developers should never call this method, it is for internal testing use only. - * - * @internal - */ - // @ts-expect-error: this method appears unused but is used privately. - private setJwtVerificationEnabled(enabled: boolean): void { - if (!enabled && !useEmulator()) { - // We only allow verification to be disabled in conjunction with - // the emulator environment variable. - throw new Error('This method is only available when connected to the Authentication emulator.'); - } - - const algorithm = enabled ? ALGORITHM_RS256 : 'none'; - this.idTokenVerifier.setAlgorithm(algorithm); - this.sessionCookieVerifier.setAlgorithm(algorithm); - } } diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index c39aa84fc7..cbb9991f4c 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -79,7 +79,7 @@ export class FirebaseTokenVerifier { constructor(private clientCertUrl: string, private algorithm: jwt.Algorithm, private issuer: string, private tokenInfo: FirebaseTokenInfo, private readonly app: FirebaseApp) { - + if (!validator.isURL(clientCertUrl)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -135,10 +135,11 @@ export class FirebaseTokenVerifier { * Verifies the format and signature of a Firebase Auth JWT token. * * @param {string} jwtToken The Firebase Auth JWT token to verify. + * @param {boolean=} isEmulator Whether to accept Auth Emulator tokens. * @return {Promise} A promise fulfilled with the decoded claims of the Firebase Auth ID * token. */ - public verifyJWT(jwtToken: string): Promise { + public verifyJWT(jwtToken: string, isEmulator = false): Promise { if (!validator.isString(jwtToken)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -148,19 +149,15 @@ export class FirebaseTokenVerifier { return util.findProjectId(this.app) .then((projectId) => { - return this.verifyJWTWithProjectId(jwtToken, projectId); + return this.verifyJWTWithProjectId(jwtToken, projectId, isEmulator); }); } - /** - * Override the JWT signing algorithm. - * @param algorithm the new signing algorithm. - */ - public setAlgorithm(algorithm: jwt.Algorithm): void { - this.algorithm = algorithm; - } - - private verifyJWTWithProjectId(jwtToken: string, projectId: string | null): Promise { + private verifyJWTWithProjectId( + jwtToken: string, + projectId: string | null, + isEmulator: boolean + ): Promise { if (!validator.isNonEmptyString(projectId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CREDENTIAL, @@ -185,7 +182,7 @@ export class FirebaseTokenVerifier { if (!fullDecodedToken) { errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed the entire string JWT ` + `which represents ${this.shortNameArticle} ${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; - } else if (typeof header.kid === 'undefined' && this.algorithm !== 'none') { + } else if (!isEmulator && typeof header.kid === 'undefined') { const isCustomToken = (payload.aud === FIREBASE_AUDIENCE); const isLegacyCustomToken = (header.alg === 'HS256' && payload.v === 0 && 'd' in payload && 'uid' in payload.d); @@ -200,7 +197,7 @@ export class FirebaseTokenVerifier { } errorMessage += verifyJwtTokenDocsMessage; - } else if (header.alg !== this.algorithm) { + } else if (!isEmulator && header.alg !== this.algorithm) { errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + '" but got ' + '"' + header.alg + '".' + verifyJwtTokenDocsMessage; } else if (payload.aud !== projectId) { @@ -209,7 +206,7 @@ export class FirebaseTokenVerifier { verifyJwtTokenDocsMessage; } else if (payload.iss !== this.issuer + projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` + - `"${this.issuer}"` + projectId + '" but got "' + + `"${this.issuer}` + projectId + '" but got "' + payload.iss + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage; } else if (typeof payload.sub !== 'string') { errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage; @@ -223,9 +220,8 @@ export class FirebaseTokenVerifier { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); } - // When the algorithm is set to 'none' there will be no signature and therefore we don't check - // the public keys. - if (this.algorithm === 'none') { + if (isEmulator) { + // Signature checks skipped for emulator; no need to fetch public keys. return this.verifyJwtSignatureWithKey(jwtToken, null); } @@ -257,26 +253,29 @@ export class FirebaseTokenVerifier { const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; return new Promise((resolve, reject) => { - jwt.verify(jwtToken, publicKey || '', { - algorithms: [this.algorithm], - }, (error: jwt.VerifyErrors | null, decodedToken: object | undefined) => { - if (error) { - if (error.name === 'TokenExpiredError') { - const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + - ` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` + - verifyJwtTokenDocsMessage; - return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage)); - } else if (error.name === 'JsonWebTokenError') { - const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; - return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); + const verifyOptions: jwt.VerifyOptions = {}; + if (publicKey !== null) { + verifyOptions.algorithms = [this.algorithm]; + } + jwt.verify(jwtToken, publicKey || '', verifyOptions, + (error: jwt.VerifyErrors | null, decodedToken: object | undefined) => { + if (error) { + if (error.name === 'TokenExpiredError') { + const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + + ` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` + + verifyJwtTokenDocsMessage; + return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage)); + } else if (error.name === 'JsonWebTokenError') { + const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; + return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); + } + return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message)); + } else { + const decodedIdToken = (decodedToken as DecodedIdToken); + decodedIdToken.uid = decodedIdToken.sub; + resolve(decodedIdToken); } - return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message)); - } else { - const decodedIdToken = (decodedToken as DecodedIdToken); - decodedIdToken.uid = decodedIdToken.sub; - resolve(decodedIdToken); - } - }); + }); }); } diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 41e278f821..a7ac8c4636 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -37,6 +37,8 @@ chai.use(chaiAsPromised); const expect = chai.expect; +const authEmulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST; + const newUserUid = generateRandomString(20); const nonexistentUid = generateRandomString(20); const newMultiFactorUserUid = generateRandomString(20); @@ -102,6 +104,9 @@ describe('admin.auth', () => { apiKey, authDomain: projectId + '.firebaseapp.com', }); + if (authEmulatorHost) { + (clientAuth() as any).useEmulator('http://' + authEmulatorHost); + } return cleanup(); }); @@ -137,7 +142,10 @@ describe('admin.auth', () => { }); }); - it('createUser() creates a new user with enrolled second factors', () => { + it('createUser() creates a new user with enrolled second factors', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } const enrolledFactors = [ { phoneNumber: '+16505550001', @@ -353,7 +361,7 @@ describe('admin.auth', () => { const metadata = userRecord!.metadata; expect(metadata.lastRefreshTime).to.exist; - expect(isUTCString(metadata.lastRefreshTime!)); + expect(isUTCString(metadata.lastRefreshTime!)).to.be.true; const creationTime = new Date(metadata.creationTime).getTime(); const lastRefreshTime = new Date(metadata.lastRefreshTime!).getTime(); expect(creationTime).lte(lastRefreshTime); @@ -410,7 +418,7 @@ describe('admin.auth', () => { }); }); - it('revokeRefreshTokens() invalidates existing sessions and ID tokens', () => { + it('revokeRefreshTokens() invalidates existing sessions and ID tokens', async () => { let currentIdToken: string; let currentUser: User; // Sign in with an email and password account. @@ -433,9 +441,14 @@ describe('admin.auth', () => { ), 1000)); }) .then(() => { - // verifyIdToken without checking revocation should still succeed. - return admin.auth().verifyIdToken(currentIdToken) - .should.eventually.be.fulfilled; + const verifyingIdToken = admin.auth().verifyIdToken(currentIdToken) + if (authEmulatorHost) { + // Check revocation is forced in emulator-mode and this should throw. + return verifyingIdToken.should.eventually.be.rejected; + } else { + // verifyIdToken without checking revocation should still succeed. + return verifyingIdToken.should.eventually.be.fulfilled; + } }) .then(() => { // verifyIdToken while checking for revocation should fail. @@ -522,6 +535,27 @@ describe('admin.auth', () => { it('updateUser() updates the user record with the given parameters', () => { const updatedDisplayName = 'Updated User ' + newUserUid; + return admin.auth().updateUser(newUserUid, { + email: updatedEmail, + phoneNumber: updatedPhone, + emailVerified: true, + displayName: updatedDisplayName, + }) + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.true; + expect(userRecord.displayName).to.equal(updatedDisplayName); + // Confirm expected email. + expect(userRecord.email).to.equal(updatedEmail); + // Confirm expected phone number. + expect(userRecord.phoneNumber).to.equal(updatedPhone); + }); + }); + + it('updateUser() creates, updates, and removes second factors', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + const now = new Date(1476235905000).toUTCString(); // Update user with enrolled second factors. const enrolledFactors = [ @@ -541,21 +575,11 @@ describe('admin.auth', () => { }, ]; return admin.auth().updateUser(newUserUid, { - email: updatedEmail, - phoneNumber: updatedPhone, - emailVerified: true, - displayName: updatedDisplayName, multiFactor: { enrolledFactors, }, }) .then((userRecord) => { - expect(userRecord.emailVerified).to.be.true; - expect(userRecord.displayName).to.equal(updatedDisplayName); - // Confirm expected email. - expect(userRecord.email).to.equal(updatedEmail); - // Confirm expected phone number. - expect(userRecord.phoneNumber).to.equal(updatedPhone); // Confirm second factors added to user. const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); expect(actualUserRecord.multiFactor.enrolledFactors.length).to.equal(2); @@ -655,6 +679,49 @@ describe('admin.auth', () => { .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); + if (authEmulatorHost) { + describe('Auth emulator support', () => { + const uid = 'authEmulatorUser'; + before(() => { + return admin.auth().createUser({ + uid, + email: 'lastRefreshTimeUser@example.com', + password: 'p4ssword', + }); + }); + after(() => { + return admin.auth().deleteUser(uid); + }); + + it('verifyIdToken() succeeds when called with an unsigned token', () => { + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + audience: projectId, + issuer: 'https://securetoken.google.com/' + projectId, + subject: uid, + }); + return admin.auth().verifyIdToken(unsignedToken); + }); + + it('verifyIdToken() fails when called with a token with wrong project', () => { + const unsignedToken = mocks.generateIdToken({ algorithm: 'none', audience: 'nosuch' }); + return admin.auth().verifyIdToken(unsignedToken) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('verifyIdToken() fails when called with a token that does not belong to a user', () => { + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + audience: projectId, + issuer: 'https://securetoken.google.com/' + projectId, + subject: 'nosuch', + }); + return admin.auth().verifyIdToken(unsignedToken) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); + }); + } + describe('Link operations', () => { const uid = generateRandomString(20).toLowerCase(); const email = uid + '@example.com'; @@ -1253,7 +1320,10 @@ describe('admin.auth', () => { }; // Clean up temp configurations used for test. - before(() => { + before(function () { + if (authEmulatorHost) { + return this.skip(); // Not implemented. + } return removeTempConfigs().then(() => admin.auth().createProviderConfig(authProviderConfig1)); }); @@ -1385,7 +1455,10 @@ describe('admin.auth', () => { }; // Clean up temp configurations used for test. - before(() => { + before(function () { + if (authEmulatorHost) { + return this.skip(); // Not implemented. + } return removeTempConfigs().then(() => admin.auth().createProviderConfig(authProviderConfig1)); }); @@ -1484,7 +1557,6 @@ describe('admin.auth', () => { it('deleteUser() deletes the user with the given UID', () => { return Promise.all([ admin.auth().deleteUser(newUserUid), - admin.auth().deleteUser(newMultiFactorUserUid), admin.auth().deleteUser(uidFromCreateUserWithoutUid), ]).should.eventually.be.fulfilled; }); @@ -1610,8 +1682,14 @@ describe('admin.auth', () => { ), 1000)); }) .then(() => { - return admin.auth().verifySessionCookie(currentSessionCookie) - .should.eventually.be.fulfilled; + const verifyingSessionCookie = admin.auth().verifySessionCookie(currentSessionCookie); + if (authEmulatorHost) { + // Check revocation is forced in emulator-mode and this should throw. + return verifyingSessionCookie.should.eventually.be.rejected; + } else { + // verifyIdToken without checking revocation should still succeed. + return verifyingSessionCookie.should.eventually.be.fulfilled; + } }) .then(() => { return admin.auth().verifySessionCookie(currentSessionCookie, true) @@ -1824,7 +1902,10 @@ describe('admin.auth', () => { ]; fixtures.forEach((fixture) => { - it(`successfully imports users with ${fixture.name} to Firebase Auth.`, () => { + it(`successfully imports users with ${fixture.name} to Firebase Auth.`, function () { + if (authEmulatorHost) { + return this.skip(); // Auth Emulator does not support real hashes. + } importUserRecord = { uid: randomUid, email: randomUid + '@example.com', @@ -1893,10 +1974,13 @@ describe('admin.auth', () => { expect(JSON.stringify(actualUserRecord[key])) .to.be.equal(JSON.stringify((importUserRecord as any)[key])); } - }).should.eventually.be.fulfilled; + }); }); - it('successfully imports users with enrolled second factors', () => { + it('successfully imports users with enrolled second factors', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet implemented. + } const uid = generateRandomString(20).toLowerCase(); const email = uid + '@example.com'; const now = new Date(1476235905000).toUTCString(); @@ -1958,25 +2042,41 @@ describe('admin.auth', () => { it('fails when invalid users are provided', () => { const users = [ - { uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1error' }, { uid: generateRandomString(20).toLowerCase(), email: 'invalid' }, - { uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1invalid' }, { uid: generateRandomString(20).toLowerCase(), emailVerified: 'invalid' } as any, ]; return admin.auth().importUsers(users) .then((result) => { expect(result.successCount).to.equal(0); - expect(result.failureCount).to.equal(4); - expect(result.errors.length).to.equal(4); + expect(result.failureCount).to.equal(2); + expect(result.errors.length).to.equal(2); + expect(result.errors[0].index).to.equal(0); + expect(result.errors[0].error.code).to.equals('auth/invalid-email'); + expect(result.errors[1].index).to.equal(1); + expect(result.errors[1].error.code).to.equals('auth/invalid-email-verified'); + }); + }); + + it('fails when users with invalid phone numbers are provided', function () { + if (authEmulatorHost) { + // Auth Emulator's phoneNumber validation is also lax and won't throw. + return this.skip(); + } + const users = [ + // These phoneNumbers passes local (lax) validator but fails remotely. + { uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1error' }, + { uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1invalid' }, + ]; + return admin.auth().importUsers(users) + .then((result) => { + expect(result.successCount).to.equal(0); + expect(result.failureCount).to.equal(2); + expect(result.errors.length).to.equal(2); expect(result.errors[0].index).to.equal(0); expect(result.errors[0].error.code).to.equals('auth/invalid-user-import'); expect(result.errors[1].index).to.equal(1); - expect(result.errors[1].error.code).to.equals('auth/invalid-email'); - expect(result.errors[2].index).to.equal(2); - expect(result.errors[2].error.code).to.equals('auth/invalid-user-import'); - expect(result.errors[3].index).to.equal(3); - expect(result.errors[3].error.code).to.equals('auth/invalid-email-verified'); - }).should.eventually.be.fulfilled; + expect(result.errors[1].error.code).to.equals('auth/invalid-user-import'); + }); }); }); }); @@ -2132,12 +2232,11 @@ function safeDelete(uid: string): Promise { * @param {string[]} uids The list of user identifiers to delete. * @return {Promise} A promise that resolves when delete operation resolves. */ -function deleteUsersWithDelay(uids: string[]): Promise { - return new Promise((resolve) => { - setTimeout(resolve, 1000); - }).then(() => { - return admin.auth().deleteUsers(uids); - }); +async function deleteUsersWithDelay(uids: string[]): Promise { + if (!authEmulatorHost) { + await new Promise((resolve) => { setTimeout(resolve, 1000); }); + } + return admin.auth().deleteUsers(uids); } /** diff --git a/test/integration/setup.ts b/test/integration/setup.ts index c70362fa89..5880a6cd0d 100644 --- a/test/integration/setup.ts +++ b/test/integration/setup.ts @@ -36,51 +36,72 @@ export let noServiceAccountApp: admin.app.App; export let cmdArgs: any; +export const isEmulator = !!process.env.FIREBASE_EMULATOR_HUB; + before(() => { - /* tslint:disable:no-console */ - let serviceAccount: any; - try { - serviceAccount = require('../resources/key.json'); - } catch (error) { - console.log(chalk.red( - 'The integration test suite requires a service account JSON file for a ' + - 'Firebase project to be saved to `test/resources/key.json`.', - error, - )); - throw error; - } + let getCredential: () => {credential?: admin.credential.Credential}; + let serviceAccountId: string; - try { - apiKey = fs.readFileSync(path.join(__dirname, '../resources/apikey.txt')).toString().trim(); - } catch (error) { - console.log(chalk.red( - 'The integration test suite requires an API key for a ' + - 'Firebase project to be saved to `test/resources/apikey.txt`.', - error, + /* tslint:disable:no-console */ + if (isEmulator) { + console.log(chalk.yellow( + 'Running integration tests against Emulator Suite. ' + + 'Some tests may be skipped due to lack of emulator support.', )); - throw error; + getCredential = () => ({}); + projectId = process.env.GCLOUD_PROJECT!; + apiKey = 'fake-api-key'; + serviceAccountId = 'fake-client-email@example.com'; + } else { + let serviceAccount: any; + try { + serviceAccount = require('../resources/key.json'); + } catch (error) { + console.log(chalk.red( + 'The integration test suite requires a service account JSON file for a ' + + 'Firebase project to be saved to `test/resources/key.json`.', + error, + )); + throw error; + } + + try { + apiKey = fs.readFileSync(path.join(__dirname, '../resources/apikey.txt')).toString().trim(); + } catch (error) { + console.log(chalk.red( + 'The integration test suite requires an API key for a ' + + 'Firebase project to be saved to `test/resources/apikey.txt`.', + error, + )); + throw error; + } + getCredential = () => ({ credential: admin.credential.cert(serviceAccount) }); + projectId = serviceAccount.project_id; + serviceAccountId = serviceAccount.client_email; } /* tslint:enable:no-console */ - projectId = serviceAccount.project_id; databaseUrl = 'https://' + projectId + '.firebaseio.com'; storageBucket = projectId + '.appspot.com'; defaultApp = admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), + ...getCredential(), + projectId, databaseURL: databaseUrl, storageBucket, }); nullApp = admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), + ...getCredential(), + projectId, databaseURL: databaseUrl, databaseAuthVariableOverride: null, storageBucket, }, 'null'); nonNullApp = admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), + ...getCredential(), + projectId, databaseURL: databaseUrl, databaseAuthVariableOverride: { uid: generateRandomString(20), @@ -88,9 +109,14 @@ before(() => { storageBucket, }, 'nonNull'); + const noServiceAccountAppCreds = getCredential(); + if (noServiceAccountAppCreds.credential) { + noServiceAccountAppCreds.credential = new CertificatelessCredential( + noServiceAccountAppCreds.credential) + } noServiceAccountApp = admin.initializeApp({ - credential: new CertificatelessCredential(admin.credential.cert(serviceAccount)), - serviceAccountId: serviceAccount.client_email, + ...noServiceAccountAppCreds, + serviceAccountId, projectId, }, 'noServiceAccount'); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 26521b30cd..6991cc7617 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -3193,14 +3193,22 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('auth emulator support', () => { let mockAuth = testConfig.init(mocks.app()); + const userRecord = getValidUserRecord(getValidGetAccountInfoResponse()); + const validSince = new Date(userRecord.tokensValidAfterTime!); + + const stubs: sinon.SinonStub[] = []; + let clock: sinon.SinonFakeTimers; beforeEach(() => { - process.env.FIREBASE_AUTH_EMULATOR_HOST = 'localhost:9099'; + process.env.FIREBASE_AUTH_EMULATOR_HOST = '127.0.0.1:9099'; mockAuth = testConfig.init(mocks.app()); + clock = sinon.useFakeTimers(validSince.getTime()); }); afterEach(() => { + _.forEach(stubs, (s) => s.restore()); delete process.env.FIREBASE_AUTH_EMULATOR_HOST; + clock.restore(); }); it('createCustomToken() generates an unsigned token', async () => { @@ -3215,39 +3223,78 @@ AUTH_CONFIGS.forEach((testConfig) => { jwt.verify(token, '', { algorithms: ['none'] }); }); - it('verifyIdToken() rejects an unsigned token when only the env var is set', async () => { + it('verifyIdToken() should reject revoked ID tokens', () => { + const uid = userRecord.uid; + // One second before validSince. + const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(userRecord); + stubs.push(getUserStub); + const unsignedToken = mocks.generateIdToken({ - algorithm: 'none' + algorithm: 'none', + subject: uid, + }, { + iat: oneSecBeforeValidSince, + auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase }); - await expect(mockAuth.verifyIdToken(unsignedToken)) - .to.be.rejectedWith('Firebase ID token has incorrect algorithm. Expected "RS256"'); + // verifyIdToken should force checking revocation in emulator mode, + // even if checkRevoked=false. + return mockAuth.verifyIdToken(unsignedToken, false) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/id-token-revoked'); + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + }); }); - it('verifyIdToken() accepts an unsigned token when private method is called and env var is set', async () => { - (mockAuth as any).setJwtVerificationEnabled(false); + it('verifySessionCookie() should reject revoked session cookies', () => { + const uid = userRecord.uid; + // One second before validSince. + const oneSecBeforeValidSince = Math.floor(validSince.getTime() / 1000 - 1); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(userRecord); + stubs.push(getUserStub); + + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + subject: uid, + issuer: 'https://session.firebase.google.com/' + mocks.projectId, + }, { + iat: oneSecBeforeValidSince, + auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase + }); - let claims = {}; - if (testConfig.Auth === TenantAwareAuth) { - claims = { - firebase: { - tenant: TENANT_ID - } - } - } + // verifySessionCookie should force checking revocation in emulator + // mode, even if checkRevoked=false. + return mockAuth.verifySessionCookie(unsignedToken, false) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/session-cookie-revoked'); + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + }); + }); + it('verifyIdToken() rejects an unsigned token if auth emulator is unreachable', async () => { const unsignedToken = mocks.generateIdToken({ algorithm: 'none' - }, claims); + }); - const decoded = await mockAuth.verifyIdToken(unsignedToken); - expect(decoded).to.be.ok; - }); + const errorMessage = 'Error while making request: connect ECONNREFUSED 127.0.0.1. Error code: ECONNREFUSED'; + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser').rejects(new Error(errorMessage)); + stubs.push(getUserStub); - it('private method throws when env var is unset', async () => { - delete process.env.FIREBASE_AUTH_EMULATOR_HOST; - await expect(() => (mockAuth as any).setJwtVerificationEnabled(false)) - .to.throw('This method is only available when connected to the Authentication emulator.') + // Since revocation check is forced on in emulator mode, this will call + // the getUser method and get rejected (instead of succeed locally). + await expect(mockAuth.verifyIdToken(unsignedToken)) + .to.be.rejectedWith(errorMessage); }); }); }); diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 6b370a8bb4..d863a0e849 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -106,7 +106,7 @@ function mockFailedFetchPublicKeys(): nock.Scope { } function createTokenVerifier( - app: FirebaseApp, + app: FirebaseApp, options: { algorithm?: Algorithm } = {} ): verifier.FirebaseTokenVerifier { const algorithm = options.algorithm || 'RS256'; @@ -544,16 +544,17 @@ describe('FirebaseTokenVerifier', () => { }); }); - it('should decode an unsigned token when the algorithm is set to none (emulator)', async () => { + it('should decode an unsigned token if isEmulator=true', async () => { clock = sinon.useFakeTimers(1000); - const emulatorVerifier = createTokenVerifier(app, { algorithm: 'none' }); + const emulatorVerifier = createTokenVerifier(app); const mockIdToken = mocks.generateIdToken({ algorithm: 'none', header: {} }); - const decoded = await emulatorVerifier.verifyJWT(mockIdToken); + const isEmulator = true; + const decoded = await emulatorVerifier.verifyJWT(mockIdToken, isEmulator); expect(decoded).to.deep.equal({ one: 'uno', two: 'dos', From 01d8177650676a142474229538f0b469fc363a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Borntr=C3=A4ger?= Date: Thu, 4 Feb 2021 20:04:19 +0100 Subject: [PATCH 076/160] feat: Exporting all types of Messages so they can be used by consumers (#1147) * feat: Exporting all types of Messages so they can be used by consumers Fixes https://github.com/firebase/firebase-admin-node/issues/1146 * feat(exportMessageTypes): Testing TokenMessage * feat(exportMessageTypes): Added tests for all Message types * feat(exportMessageTypes): Fixed build * feat(exportMessageTypes): Better unit tests * feat(exportMessageTypes): Deleted unneeded separate TS test * feat(exportMessageTypes): Fixed build * feat(exportMessageTypes): Fixed linting --- src/messaging/index.ts | 6 ++--- test/unit/messaging/messaging.spec.ts | 33 +++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/messaging/index.ts b/src/messaging/index.ts index 020ee7c875..6c074cea7c 100644 --- a/src/messaging/index.ts +++ b/src/messaging/index.ts @@ -58,15 +58,15 @@ export namespace messaging { fcmOptions?: FcmOptions; } - interface TokenMessage extends BaseMessage { + export interface TokenMessage extends BaseMessage { token: string; } - interface TopicMessage extends BaseMessage { + export interface TopicMessage extends BaseMessage { topic: string; } - interface ConditionMessage extends BaseMessage { + export interface ConditionMessage extends BaseMessage { condition: string; } diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index 82f0973f67..236c18213a 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -41,6 +41,9 @@ chai.use(chaiAsPromised); const expect = chai.expect; import Message = messaging.Message; +import TokenMessage = messaging.TokenMessage; +import TopicMessage = messaging.TopicMessage; +import ConditionMessage = messaging.ConditionMessage; import MessagingOptions = messaging.MessagingOptions; import MessagingPayload = messaging.MessagingPayload; import MessagingDevicesResponse = messaging.MessagingDevicesResponse; @@ -823,6 +826,32 @@ describe('Messaging', () => { [validMessage], ).should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); + + // This test was added to also verify https://github.com/firebase/firebase-admin-node/issues/1146 + it('should be fulfilled when called with different message types', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + const tokenMessage: TokenMessage = { token: 'test' }; + const topicMessage: TopicMessage = { topic: 'test' }; + const conditionMessage: ConditionMessage = { condition: 'test' }; + const messages: Message[] = [tokenMessage, topicMessage, conditionMessage]; + + mockedRequests.push(mockBatchRequest(messageIds)); + + return messaging.sendAll(messages) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(3); + expect(response.failureCount).to.equal(0); + response.responses.forEach((resp, idx) => { + expect(resp.success).to.be.true; + expect(resp.messageId).to.equal(messageIds[idx]); + expect(resp.error).to.be.undefined; + }); + }); + }); }); describe('sendMulticast()', () => { @@ -887,7 +916,7 @@ describe('Messaging', () => { expect(messages.length).to.equal(3); expect(stub!.args[0][1]).to.be.undefined; messages.forEach((message, idx) => { - expect((message as any).token).to.equal(tokens[idx]); + expect((message as TokenMessage).token).to.equal(tokens[idx]); expect(message.android).to.be.undefined; expect(message.apns).to.be.undefined; expect(message.data).to.be.undefined; @@ -917,7 +946,7 @@ describe('Messaging', () => { expect(messages.length).to.equal(3); expect(stub!.args[0][1]).to.be.undefined; messages.forEach((message, idx) => { - expect((message as any).token).to.equal(tokens[idx]); + expect((message as TokenMessage).token).to.equal(tokens[idx]); expect(message.android).to.deep.equal(multicast.android); expect(message.apns).to.be.deep.equal(multicast.apns); expect(message.data).to.be.deep.equal(multicast.data); From fc2f557223ac1f44bebc039cffabab7de500063c Mon Sep 17 00:00:00 2001 From: rsgowman Date: Mon, 8 Feb 2021 15:53:05 -0500 Subject: [PATCH 077/160] feat(auth): Implement getUserByProviderId (#769) RELEASE NOTE: Added a new getUserByProviderId() to lookup user accounts by their providers. --- etc/firebase-admin.api.md | 1 + src/auth/auth-api-request.ts | 15 ++++ src/auth/auth.ts | 30 +++++++ src/auth/index.ts | 15 ++++ test/integration/auth.spec.ts | 55 ++++++++++++ test/unit/auth/auth-api-request.spec.ts | 6 ++ test/unit/auth/auth.spec.ts | 114 ++++++++++++++++++++++++ 7 files changed, 236 insertions(+) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index b5b810a5e6..3941798ebd 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -106,6 +106,7 @@ export namespace auth { getUser(uid: string): Promise; getUserByEmail(email: string): Promise; getUserByPhoneNumber(phoneNumber: string): Promise; + getUserByProviderUid(providerId: string, uid: string): Promise; getUsers(identifiers: UserIdentifier[]): Promise; importUsers(users: UserImportRecord[], options?: UserImportOptions): Promise; listProviderConfigs(options: AuthProviderConfigFilter): Promise; diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 56c06c1423..33be9da092 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -1079,6 +1079,21 @@ export abstract class AbstractAuthRequestHandler { return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } + public getAccountInfoByFederatedUid(providerId: string, rawId: string): Promise { + if (!validator.isNonEmptyString(providerId) || !validator.isNonEmptyString(rawId)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + } + + const request = { + federatedUserId: [{ + providerId, + rawId, + }], + }; + + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request); + } + /** * Looks up multiple users by their identifiers (uid, email, etc). * diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 56f9ac64b4..76a7cf336c 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -173,6 +173,36 @@ export class BaseAuth implements BaseAuthI }); } + /** + * Gets the user data for the user corresponding to a given provider id. + * + * See [Retrieve user data](/docs/auth/admin/manage-users#retrieve_user_data) + * for code samples and detailed documentation. + * + * @param providerId The provider ID, for example, "google.com" for the + * Google provider. + * @param uid The user identifier for the given provider. + * + * @return A promise fulfilled with the user data corresponding to the + * given provider id. + */ + public getUserByProviderUid(providerId: string, uid: string): Promise { + // Although we don't really advertise it, we want to also handle + // non-federated idps with this call. So if we detect one of them, we'll + // reroute this request appropriately. + if (providerId === 'phone') { + return this.getUserByPhoneNumber(uid); + } else if (providerId === 'email') { + return this.getUserByEmail(uid); + } + + return this.authRequestHandler.getAccountInfoByFederatedUid(providerId, uid) + .then((response: any) => { + // Returns the user record populated with server response. + return new UserRecord(response.users[0]); + }); + } + /** * Gets the user data corresponding to the specified identifiers. * diff --git a/src/auth/index.ts b/src/auth/index.ts index 1ba9b56af5..a1ff346b97 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1516,6 +1516,21 @@ export namespace auth { */ getUserByPhoneNumber(phoneNumber: string): Promise; + /** + * Gets the user data for the user corresponding to a given provider ID. + * + * See [Retrieve user data](/docs/auth/admin/manage-users#retrieve_user_data) + * for code samples and detailed documentation. + * + * @param providerId The provider ID, for example, "google.com" for the + * Google provider. + * @param uid The user identifier for the given provider. + * + * @return A promise fulfilled with the user data corresponding to the + * given provider id. + */ + getUserByProviderUid(providerId: string, uid: string): Promise; + /** * Gets the user data corresponding to the specified identifiers. * diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index a7ac8c4636..1eb7bfe6a8 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -221,6 +221,56 @@ describe('admin.auth', () => { }); }); + it('getUserByProviderUid() returns a user record with the matching provider id', async () => { + // TODO(rsgowman): Once we can link a provider id with a user, just do that + // here instead of creating a new user. + const randomUid = 'import_' + generateRandomString(20).toLowerCase(); + const importUser: admin.auth.UserImportRecord = { + uid: randomUid, + email: 'user@example.com', + phoneNumber: '+15555550000', + emailVerified: true, + disabled: false, + metadata: { + lastSignInTime: 'Thu, 01 Jan 1970 00:00:00 UTC', + creationTime: 'Thu, 01 Jan 1970 00:00:00 UTC', + }, + providerData: [{ + displayName: 'User Name', + email: 'user@example.com', + phoneNumber: '+15555550000', + photoURL: 'http://example.com/user', + providerId: 'google.com', + uid: 'google_uid', + }], + }; + + await admin.auth().importUsers([importUser]); + + try { + await admin.auth().getUserByProviderUid('google.com', 'google_uid') + .then((userRecord) => { + expect(userRecord.uid).to.equal(importUser.uid); + }); + } finally { + await safeDelete(importUser.uid); + } + }); + + it('getUserByProviderUid() redirects to getUserByEmail if given an email', () => { + return admin.auth().getUserByProviderUid('email', mockUserData.email) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + + it('getUserByProviderUid() redirects to getUserByPhoneNumber if given a phone number', () => { + return admin.auth().getUserByProviderUid('phone', mockUserData.phoneNumber) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + describe('getUsers()', () => { /** * Filters a list of object to another list of objects that only contains @@ -623,6 +673,11 @@ describe('admin.auth', () => { .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); }); + it('getUserByProviderUid() fails when called with a non-existing provider id', () => { + return admin.auth().getUserByProviderUid('google.com', nonexistentUid) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); + it('updateUser() fails when called with a non-existing UID', () => { return admin.auth().updateUser(nonexistentUid, { emailVerified: true, diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index ff154d3b2c..2df01889dd 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -360,6 +360,12 @@ describe('FIREBASE_AUTH_GET_ACCOUNT_INFO', () => { return requestValidator(validRequest); }).not.to.throw(); }); + it('should succeed with federatedUserId passed', () => { + const validRequest = { federatedUserId: { providerId: 'google.com', rawId: 'google_uid_1234' } }; + expect(() => { + return requestValidator(validRequest); + }).not.to.throw(); + }); it('should fail when neither localId, email or phoneNumber are passed', () => { const invalidRequest = { bla: ['1234'] }; expect(() => { diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 6991cc7617..4d134e2949 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -1151,6 +1151,120 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); + describe('getUserByProviderUid()', () => { + const providerId = 'google.com'; + const providerUid = 'google_uid'; + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); + const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + beforeEach(() => sinon.spy(validator, 'isEmail')); + afterEach(() => { + (validator.isEmail as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no provider id', () => { + expect(() => (auth as any).getUserByProviderUid()) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/invalid-provider-id'); + }); + + it('should be rejected given an invalid provider id', () => { + expect(() => auth.getUserByProviderUid('', 'uid')) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/invalid-provider-id'); + }); + + it('should be rejected given an invalid provider uid', () => { + expect(() => auth.getUserByProviderUid('id', '')) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/invalid-provider-id'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.getUserByProviderUid(providerId, providerUid) + .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.getUserByProviderUid(providerId, providerUid) + .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.getUserByProviderUid(providerId, providerUid) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a UserRecord on success', () => { + // Stub getAccountInfoByEmail to return expected result. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByFederatedUid') + .resolves(expectedGetAccountInfoResult); + stubs.push(stub); + return auth.getUserByProviderUid(providerId, providerUid) + .then((userRecord) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId, providerUid); + // Confirm expected user record response returned. + expect(userRecord).to.deep.equal(expectedUserRecord); + }); + }); + + describe('non-federated providers', () => { + let invokeRequestHandlerStub: sinon.SinonStub; + beforeEach(() => { + invokeRequestHandlerStub = sinon.stub(testConfig.RequestHandler.prototype, 'invokeRequestHandler') + .resolves({ + // nothing here is checked; we just need enough to not crash. + users: [{ + localId: 1, + }], + }); + + }); + afterEach(() => { + invokeRequestHandlerStub.restore(); + }); + + it('phone lookups should use phoneNumber field', async () => { + await auth.getUserByProviderUid('phone', '+15555550001'); + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + phoneNumber: ['+15555550001'], + }); + }); + + it('email lookups should use email field', async () => { + await auth.getUserByProviderUid('email', 'user@example.com'); + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + email: ['user@example.com'], + }); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getAccountInfoByFederatedUid to throw a backend error. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByFederatedUid') + .rejects(expectedError); + stubs.push(stub); + return auth.getUserByProviderUid(providerId, providerUid) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId, providerUid); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + describe('getUsers()', () => { let stubs: sinon.SinonStub[] = []; From a00ce05ee967ae6070643435e65d64b4f41e8840 Mon Sep 17 00:00:00 2001 From: rsgowman Date: Mon, 8 Feb 2021 17:02:30 -0500 Subject: [PATCH 078/160] Allow enabling of anonymous provider via tenant configuration (#802) RELEASE NOTES: Allow enabling of anonymous provider via tenant configuration. --- etc/firebase-admin.api.md | 2 ++ src/auth/index.ts | 10 ++++++++ src/auth/tenant.ts | 9 +++++++ test/integration/auth.spec.ts | 36 +++++++++++++++++++++++++++ test/unit/auth/tenant-manager.spec.ts | 3 +++ test/unit/auth/tenant.spec.ts | 2 ++ 6 files changed, 62 insertions(+) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index 3941798ebd..38981491e2 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -251,6 +251,7 @@ export namespace auth { expiresIn: number; } export interface Tenant { + anonymousSignInEnabled: boolean; displayName?: string; emailSignInConfig?: { enabled: boolean; @@ -301,6 +302,7 @@ export namespace auth { photoURL?: string | null; } export interface UpdateTenantRequest { + anonymousSignInEnabled?: boolean; displayName?: string; emailSignInConfig?: EmailSignInProviderConfig; multiFactorConfig?: MultiFactorConfig; diff --git a/src/auth/index.ts b/src/auth/index.ts index a1ff346b97..f3a387393d 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1014,6 +1014,11 @@ export namespace auth { passwordRequired?: boolean; }; + /** + * Whether the anonymous provider is enabled. + */ + anonymousSignInEnabled: boolean; + /** * The multi-factor auth configuration on the current tenant. */ @@ -1089,6 +1094,11 @@ export namespace auth { */ emailSignInConfig?: EmailSignInProviderConfig; + /** + * Whether the anonymous provider is enabled. + */ + anonymousSignInEnabled?: boolean; + /** * The multi-factor auth configuration to update on the tenant. */ diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index a7b1d188f4..392494739b 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -29,6 +29,7 @@ import UpdateTenantRequest = auth.UpdateTenantRequest; /** The corresponding server side representation of a TenantOptions object. */ export interface TenantOptionsServerRequest extends EmailSignInConfigServerRequest { displayName?: string; + enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; } @@ -39,6 +40,7 @@ export interface TenantServerResponse { displayName?: string; allowPasswordSignup?: boolean; enableEmailLinkSignin?: boolean; + enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; } @@ -50,6 +52,7 @@ export class Tenant implements TenantInterface { public readonly tenantId: string; public readonly displayName?: string; public readonly emailSignInConfig?: EmailSignInConfig; + public readonly anonymousSignInEnabled: boolean; public readonly multiFactorConfig?: MultiFactorAuthConfig; public readonly testPhoneNumbers?: {[phoneNumber: string]: string}; @@ -70,6 +73,9 @@ export class Tenant implements TenantInterface { if (typeof tenantOptions.displayName !== 'undefined') { request.displayName = tenantOptions.displayName; } + if (typeof tenantOptions.anonymousSignInEnabled !== 'undefined') { + request.enableAnonymousUser = tenantOptions.anonymousSignInEnabled; + } if (typeof tenantOptions.multiFactorConfig !== 'undefined') { request.mfaConfig = MultiFactorAuthConfig.buildServerRequest(tenantOptions.multiFactorConfig); } @@ -105,6 +111,7 @@ export class Tenant implements TenantInterface { const validKeys = { displayName: true, emailSignInConfig: true, + anonymousSignInEnabled: true, multiFactorConfig: true, testPhoneNumbers: true, }; @@ -179,6 +186,7 @@ export class Tenant implements TenantInterface { allowPasswordSignup: false, }); } + this.anonymousSignInEnabled = !!response.enableAnonymousUser; if (typeof response.mfaConfig !== 'undefined') { this.multiFactorConfig = new MultiFactorAuthConfig(response.mfaConfig); } @@ -193,6 +201,7 @@ export class Tenant implements TenantInterface { tenantId: this.tenantId, displayName: this.displayName, emailSignInConfig: this.emailSignInConfig?.toJSON(), + anonymousSignInEnabled: this.anonymousSignInEnabled, multiFactorConfig: this.multiFactorConfig?.toJSON(), testPhoneNumbers: this.testPhoneNumbers, }; diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 1eb7bfe6a8..76eee8c85e 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -886,6 +886,7 @@ describe('admin.auth', () => { enabled: true, passwordRequired: true, }, + anonymousSignInEnabled: false, multiFactorConfig: { state: 'ENABLED', factorIds: ['phone'], @@ -901,6 +902,7 @@ describe('admin.auth', () => { enabled: false, passwordRequired: true, }, + anonymousSignInEnabled: false, multiFactorConfig: { state: 'DISABLED', factorIds: [], @@ -915,6 +917,7 @@ describe('admin.auth', () => { enabled: true, passwordRequired: false, }, + anonymousSignInEnabled: false, multiFactorConfig: { state: 'ENABLED', factorIds: ['phone'], @@ -957,6 +960,20 @@ describe('admin.auth', () => { }); }); + it('createTenant() can enable anonymous users', async () => { + const tenant = await admin.auth().tenantManager().createTenant({ + displayName: 'testTenantWithAnon', + emailSignInConfig: { + enabled: false, + passwordRequired: true, + }, + anonymousSignInEnabled: true, + }); + createdTenants.push(tenant.tenantId); + + expect(tenant.anonymousSignInEnabled).to.be.true; + }); + // Sanity check user management + email link generation + custom attribute APIs. // TODO: Confirm behavior in client SDK when it starts supporting it. describe('supports user management, email link generation, custom attribute and token revocation APIs', () => { @@ -1300,6 +1317,25 @@ describe('admin.auth', () => { }); }); + it('updateTenant() should be able to enable/disable anon provider', async () => { + const tenantManager = admin.auth().tenantManager(); + let tenant = await tenantManager.createTenant({ + displayName: 'testTenantUpdateAnon', + }); + createdTenants.push(tenant.tenantId); + expect(tenant.anonymousSignInEnabled).to.be.false; + + tenant = await tenantManager.updateTenant(tenant.tenantId, { + anonymousSignInEnabled: true, + }); + expect(tenant.anonymousSignInEnabled).to.be.true; + + tenant = await tenantManager.updateTenant(tenant.tenantId, { + anonymousSignInEnabled: false, + }); + expect(tenant.anonymousSignInEnabled).to.be.false; + }); + it('listTenants() should resolve with expected number of tenants', () => { const allTenantIds: string[] = []; const tenantOptions2 = deepCopy(tenantOptions); diff --git a/test/unit/auth/tenant-manager.spec.ts b/test/unit/auth/tenant-manager.spec.ts index 9c7ef80be1..52bf955ccd 100644 --- a/test/unit/auth/tenant-manager.spec.ts +++ b/test/unit/auth/tenant-manager.spec.ts @@ -52,6 +52,7 @@ describe('TenantManager', () => { displayName: 'TENANT-DISPLAY-NAME', allowPasswordSignup: true, enableEmailLinkSignin: false, + enableAnonymousUser: true, }; before(() => { @@ -388,6 +389,7 @@ describe('TenantManager', () => { enabled: true, passwordRequired: true, }, + anonymousSignInEnabled: true, }; const expectedTenant = new Tenant(GET_TENANT_RESPONSE); const expectedError = new FirebaseAuthError( @@ -480,6 +482,7 @@ describe('TenantManager', () => { enabled: true, passwordRequired: true, }, + anonymousSignInEnabled: true, }; const expectedTenant = new Tenant(GET_TENANT_RESPONSE); const expectedError = new FirebaseAuthError( diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index db090b3279..b2ebe6a5d1 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -351,6 +351,7 @@ describe('Tenant', () => { enabled: true, passwordRequired: false, }, + anonymousSignInEnabled: false, multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), }); @@ -368,6 +369,7 @@ describe('Tenant', () => { enabled: true, passwordRequired: false, }, + anonymousSignInEnabled: false, }); }); }); From bea66a90b12408313c3f1b54b67e19cc32c25421 Mon Sep 17 00:00:00 2001 From: rsgowman Date: Tue, 9 Feb 2021 18:07:39 -0500 Subject: [PATCH 079/160] feat(auth): Add ability to link a federated ID with the `updateUser()` method. (#770) --- etc/firebase-admin.api.md | 11 ++ src/auth/auth-api-request.ts | 57 ++++++++- src/auth/auth.ts | 45 +++++++ src/auth/index.ts | 56 ++++++++ test/integration/auth.spec.ts | 234 +++++++++++++++++++++++---------- test/unit/auth/auth.spec.ts | 235 ++++++++++++++++++++++++++++++---- 6 files changed, 543 insertions(+), 95 deletions(-) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index 38981491e2..f90136685f 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -300,6 +300,8 @@ export namespace auth { password?: string; phoneNumber?: string | null; photoURL?: string | null; + providersToUnlink?: string[]; + providerToLink?: UserProvider; } export interface UpdateTenantRequest { anonymousSignInEnabled?: boolean; @@ -365,6 +367,14 @@ export namespace auth { creationTime?: string; lastSignInTime?: string; } + export interface UserProvider { + displayName?: string; + email?: string; + phoneNumber?: string; + photoURL?: string; + providerId?: string; + uid?: string; + } export interface UserProviderRequest { displayName?: string; email?: string; @@ -393,6 +403,7 @@ export namespace auth { tokensValidAfterTime?: string; uid: string; } + {}; } // @public (undocumented) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 33be9da092..d3ecf73fc7 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -403,6 +403,8 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat phoneNumber: true, customAttributes: true, validSince: true, + // Pass linkProviderUserInfo only for updates (i.e. not for uploads.) + linkProviderUserInfo: !uploadAccountRequest, // Pass tenantId only for uploadAccount requests. tenantId: uploadAccountRequest, passwordHash: uploadAccountRequest, @@ -551,6 +553,12 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat validateProviderUserInfo(providerUserInfoEntry); }); } + + // linkProviderUserInfo must be a (single) UserProvider value. + if (typeof request.linkProviderUserInfo !== 'undefined') { + validateProviderUserInfo(request.linkProviderUserInfo); + } + // mfaInfo is used for importUsers. // mfa.enrollments is used for setAccountInfo. // enrollments has to be an array of valid AuthFactorInfo requests. @@ -1306,6 +1314,33 @@ export abstract class AbstractAuthRequestHandler { 'Properties argument must be a non-null object.', ), ); + } else if (validator.isNonNullObject(properties.providerToLink)) { + // TODO(rsgowman): These checks overlap somewhat with + // validateProviderUserInfo. It may be possible to refactor a bit. + if (!validator.isNonEmptyString(properties.providerToLink.providerId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'providerToLink.providerId of properties argument must be a non-empty string.'); + } + if (!validator.isNonEmptyString(properties.providerToLink.uid)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'providerToLink.uid of properties argument must be a non-empty string.'); + } + } else if (typeof properties.providersToUnlink !== 'undefined') { + if (!validator.isArray(properties.providersToUnlink)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'providersToUnlink of properties argument must be an array of strings.'); + } + + properties.providersToUnlink.forEach((providerId) => { + if (!validator.isNonEmptyString(providerId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'providersToUnlink of properties argument must be an array of strings.'); + } + }); } // Build the setAccountInfo request. @@ -1340,13 +1375,25 @@ export abstract class AbstractAuthRequestHandler { // It will be removed from the backend request and an additional parameter // deleteProvider: ['phone'] with an array of providerIds (phone in this case), // will be passed. - // Currently this applies to phone provider only. if (request.phoneNumber === null) { - request.deleteProvider = ['phone']; + request.deleteProvider ? request.deleteProvider.push('phone') : request.deleteProvider = ['phone']; delete request.phoneNumber; - } else { - // Doesn't apply to other providers in admin SDK. - delete request.deleteProvider; + } + + if (typeof(request.providerToLink) !== 'undefined') { + request.linkProviderUserInfo = deepCopy(request.providerToLink); + delete request.providerToLink; + + request.linkProviderUserInfo.rawId = request.linkProviderUserInfo.uid; + delete request.linkProviderUserInfo.uid; + } + + if (typeof(request.providersToUnlink) !== 'undefined') { + if (!validator.isArray(request.deleteProvider)) { + request.deleteProvider = []; + } + request.deleteProvider = request.deleteProvider.concat(request.providersToUnlink); + delete request.providersToUnlink; } // Rewrite photoURL to photoUrl. diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 76a7cf336c..aa5d7b11ef 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { deepCopy } from '../utils/deep-copy'; import { UserRecord } from './user-record'; import { isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, @@ -381,6 +382,50 @@ export class BaseAuth implements BaseAuthI * @return {Promise} A promise that resolves with the modified user record. */ public updateUser(uid: string, properties: UpdateRequest): Promise { + // Although we don't really advertise it, we want to also handle linking of + // non-federated idps with this call. So if we detect one of them, we'll + // adjust the properties parameter appropriately. This *does* imply that a + // conflict could arise, e.g. if the user provides a phoneNumber property, + // but also provides a providerToLink with a 'phone' provider id. In that + // case, we'll throw an error. + properties = deepCopy(properties); + + if (properties?.providerToLink) { + if (properties.providerToLink.providerId === 'email') { + if (typeof properties.email !== 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "Both UpdateRequest.email and UpdateRequest.providerToLink.providerId='email' were set. To " + + 'link to the email/password provider, only specify the UpdateRequest.email field.'); + } + properties.email = properties.providerToLink.uid; + delete properties.providerToLink; + } else if (properties.providerToLink.providerId === 'phone') { + if (typeof properties.phoneNumber !== 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "Both UpdateRequest.phoneNumber and UpdateRequest.providerToLink.providerId='phone' were set. To " + + 'link to a phone provider, only specify the UpdateRequest.phoneNumber field.'); + } + properties.phoneNumber = properties.providerToLink.uid; + delete properties.providerToLink; + } + } + if (properties?.providersToUnlink) { + if (properties.providersToUnlink.indexOf('phone') !== -1) { + // If we've been told to unlink the phone provider both via setting + // phoneNumber to null *and* by setting providersToUnlink to include + // 'phone', then we'll reject that. Though it might also be reasonable + // to relax this restriction and just unlink it. + if (properties.phoneNumber === null) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "Both UpdateRequest.phoneNumber=null and UpdateRequest.providersToUnlink=['phone'] were set. To " + + 'unlink from a phone provider, only specify the UpdateRequest.phoneNumber=null field.'); + } + } + } + return this.authRequestHandler.updateExistingAccount(uid, properties) .then((existingUid) => { // Return the corresponding user record. diff --git a/src/auth/index.ts b/src/auth/index.ts index f3a387393d..7817435c14 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -153,6 +153,42 @@ export namespace auth { phoneNumber: string; } + /** + * Represents a user identity provider that can be associated with a Firebase user. + */ + interface UserProvider { + + /** + * The user identifier for the linked provider. + */ + uid?: string; + + /** + * The display name for the linked provider. + */ + displayName?: string; + + /** + * The email for the linked provider. + */ + email?: string; + + /** + * The phone number for the linked provider. + */ + phoneNumber?: string; + + /** + * The photo URL for the linked provider. + */ + photoURL?: string; + + /** + * The linked provider ID (for example, "google.com" for the Google provider). + */ + providerId?: string; + } + /** * Interface representing a user. */ @@ -384,6 +420,26 @@ export namespace auth { * The user's updated multi-factor related properties. */ multiFactor?: MultiFactorUpdateSettings; + + /** + * Links this user to the specified provider. + * + * Linking a provider to an existing user account does not invalidate the + * refresh token of that account. In other words, the existing account + * would continue to be able to access resources, despite not having used + * the newly linked provider to log in. If you wish to force the user to + * authenticate with this new provider, you need to (a) revoke their + * refresh token (see + * https://firebase.google.com/docs/auth/admin/manage-sessions#revoke_refresh_tokens), + * and (b) ensure no other authentication methods are present on this + * account. + */ + providerToLink?: UserProvider; + + /** + * Unlinks this user from the specified providers. + */ + providersToUnlink?: string[]; } /** diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 76eee8c85e..bf9f7f134d 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -583,79 +583,177 @@ describe('admin.auth', () => { }); }); - it('updateUser() updates the user record with the given parameters', () => { - const updatedDisplayName = 'Updated User ' + newUserUid; - return admin.auth().updateUser(newUserUid, { - email: updatedEmail, - phoneNumber: updatedPhone, - emailVerified: true, - displayName: updatedDisplayName, - }) - .then((userRecord) => { - expect(userRecord.emailVerified).to.be.true; - expect(userRecord.displayName).to.equal(updatedDisplayName); - // Confirm expected email. - expect(userRecord.email).to.equal(updatedEmail); - // Confirm expected phone number. - expect(userRecord.phoneNumber).to.equal(updatedPhone); + describe('updateUser()', () => { + /** + * Creates a new user for testing purposes. The user's uid will be + * '$name_$tenRandomChars' and email will be + * '$name_$tenRandomChars@example.com'. + */ + // TODO(rsgowman): This function could usefully be employed throughout this file. + function createTestUser(name: string): Promise { + const tenRandomChars = generateRandomString(10); + return admin.auth().createUser({ + uid: name + '_' + tenRandomChars, + displayName: name, + email: name + '_' + tenRandomChars + '@example.com', }); - }); - - it('updateUser() creates, updates, and removes second factors', function () { - if (authEmulatorHost) { - return this.skip(); // Not yet supported in Auth Emulator. } - const now = new Date(1476235905000).toUTCString(); - // Update user with enrolled second factors. - const enrolledFactors = [ - { - uid: 'mfaUid1', - phoneNumber: '+16505550001', - displayName: 'Work phone number', - factorId: 'phone', - enrollmentTime: now, - }, - { - uid: 'mfaUid2', - phoneNumber: '+16505550002', - displayName: 'Personal phone number', - factorId: 'phone', - enrollmentTime: now, - }, - ]; - return admin.auth().updateUser(newUserUid, { - multiFactor: { - enrolledFactors, - }, - }) - .then((userRecord) => { - // Confirm second factors added to user. - const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); - expect(actualUserRecord.multiFactor.enrolledFactors.length).to.equal(2); - expect(actualUserRecord.multiFactor.enrolledFactors).to.deep.equal(enrolledFactors); - // Update list of second factors. - return admin.auth().updateUser(newUserUid, { - multiFactor: { - enrolledFactors: [enrolledFactors[0]], - }, - }); + let updateUser: admin.auth.UserRecord; + before(async () => { + updateUser = await createTestUser('UpdateUser'); + }); + + after(() => { + return safeDelete(updateUser.uid); + }); + + it('updates the user record with the given parameters', () => { + const updatedDisplayName = 'Updated User ' + updateUser.uid; + return admin.auth().updateUser(updateUser.uid, { + email: updatedEmail, + phoneNumber: updatedPhone, + emailVerified: true, + displayName: updatedDisplayName, }) - .then((userRecord) => { - expect(userRecord.multiFactor!.enrolledFactors.length).to.equal(1); - const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); - expect(actualUserRecord.multiFactor.enrolledFactors[0]).to.deep.equal(enrolledFactors[0]); - // Remove all second factors. - return admin.auth().updateUser(newUserUid, { - multiFactor: { - enrolledFactors: null, - }, + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.true; + expect(userRecord.displayName).to.equal(updatedDisplayName); + // Confirm expected email. + expect(userRecord.email).to.equal(updatedEmail); + // Confirm expected phone number. + expect(userRecord.phoneNumber).to.equal(updatedPhone); }); + }); + + it('creates, updates, and removes second factors', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + + const now = new Date(1476235905000).toUTCString(); + // Update user with enrolled second factors. + const enrolledFactors = [ + { + uid: 'mfaUid1', + phoneNumber: '+16505550001', + displayName: 'Work phone number', + factorId: 'phone', + enrollmentTime: now, + }, + { + uid: 'mfaUid2', + phoneNumber: '+16505550002', + displayName: 'Personal phone number', + factorId: 'phone', + enrollmentTime: now, + }, + ]; + return admin.auth().updateUser(updateUser.uid, { + multiFactor: { + enrolledFactors, + }, }) - .then((userRecord) => { - // Confirm all second factors removed. - expect(userRecord.multiFactor).to.be.undefined; + .then((userRecord) => { + // Confirm second factors added to user. + const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); + expect(actualUserRecord.multiFactor.enrolledFactors.length).to.equal(2); + expect(actualUserRecord.multiFactor.enrolledFactors).to.deep.equal(enrolledFactors); + // Update list of second factors. + return admin.auth().updateUser(updateUser.uid, { + multiFactor: { + enrolledFactors: [enrolledFactors[0]], + }, + }); + }) + .then((userRecord) => { + expect(userRecord.multiFactor!.enrolledFactors.length).to.equal(1); + const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); + expect(actualUserRecord.multiFactor.enrolledFactors[0]).to.deep.equal(enrolledFactors[0]); + // Remove all second factors. + return admin.auth().updateUser(updateUser.uid, { + multiFactor: { + enrolledFactors: null, + }, + }); + }) + .then((userRecord) => { + // Confirm all second factors removed. + expect(userRecord.multiFactor).to.be.undefined; + }); + }); + + it('can link/unlink with a federated provider', async () => { + const googleFederatedUid = 'google_uid_' + generateRandomString(10); + let userRecord = await admin.auth().updateUser(updateUser.uid, { + providerToLink: { + providerId: 'google.com', + uid: googleFederatedUid, + }, + }); + + let providerUids = userRecord.providerData.map((userInfo) => userInfo.uid); + let providerIds = userRecord.providerData.map((userInfo) => userInfo.providerId); + expect(providerUids).to.deep.include(googleFederatedUid); + expect(providerIds).to.deep.include('google.com'); + + userRecord = await admin.auth().updateUser(updateUser.uid, { + providersToUnlink: ['google.com'], + }); + + providerUids = userRecord.providerData.map((userInfo) => userInfo.uid); + providerIds = userRecord.providerData.map((userInfo) => userInfo.providerId); + expect(providerUids).to.not.deep.include(googleFederatedUid); + expect(providerIds).to.not.deep.include('google.com'); + }); + + it('can unlink multiple providers at once, incl a non-federated provider', async () => { + await deletePhoneNumberUser('+15555550001'); + + const googleFederatedUid = 'google_uid_' + generateRandomString(10); + const facebookFederatedUid = 'facebook_uid_' + generateRandomString(10); + + let userRecord = await admin.auth().updateUser(updateUser.uid, { + phoneNumber: '+15555550001', + providerToLink: { + providerId: 'google.com', + uid: googleFederatedUid, + }, + }); + userRecord = await admin.auth().updateUser(updateUser.uid, { + providerToLink: { + providerId: 'facebook.com', + uid: facebookFederatedUid, + }, + }); + + let providerUids = userRecord.providerData.map((userInfo) => userInfo.uid); + let providerIds = userRecord.providerData.map((userInfo) => userInfo.providerId); + expect(providerUids).to.deep.include.members([googleFederatedUid, facebookFederatedUid, '+15555550001']); + expect(providerIds).to.deep.include.members(['google.com', 'facebook.com', 'phone']); + + userRecord = await admin.auth().updateUser(updateUser.uid, { + providersToUnlink: ['google.com', 'facebook.com', 'phone'], }); + + providerUids = userRecord.providerData.map((userInfo) => userInfo.uid); + providerIds = userRecord.providerData.map((userInfo) => userInfo.providerId); + expect(providerUids).to.not.deep.include.members([googleFederatedUid, facebookFederatedUid, '+15555550001']); + expect(providerIds).to.not.deep.include.members(['google.com', 'facebook.com', 'phone']); + }); + + it('noops successfully when given an empty providersToUnlink list', async () => { + const userRecord = await createTestUser('NoopWithEmptyProvidersToDeleteUser'); + try { + const updatedUserRecord = await admin.auth().updateUser(userRecord.uid, { + providersToUnlink: [], + }); + + expect(updatedUserRecord).to.deep.equal(userRecord); + } finally { + safeDelete(userRecord.uid); + } + }); }); it('getUser() fails when called with a non-existing UID', () => { @@ -2208,8 +2306,8 @@ function testImportAndSignInUser( /** * Helper function that deletes the user with the specified phone number * if it exists. - * @param {string} phoneNumber The phone number of the user to delete. - * @return {Promise} A promise that resolves when the user is deleted + * @param phoneNumber The phone number of the user to delete. + * @return A promise that resolves when the user is deleted * or is found not to exist. */ function deletePhoneNumberUser(phoneNumber: string): Promise { diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 4d134e2949..71957bc4b4 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -241,6 +241,8 @@ function getSAMLConfigServerResponse(providerId: string): SAMLConfigServerRespon } +const INVALID_PROVIDER_IDS = [ + undefined, null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; const TENANT_ID = 'tenantId'; const AUTH_CONFIGS: AuthTest[] = [ { @@ -1628,6 +1630,10 @@ AUTH_CONFIGS.forEach((testConfig) => { emailVerified: expectedUserRecord.emailVerified, password: 'password', phoneNumber: expectedUserRecord.phoneNumber, + providerToLink: { + providerId: 'google.com', + uid: 'google_uid', + }, }; // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; @@ -1671,8 +1677,193 @@ AUTH_CONFIGS.forEach((testConfig) => { }) .catch((error) => { expect(error).to.have.property('code', 'auth/argument-error'); - expect(validator.isNonNullObject).to.have.been.calledOnce.and.calledWith(null); + expect(validator.isNonNullObject).to.have.been.calledWith(null); + }); + }); + + const invalidUpdateRequests: UpdateRequest[] = [ + { providerToLink: { uid: 'google_uid' } }, + { providerToLink: { providerId: 'google.com' } }, + { providerToLink: { providerId: 'google.com', uid: '' } }, + { providerToLink: { providerId: '', uid: 'google_uid' } }, + ]; + invalidUpdateRequests.forEach((invalidUpdateRequest) => { + it('should be rejected given an UpdateRequest with an invalid providerToLink parameter', () => { + expect(() => { + auth.updateUser(uid, invalidUpdateRequest); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + }); + + it('should rename providerToLink property to linkProviderUserInfo', async () => { + const invokeRequestHandlerStub = sinon.stub(testConfig.RequestHandler.prototype, 'invokeRequestHandler') + .resolves({ + localId: uid, + }); + + // Stub getAccountInfoByUid to return a valid result (unchecked; we + // just need it to be valid so as to not crash.) + const getUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .resolves(expectedGetAccountInfoResult); + + stubs.push(invokeRequestHandlerStub); + stubs.push(getUserStub); + + await auth.updateUser(uid, { + providerToLink: { + providerId: 'google.com', + uid: 'google_uid', + }, + }); + + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + localId: uid, + linkProviderUserInfo: { + providerId: 'google.com', + rawId: 'google_uid', + }, + }); + }); + + INVALID_PROVIDER_IDS.forEach((invalidProviderId) => { + it('should be rejected given a deleteProvider list with an invalid provider ID ' + + JSON.stringify(invalidProviderId), () => { + expect(() => { + auth.updateUser(uid, { + providersToUnlink: [ invalidProviderId as any ], + }); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + }); + + it('should merge deletion of phone provider with the providersToUnlink list', async () => { + const invokeRequestHandlerStub = sinon.stub(testConfig.RequestHandler.prototype, 'invokeRequestHandler') + .resolves({ + localId: uid, + }); + + // Stub getAccountInfoByUid to return a valid result (unchecked; we + // just need it to be valid so as to not crash.) + const getUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .resolves(expectedGetAccountInfoResult); + + stubs.push(invokeRequestHandlerStub); + stubs.push(getUserStub); + + await auth.updateUser(uid, { + phoneNumber: null, + providersToUnlink: [ 'google.com' ], + }); + + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + localId: uid, + deleteProvider: [ 'phone', 'google.com' ], + }); + }); + + describe('non-federated providers', () => { + let invokeRequestHandlerStub: sinon.SinonStub; + let getAccountInfoByUidStub: sinon.SinonStub; + beforeEach(() => { + invokeRequestHandlerStub = sinon.stub(testConfig.RequestHandler.prototype, 'invokeRequestHandler') + .resolves({ + // nothing here is checked; we just need enough to not crash. + users: [{ + localId: 1, + }], + }); + + getAccountInfoByUidStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .resolves({ + // nothing here is checked; we just need enough to not crash. + users: [{ + localId: 1, + }], + }); + }); + afterEach(() => { + invokeRequestHandlerStub.restore(); + getAccountInfoByUidStub.restore(); + }); + + it('specifying both email and providerId=email should be rejected', () => { + expect(() => { + auth.updateUser(uid, { + email: 'user@example.com', + providerToLink: { + providerId: 'email', + uid: 'user@example.com', + }, + }); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + it('specifying both phoneNumber and providerId=phone should be rejected', () => { + expect(() => { + auth.updateUser(uid, { + phoneNumber: '+15555550001', + providerToLink: { + providerId: 'phone', + uid: '+15555550001', + }, + }); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + it('email linking should use email field', async () => { + await auth.updateUser(uid, { + providerToLink: { + providerId: 'email', + uid: 'user@example.com', + }, + }); + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + localId: uid, + email: 'user@example.com', + }); + }); + + it('phone linking should use phoneNumber field', async () => { + await auth.updateUser(uid, { + providerToLink: { + providerId: 'phone', + uid: '+15555550001', + }, }); + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + localId: uid, + phoneNumber: '+15555550001', + }); + }); + + it('specifying both phoneNumber=null and providersToUnlink=phone should be rejected', () => { + expect(() => { + auth.updateUser(uid, { + phoneNumber: null, + providersToUnlink: ['phone'], + }); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + it('doesnt mutate the properties parameter', async () => { + const properties: UpdateRequest = { + providerToLink: { + providerId: 'email', + uid: 'user@example.com', + }, + }; + await auth.updateUser(uid, properties); + expect(properties).to.deep.equal({ + providerToLink: { + providerId: 'email', + uid: 'user@example.com', + }, + }); + }); }); it('should be rejected given an app which returns null access tokens', () => { @@ -2487,9 +2678,7 @@ AUTH_CONFIGS.forEach((testConfig) => { .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-provider-id'); }); - const invalidProviderIds = [ - undefined, null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; - invalidProviderIds.forEach((invalidProviderId) => { + INVALID_PROVIDER_IDS.forEach((invalidProviderId) => { it(`should be rejected given an invalid provider ID "${JSON.stringify(invalidProviderId)}"`, () => { return (auth as Auth).getProviderConfig(invalidProviderId as any) .then(() => { @@ -2860,15 +3049,16 @@ AUTH_CONFIGS.forEach((testConfig) => { .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-provider-id'); }); - it('should be rejected given an invalid provider ID', () => { - const invalidProviderId = ''; - return (auth as Auth).deleteProviderConfig(invalidProviderId) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-provider-id'); - }); + INVALID_PROVIDER_IDS.forEach((invalidProviderId) => { + it(`should be rejected given an invalid provider ID "${JSON.stringify(invalidProviderId)}"`, () => { + return (auth as Auth).deleteProviderConfig(invalidProviderId as any) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-provider-id'); + }); + }); }); it('should be rejected given an app which returns null access tokens', () => { @@ -2979,15 +3169,16 @@ AUTH_CONFIGS.forEach((testConfig) => { .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-provider-id'); }); - it('should be rejected given an invalid provider ID', () => { - const invalidProviderId = ''; - return (auth as Auth).updateProviderConfig(invalidProviderId, oidcConfigOptions) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-provider-id'); - }); + INVALID_PROVIDER_IDS.forEach((invalidProviderId) => { + it(`should be rejected given an invalid provider ID "${JSON.stringify(invalidProviderId)}"`, () => { + return (auth as Auth).updateProviderConfig(invalidProviderId as any, oidcConfigOptions) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-provider-id'); + }); + }); }); it('should be rejected given no options', () => { From 5c60cc4a9f7bb669fe26032c9badeeb7dac4436e Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 10 Feb 2021 15:08:18 -0500 Subject: [PATCH 080/160] (chore): Export UserProvider type and add it to toc.yaml (#1165) - Export UserProvider type - Add UserProvider to toc.yaml --- docgen/content-sources/node/toc.yaml | 2 ++ etc/firebase-admin.api.md | 1 - src/auth/index.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index a563b7417a..487d3fcc39 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -94,6 +94,8 @@ toc: path: /docs/reference/admin/node/admin.auth.UserProviderRequest - title: "UserRecord" path: /docs/reference/admin/node/admin.auth.UserRecord + - title: "UserProvider" + path: /docs/reference/admin/node/admin.auth.UserProvider - title: "SessionCookieOptions" path: /docs/reference/admin/node/admin.auth.SessionCookieOptions - title: "BaseAuth" diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index f90136685f..eaf280aec8 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -403,7 +403,6 @@ export namespace auth { tokensValidAfterTime?: string; uid: string; } - {}; } // @public (undocumented) diff --git a/src/auth/index.ts b/src/auth/index.ts index 7817435c14..b1bc47f725 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -156,7 +156,7 @@ export namespace auth { /** * Represents a user identity provider that can be associated with a Firebase user. */ - interface UserProvider { + export interface UserProvider { /** * The user identifier for the linked provider. From 95857754bdaf52d401837567ac6692702d1bd355 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 10 Feb 2021 17:41:58 -0500 Subject: [PATCH 081/160] [chore] Release 9.5.0 (#1167) Release 9.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee75e5e196..aa1c323a75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.4.2", + "version": "9.5.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 93362d5721fc45f3fc541543813d5739eda8ba2d Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 11 Feb 2021 11:12:30 -0800 Subject: [PATCH 082/160] chore: Updated doc generator for typedoc 0.19.0 (#1166) --- docgen/generate-docs.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js index 4f8b8df736..12f93706b7 100644 --- a/docgen/generate-docs.js +++ b/docgen/generate-docs.js @@ -46,7 +46,11 @@ const contentPath = path.resolve(`${__dirname}/content-sources/node`); const tempHomePath = path.resolve(`${contentPath}/HOME_TEMP.md`); const devsitePath = `/docs/reference/admin/node/`; -const firestoreExcludes = ['v1', 'v1beta1', 'setLogFunction','DocumentData']; +const firestoreExcludes = [ + 'v1', 'v1beta1', 'setLogFunction','DocumentData', + 'BulkWriterOptions', 'DocumentChangeType', 'FirestoreDataConverter', + 'GrpcStatus', 'Precondition', 'ReadOptions', 'UpdateData', 'Settings', +]; const firestoreHtmlPath = `${docPath}/admin.firestore.html`; const firestoreHeader = `

    Type aliases

    @@ -278,6 +282,18 @@ function updateHtml(htmlPath, contentBlock) { const dom = new jsdom.JSDOM(fs.readFileSync(htmlPath)); const contentNode = dom.window.document.body.querySelector('.col-12'); + // Recent versions of Typedoc generates an additional index section and a variables + // section for namespaces with re-exports. We iterate through these nodes and remove + // them from the output. + const sections = []; + contentNode.childNodes.forEach((child) => { + if (child.nodeName === 'SECTION') { + sections.push(child); + } + }); + contentNode.removeChild(sections[1]); + contentNode.removeChild(sections[2]); + const newSection = new jsdom.JSDOM(contentBlock); contentNode.appendChild(newSection.window.document.body.firstChild); fs.writeFileSync(htmlPath, dom.window.document.documentElement.outerHTML); From 6bcffa2fdca8ee9b9fb49a0109ddc72f4f6d4c2e Mon Sep 17 00:00:00 2001 From: egilmorez Date: Fri, 26 Feb 2021 13:53:01 -0700 Subject: [PATCH 083/160] Update HOME.md (#1181) Quick addition of a little bit of clarifying verbiage per an internal bug report. Thanks! --- docgen/content-sources/node/HOME.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docgen/content-sources/node/HOME.md b/docgen/content-sources/node/HOME.md index bf22c94da8..4e253f7733 100644 --- a/docgen/content-sources/node/HOME.md +++ b/docgen/content-sources/node/HOME.md @@ -1,6 +1,6 @@ # Firebase Admin Node.js SDK Reference -The Admin SDK lets you interact with Firebase from privileged environments. +The Admin SDK is a set of server libraries that lets you interact with Firebase from privileged environments. You can install it via our [npm package](https://www.npmjs.com/package/firebase-admin). To get started using the Firebase Admin Node.js SDK, see From 994fd43e36c0824deba176763010dc141e9bb0f4 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 10 Mar 2021 12:10:26 -0800 Subject: [PATCH 084/160] feat(rtdb): Support emulator mode for rules management operations (#1190) * feat(rtdb): Support emulator mode for rules management operations * fix: Adding namespace to emulated URL string * fix: Consolidated unit testing * fix: Removed extra whitespace --- src/database/database-internal.ts | 23 +++++- test/unit/database/database.spec.ts | 117 +++++++++++++++++++++++++--- 2 files changed, 125 insertions(+), 15 deletions(-) diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts index a469a41773..b77f536b97 100644 --- a/src/database/database-internal.ts +++ b/src/database/database-internal.ts @@ -63,7 +63,7 @@ export class DatabaseService { /** * Returns the app associated with this DatabaseService instance. * - * @return {FirebaseApp} The app associated with this DatabaseService instance. + * @return The app associated with this DatabaseService instance. */ get app(): FirebaseApp { return this.appInternal; @@ -123,7 +123,13 @@ class DatabaseRulesClient { private readonly httpClient: AuthorizedHttpClient; constructor(app: FirebaseApp, dbUrl: string) { - const parsedUrl = new URL(dbUrl); + let parsedUrl = new URL(dbUrl); + const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST; + if (emulatorHost) { + const namespace = extractNamespace(parsedUrl); + parsedUrl = new URL(`http://${emulatorHost}?ns=${namespace}`); + } + parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH); this.dbUrl = parsedUrl.toString(); this.httpClient = new AuthorizedHttpClient(app); @@ -133,7 +139,7 @@ class DatabaseRulesClient { * Gets the currently applied security rules as a string. The return value consists of * the rules source including comments. * - * @return {Promise} A promise fulfilled with the rules as a raw string. + * @return A promise fulfilled with the rules as a raw string. */ public getRules(): Promise { const req: HttpRequestConfig = { @@ -233,3 +239,14 @@ class DatabaseRulesClient { return `${intro}: ${err.response.text}`; } } + +function extractNamespace(parsedUrl: URL): string { + const ns = parsedUrl.searchParams.get('ns'); + if (ns) { + return ns; + } + + const hostname = parsedUrl.hostname; + const dotIndex = hostname.indexOf('.'); + return hostname.substring(0, dotIndex).toLowerCase(); +} diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 4a4f969b1e..3979719ab6 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -48,7 +48,7 @@ describe('Database', () => { describe('Constructor', () => { const invalidApps = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidApps.forEach((invalidApp) => { - it(`should throw given invalid app: ${ JSON.stringify(invalidApp) }`, () => { + it(`should throw given invalid app: ${JSON.stringify(invalidApp)}`, () => { expect(() => { const databaseAny: any = DatabaseService; return new databaseAny(invalidApp); @@ -154,11 +154,8 @@ describe('Database', () => { }`; const rulesPath = '.settings/rules.json'; - function callParamsForGet( - strict = false, - url = `https://databasename.firebaseio.com/${rulesPath}`, - ): HttpRequestConfig { - + function callParamsForGet(options?: { strict?: boolean; url?: string }): HttpRequestConfig { + const url = options?.url || `https://databasename.firebaseio.com/${rulesPath}`; const params: HttpRequestConfig = { method: 'GET', url, @@ -167,7 +164,7 @@ describe('Database', () => { }, }; - if (strict) { + if (options?.strict) { params.data = { format: 'strict' }; } @@ -215,7 +212,7 @@ describe('Database', () => { return db.getRules().then((result) => { expect(result).to.equal(rulesString); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(false, `https://custom.firebaseio.com/${rulesPath}`)); + callParamsForGet({ url: `https://custom.firebaseio.com/${rulesPath}` })); }); }); @@ -225,7 +222,7 @@ describe('Database', () => { return db.getRules().then((result) => { expect(result).to.equal(rulesString); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(false, `http://localhost:9000/${rulesPath}?ns=foo`)); + callParamsForGet({ url: `http://localhost:9000/${rulesPath}?ns=foo` })); }); }); @@ -259,7 +256,7 @@ describe('Database', () => { return db.getRulesJSON().then((result) => { expect(result).to.deep.equal(rules); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(true)); + callParamsForGet({ strict: true })); }); }); @@ -269,7 +266,7 @@ describe('Database', () => { return db.getRulesJSON().then((result) => { expect(result).to.deep.equal(rules); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(true, `https://custom.firebaseio.com/${rulesPath}`)); + callParamsForGet({ strict: true, url: `https://custom.firebaseio.com/${rulesPath}` })); }); }); @@ -279,7 +276,7 @@ describe('Database', () => { return db.getRulesJSON().then((result) => { expect(result).to.deep.equal(rules); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(true, `http://localhost:9000/${rulesPath}?ns=foo`)); + callParamsForGet({ strict: true, url: `http://localhost:9000/${rulesPath}?ns=foo` })); }); }); @@ -409,5 +406,101 @@ describe('Database', () => { return db.setRules(rules).should.eventually.be.rejectedWith('network error'); }); }); + + describe('emulator mode', () => { + interface EmulatorTestConfig { + name: string; + setUp: () => FirebaseApp; + tearDown?: () => void; + url: string; + } + + const configs: EmulatorTestConfig[] = [ + { + name: 'with environment variable', + setUp: () => { + process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090'; + return mocks.app(); + }, + tearDown: () => { + delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; + }, + url: `http://localhost:9090/${rulesPath}?ns=databasename`, + }, + { + name: 'with app options', + setUp: () => { + return mocks.appWithOptions({ + databaseURL: 'http://localhost:9091?ns=databasename', + }); + }, + url: `http://localhost:9091/${rulesPath}?ns=databasename`, + }, + { + name: 'with environment variable overriding app options', + setUp: () => { + process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090'; + return mocks.appWithOptions({ + databaseURL: 'http://localhost:9091?ns=databasename', + }); + }, + tearDown: () => { + delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; + }, + url: `http://localhost:9090/${rulesPath}?ns=databasename`, + }, + ]; + + configs.forEach((config) => { + describe(config.name, () => { + let emulatorApp: FirebaseApp; + let emulatorDatabase: DatabaseService; + + before(() => { + emulatorApp = config.setUp(); + emulatorDatabase = new DatabaseService(emulatorApp); + }); + + after(() => { + if (config.tearDown) { + config.tearDown(); + } + + return emulatorDatabase.delete().then(() => { + return emulatorApp.delete(); + }); + }); + + it('getRules should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRules().then((result) => { + expect(result).to.equal(rulesString); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ url: config.url })); + }); + }); + + it('getRulesJSON should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRulesJSON().then((result) => { + expect(result).to.equal(rules); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ strict: true, url: config.url })); + }); + }); + + it('setRules should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse({}); + return db.setRules(rulesString).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rulesString, config.url)); + }); + }); + }); + }); + }); }); }); From bf4bacb18dc2e500a54ae7aa93b2db334c6ad4db Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 18 Mar 2021 16:20:46 -0700 Subject: [PATCH 085/160] fix: Decoupled proactive token refresh from FirebaseApp (#1194) * fix: Decoupled proactive token refresh from FirebaseApp * fix: Defined constants for duration values * fix: Logging errors encountered while scheduling a refresh * fix: Renamed some variables for clarity --- src/database/database-internal.ts | 44 ++++++ src/firebase-app.ts | 210 +++++++++------------------- test/unit/database/database.spec.ts | 174 +++++++++++++++++++++++ test/unit/firebase-app.spec.ts | 201 -------------------------- 4 files changed, 283 insertions(+), 346 deletions(-) diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts index b77f536b97..dd28bfd919 100644 --- a/src/database/database-internal.ts +++ b/src/database/database-internal.ts @@ -28,9 +28,13 @@ import { getSdkVersion } from '../utils/index'; import Database = database.Database; +const TOKEN_REFRESH_THRESHOLD_MILLIS = 5 * 60 * 1000; + export class DatabaseService { private readonly appInternal: FirebaseApp; + private tokenListenerRegistered: boolean; + private tokenRefreshTimeout: NodeJS.Timeout; private databases: { [dbUrl: string]: Database; @@ -50,6 +54,12 @@ export class DatabaseService { * @internal */ public delete(): Promise { + if (this.tokenListenerRegistered) { + this.appInternal.INTERNAL.removeAuthTokenListener(this.onTokenChange); + clearTimeout(this.tokenRefreshTimeout); + this.tokenListenerRegistered = false; + } + const promises = []; for (const dbUrl of Object.keys(this.databases)) { const db: DatabaseImpl = ((this.databases[dbUrl] as any) as DatabaseImpl); @@ -96,9 +106,43 @@ export class DatabaseService { this.databases[dbUrl] = db; } + + if (!this.tokenListenerRegistered) { + this.tokenListenerRegistered = true; + this.appInternal.INTERNAL.addAuthTokenListener(this.onTokenChange); + } + return db; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private onTokenChange(_: string): void { + this.appInternal.INTERNAL.getToken() + .then((token) => { + const delayMillis = token.expirationTime - TOKEN_REFRESH_THRESHOLD_MILLIS - Date.now(); + // If the new token is set to expire soon (unlikely), do nothing. Somebody will eventually + // notice and refresh the token, at which point this callback will fire again. + if (delayMillis > 0) { + this.scheduleTokenRefresh(delayMillis); + } + }) + .catch((err) => { + console.error('Unexpected error while attempting to schedule a token refresh:', err); + }); + } + + private scheduleTokenRefresh(delayMillis: number): void { + clearTimeout(this.tokenRefreshTimeout); + this.tokenRefreshTimeout = setTimeout(() => { + this.appInternal.INTERNAL.getToken(/*forceRefresh=*/ true) + .catch(() => { + // Ignore the error since this might just be an intermittent failure. If we really cannot + // refresh the token, an error will be logged once the existing token expires and we try + // to fetch a fresh one. + }); + }, delayMillis); + } + private ensureUrl(url?: string): string { if (typeof url !== 'undefined') { return url; diff --git a/src/firebase-app.ts b/src/firebase-app.ts index fb8ad8b0f5..88947bec4c 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -16,7 +16,7 @@ */ import { AppOptions, app } from './firebase-namespace-api'; -import { credential, GoogleOAuthAccessToken } from './credential/index'; +import { credential } from './credential/index'; import { getApplicationDefault } from './credential/credential-internal'; import * as validator from './utils/validator'; import { deepCopy } from './utils/deep-copy'; @@ -39,6 +39,8 @@ import { RemoteConfig } from './remote-config/remote-config'; import Credential = credential.Credential; import Database = database.Database; +const TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1000; + /** * Type representing a callback which is called every time an app lifecycle event occurs. */ @@ -57,129 +59,80 @@ export interface FirebaseAccessToken { * Internals of a FirebaseApp instance. */ export class FirebaseAppInternals { - private isDeleted_ = false; private cachedToken_: FirebaseAccessToken; - private cachedTokenPromise_: Promise | null; private tokenListeners_: Array<(token: string) => void>; - private tokenRefreshTimeout_: NodeJS.Timer; constructor(private credential_: Credential) { this.tokenListeners_ = []; } - /** - * Gets an auth token for the associated app. - * - * @param {boolean} forceRefresh Whether or not to force a token refresh. - * @return {Promise} A Promise that will be fulfilled with the current or - * new token. - */ - public getToken(forceRefresh?: boolean): Promise { - const expired = this.cachedToken_ && this.cachedToken_.expirationTime < Date.now(); - if (this.cachedTokenPromise_ && !forceRefresh && !expired) { - return this.cachedTokenPromise_ - .catch((error) => { - // Update the cached token promise to avoid caching errors. Set it to resolve with the - // cached token if we have one (and return that promise since the token has still not - // expired). - if (this.cachedToken_) { - this.cachedTokenPromise_ = Promise.resolve(this.cachedToken_); - return this.cachedTokenPromise_; - } - - // Otherwise, set the cached token promise to null so that it will force a refresh next - // time getToken() is called. - this.cachedTokenPromise_ = null; - - // And re-throw the caught error. - throw error; - }); - } else { - // Clear the outstanding token refresh timeout. This is a noop if the timeout is undefined. - clearTimeout(this.tokenRefreshTimeout_); - - // this.credential_ may be an external class; resolving it in a promise helps us - // protect against exceptions and upgrades the result to a promise in all cases. - this.cachedTokenPromise_ = Promise.resolve(this.credential_.getAccessToken()) - .then((result: GoogleOAuthAccessToken) => { - // Since the developer can provide the credential implementation, we want to weakly verify - // the return type until the type is properly exported. - if (!validator.isNonNullObject(result) || - typeof result.expires_in !== 'number' || - typeof result.access_token !== 'string') { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Invalid access token generated: "${JSON.stringify(result)}". Valid access ` + - 'tokens must be an object with the "expires_in" (number) and "access_token" ' + - '(string) properties.', - ); - } - - const token: FirebaseAccessToken = { - accessToken: result.access_token, - expirationTime: Date.now() + (result.expires_in * 1000), - }; - - const hasAccessTokenChanged = (this.cachedToken_ && this.cachedToken_.accessToken !== token.accessToken); - const hasExpirationChanged = (this.cachedToken_ && this.cachedToken_.expirationTime !== token.expirationTime); - if (!this.cachedToken_ || hasAccessTokenChanged || hasExpirationChanged) { - this.cachedToken_ = token; - this.tokenListeners_.forEach((listener) => { - listener(token.accessToken); - }); - } - - // Establish a timeout to proactively refresh the token every minute starting at five - // minutes before it expires. Once a token refresh succeeds, no further retries are - // needed; if it fails, retry every minute until the token expires (resulting in a total - // of four retries: at 4, 3, 2, and 1 minutes). - let refreshTimeInSeconds = (result.expires_in - (5 * 60)); - let numRetries = 4; - - // In the rare cases the token is short-lived (that is, it expires in less than five - // minutes from when it was fetched), establish the timeout to refresh it after the - // current minute ends and update the number of retries that should be attempted before - // the token expires. - if (refreshTimeInSeconds <= 0) { - refreshTimeInSeconds = result.expires_in % 60; - numRetries = Math.floor(result.expires_in / 60) - 1; - } - - // The token refresh timeout keeps the Node.js process alive, so only create it if this - // instance has not already been deleted. - if (numRetries && !this.isDeleted_) { - this.setTokenRefreshTimeout(refreshTimeInSeconds * 1000, numRetries); - } - - return token; - }) - .catch((error) => { - let errorMessage = (typeof error === 'string') ? error : error.message; - - errorMessage = 'Credential implementation provided to initializeApp() via the ' + - '"credential" property failed to fetch a valid Google OAuth2 access token with the ' + - `following error: "${errorMessage}".`; - - if (errorMessage.indexOf('invalid_grant') !== -1) { - errorMessage += ' There are two likely causes: (1) your server time is not properly ' + - 'synced or (2) your certificate key file has been revoked. To solve (1), re-sync the ' + - 'time on your server. To solve (2), make sure the key ID for your key file is still ' + - 'present at https://console.firebase.google.com/iam-admin/serviceaccounts/project. If ' + - 'not, generate a new key file at ' + - 'https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.'; - } - - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); - }); - - return this.cachedTokenPromise_; + public getToken(forceRefresh = false): Promise { + if (forceRefresh || this.shouldRefresh()) { + return this.refreshToken(); } + + return Promise.resolve(this.cachedToken_); + } + + private refreshToken(): Promise { + return Promise.resolve(this.credential_.getAccessToken()) + .then((result) => { + // Since the developer can provide the credential implementation, we want to weakly verify + // the return type until the type is properly exported. + if (!validator.isNonNullObject(result) || + typeof result.expires_in !== 'number' || + typeof result.access_token !== 'string') { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + `Invalid access token generated: "${JSON.stringify(result)}". Valid access ` + + 'tokens must be an object with the "expires_in" (number) and "access_token" ' + + '(string) properties.', + ); + } + + const token = { + accessToken: result.access_token, + expirationTime: Date.now() + (result.expires_in * 1000), + }; + if (!this.cachedToken_ + || this.cachedToken_.accessToken !== token.accessToken + || this.cachedToken_.expirationTime !== token.expirationTime) { + this.cachedToken_ = token; + this.tokenListeners_.forEach((listener) => { + listener(token.accessToken); + }); + } + + return token; + }) + .catch((error) => { + let errorMessage = (typeof error === 'string') ? error : error.message; + + errorMessage = 'Credential implementation provided to initializeApp() via the ' + + '"credential" property failed to fetch a valid Google OAuth2 access token with the ' + + `following error: "${errorMessage}".`; + + if (errorMessage.indexOf('invalid_grant') !== -1) { + errorMessage += ' There are two likely causes: (1) your server time is not properly ' + + 'synced or (2) your certificate key file has been revoked. To solve (1), re-sync the ' + + 'time on your server. To solve (2), make sure the key ID for your key file is still ' + + 'present at https://console.firebase.google.com/iam-admin/serviceaccounts/project. If ' + + 'not, generate a new key file at ' + + 'https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.'; + } + + throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); + }); + } + + private shouldRefresh(): boolean { + return !this.cachedToken_ || (this.cachedToken_.expirationTime - Date.now()) <= TOKEN_EXPIRY_THRESHOLD_MILLIS; } /** * Adds a listener that is called each time a token changes. * - * @param {function(string)} listener The listener that will be called with each new token. + * @param listener The listener that will be called with each new token. */ public addAuthTokenListener(listener: (token: string) => void): void { this.tokenListeners_.push(listener); @@ -191,42 +144,11 @@ export class FirebaseAppInternals { /** * Removes a token listener. * - * @param {function(string)} listener The listener to remove. + * @param listener The listener to remove. */ public removeAuthTokenListener(listener: (token: string) => void): void { this.tokenListeners_ = this.tokenListeners_.filter((other) => other !== listener); } - - /** - * Deletes the FirebaseAppInternals instance. - */ - public delete(): void { - this.isDeleted_ = true; - - // Clear the token refresh timeout so it doesn't keep the Node.js process alive. - clearTimeout(this.tokenRefreshTimeout_); - } - - /** - * Establishes timeout to refresh the Google OAuth2 access token used by the SDK. - * - * @param {number} delayInMilliseconds The delay to use for the timeout. - * @param {number} numRetries The number of times to retry fetching a new token if the prior fetch - * failed. - */ - private setTokenRefreshTimeout(delayInMilliseconds: number, numRetries: number): void { - this.tokenRefreshTimeout_ = setTimeout(() => { - this.getToken(/* forceRefresh */ true) - .catch(() => { - // Ignore the error since this might just be an intermittent failure. If we really cannot - // refresh the token, an error will be logged once the existing token expires and we try - // to fetch a fresh one. - if (numRetries > 0) { - this.setTokenRefreshTimeout(60 * 1000, numRetries - 1); - } - }); - }, delayInMilliseconds); - } } /** @@ -419,8 +341,6 @@ export class FirebaseApp implements app.App { this.checkDestroyed_(); this.firebaseInternals_.removeApp(this.name_); - this.INTERNAL.delete(); - return Promise.all(Object.keys(this.services_).map((serviceName) => { const service = this.services_[serviceName]; if (isStateful(service)) { diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 3979719ab6..7f7626321b 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -25,6 +25,7 @@ import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; import { DatabaseService } from '../../../src/database/database-internal'; import { database } from '../../../src/database/index'; +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import * as utils from '../utils'; import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; @@ -118,6 +119,179 @@ describe('Database', () => { }); }); + describe('Token refresh', () => { + const MINUTE_IN_MILLIS = 60 * 1000; + + let clock: sinon.SinonFakeTimers; + let getTokenStub: sinon.SinonStub; + + beforeEach(() => { + getTokenStub = stubCredentials(); + clock = sinon.useFakeTimers(1000); + }); + + afterEach(() => { + getTokenStub.restore(); + clock.restore(); + }); + + function stubCredentials(options?: { + accessToken?: string; + expiresIn?: number; + err?: any; + }): sinon.SinonStub { + if (options?.err) { + return sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken') + .rejects(options.err); + } + + return sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken') + .resolves({ + access_token: options?.accessToken || 'mock-access-token', // eslint-disable-line @typescript-eslint/camelcase + expires_in: options?.expiresIn || 3600, // eslint-disable-line @typescript-eslint/camelcase + }); + } + + it('should refresh the token 5 minutes before expiration', () => { + database.getDatabase(mockApp.options.databaseURL); + expect(getTokenStub).to.have.not.been.called; + mockApp.INTERNAL.getToken() + .then((token) => { + expect(getTokenStub).to.have.been.calledOnce; + + const expiryInMillis = token.expirationTime - Date.now(); + clock.tick(expiryInMillis - (5 * MINUTE_IN_MILLIS) - 1000); + expect(getTokenStub).to.have.been.calledOnce; + + clock.tick(1000); + expect(getTokenStub).to.have.been.calledTwice; + }); + }); + + it('should not start multiple token refresher tasks', () => { + database.getDatabase(mockApp.options.databaseURL); + database.getDatabase('https://other-database.firebaseio.com'); + expect(getTokenStub).to.have.not.been.called; + mockApp.INTERNAL.getToken() + .then((token) => { + expect(getTokenStub).to.have.been.calledOnce; + + const expiryInMillis = token.expirationTime - Date.now(); + clock.tick(expiryInMillis - (5 * MINUTE_IN_MILLIS)); + expect(getTokenStub).to.have.been.calledTwice; + }); + }); + + it('should reschedule the token refresher when the underlying token changes', () => { + database.getDatabase(mockApp.options.databaseURL); + mockApp.INTERNAL.getToken() + .then((token1) => { + expect(getTokenStub).to.have.been.calledOnce; + + // Forward the clock to 30 minutes before expiry. + const expiryInMillis = token1.expirationTime - Date.now(); + clock.tick(expiryInMillis - (30 * MINUTE_IN_MILLIS)); + + // Force a token refresh + return mockApp.INTERNAL.getToken(true) + .then((token2) => { + expect(getTokenStub).to.have.been.calledTwice; + // Forward the clock to 5 minutes before old expiry time. + clock.tick(25 * MINUTE_IN_MILLIS); + expect(getTokenStub).to.have.been.calledTwice; + + // Forward the clock 1 second past old expiry time. + clock.tick(5 * MINUTE_IN_MILLIS + 1000); + expect(getTokenStub).to.have.been.calledTwice; + + const newExpiryTimeInMillis = token2.expirationTime - Date.now(); + clock.tick(newExpiryTimeInMillis - (5 * MINUTE_IN_MILLIS)); + expect(getTokenStub).to.have.been.calledThrice; + }); + }); + }); + + it('should not reschedule when the token is about to expire in 5 minutes', () => { + database.getDatabase(mockApp.options.databaseURL); + mockApp.INTERNAL.getToken() + .then((token1) => { + expect(getTokenStub).to.have.been.calledOnce; + + // Forward the clock to 30 minutes before expiry. + const expiryInMillis = token1.expirationTime - Date.now(); + clock.tick(expiryInMillis - (30 * MINUTE_IN_MILLIS)); + + getTokenStub.restore(); + getTokenStub = stubCredentials({ expiresIn: 5 * 60 }); + // Force a token refresh + return mockApp.INTERNAL.getToken(true); + }) + .then((token2) => { + expect(getTokenStub).to.have.been.calledTwice; + + const newExpiryTimeInMillis = token2.expirationTime - Date.now(); + clock.tick(newExpiryTimeInMillis); + expect(getTokenStub).to.have.been.calledTwice; + + getTokenStub.restore(); + getTokenStub = stubCredentials({ expiresIn: 60 * 60 }); + // Force a token refresh + return mockApp.INTERNAL.getToken(true); + }) + .then((token3) => { + expect(getTokenStub).to.have.been.calledThrice; + + const newExpiryTimeInMillis = token3.expirationTime - Date.now(); + clock.tick(newExpiryTimeInMillis - (5 * MINUTE_IN_MILLIS)); + expect(getTokenStub).to.have.callCount(4); + }); + }); + + it('should gracefully handle errors during token refresh', () => { + database.getDatabase(mockApp.options.databaseURL); + mockApp.INTERNAL.getToken() + .then((token1) => { + expect(getTokenStub).to.have.been.calledOnce; + + getTokenStub.restore(); + getTokenStub = stubCredentials({ err: new Error('Test error') }); + expect(getTokenStub).to.have.not.been.called; + + const expiryInMillis = token1.expirationTime - Date.now(); + clock.tick(expiryInMillis); + expect(getTokenStub).to.have.been.calledOnce; + + getTokenStub.restore(); + getTokenStub = stubCredentials(); + expect(getTokenStub).to.have.not.been.called; + // Force a token refresh + return mockApp.INTERNAL.getToken(true); + }) + .then((token2) => { + expect(getTokenStub).to.have.been.calledOnce; + + const newExpiryTimeInMillis = token2.expirationTime - Date.now(); + clock.tick(newExpiryTimeInMillis - (5 * MINUTE_IN_MILLIS)); + expect(getTokenStub).to.have.been.calledTwice; + }); + }); + + it('should stop the token refresher task at delete', () => { + database.getDatabase(mockApp.options.databaseURL); + mockApp.INTERNAL.getToken() + .then((token) => { + expect(getTokenStub).to.have.been.calledOnce; + return database.delete() + .then(() => { + // Forward the clock to five minutes before expiry. + const expiryInMillis = token.expirationTime - Date.now(); + clock.tick(expiryInMillis - (5 * MINUTE_IN_MILLIS)); + expect(getTokenStub).to.have.been.calledOnce; + }); + }); + }); + }); + describe('Rules', () => { const mockAccessToken: string = utils.generateRandomAccessToken(); let getTokenStub: sinon.SinonStub; diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index 49da6736c5..6edf73b7ef 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -90,9 +90,6 @@ describe('FirebaseApp', () => { }); clock = sinon.useFakeTimers(1000); - - mockApp = mocks.app(); - firebaseConfigVar = process.env[FIREBASE_CONFIG_VAR]; delete process.env[FIREBASE_CONFIG_VAR]; firebaseNamespace = new FirebaseNamespace(); @@ -767,204 +764,6 @@ describe('FirebaseApp', () => { }); }); - it('retries to proactively refresh the token if a proactive refresh attempt fails', () => { - // Force a token refresh. - return mockApp.INTERNAL.getToken(true).then((token1) => { - // Stub the getToken() method to return a rejected promise. - getTokenStub.restore(); - expect(mockApp.options.credential).to.exist; - getTokenStub = sinon.stub(mockApp.options.credential!, 'getAccessToken') - .rejects(new Error('Intentionally rejected')); - - // Forward the clock to exactly five minutes before expiry. - const expiryInMilliseconds = token1.expirationTime - Date.now(); - clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS)); - - // Forward the clock to exactly four minutes before expiry. - clock.tick(60 * 1000); - - // Restore the stubbed getAccessToken() method. - getTokenStub.restore(); - getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').resolves({ - access_token: 'mock-access-token', // eslint-disable-line @typescript-eslint/camelcase - expires_in: 3600, // eslint-disable-line @typescript-eslint/camelcase - }); - - return mockApp.INTERNAL.getToken().then((token2) => { - // Ensure the token has not been proactively refreshed. - expect(token1).to.deep.equal(token2); - expect(getTokenStub).to.have.not.been.called; - - // Forward the clock to exactly three minutes before expiry. - clock.tick(60 * 1000); - - return mockApp.INTERNAL.getToken().then((token3) => { - // Ensure the token was proactively refreshed. - expect(token1).to.not.deep.equal(token3); - expect(getTokenStub).to.have.been.calledOnce; - }); - }); - }); - }); - - it('stops retrying to proactively refresh the token after five attempts', () => { - // Force a token refresh. - let originalToken: FirebaseAccessToken; - return mockApp.INTERNAL.getToken(true).then((token) => { - originalToken = token; - - // Stub the credential's getAccessToken() method to always return a rejected promise. - getTokenStub.restore(); - expect(mockApp.options.credential).to.exist; - getTokenStub = sinon.stub(mockApp.options.credential!, 'getAccessToken') - .rejects(new Error('Intentionally rejected')); - - // Expect the call count to initially be zero. - expect(getTokenStub.callCount).to.equal(0); - - // Forward the clock to exactly five minutes before expiry. - const expiryInMilliseconds = token.expirationTime - Date.now(); - clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS)); - - // Due to synchronous timing issues when the timer is mocked, make a call to getToken() - // without forcing a refresh to ensure there is enough time for the underlying token refresh - // timeout to fire and complete. - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was attempted to be proactively refreshed one time. - expect(getTokenStub.callCount).to.equal(1); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to four minutes before expiry. - clock.tick(ONE_MINUTE_IN_MILLISECONDS); - - // See note above about calling getToken(). - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was attempted to be proactively refreshed two times. - expect(getTokenStub.callCount).to.equal(2); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to three minutes before expiry. - clock.tick(ONE_MINUTE_IN_MILLISECONDS); - - // See note above about calling getToken(). - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was attempted to be proactively refreshed three times. - expect(getTokenStub.callCount).to.equal(3); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to two minutes before expiry. - clock.tick(ONE_MINUTE_IN_MILLISECONDS); - - // See note above about calling getToken(). - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was attempted to be proactively refreshed four times. - expect(getTokenStub.callCount).to.equal(4); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to one minute before expiry. - clock.tick(ONE_MINUTE_IN_MILLISECONDS); - - // See note above about calling getToken(). - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was attempted to be proactively refreshed five times. - expect(getTokenStub.callCount).to.equal(5); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to expiry. - clock.tick(ONE_MINUTE_IN_MILLISECONDS); - - // See note above about calling getToken(). - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was not attempted to be proactively refreshed a sixth time. - expect(getTokenStub.callCount).to.equal(5); - - // Ensure the token has never been refresh. - expect(token).to.deep.equal(originalToken); - }); - }); - - it('resets the proactive refresh timeout upon a force refresh', () => { - // Force a token refresh. - return mockApp.INTERNAL.getToken(true).then((token1) => { - // Forward the clock to five minutes and one second before expiry. - let expiryInMilliseconds = token1.expirationTime - Date.now(); - clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS) - 1000); - - // Force a token refresh. - return mockApp.INTERNAL.getToken(true).then((token2) => { - // Ensure the token was force refreshed. - expect(token1).to.not.deep.equal(token2); - expect(getTokenStub).to.have.been.calledTwice; - - // Forward the clock to exactly five minutes before the original token's expiry. - clock.tick(1000); - - return mockApp.INTERNAL.getToken().then((token3) => { - // Ensure the token hasn't changed, meaning the proactive refresh was canceled. - expect(token2).to.deep.equal(token3); - expect(getTokenStub).to.have.been.calledTwice; - - // Forward the clock to exactly five minutes before the refreshed token's expiry. - expiryInMilliseconds = token3.expirationTime - Date.now(); - clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS)); - - return mockApp.INTERNAL.getToken().then((token4) => { - // Ensure the token was proactively refreshed. - expect(token3).to.not.deep.equal(token4); - expect(getTokenStub).to.have.been.calledThrice; - }); - }); - }); - }); - }); - - it('proactively refreshes the token at the next full minute if it expires in five minutes or less', () => { - // Turn off default mocking of one hour access tokens and replace it with a short-lived token. - getTokenStub.restore(); - expect(mockApp.options.credential).to.exist; - getTokenStub = sinon.stub(mockApp.options.credential!, 'getAccessToken').resolves({ - access_token: utils.generateRandomAccessToken(), // eslint-disable-line @typescript-eslint/camelcase - expires_in: 3 * 60 + 10, // eslint-disable-line @typescript-eslint/camelcase - }); - // Expect the call count to initially be zero. - expect(getTokenStub.callCount).to.equal(0); - - // Force a token refresh. - return mockApp.INTERNAL.getToken(true).then((token1) => { - - // Move the clock forward to three minutes and one second before expiry. - clock.tick(9 * 1000); - expect(getTokenStub.callCount).to.equal(1); - - // Move the clock forward to exactly three minutes before expiry. - clock.tick(1000); - - // Expect the underlying getAccessToken() method to have been called once. - expect(getTokenStub.callCount).to.equal(2); - - return mockApp.INTERNAL.getToken().then((token2) => { - // Ensure the token was proactively refreshed. - expect(token1).to.not.deep.equal(token2); - }); - }); - }); - it('Includes the original error in exception', () => { getTokenStub.restore(); const mockError = new FirebaseAppError( From 738eba78a15756706d92d4844074d71d915eb585 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 19 Mar 2021 15:24:31 -0700 Subject: [PATCH 086/160] fix(rtdb): Fixing the RTDB token listener callback (#1203) --- src/database/database-internal.ts | 13 +++++------ test/unit/database/database.spec.ts | 36 +++++++++++++++-------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts index dd28bfd919..66d8db883c 100644 --- a/src/database/database-internal.ts +++ b/src/database/database-internal.ts @@ -33,7 +33,7 @@ const TOKEN_REFRESH_THRESHOLD_MILLIS = 5 * 60 * 1000; export class DatabaseService { private readonly appInternal: FirebaseApp; - private tokenListenerRegistered: boolean; + private tokenListener: (token: string) => void; private tokenRefreshTimeout: NodeJS.Timeout; private databases: { @@ -54,10 +54,9 @@ export class DatabaseService { * @internal */ public delete(): Promise { - if (this.tokenListenerRegistered) { - this.appInternal.INTERNAL.removeAuthTokenListener(this.onTokenChange); + if (this.tokenListener) { + this.appInternal.INTERNAL.removeAuthTokenListener(this.tokenListener); clearTimeout(this.tokenRefreshTimeout); - this.tokenListenerRegistered = false; } const promises = []; @@ -107,9 +106,9 @@ export class DatabaseService { this.databases[dbUrl] = db; } - if (!this.tokenListenerRegistered) { - this.tokenListenerRegistered = true; - this.appInternal.INTERNAL.addAuthTokenListener(this.onTokenChange); + if (!this.tokenListener) { + this.tokenListener = this.onTokenChange.bind(this); + this.appInternal.INTERNAL.addAuthTokenListener(this.tokenListener); } return db; diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 7f7626321b..739dcbccfc 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -153,9 +153,9 @@ describe('Database', () => { } it('should refresh the token 5 minutes before expiration', () => { - database.getDatabase(mockApp.options.databaseURL); + database.getDatabase(); expect(getTokenStub).to.have.not.been.called; - mockApp.INTERNAL.getToken() + return mockApp.INTERNAL.getToken() .then((token) => { expect(getTokenStub).to.have.been.calledOnce; @@ -169,10 +169,10 @@ describe('Database', () => { }); it('should not start multiple token refresher tasks', () => { - database.getDatabase(mockApp.options.databaseURL); + database.getDatabase(); database.getDatabase('https://other-database.firebaseio.com'); expect(getTokenStub).to.have.not.been.called; - mockApp.INTERNAL.getToken() + return mockApp.INTERNAL.getToken() .then((token) => { expect(getTokenStub).to.have.been.calledOnce; @@ -183,8 +183,8 @@ describe('Database', () => { }); it('should reschedule the token refresher when the underlying token changes', () => { - database.getDatabase(mockApp.options.databaseURL); - mockApp.INTERNAL.getToken() + database.getDatabase(); + return mockApp.INTERNAL.getToken() .then((token1) => { expect(getTokenStub).to.have.been.calledOnce; @@ -211,9 +211,11 @@ describe('Database', () => { }); }); - it('should not reschedule when the token is about to expire in 5 minutes', () => { - database.getDatabase(mockApp.options.databaseURL); - mockApp.INTERNAL.getToken() + // Currently doesn't work as expected since onTokenChange() can force a token refresh + // by calling getToken(). Skipping for now. + xit('should not reschedule when the token is about to expire in 5 minutes', () => { + database.getDatabase(); + return mockApp.INTERNAL.getToken() .then((token1) => { expect(getTokenStub).to.have.been.calledOnce; @@ -227,11 +229,11 @@ describe('Database', () => { return mockApp.INTERNAL.getToken(true); }) .then((token2) => { - expect(getTokenStub).to.have.been.calledTwice; + expect(getTokenStub).to.have.been.calledOnce; const newExpiryTimeInMillis = token2.expirationTime - Date.now(); clock.tick(newExpiryTimeInMillis); - expect(getTokenStub).to.have.been.calledTwice; + expect(getTokenStub).to.have.been.calledOnce; getTokenStub.restore(); getTokenStub = stubCredentials({ expiresIn: 60 * 60 }); @@ -239,17 +241,17 @@ describe('Database', () => { return mockApp.INTERNAL.getToken(true); }) .then((token3) => { - expect(getTokenStub).to.have.been.calledThrice; + expect(getTokenStub).to.have.been.calledOnce; const newExpiryTimeInMillis = token3.expirationTime - Date.now(); clock.tick(newExpiryTimeInMillis - (5 * MINUTE_IN_MILLIS)); - expect(getTokenStub).to.have.callCount(4); + expect(getTokenStub).to.have.been.calledTwice; }); }); it('should gracefully handle errors during token refresh', () => { - database.getDatabase(mockApp.options.databaseURL); - mockApp.INTERNAL.getToken() + database.getDatabase(); + return mockApp.INTERNAL.getToken() .then((token1) => { expect(getTokenStub).to.have.been.calledOnce; @@ -277,8 +279,8 @@ describe('Database', () => { }); it('should stop the token refresher task at delete', () => { - database.getDatabase(mockApp.options.databaseURL); - mockApp.INTERNAL.getToken() + database.getDatabase(); + return mockApp.INTERNAL.getToken() .then((token) => { expect(getTokenStub).to.have.been.calledOnce; return database.delete() From 97d382352542c4774e7c55b6fdfa29c3bc848d79 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Mon, 22 Mar 2021 17:47:57 -0700 Subject: [PATCH 087/160] Add emulator-based integration tests. (#1155) * Add emulator-based integration tests. * Move emulator stuff out of package.json. * Update CONTRIBUTING.md too. * Add npx. * Skip new unsupported tests. * Inline commands in ci.yml. --- .github/workflows/ci.yml | 13 ++++++++++--- CONTRIBUTING.md | 23 ++++++++++++++++++++++- test/integration/auth.spec.ts | 10 ++++++++-- test/integration/database.spec.ts | 22 ++++++++++++++++++---- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5810ed895..244d123dd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,10 +17,17 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - - name: Install, build and test + - name: Install and build run: | npm ci npm run build npm run build:tests - npm test - npm run api-extractor + - name: Lint and run unit tests + run: npm test + - name: Run api-extractor + run: npm run api-extractor + - name: Run emulator-based integration tests + run: | + npm install -g firebase-tools + firebase emulators:exec --project fake-project-id --only auth,database,firestore \ + 'npx mocha \"test/integration/{auth,database,firestore}.spec.ts\" --slow 5000 --timeout 20000 --require ts-node/register' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbb374fa14..7ac6a71cb1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,6 +123,8 @@ There are two test suites: unit and integration. The unit test suite is intended development, and the integration test suite is intended to be run before packaging up release candidates. +#### Unit Tests + To run the unit test suite: ```bash @@ -135,7 +137,26 @@ If you wish to skip the linter, and only run the unit tests: $ npm run test:unit ``` -The integration tests run against an actual Firebase project. Create a new +#### Integration Tests with Emulator Suite + +Some of the integration tests work with the Emulator Suite and you can run them +without an actual Firebase project. + +First, make sure to [install Firebase CLI](https://firebase.google.com/docs/cli#install_the_firebase_cli). +And then: + +```bash + firebase emulators:exec --project fake-project-id --only auth,database,firestore \ + 'npx mocha \"test/integration/{auth,database,firestore}.spec.ts\" --slow 5000 --timeout 20000 --require ts-node/register' +``` + +Currently, only the Auth, Database, and Firestore test suites work. Some test +cases will be automatically skipped due to lack of emulator support. The section +below covers how to run the full test suite against an actual Firebase project. + +#### Integration Tests with an actual Firebase project + +Other integration tests require an actual Firebase project. Create a new project in the [Firebase Console](https://console.firebase.google.com), if you do not already have one suitable for running the tests against. Then obtain the following credentials from the project: diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index bf9f7f134d..29060b4cdd 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -683,7 +683,10 @@ describe('admin.auth', () => { }); }); - it('can link/unlink with a federated provider', async () => { + it('can link/unlink with a federated provider', async function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } const googleFederatedUid = 'google_uid_' + generateRandomString(10); let userRecord = await admin.auth().updateUser(updateUser.uid, { providerToLink: { @@ -707,7 +710,10 @@ describe('admin.auth', () => { expect(providerIds).to.not.deep.include('google.com'); }); - it('can unlink multiple providers at once, incl a non-federated provider', async () => { + it('can unlink multiple providers at once, incl a non-federated provider', async function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } await deletePhoneNumberUser('+15555550001'); const googleFederatedUid = 'google_uid_' + generateRandomString(10); diff --git a/test/integration/database.spec.ts b/test/integration/database.spec.ts index 708c3a153a..77204a1839 100644 --- a/test/integration/database.spec.ts +++ b/test/integration/database.spec.ts @@ -17,7 +17,7 @@ import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import { defaultApp, nullApp, nonNullApp, cmdArgs, databaseUrl } from './setup'; +import { defaultApp, nullApp, nonNullApp, cmdArgs, databaseUrl, isEmulator } from './setup'; // eslint-disable-next-line @typescript-eslint/no-var-requires const chalk = require('chalk'); @@ -64,7 +64,13 @@ describe('admin.database', () => { .should.eventually.be.fulfilled; }); - it('App with null auth overrides is blocked by security rules', () => { + it('App with null auth overrides is blocked by security rules', function () { + if (isEmulator) { + // RTDB emulator has open security rules by default and won't block this. + // TODO(https://github.com/firebase/firebase-admin-node/issues/1149): + // remove this once updating security rules through admin is in place. + return this.skip(); + } return nullApp.database().ref('blocked').set(admin.database.ServerValue.TIMESTAMP) .should.eventually.be.rejectedWith('PERMISSION_DENIED: Permission denied'); }); @@ -157,13 +163,21 @@ describe('admin.database', () => { }); }); - it('admin.database().getRules() returns currently defined rules as a string', () => { + it('admin.database().getRules() returns currently defined rules as a string', function () { + if (isEmulator) { + // https://github.com/firebase/firebase-admin-node/issues/1149 + return this.skip(); + } return admin.database().getRules().then((result) => { return expect(result).to.be.not.empty; }); }); - it('admin.database().getRulesJSON() returns currently defined rules as an object', () => { + it('admin.database().getRulesJSON() returns currently defined rules as an object', function () { + if (isEmulator) { + // https://github.com/firebase/firebase-admin-node/issues/1149 + return this.skip(); + } return admin.database().getRulesJSON().then((result) => { return expect(result).to.be.not.undefined; }); From 19660d921d20732857bf54393a09e8b5bce15d63 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Thu, 25 Mar 2021 10:56:53 -0700 Subject: [PATCH 088/160] Disable one flaky tests in emulator. (#1205) --- test/integration/auth.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 29060b4cdd..53174f2ff5 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -441,8 +441,12 @@ describe('admin.auth', () => { .then((listUsersResult) => { // Confirm expected number of users. expect(listUsersResult.users.length).to.equal(2); - // Confirm next page token present. - expect(typeof listUsersResult.pageToken).to.equal('string'); + // TODO(yuchenshi): Investigate on why this is flaky in emulator. + if (!authEmulatorHost) { + // Confirm next page token present. + expect(typeof listUsersResult.pageToken).to.equal('string'); + } + // Confirm each user's uid and the hashed passwords. expect(listUsersResult.users[0].uid).to.equal(uids[1]); From 2a5b7f6366bda8802f1636dadcfa69c4e25ec8c0 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 29 Mar 2021 10:25:09 -0700 Subject: [PATCH 089/160] [chore] Release 9.6.0 (#1209) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aa1c323a75..240abe3cd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.5.0", + "version": "9.6.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 60b4e2933d4bc7c2d7066ea0eae12e793c558549 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 30 Mar 2021 13:57:10 -0400 Subject: [PATCH 090/160] (chore): Add JWT Decoder and Signature Verifier (#1204) * (chore): Add JWT Decoder * Add signature verifier and key fetcher abstractions * Add unit tests for utils/jwt --- src/auth/token-verifier.ts | 260 +++++-------- src/utils/jwt.ts | 275 +++++++++++++ test/unit/auth/token-verifier.spec.ts | 339 ++++------------ test/unit/index.spec.ts | 1 + test/unit/utils/jwt.spec.ts | 541 ++++++++++++++++++++++++++ 5 files changed, 1000 insertions(+), 416 deletions(-) create mode 100644 src/utils/jwt.ts create mode 100644 test/unit/utils/jwt.spec.ts diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index cbb9991f4c..e100cc25f6 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -17,8 +17,10 @@ import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; import * as util from '../utils/index'; import * as validator from '../utils/validator'; -import * as jwt from 'jsonwebtoken'; -import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; +import { + DecodedToken, decodeJwt, JwtError, JwtErrorCode, + EmulatorSignatureVerifier, PublicKeySignatureVerifier, ALGORITHM_RS256, SignatureVerifier, +} from '../utils/jwt'; import { FirebaseApp } from '../firebase-app'; import { auth } from './index'; @@ -27,8 +29,6 @@ import DecodedIdToken = auth.DecodedIdToken; // Audience to use for Firebase Auth Custom tokens const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; -export const ALGORITHM_RS256 = 'RS256'; - // URL containing the public keys for the Google certs (whose private keys are used to sign Firebase // Auth ID tokens) const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; @@ -36,6 +36,8 @@ const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/secur // URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon. const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys'; +const EMULATOR_VERIFIER = new EmulatorSignatureVerifier(); + /** User facing token information related to the Firebase ID token. */ export const ID_TOKEN_INFO: FirebaseTokenInfo = { url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens', @@ -69,15 +71,13 @@ export interface FirebaseTokenInfo { } /** - * Class for verifying general purpose Firebase JWTs. This verifies ID tokens and session cookies. + * Class for verifying ID tokens and session cookies. */ export class FirebaseTokenVerifier { - private publicKeys: {[key: string]: string}; - private publicKeysExpireAt: number; private readonly shortNameArticle: string; + private readonly signatureVerifier: SignatureVerifier; - constructor(private clientCertUrl: string, private algorithm: jwt.Algorithm, - private issuer: string, private tokenInfo: FirebaseTokenInfo, + constructor(clientCertUrl: string, private issuer: string, private tokenInfo: FirebaseTokenInfo, private readonly app: FirebaseApp) { if (!validator.isURL(clientCertUrl)) { @@ -85,11 +85,6 @@ export class FirebaseTokenVerifier { AuthClientErrorCode.INVALID_ARGUMENT, 'The provided public client certificate URL is an invalid URL.', ); - } else if (!validator.isNonEmptyString(algorithm)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'The provided JWT algorithm is an empty string.', - ); } else if (!validator.isURL(issuer)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -128,16 +123,18 @@ export class FirebaseTokenVerifier { } this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; + this.signatureVerifier = + PublicKeySignatureVerifier.withCertificateUrl(clientCertUrl, app.options.httpAgent); + // For backward compatibility, the project ID is validated in the verification call. } /** * Verifies the format and signature of a Firebase Auth JWT token. * - * @param {string} jwtToken The Firebase Auth JWT token to verify. - * @param {boolean=} isEmulator Whether to accept Auth Emulator tokens. - * @return {Promise} A promise fulfilled with the decoded claims of the Firebase Auth ID - * token. + * @param jwtToken The Firebase Auth JWT token to verify. + * @param isEmulator Whether to accept Auth Emulator tokens. + * @return A promise fulfilled with the decoded claims of the Firebase Auth ID token. */ public verifyJWT(jwtToken: string, isEmulator = false): Promise { if (!validator.isString(jwtToken)) { @@ -147,29 +144,68 @@ export class FirebaseTokenVerifier { ); } - return util.findProjectId(this.app) + return this.ensureProjectId() .then((projectId) => { - return this.verifyJWTWithProjectId(jwtToken, projectId, isEmulator); + return this.decodeAndVerify(jwtToken, projectId, isEmulator); + }) + .then((decoded) => { + const decodedIdToken = decoded.payload as DecodedIdToken; + decodedIdToken.uid = decodedIdToken.sub; + return decodedIdToken; }); } - private verifyJWTWithProjectId( - jwtToken: string, - projectId: string | null, - isEmulator: boolean - ): Promise { - if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, - 'Must initialize app with a cert credential or set your Firebase project ID as the ' + - `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`, - ); - } + private ensureProjectId(): Promise { + return util.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CREDENTIAL, + 'Must initialize app with a cert credential or set your Firebase project ID as the ' + + `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`, + ); + } + return Promise.resolve(projectId); + }) + } - const fullDecodedToken: any = jwt.decode(jwtToken, { - complete: true, - }); + private decodeAndVerify(token: string, projectId: string, isEmulator: boolean): Promise { + return this.safeDecode(token) + .then((decodedToken) => { + this.verifyContent(decodedToken, projectId, isEmulator); + return this.verifySignature(token, isEmulator) + .then(() => decodedToken); + }); + } + private safeDecode(jwtToken: string): Promise { + return decodeJwt(jwtToken) + .catch((err: JwtError) => { + if (err.code == JwtErrorCode.INVALID_ARGUMENT) { + const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; + const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` + + `the entire string JWT which represents ${this.shortNameArticle} ` + + `${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, + errorMessage); + } + throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, err.message); + }); + } + + /** + * Verifies the content of a Firebase Auth JWT. + * + * @param fullDecodedToken The decoded JWT. + * @param projectId The Firebase Project Id. + * @param isEmulator Whether the token is an Emulator token. + */ + private verifyContent( + fullDecodedToken: DecodedToken, + projectId: string | null, + isEmulator: boolean): void { + const header = fullDecodedToken && fullDecodedToken.header; const payload = fullDecodedToken && fullDecodedToken.payload; @@ -179,10 +215,7 @@ export class FirebaseTokenVerifier { `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; let errorMessage: string | undefined; - if (!fullDecodedToken) { - errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed the entire string JWT ` + - `which represents ${this.shortNameArticle} ${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; - } else if (!isEmulator && typeof header.kid === 'undefined') { + if (!isEmulator && typeof header.kid === 'undefined') { const isCustomToken = (payload.aud === FIREBASE_AUDIENCE); const isLegacyCustomToken = (header.alg === 'HS256' && payload.v === 0 && 'd' in payload && 'uid' in payload.d); @@ -197,8 +230,8 @@ export class FirebaseTokenVerifier { } errorMessage += verifyJwtTokenDocsMessage; - } else if (!isEmulator && header.alg !== this.algorithm) { - errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + '" but got ' + + } else if (!isEmulator && header.alg !== ALGORITHM_RS256) { + errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_RS256 + '" but got ' + '"' + header.alg + '".' + verifyJwtTokenDocsMessage; } else if (payload.aud !== projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + @@ -217,135 +250,55 @@ export class FirebaseTokenVerifier { verifyJwtTokenDocsMessage; } if (errorMessage) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); } + } - if (isEmulator) { - // Signature checks skipped for emulator; no need to fetch public keys. - return this.verifyJwtSignatureWithKey(jwtToken, null); - } - - return this.fetchPublicKeys().then((publicKeys) => { - if (!Object.prototype.hasOwnProperty.call(publicKeys, header.kid)) { - return Promise.reject( - new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `${this.tokenInfo.jwtName} has "kid" claim which does not correspond to a known public key. ` + - `Most likely the ${this.tokenInfo.shortName} is expired, so get a fresh token from your ` + - 'client app and try again.', - ), - ); - } else { - return this.verifyJwtSignatureWithKey(jwtToken, publicKeys[header.kid]); - } - - }); + private verifySignature(jwtToken: string, isEmulator: boolean): + Promise { + const verifier = isEmulator ? EMULATOR_VERIFIER : this.signatureVerifier; + return verifier.verify(jwtToken) + .catch((error) => { + throw this.mapJwtErrorToAuthError(error); + }); } /** - * Verifies the JWT signature using the provided public key. - * @param {string} jwtToken The JWT token to verify. - * @param {string} publicKey The public key certificate. - * @return {Promise} A promise that resolves with the decoded JWT claims on successful - * verification. + * Maps JwtError to FirebaseAuthError + * + * @param error JwtError to be mapped. + * @returns FirebaseAuthError or Error instance. */ - private verifyJwtSignatureWithKey(jwtToken: string, publicKey: string | null): Promise { + private mapJwtErrorToAuthError(error: JwtError): Error { const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; - return new Promise((resolve, reject) => { - const verifyOptions: jwt.VerifyOptions = {}; - if (publicKey !== null) { - verifyOptions.algorithms = [this.algorithm]; - } - jwt.verify(jwtToken, publicKey || '', verifyOptions, - (error: jwt.VerifyErrors | null, decodedToken: object | undefined) => { - if (error) { - if (error.name === 'TokenExpiredError') { - const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + - ` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` + - verifyJwtTokenDocsMessage; - return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage)); - } else if (error.name === 'JsonWebTokenError') { - const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; - return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); - } - return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message)); - } else { - const decodedIdToken = (decodedToken as DecodedIdToken); - decodedIdToken.uid = decodedIdToken.sub; - resolve(decodedIdToken); - } - }); - }); - } - - /** - * Fetches the public keys for the Google certs. - * - * @return {Promise} A promise fulfilled with public keys for the Google certs. - */ - private fetchPublicKeys(): Promise<{[key: string]: string}> { - const publicKeysExist = (typeof this.publicKeys !== 'undefined'); - const publicKeysExpiredExists = (typeof this.publicKeysExpireAt !== 'undefined'); - const publicKeysStillValid = (publicKeysExpiredExists && Date.now() < this.publicKeysExpireAt); - if (publicKeysExist && publicKeysStillValid) { - return Promise.resolve(this.publicKeys); + if (error.code === JwtErrorCode.TOKEN_EXPIRED) { + const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + + ` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` + + verifyJwtTokenDocsMessage; + return new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage); + } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) { + const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; + return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); + } else if (error.code === JwtErrorCode.NO_MATCHING_KID) { + const errorMessage = `${this.tokenInfo.jwtName} has "kid" claim which does not ` + + `correspond to a known public key. Most likely the ${this.tokenInfo.shortName} ` + + 'is expired, so get a fresh token from your client app and try again.'; + return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); } - - const client = new HttpClient(); - const request: HttpRequestConfig = { - method: 'GET', - url: this.clientCertUrl, - httpAgent: this.app.options.httpAgent, - }; - return client.send(request).then((resp) => { - if (!resp.isJson() || resp.data.error) { - // Treat all non-json messages and messages with an 'error' field as - // error responses. - throw new HttpError(resp); - } - if (Object.prototype.hasOwnProperty.call(resp.headers, 'cache-control')) { - const cacheControlHeader: string = resp.headers['cache-control']; - const parts = cacheControlHeader.split(','); - parts.forEach((part) => { - const subParts = part.trim().split('='); - if (subParts[0] === 'max-age') { - const maxAge: number = +subParts[1]; - this.publicKeysExpireAt = Date.now() + (maxAge * 1000); - } - }); - } - this.publicKeys = resp.data; - return resp.data; - }).catch((err) => { - if (err instanceof HttpError) { - let errorMessage = 'Error fetching public keys for Google certs: '; - const resp = err.response; - if (resp.isJson() && resp.data.error) { - errorMessage += `${resp.data.error}`; - if (resp.data.error_description) { - errorMessage += ' (' + resp.data.error_description + ')'; - } - } else { - errorMessage += `${resp.text}`; - } - throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, errorMessage); - } - throw err; - }); + return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message); } } /** * Creates a new FirebaseTokenVerifier to verify Firebase ID tokens. * - * @param {FirebaseApp} app Firebase app instance. - * @return {FirebaseTokenVerifier} + * @param app Firebase app instance. + * @return FirebaseTokenVerifier */ export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier { return new FirebaseTokenVerifier( CLIENT_CERT_URL, - ALGORITHM_RS256, 'https://securetoken.google.com/', ID_TOKEN_INFO, app @@ -355,13 +308,12 @@ export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier { /** * Creates a new FirebaseTokenVerifier to verify Firebase session cookies. * - * @param {FirebaseApp} app Firebase app instance. - * @return {FirebaseTokenVerifier} + * @param app Firebase app instance. + * @return FirebaseTokenVerifier */ export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier { return new FirebaseTokenVerifier( SESSION_COOKIE_CERT_URL, - ALGORITHM_RS256, 'https://session.firebase.google.com/', SESSION_COOKIE_INFO, app diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 0000000000..d048567061 --- /dev/null +++ b/src/utils/jwt.ts @@ -0,0 +1,275 @@ +/*! + * 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 * as validator from './validator'; +import * as jwt from 'jsonwebtoken'; +import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; +import { Agent } from 'http'; + +export const ALGORITHM_RS256: jwt.Algorithm = 'RS256' as const; + +// `jsonwebtoken` converts errors from the `getKey` callback to its own `JsonWebTokenError` type +// and prefixes the error message with the following. Use the prefix to identify errors thrown +// from the key provider callback. +// https://github.com/auth0/node-jsonwebtoken/blob/d71e383862fc735991fd2e759181480f066bf138/verify.js#L96 +const JWT_CALLBACK_ERROR_PREFIX = 'error in secret or public key callback: '; + +const NO_MATCHING_KID_ERROR_MESSAGE = 'no-matching-kid-error'; + +export type Dictionary = { [key: string]: any } + +export type DecodedToken = { + header: Dictionary; + payload: Dictionary; +} + +export interface SignatureVerifier { + verify(token: string): Promise; +} + +interface KeyFetcher { + fetchPublicKeys(): Promise<{ [key: string]: string }>; +} + +/** + * Class to fetch public keys from a client certificates URL. + */ +export class UrlKeyFetcher implements KeyFetcher { + private publicKeys: { [key: string]: string }; + private publicKeysExpireAt = 0; + + constructor(private clientCertUrl: string, private readonly httpAgent?: Agent) { + if (!validator.isURL(clientCertUrl)) { + throw new Error( + 'The provided public client certificate URL is not a valid URL.', + ); + } + } + + /** + * Fetches the public keys for the Google certs. + * + * @return A promise fulfilled with public keys for the Google certs. + */ + public fetchPublicKeys(): Promise<{ [key: string]: string }> { + if (this.shouldRefresh()) { + return this.refresh(); + } + return Promise.resolve(this.publicKeys); + } + + /** + * Checks if the cached public keys need to be refreshed. + * + * @returns Whether the keys should be fetched from the client certs url or not. + */ + private shouldRefresh(): boolean { + return !this.publicKeys || this.publicKeysExpireAt <= Date.now(); + } + + private refresh(): Promise<{ [key: string]: string }> { + const client = new HttpClient(); + const request: HttpRequestConfig = { + method: 'GET', + url: this.clientCertUrl, + httpAgent: this.httpAgent, + }; + return client.send(request).then((resp) => { + if (!resp.isJson() || resp.data.error) { + // Treat all non-json messages and messages with an 'error' field as + // error responses. + throw new HttpError(resp); + } + // reset expire at from previous set of keys. + this.publicKeysExpireAt = 0; + if (Object.prototype.hasOwnProperty.call(resp.headers, 'cache-control')) { + const cacheControlHeader: string = resp.headers['cache-control']; + const parts = cacheControlHeader.split(','); + parts.forEach((part) => { + const subParts = part.trim().split('='); + if (subParts[0] === 'max-age') { + const maxAge: number = +subParts[1]; + this.publicKeysExpireAt = Date.now() + (maxAge * 1000); + } + }); + } + this.publicKeys = resp.data; + return resp.data; + }).catch((err) => { + if (err instanceof HttpError) { + let errorMessage = 'Error fetching public keys for Google certs: '; + const resp = err.response; + if (resp.isJson() && resp.data.error) { + errorMessage += `${resp.data.error}`; + if (resp.data.error_description) { + errorMessage += ' (' + resp.data.error_description + ')'; + } + } else { + errorMessage += `${resp.text}`; + } + throw new Error(errorMessage); + } + throw err; + }); + } +} + +/** + * Class for verifing JWT signature with a public key. + */ +export class PublicKeySignatureVerifier implements SignatureVerifier { + constructor(private keyFetcher: KeyFetcher) { + if (!validator.isNonNullObject(keyFetcher)) { + throw new Error('The provided key fetcher is not an object or null.'); + } + } + + public static withCertificateUrl(clientCertUrl: string, httpAgent?: Agent): PublicKeySignatureVerifier { + return new PublicKeySignatureVerifier(new UrlKeyFetcher(clientCertUrl, httpAgent)); + } + + public verify(token: string): Promise { + if (!validator.isString(token)) { + return Promise.reject(new JwtError(JwtErrorCode.INVALID_ARGUMENT, + 'The provided token must be a string.')); + } + + return verifyJwtSignature(token, getKeyCallback(this.keyFetcher), { algorithms: [ALGORITHM_RS256] }); + } +} + +/** + * Class for verifing unsigned (emulator) JWTs. + */ +export class EmulatorSignatureVerifier implements SignatureVerifier { + public verify(token: string): Promise { + // Signature checks skipped for emulator; no need to fetch public keys. + return verifyJwtSignature(token, ''); + } +} + +/** + * Provides a callback to fetch public keys. + * + * @param fetcher KeyFetcher to fetch the keys from. + * @returns A callback function that can be used to get keys in `jsonwebtoken`. + */ +function getKeyCallback(fetcher: KeyFetcher): jwt.GetPublicKeyOrSecret { + return (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => { + const kid = header.kid || ''; + fetcher.fetchPublicKeys().then((publicKeys) => { + if (!Object.prototype.hasOwnProperty.call(publicKeys, kid)) { + callback(new Error(NO_MATCHING_KID_ERROR_MESSAGE)); + } else { + callback(null, publicKeys[kid]); + } + }) + .catch(error => { + callback(error); + }); + } +} + +/** + * Verifies the signature of a JWT using the provided secret or a function to fetch + * the secret or public key. + * + * @param token The JWT to be verfied. + * @param secretOrPublicKey The secret or a function to fetch the secret or public key. + * @param options JWT verification options. + * @returns A Promise resolving for a token with a valid signature. + */ +export function verifyJwtSignature(token: string, secretOrPublicKey: jwt.Secret | jwt.GetPublicKeyOrSecret, + options?: jwt.VerifyOptions): Promise { + if (!validator.isString(token)) { + return Promise.reject(new JwtError(JwtErrorCode.INVALID_ARGUMENT, + 'The provided token must be a string.')); + } + + return new Promise((resolve, reject) => { + jwt.verify(token, secretOrPublicKey, options, + (error: jwt.VerifyErrors | null) => { + if (!error) { + return resolve(); + } + if (error.name === 'TokenExpiredError') { + return reject(new JwtError(JwtErrorCode.TOKEN_EXPIRED, + 'The provided token has expired. Get a fresh token from your ' + + 'client app and try again.')); + } else if (error.name === 'JsonWebTokenError') { + if (error.message && error.message.includes(JWT_CALLBACK_ERROR_PREFIX)) { + const message = error.message.split(JWT_CALLBACK_ERROR_PREFIX).pop() || 'Error fetching public keys.'; + const code = (message === NO_MATCHING_KID_ERROR_MESSAGE) ? JwtErrorCode.NO_MATCHING_KID : + JwtErrorCode.KEY_FETCH_ERROR; + return reject(new JwtError(code, message)); + } + } + return reject(new JwtError(JwtErrorCode.INVALID_SIGNATURE, error.message)); + }); + }); +} + +/** + * Decodes general purpose Firebase JWTs. + * + * @param jwtToken JWT token to be decoded. + * @returns Decoded token containing the header and payload. + */ +export function decodeJwt(jwtToken: string): Promise { + if (!validator.isString(jwtToken)) { + return Promise.reject(new JwtError(JwtErrorCode.INVALID_ARGUMENT, + 'The provided token must be a string.')); + } + + const fullDecodedToken: any = jwt.decode(jwtToken, { + complete: true, + }); + + if (!fullDecodedToken) { + return Promise.reject(new JwtError(JwtErrorCode.INVALID_ARGUMENT, + 'Decoding token failed.')); + } + + const header = fullDecodedToken?.header; + const payload = fullDecodedToken?.payload; + return Promise.resolve({ header, payload }); +} + +/** + * Jwt error code structure. + * + * @param code The error code. + * @param message The error message. + * @constructor + */ +export class JwtError extends Error { + constructor(readonly code: JwtErrorCode, readonly message: string) { + super(message); + (this as any).__proto__ = JwtError.prototype; + } +} + +/** + * JWT error codes. + */ +export enum JwtErrorCode { + INVALID_ARGUMENT = 'invalid-argument', + INVALID_CREDENTIAL = 'invalid-credential', + TOKEN_EXPIRED = 'token-expired', + INVALID_SIGNATURE = 'invalid-token', + NO_MATCHING_KID = 'no-matching-kid-error', + KEY_FETCH_ERROR = 'key-fetch-error', +} diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index d863a0e849..1f7e3546f8 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -16,15 +16,14 @@ 'use strict'; -// Use untyped import syntax for Node built-ins -import https = require('https'); - import * as _ from 'lodash'; import * as chai from 'chai'; import * as nock from 'nock'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; +import { Agent } from 'http'; + import LegacyFirebaseTokenGenerator = require('firebase-token-generator'); import * as mocks from '../../resources/mocks'; @@ -34,7 +33,7 @@ import * as verifier from '../../../src/auth/token-verifier'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { AuthClientErrorCode } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; -import { Algorithm } from 'jsonwebtoken'; +import { JwtError, JwtErrorCode, PublicKeySignatureVerifier } from '../../../src/utils/jwt'; chai.should(); chai.use(sinonChai); @@ -43,76 +42,12 @@ chai.use(chaiAsPromised); const expect = chai.expect; const ONE_HOUR_IN_SECONDS = 60 * 60; -const idTokenPublicCertPath = '/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; - -/** - * Returns a mocked out success response from the URL containing the public keys for the Google certs. - * - * @param {string=} path URL path to which the mock request should be made. If not specified, defaults - * to the URL path of ID token public key certificates. - * @return {Object} A nock response object. - */ -function mockFetchPublicKeys(path: string = idTokenPublicCertPath): nock.Scope { - const mockedResponse: {[key: string]: string} = {}; - mockedResponse[mocks.certificateObject.private_key_id] = mocks.keyPairs[0].public; - return nock('https://www.googleapis.com') - .get(path) - .reply(200, mockedResponse, { - 'cache-control': 'public, max-age=1, must-revalidate, no-transform', - }); -} - -/** - * Returns a mocked out success response from the URL containing the public keys for the Google certs - * which contains a public key which won't match the mocked token. - * - * @return {Object} A nock response object. - */ -function mockFetchWrongPublicKeys(): nock.Scope { - const mockedResponse: {[key: string]: string} = {}; - mockedResponse[mocks.certificateObject.private_key_id] = mocks.keyPairs[1].public; - return nock('https://www.googleapis.com') - .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com') - .reply(200, mockedResponse, { - 'cache-control': 'public, max-age=1, must-revalidate, no-transform', - }); -} - -/** - * Returns a mocked out error response from the URL containing the public keys for the Google certs. - * The status code is 200 but the response itself will contain an 'error' key. - * - * @return {Object} A nock response object. - */ -function mockFetchPublicKeysWithErrorResponse(): nock.Scope { - return nock('https://www.googleapis.com') - .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com') - .reply(200, { - error: 'message', - error_description: 'description', // eslint-disable-line @typescript-eslint/camelcase - }); -} - -/** - * Returns a mocked out failed response from the URL containing the public keys for the Google certs. - * The status code is non-200 and the response itself will fail. - * - * @return {Object} A nock response object. - */ -function mockFailedFetchPublicKeys(): nock.Scope { - return nock('https://www.googleapis.com') - .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com') - .replyWithError('message'); -} function createTokenVerifier( - app: FirebaseApp, - options: { algorithm?: Algorithm } = {} + app: FirebaseApp ): verifier.FirebaseTokenVerifier { - const algorithm = options.algorithm || 'RS256'; return new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', - algorithm, 'https://securetoken.google.com/', verifier.ID_TOKEN_INFO, app @@ -125,14 +60,12 @@ describe('FirebaseTokenVerifier', () => { let tokenVerifier: verifier.FirebaseTokenVerifier; let tokenGenerator: FirebaseTokenGenerator; let clock: sinon.SinonFakeTimers | undefined; - let httpsSpy: sinon.SinonSpy; beforeEach(() => { // Needed to generate custom token for testing. app = mocks.app(); const cert = new ServiceAccountCredential(mocks.certificateObject); tokenGenerator = new FirebaseTokenGenerator(new ServiceAccountSigner(cert)); tokenVerifier = createTokenVerifier(app); - httpsSpy = sinon.spy(https, 'request'); }); afterEach(() => { @@ -140,7 +73,6 @@ describe('FirebaseTokenVerifier', () => { clock.restore(); clock = undefined; } - httpsSpy.restore(); }); after(() => { @@ -152,7 +84,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { tokenVerifier = new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', 'https://www.example.com/issuer/', { url: 'https://docs.example.com/verify-tokens', @@ -172,7 +103,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { new verifier.FirebaseTokenVerifier( invalidCertUrl as any, - 'RS256', 'https://www.example.com/issuer/', verifier.ID_TOKEN_INFO, app, @@ -181,27 +111,12 @@ describe('FirebaseTokenVerifier', () => { }); }); - const invalidAlgorithms = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '']; - invalidAlgorithms.forEach((invalidAlgorithm) => { - it('should throw given an invalid algorithm: ' + JSON.stringify(invalidAlgorithm), () => { - expect(() => { - new verifier.FirebaseTokenVerifier( - 'https://www.example.com/publicKeys', - invalidAlgorithm as any, - 'https://www.example.com/issuer/', - verifier.ID_TOKEN_INFO, - app); - }).to.throw('The provided JWT algorithm is an empty string.'); - }); - }); - const invalidIssuers = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, 'file://invalid']; invalidIssuers.forEach((invalidIssuer) => { it('should throw given a non-URL issuer: ' + JSON.stringify(invalidIssuer), () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', invalidIssuer as any, verifier.ID_TOKEN_INFO, app, @@ -216,7 +131,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', 'https://www.example.com/issuer/', { url: 'https://docs.example.com/verify-tokens', @@ -237,7 +151,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', 'https://www.example.com/issuer/', { url: 'https://docs.example.com/verify-tokens', @@ -258,7 +171,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', 'https://www.example.com/issuer/', { url: 'https://docs.example.com/verify-tokens', @@ -279,7 +191,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', 'https://www.example.com/issuer/', { url: 'https://docs.example.com/verify-tokens', @@ -297,10 +208,14 @@ describe('FirebaseTokenVerifier', () => { describe('verifyJWT()', () => { let mockedRequests: nock.Scope[] = []; + let stubs: sinon.SinonStub[] = []; afterEach(() => { _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); mockedRequests = []; + + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; }); it('should throw given no Firebase JWT token', () => { @@ -331,7 +246,6 @@ describe('FirebaseTokenVerifier', () => { it('should throw if the token verifier was initialized with no "project_id"', () => { const tokenVerifierWithNoProjectId = new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', - 'RS256', 'https://securetoken.google.com/', verifier.ID_TOKEN_INFO, mocks.mockCredentialApp(), @@ -351,21 +265,6 @@ describe('FirebaseTokenVerifier', () => { .should.eventually.be.rejectedWith('Firebase ID token has no "kid" claim'); }); - it('should be rejected given a Firebase JWT token with a kid which does not match any of the ' + - 'actual public keys', () => { - mockedRequests.push(mockFetchPublicKeys()); - - const mockIdToken = mocks.generateIdToken({ - header: { - kid: 'wrongkid', - }, - }); - - return tokenVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has "kid" claim which does not ' + - 'correspond to a known public key'); - }); - it('should be rejected given a Firebase JWT token with an incorrect algorithm', () => { const mockIdToken = mocks.generateIdToken({ algorithm: 'HS256', @@ -392,8 +291,26 @@ describe('FirebaseTokenVerifier', () => { .should.eventually.be.rejectedWith('Firebase ID token has incorrect "iss" (issuer) claim'); }); + it('should be rejected when the verifier throws no maching kid error', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.NO_MATCHING_KID, 'No matching key ID.')); + stubs.push(verifierStub); + + const mockIdToken = mocks.generateIdToken({ + header: { + kid: 'wrongkid', + }, + }); + + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has "kid" claim which does not ' + + 'correspond to a known public key'); + }); + it('should be rejected given a Firebase JWT token with a subject with greater than 128 characters', () => { - mockedRequests.push(mockFetchPublicKeys()); + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); // uid of length 128 should be fulfilled let uid = Array(129).join('a'); @@ -414,62 +331,59 @@ describe('FirebaseTokenVerifier', () => { }); }); - it('should be rejected given an expired Firebase JWT token', () => { - mockedRequests.push(mockFetchPublicKeys()); - - clock = sinon.useFakeTimers(1000); + it('should be rejected when the verifier throws for expired Firebase JWT token', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.TOKEN_EXPIRED, 'Expired token.')); + stubs.push(verifierStub); const mockIdToken = mocks.generateIdToken(); - clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); - - // Token should still be valid - return tokenVerifier.verifyJWT(mockIdToken).then(() => { - clock!.tick(1); - - // Token should now be invalid - return tokenVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has expired. Get a fresh ID token from your client ' + - 'app and try again (auth/id-token-expired)') - .and.have.property('code', 'auth/id-token-expired'); - }); + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has expired. Get a fresh ID token from your client ' + + 'app and try again (auth/id-token-expired)') + .and.have.property('code', 'auth/id-token-expired'); }); - it('should be rejected given an expired Firebase session cookie', () => { + it('should be rejected when the verifier throws for expired Firebase session cookie', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.TOKEN_EXPIRED, 'Expired token.')); + stubs.push(verifierStub); + const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', - 'RS256', 'https://session.firebase.google.com/', verifier.SESSION_COOKIE_INFO, app, ); - mockedRequests.push(mockFetchPublicKeys('/identitytoolkit/v3/relyingparty/publicKeys')); - - clock = sinon.useFakeTimers(1000); const mockSessionCookie = mocks.generateSessionCookie(); - clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie) + .should.eventually.be.rejectedWith('Firebase session cookie has expired. Get a fresh session cookie from ' + + 'your client app and try again (auth/session-cookie-expired).') + .and.have.property('code', 'auth/session-cookie-expired'); + }); - // Cookie should still be valid - return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie).then(() => { - clock!.tick(1); + it('should be rejected when the verifier throws invalid signature for a Firebase JWT token.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.INVALID_SIGNATURE, 'invalid signature.')); + stubs.push(verifierStub); - // Cookie should now be invalid - return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie) - .should.eventually.be.rejectedWith('Firebase session cookie has expired. Get a fresh session cookie from ' + - 'your client app and try again (auth/session-cookie-expired).') - .and.have.property('code', 'auth/session-cookie-expired'); - }); + const mockIdToken = mocks.generateIdToken(); + + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has invalid signature'); }); - it('should be rejected given a Firebase JWT token which was not signed with the kid it specifies', () => { - mockedRequests.push(mockFetchWrongPublicKeys()); + it('should be rejected when the verifier throws key fetch error.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.KEY_FETCH_ERROR, 'Error fetching public keys.')); + stubs.push(verifierStub); const mockIdToken = mocks.generateIdToken(); return tokenVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has invalid signature'); + .should.eventually.be.rejectedWith('Error fetching public keys.'); }); it('should be rejected given a custom token with error using article "an" before JWT short name', () => { @@ -483,7 +397,6 @@ describe('FirebaseTokenVerifier', () => { it('should be rejected given a custom token with error using article "a" before JWT short name', () => { const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', - 'RS256', 'https://session.firebase.google.com/', verifier.SESSION_COOKIE_INFO, app, @@ -509,7 +422,6 @@ describe('FirebaseTokenVerifier', () => { it('should be rejected given a legacy custom token with error using article "a" before JWT short name', () => { const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', - 'RS256', 'https://session.firebase.google.com/', verifier.SESSION_COOKIE_INFO, app, @@ -524,8 +436,26 @@ describe('FirebaseTokenVerifier', () => { 'verifySessionCookie() expects a session cookie, but was given a legacy custom token'); }); + it('AppOptions.httpAgent should be passed to the verifier', () => { + const mockAppWithAgent = mocks.appWithOptions({ + httpAgent: new Agent() + }); + const agentForApp = mockAppWithAgent.options.httpAgent; + const verifierSpy = sinon.spy(PublicKeySignatureVerifier, 'withCertificateUrl'); + + expect(verifierSpy.args).to.be.empty; + + createTokenVerifier(mockAppWithAgent); + + expect(verifierSpy.args[0][1]).to.equal(agentForApp); + + verifierSpy.restore(); + }); + it('should be fulfilled with decoded claims given a valid Firebase JWT token', () => { - mockedRequests.push(mockFetchPublicKeys()); + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); clock = sinon.useFakeTimers(1000); @@ -567,16 +497,6 @@ describe('FirebaseTokenVerifier', () => { }); }); - it('should not decode a signed token when the algorithm is set to none (emulator)', async () => { - clock = sinon.useFakeTimers(1000); - - const emulatorVerifier = createTokenVerifier(app, { algorithm: 'none' }); - const mockIdToken = mocks.generateIdToken(); - - await emulatorVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has incorrect algorithm. Expected "none"'); - }); - it('should not decode an unsigned token when the algorithm is not overridden (emulator)', async () => { clock = sinon.useFakeTimers(1000); @@ -593,110 +513,5 @@ describe('FirebaseTokenVerifier', () => { await tokenVerifier.verifyJWT(idTokenNoHeader) .should.eventually.be.rejectedWith('Firebase ID token has no "kid" claim.'); }); - - it('should use the given HTTP Agent', () => { - const agent = new https.Agent(); - const appWithAgent = mocks.appWithOptions({ - credential: mocks.credential, - httpAgent: agent, - }); - tokenVerifier = new verifier.FirebaseTokenVerifier( - 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', - 'RS256', - 'https://securetoken.google.com/', - verifier.ID_TOKEN_INFO, - appWithAgent, - ); - mockedRequests.push(mockFetchPublicKeys()); - - clock = sinon.useFakeTimers(1000); - - const mockIdToken = mocks.generateIdToken(); - - return tokenVerifier.verifyJWT(mockIdToken) - .then(() => { - expect(https.request).to.have.been.calledOnce; - expect(httpsSpy.args[0][0].agent).to.equal(agent); - }); - }); - - it('should not fetch the Google cert public keys until the first time verifyJWT() is called', () => { - mockedRequests.push(mockFetchPublicKeys()); - - const testTokenVerifier = new verifier.FirebaseTokenVerifier( - 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', - 'RS256', - 'https://securetoken.google.com/', - verifier.ID_TOKEN_INFO, - app, - ); - expect(https.request).not.to.have.been.called; - - const mockIdToken = mocks.generateIdToken(); - - return testTokenVerifier.verifyJWT(mockIdToken) - .then(() => expect(https.request).to.have.been.calledOnce); - }); - - it('should not re-fetch the Google cert public keys every time verifyJWT() is called', () => { - mockedRequests.push(mockFetchPublicKeys()); - - const mockIdToken = mocks.generateIdToken(); - - return tokenVerifier.verifyJWT(mockIdToken).then(() => { - expect(https.request).to.have.been.calledOnce; - return tokenVerifier.verifyJWT(mockIdToken); - }).then(() => expect(https.request).to.have.been.calledOnce); - }); - - it('should refresh the Google cert public keys after the "max-age" on the request expires', () => { - mockedRequests.push(mockFetchPublicKeys()); - mockedRequests.push(mockFetchPublicKeys()); - mockedRequests.push(mockFetchPublicKeys()); - - clock = sinon.useFakeTimers(1000); - - const mockIdToken = mocks.generateIdToken(); - - return tokenVerifier.verifyJWT(mockIdToken).then(() => { - expect(https.request).to.have.been.calledOnce; - clock!.tick(999); - return tokenVerifier.verifyJWT(mockIdToken); - }).then(() => { - expect(https.request).to.have.been.calledOnce; - clock!.tick(1); - return tokenVerifier.verifyJWT(mockIdToken); - }).then(() => { - // One second has passed - expect(https.request).to.have.been.calledTwice; - clock!.tick(999); - return tokenVerifier.verifyJWT(mockIdToken); - }).then(() => { - expect(https.request).to.have.been.calledTwice; - clock!.tick(1); - return tokenVerifier.verifyJWT(mockIdToken); - }).then(() => { - // Two seconds have passed - expect(https.request).to.have.been.calledThrice; - }); - }); - - it('should be rejected if fetching the Google public keys fails', () => { - mockedRequests.push(mockFailedFetchPublicKeys()); - - const mockIdToken = mocks.generateIdToken(); - - return tokenVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('message'); - }); - - it('should be rejected if fetching the Google public keys returns a response with an error message', () => { - mockedRequests.push(mockFetchPublicKeysWithErrorResponse()); - - const mockIdToken = mocks.generateIdToken(); - - return tokenVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('Error fetching public keys for Google certs: message (description)'); - }); }); }); diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index efbe059e96..e8c5a6d17d 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -25,6 +25,7 @@ import './utils/index.spec'; import './utils/error.spec'; import './utils/validator.spec'; import './utils/api-request.spec'; +import './utils/jwt.spec'; // Auth import './auth/auth.spec'; diff --git a/test/unit/utils/jwt.spec.ts b/test/unit/utils/jwt.spec.ts new file mode 100644 index 0000000000..525608feef --- /dev/null +++ b/test/unit/utils/jwt.spec.ts @@ -0,0 +1,541 @@ +/*! + * 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'; + +// Use untyped import syntax for Node built-ins +import https = require('https'); + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as nock from 'nock'; +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 * as jwtUtil from '../../../src/utils/jwt'; + +const expect = chai.expect; + +const ONE_HOUR_IN_SECONDS = 60 * 60; +const publicCertPath = '/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; + +/** + * Returns a mocked out success response from the URL containing the public keys for the Google certs. + * + * @param {string=} path URL path to which the mock request should be made. If not specified, defaults + * to the URL path of ID token public key certificates. + * @return {Object} A nock response object. + */ +function mockFetchPublicKeys(path: string = publicCertPath): nock.Scope { + const mockedResponse: { [key: string]: string } = {}; + mockedResponse[mocks.certificateObject.private_key_id] = mocks.keyPairs[0].public; + return nock('https://www.googleapis.com') + .get(path) + .reply(200, mockedResponse, { + 'cache-control': 'public, max-age=1, must-revalidate, no-transform', + }); +} + +/** + * Returns a mocked out error response from the URL containing the public keys for the Google certs. + * The status code is 200 but the response itself will contain an 'error' key. + * + * @return {Object} A nock response object. + */ + +function mockFetchPublicKeysWithErrorResponse(): nock.Scope { + return nock('https://www.googleapis.com') + .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com') + .reply(200, { + error: 'message', + error_description: 'description', // eslint-disable-line @typescript-eslint/camelcase + }); +} + +/** + * Returns a mocked out failed response from the URL containing the public keys for the Google certs. + * The status code is non-200 and the response itself will fail. + * + * @return {Object} A nock response object. + */ + +function mockFailedFetchPublicKeys(): nock.Scope { + return nock('https://www.googleapis.com') + .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com') + .replyWithError('message'); +} + + +const TOKEN_PAYLOAD = { + one: 'uno', + two: 'dos', + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: mocks.projectId, + iss: 'https://securetoken.google.com/' + mocks.projectId, + sub: mocks.uid, +}; + +const DECODED_SIGNED_TOKEN: jwtUtil.DecodedToken = { + header: { + alg: 'RS256', + kid: 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd', + typ: 'JWT', + }, + payload: TOKEN_PAYLOAD +}; + +const DECODED_UNSIGNED_TOKEN: jwtUtil.DecodedToken = { + header: { + alg: 'none', + typ: 'JWT', + }, + payload: TOKEN_PAYLOAD +}; + +const VALID_PUBLIC_KEYS_RESPONSE: { [key: string]: string } = {}; +VALID_PUBLIC_KEYS_RESPONSE[mocks.certificateObject.private_key_id] = mocks.keyPairs[0].public; + +describe('decodeJwt', () => { + let clock: sinon.SinonFakeTimers | undefined; + + afterEach(() => { + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + it('should reject given no token', () => { + return (jwtUtil.decodeJwt as any)() + .should.eventually.be.rejectedWith('The provided token must be a string.'); + }); + + const invalidIdTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidIdTokens.forEach((invalidIdToken) => { + it('should reject given a non-string token: ' + JSON.stringify(invalidIdToken), () => { + return jwtUtil.decodeJwt(invalidIdToken as any) + .should.eventually.be.rejectedWith('The provided token must be a string.'); + }); + }); + + it('should reject given an empty string token', () => { + return jwtUtil.decodeJwt('') + .should.eventually.be.rejectedWith('Decoding token failed.'); + }); + + it('should reject given an invalid token', () => { + return jwtUtil.decodeJwt('invalid-token') + .should.eventually.be.rejectedWith('Decoding token failed.'); + }); + + it('should be fulfilled with decoded claims given a valid signed token', () => { + clock = sinon.useFakeTimers(1000); + + const mockIdToken = mocks.generateIdToken(); + + return jwtUtil.decodeJwt(mockIdToken) + .should.eventually.be.fulfilled.and.deep.equal(DECODED_SIGNED_TOKEN); + }); + + it('should be fulfilled with decoded claims given a valid unsigned token', () => { + clock = sinon.useFakeTimers(1000); + + const mockIdToken = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }); + + return jwtUtil.decodeJwt(mockIdToken) + .should.eventually.be.fulfilled.and.deep.equal(DECODED_UNSIGNED_TOKEN); + }); +}); + + +describe('verifyJwtSignature', () => { + let clock: sinon.SinonFakeTimers | undefined; + + afterEach(() => { + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + it('should throw given no token', () => { + return (jwtUtil.verifyJwtSignature as any)() + .should.eventually.be.rejectedWith('The provided token must be a string.'); + }); + + const invalidIdTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidIdTokens.forEach((invalidIdToken) => { + it('should reject given a non-string token: ' + JSON.stringify(invalidIdToken), () => { + return jwtUtil.verifyJwtSignature(invalidIdToken as any, mocks.keyPairs[0].public) + .should.eventually.be.rejectedWith('The provided token must be a string.'); + }); + }); + + it('should reject given an empty string token', () => { + return jwtUtil.verifyJwtSignature('', mocks.keyPairs[0].public) + .should.eventually.be.rejectedWith('jwt must be provided'); + }); + + it('should be fulfilled given a valid signed token and public key', () => { + const mockIdToken = mocks.generateIdToken(); + + return jwtUtil.verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + { algorithms: [jwtUtil.ALGORITHM_RS256] }) + .should.eventually.be.fulfilled; + }); + + it('should be fulfilled given a valid unsigned (emulator) token and no public key', () => { + const mockIdToken = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }); + + return jwtUtil.verifyJwtSignature(mockIdToken, '') + .should.eventually.be.fulfilled; + }); + + it('should be fulfilled given a valid signed token and a function to provide public keys', () => { + const mockIdToken = mocks.generateIdToken(); + const getKeyCallback = (_: any, callback: any): void => callback(null, mocks.keyPairs[0].public); + + return jwtUtil.verifyJwtSignature(mockIdToken, getKeyCallback, + { algorithms: [jwtUtil.ALGORITHM_RS256] }) + .should.eventually.be.fulfilled; + }); + + it('should be rejected when the given algorithm does not match the token', () => { + const mockIdToken = mocks.generateIdToken(); + + return jwtUtil.verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + { algorithms: ['RS384'] }) + .should.eventually.be.rejectedWith('invalid algorithm') + .with.property('code', jwtUtil.JwtErrorCode.INVALID_SIGNATURE); + }); + + it('should be rejected given an expired token', () => { + clock = sinon.useFakeTimers(1000); + const mockIdToken = mocks.generateIdToken(); + clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + + // token should still be valid + return jwtUtil.verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + { algorithms: [jwtUtil.ALGORITHM_RS256] }) + .then(() => { + clock!.tick(1); + + // token should now be invalid + return jwtUtil.verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + { algorithms: [jwtUtil.ALGORITHM_RS256] }) + .should.eventually.be.rejectedWith( + 'The provided token has expired. Get a fresh token from your client app and try again.' + ) + .with.property('code', jwtUtil.JwtErrorCode.TOKEN_EXPIRED); + }); + }); + + it('should be rejected with correct public key fetch error.', () => { + const mockIdToken = mocks.generateIdToken(); + const getKeyCallback = (_: any, callback: any): void => + callback(new Error('key fetch failed.')); + + return jwtUtil.verifyJwtSignature(mockIdToken, getKeyCallback, + { algorithms: [jwtUtil.ALGORITHM_RS256] }) + .should.eventually.be.rejectedWith('key fetch failed.') + .with.property('code', jwtUtil.JwtErrorCode.KEY_FETCH_ERROR); + }); + + it('should be rejected with correct no matching key id found error.', () => { + const mockIdToken = mocks.generateIdToken(); + const getKeyCallback = (_: any, callback: any): void => + callback(new Error('no-matching-kid-error')); + + return jwtUtil.verifyJwtSignature(mockIdToken, getKeyCallback, + { algorithms: [jwtUtil.ALGORITHM_RS256] }) + .should.eventually.be.rejectedWith('no-matching-kid-error') + .with.property('code', jwtUtil.JwtErrorCode.NO_MATCHING_KID); + }); + + it('should be rejected given a public key that does not match the token.', () => { + const mockIdToken = mocks.generateIdToken(); + + return jwtUtil.verifyJwtSignature(mockIdToken, mocks.keyPairs[1].public, + { algorithms: [jwtUtil.ALGORITHM_RS256] }) + .should.eventually.be.rejectedWith('invalid signature') + .with.property('code', jwtUtil.JwtErrorCode.INVALID_SIGNATURE); + }); + + it('should be rejected given an invalid JWT.', () => { + return jwtUtil.verifyJwtSignature('invalid-token', mocks.keyPairs[0].public) + .should.eventually.be.rejectedWith('jwt malformed') + .with.property('code', jwtUtil.JwtErrorCode.INVALID_SIGNATURE); + }); +}); + +describe('PublicKeySignatureVerifier', () => { + let stubs: sinon.SinonStub[] = []; + const verifier = new jwtUtil.PublicKeySignatureVerifier( + new jwtUtil.UrlKeyFetcher('https://www.example.com/publicKeys')); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + describe('Constructor', () => { + it('should not throw when valid key fetcher is provided', () => { + expect(() => { + new jwtUtil.PublicKeySignatureVerifier( + new jwtUtil.UrlKeyFetcher('https://www.example.com/publicKeys')); + }).not.to.throw(); + }); + + const invalidKeyFetchers = [null, NaN, 0, 1, true, false, [], ['a'], _.noop, '', 'a']; + invalidKeyFetchers.forEach((invalidKeyFetcher) => { + it('should throw given an invalid key fetcher: ' + JSON.stringify(invalidKeyFetcher), () => { + expect(() => { + new jwtUtil.PublicKeySignatureVerifier(invalidKeyFetchers as any); + }).to.throw('The provided key fetcher is not an object or null.'); + }); + }); + }); + + describe('withCertificateUrl', () => { + it('should return a PublicKeySignatureVerifier instance when a valid cert url is provided', () => { + expect( + jwtUtil.PublicKeySignatureVerifier.withCertificateUrl('https://www.example.com/publicKeys') + ).to.be.an.instanceOf(jwtUtil.PublicKeySignatureVerifier); + }); + }); + + describe('verify', () => { + it('should throw given no token', () => { + return (verifier.verify as any)() + .should.eventually.be.rejectedWith('The provided token must be a string.'); + }); + + const invalidIdTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidIdTokens.forEach((invalidIdToken) => { + it('should reject given a non-string token: ' + JSON.stringify(invalidIdToken), () => { + return verifier.verify(invalidIdToken as any) + .should.eventually.be.rejectedWith('The provided token must be a string.'); + }); + }); + + it('should reject given an empty string token', () => { + return verifier.verify('') + .should.eventually.be.rejectedWith('jwt must be provided'); + }); + + it('should be fullfilled given a valid token', () => { + const keyFetcherStub = sinon.stub(jwtUtil.UrlKeyFetcher.prototype, 'fetchPublicKeys') + .resolves(VALID_PUBLIC_KEYS_RESPONSE); + stubs.push(keyFetcherStub); + const mockIdToken = mocks.generateIdToken(); + + return verifier.verify(mockIdToken).should.eventually.be.fulfilled; + }); + + it('should be rejected given a token with an incorrect algorithm', () => { + const keyFetcherStub = sinon.stub(jwtUtil.UrlKeyFetcher.prototype, 'fetchPublicKeys') + .resolves(VALID_PUBLIC_KEYS_RESPONSE); + stubs.push(keyFetcherStub); + const mockIdToken = mocks.generateIdToken({ + algorithm: 'HS256', + }); + + return verifier.verify(mockIdToken).should.eventually.be + .rejectedWith('invalid algorithm') + .with.property('code', jwtUtil.JwtErrorCode.INVALID_SIGNATURE); + }); + + // tests to cover the private getKeyCallback function. + it('should reject when no matching kid found', () => { + const keyFetcherStub = sinon.stub(jwtUtil.UrlKeyFetcher.prototype, 'fetchPublicKeys') + .resolves({ 'not-a-matching-key': 'public-key' }); + stubs.push(keyFetcherStub); + const mockIdToken = mocks.generateIdToken(); + + return verifier.verify(mockIdToken).should.eventually.be + .rejectedWith('no-matching-kid-error') + .with.property('code', jwtUtil.JwtErrorCode.NO_MATCHING_KID); + }); + + it('should reject when an error occurs while fetching the keys', () => { + const keyFetcherStub = sinon.stub(jwtUtil.UrlKeyFetcher.prototype, 'fetchPublicKeys') + .rejects(new Error('Error fetching public keys.')); + stubs.push(keyFetcherStub); + const mockIdToken = mocks.generateIdToken(); + + return verifier.verify(mockIdToken).should.eventually.be + .rejectedWith('Error fetching public keys.') + .with.property('code', jwtUtil.JwtErrorCode.KEY_FETCH_ERROR); + }); + }); +}); + +describe('EmulatorSignatureVerifier', () => { + const emulatorVerifier = new jwtUtil.EmulatorSignatureVerifier(); + + describe('verify', () => { + it('should be fullfilled given a valid unsigned (emulator) token', () => { + const mockIdToken = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }); + + return emulatorVerifier.verify(mockIdToken).should.eventually.be.fulfilled; + }); + + it('should be rejected given a valid signed (non-emulator) token', () => { + const mockIdToken = mocks.generateIdToken(); + + return emulatorVerifier.verify(mockIdToken).should.eventually.be.rejected; + }); + }); +}); + +describe('UrlKeyFetcher', () => { + const agent = new https.Agent(); + let keyFetcher: jwtUtil.UrlKeyFetcher; + let clock: sinon.SinonFakeTimers | undefined; + let httpsSpy: sinon.SinonSpy; + + beforeEach(() => { + keyFetcher = new jwtUtil.UrlKeyFetcher( + 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', + agent); + httpsSpy = sinon.spy(https, 'request'); + }); + + afterEach(() => { + if (clock) { + clock.restore(); + clock = undefined; + } + httpsSpy.restore(); + }); + + after(() => { + nock.cleanAll(); + }); + + describe('Constructor', () => { + it('should not throw when valid key parameters are provided', () => { + expect(() => { + new jwtUtil.UrlKeyFetcher('https://www.example.com/publicKeys', agent); + }).not.to.throw(); + }); + + const invalidCertURLs = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, 'file://invalid']; + invalidCertURLs.forEach((invalidCertUrl) => { + it('should throw given a non-URL public cert: ' + JSON.stringify(invalidCertUrl), () => { + expect(() => { + new jwtUtil.UrlKeyFetcher(invalidCertUrl as any, agent); + }).to.throw('The provided public client certificate URL is not a valid URL.'); + }); + }); + }); + + describe('fetchPublicKeys', () => { + let mockedRequests: nock.Scope[] = []; + + afterEach(() => { + _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); + mockedRequests = []; + }); + + it('should use the given HTTP Agent', () => { + const agent = new https.Agent(); + const urlKeyFetcher = new jwtUtil.UrlKeyFetcher('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', agent); + mockedRequests.push(mockFetchPublicKeys()); + + return urlKeyFetcher.fetchPublicKeys() + .then(() => { + expect(https.request).to.have.been.calledOnce; + expect(httpsSpy.args[0][0].agent).to.equal(agent); + }); + }); + + it('should not fetch the public keys until the first time fetchPublicKeys() is called', () => { + mockedRequests.push(mockFetchPublicKeys()); + + const urlKeyFetcher = new jwtUtil.UrlKeyFetcher('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', agent); + expect(https.request).not.to.have.been.called; + + return urlKeyFetcher.fetchPublicKeys() + .then(() => expect(https.request).to.have.been.calledOnce); + }); + + it('should not re-fetch the public keys every time fetchPublicKeys() is called', () => { + mockedRequests.push(mockFetchPublicKeys()); + + return keyFetcher.fetchPublicKeys().then(() => { + expect(https.request).to.have.been.calledOnce; + return keyFetcher.fetchPublicKeys(); + }).then(() => expect(https.request).to.have.been.calledOnce); + }); + + it('should refresh the public keys after the "max-age" on the request expires', () => { + mockedRequests.push(mockFetchPublicKeys()); + mockedRequests.push(mockFetchPublicKeys()); + mockedRequests.push(mockFetchPublicKeys()); + + clock = sinon.useFakeTimers(1000); + + return keyFetcher.fetchPublicKeys().then(() => { + expect(https.request).to.have.been.calledOnce; + clock!.tick(999); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + expect(https.request).to.have.been.calledOnce; + clock!.tick(1); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + // One second has passed + expect(https.request).to.have.been.calledTwice; + clock!.tick(999); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + expect(https.request).to.have.been.calledTwice; + clock!.tick(1); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + // Two seconds have passed + expect(https.request).to.have.been.calledThrice; + }); + }); + + it('should be rejected if fetching the public keys fails', () => { + mockedRequests.push(mockFailedFetchPublicKeys()); + + return keyFetcher.fetchPublicKeys() + .should.eventually.be.rejectedWith('message'); + }); + + it('should be rejected if fetching the public keys returns a response with an error message', () => { + mockedRequests.push(mockFetchPublicKeysWithErrorResponse()); + + return keyFetcher.fetchPublicKeys() + .should.eventually.be.rejectedWith('Error fetching public keys for Google certs: message (description)'); + }); + }); +}); From 1254850f9b86367afe3b864933e8741db3e7ed54 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 31 Mar 2021 18:22:56 -0400 Subject: [PATCH 091/160] chore: Add Mailgun send email action (#1210) * Add Mailgun send email github action * Add send email action in nightly workflow --- .github/actions/send-email/README.md | 59 + .github/actions/send-email/action.yml | 44 + .github/actions/send-email/dist/index.js | 2098 ++++++++++++++++++ .github/actions/send-email/index.js | 75 + .github/actions/send-email/package-lock.json | 173 ++ .github/actions/send-email/package.json | 23 + .github/workflows/nightly.yml | 30 + test/integration/remote-config.spec.ts | 6 + 8 files changed, 2508 insertions(+) create mode 100644 .github/actions/send-email/README.md create mode 100644 .github/actions/send-email/action.yml create mode 100644 .github/actions/send-email/dist/index.js create mode 100644 .github/actions/send-email/index.js create mode 100644 .github/actions/send-email/package-lock.json create mode 100644 .github/actions/send-email/package.json diff --git a/.github/actions/send-email/README.md b/.github/actions/send-email/README.md new file mode 100644 index 0000000000..ab3f93a154 --- /dev/null +++ b/.github/actions/send-email/README.md @@ -0,0 +1,59 @@ +# Send Email GitHub Action + +This is a minimalistic GitHub Action for sending emails using the Mailgun API. +Specify the Mailgun API key along with the Mailgun domain and message to +be sent. + +## Inputs + +### `api-key` + +**Required** Mailgun API key. + +### `domain` + +**Required** Mailgun domain name. + +### `from` + +**Required** Sender's email address. Ex: 'Hello User ' (defaults to 'user@{domain}`). + +### `to` + +**Required** Recipient's email address. You can use commas to separate multiple recipients. + +### `cc` + +Email addresses to Cc. + +### `subject` + +**Required** Message subject. + +### `text` + +Text body of the message. + +### `html` + +HTML body of the message. + +## Example usage + +``` +- name: Send Email + uses: firebase/firebase-admin-node/.github/actions/send-email + with: + api-key: ${{ secrets.MAILGUN_API_KEY }} + domain: ${{ secrets.MAILGUN_DOMAIN }} + from: 'User ' + html: '

    Testing some Mailgun awesomness!

    ' + to: 'foo@example.com' +``` + +## Implementation + +This Action uses the `mailgun.js` NPM package to send Emails. + +When making a code change remember to run `npm run pack` to rebuild the +`dist/index.js` file which is the executable of this Action. diff --git a/.github/actions/send-email/action.yml b/.github/actions/send-email/action.yml new file mode 100644 index 0000000000..65956f9a12 --- /dev/null +++ b/.github/actions/send-email/action.yml @@ -0,0 +1,44 @@ +# 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. + +name: 'Send email Action' +description: 'Send emails using Mailgun from GitHub Actions workflows.' +inputs: + api-key: + description: Mailgun API key. + required: true + domain: + description: Mailgun domain name. + required: true + from: + description: Senders name and email address. + required: true + to: + description: Recipient's email address. You can use commas to separate multiple recipients. + required: true + cc: + description: Email addresses to Cc. + required: false + subject: + description: Message subject. + required: true + text: + description: Text body of the message. + required: false + html: + description: HTML body of the message. + required: false +runs: + using: 'node12' + main: 'dist/index.js' diff --git a/.github/actions/send-email/dist/index.js b/.github/actions/send-email/dist/index.js new file mode 100644 index 0000000000..22a6566c3f --- /dev/null +++ b/.github/actions/send-email/dist/index.js @@ -0,0 +1,2098 @@ +module.exports = +/******/ (function(modules, runtime) { // webpackBootstrap +/******/ "use strict"; +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ __webpack_require__.ab = __dirname + "/"; +/******/ +/******/ // the startup function +/******/ function startup() { +/******/ // Load entry module and return exports +/******/ return __webpack_require__(104); +/******/ }; +/******/ +/******/ // run startup +/******/ return startup(); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ 2: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var abort = __webpack_require__(762) + , async = __webpack_require__(792) + ; + +// API +module.exports = terminator; + +/** + * Terminates jobs in the attached state context + * + * @this AsyncKitState# + * @param {function} callback - final callback to invoke after termination + */ +function terminator(callback) +{ + if (!Object.keys(this.jobs).length) + { + return; + } + + // fast forward iteration index + this.index = this.size; + + // abort jobs + abort(this); + + // send back results we have so far + async(callback)(null, this.results); +} + + +/***/ }), + +/***/ 70: +/***/ (function(module) { + +// populates missing values +module.exports = function(dst, src) { + + Object.keys(src).forEach(function(prop) + { + dst[prop] = dst[prop] || src[prop]; + }); + + return dst; +}; + + +/***/ }), + +/***/ 82: +/***/ (function(__unusedmodule, exports) { + +"use strict"; + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Sanitizes an input into a string so it can be passed into issueCommand safely + * @param input input to sanitize into a string + */ +function toCommandValue(input) { + if (input === null || input === undefined) { + return ''; + } + else if (typeof input === 'string' || input instanceof String) { + return input; + } + return JSON.stringify(input); +} +exports.toCommandValue = toCommandValue; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 87: +/***/ (function(module) { + +module.exports = require("os"); + +/***/ }), + +/***/ 89: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var iterate = __webpack_require__(258) + , initState = __webpack_require__(125) + , terminator = __webpack_require__(2) + ; + +// Public API +module.exports = parallel; + +/** + * Runs iterator over provided array elements in parallel + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function parallel(list, iterator, callback) +{ + var state = initState(list); + + while (state.index < (state['keyedList'] || list).length) + { + iterate(list, iterator, state, function(error, result) + { + if (error) + { + callback(error, result); + return; + } + + // looks like it's the last one + if (Object.keys(state.jobs).length === 0) + { + callback(null, state.results); + return; + } + }); + + state.index++; + } + + return terminator.bind(state, callback); +} + + +/***/ }), + +/***/ 102: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +// For internal use, subject to change. +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +const fs = __importStar(__webpack_require__(747)); +const os = __importStar(__webpack_require__(87)); +const utils_1 = __webpack_require__(82); +function issueCommand(command, message) { + const filePath = process.env[`GITHUB_${command}`]; + if (!filePath) { + throw new Error(`Unable to find environment variable for file command ${command}`); + } + if (!fs.existsSync(filePath)) { + throw new Error(`Missing file at path: ${filePath}`); + } + fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, { + encoding: 'utf8' + }); +} +exports.issueCommand = issueCommand; +//# sourceMappingURL=file-command.js.map + +/***/ }), + +/***/ 104: +/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) { + +/*! + * 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. + */ + +const core = __webpack_require__(470); +const formData = __webpack_require__(416); +const Mailgun = __webpack_require__(618); + +const mailgun = new Mailgun(formData); +const optionalFields = ['cc', 'text', 'html']; + +function loadConfig() { + return { + apiKey: core.getInput('api-key'), + domain: core.getInput('domain'), + to: core.getInput('to'), + from: core.getInput('from'), + cc: core.getInput('cc'), + subject: core.getInput('subject'), + text: core.getInput('text'), + html: core.getInput('html'), + } +} + +function validate(config) { + for (param in config) { + if (optionalFields.includes(param)) { + continue; + } + validateRequiredParameter(config[param], `'${param}'`); + } +} + +function validateRequiredParameter(value, name) { + if (!isNonEmptyString(value)) { + throw new Error(`${name} must be a non-empty string.`); + } +} + +function sendEmail(config) { + const mg = mailgun.client({ + username: 'api', + key: config.apiKey, + }); + + return mg.messages + .create(domain, config) + .then((resp) => { + core.setOutput('response', resp.message); + return; + }) + .catch((err) => { + core.setFailed(err.message); + }); +} + +function isNonEmptyString(value) { + return typeof value === 'string' && value !== ''; +} + +const config = loadConfig(); +validate(config); +sendEmail(config); + + +/***/ }), + +/***/ 118: +/***/ (function(module) { + +module.exports = {"application/1d-interleaved-parityfec":{"source":"iana"},"application/3gpdash-qoe-report+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/3gpp-ims+xml":{"source":"iana","compressible":true},"application/a2l":{"source":"iana"},"application/activemessage":{"source":"iana"},"application/activity+json":{"source":"iana","compressible":true},"application/alto-costmap+json":{"source":"iana","compressible":true},"application/alto-costmapfilter+json":{"source":"iana","compressible":true},"application/alto-directory+json":{"source":"iana","compressible":true},"application/alto-endpointcost+json":{"source":"iana","compressible":true},"application/alto-endpointcostparams+json":{"source":"iana","compressible":true},"application/alto-endpointprop+json":{"source":"iana","compressible":true},"application/alto-endpointpropparams+json":{"source":"iana","compressible":true},"application/alto-error+json":{"source":"iana","compressible":true},"application/alto-networkmap+json":{"source":"iana","compressible":true},"application/alto-networkmapfilter+json":{"source":"iana","compressible":true},"application/alto-updatestreamcontrol+json":{"source":"iana","compressible":true},"application/alto-updatestreamparams+json":{"source":"iana","compressible":true},"application/aml":{"source":"iana"},"application/andrew-inset":{"source":"iana","extensions":["ez"]},"application/applefile":{"source":"iana"},"application/applixware":{"source":"apache","extensions":["aw"]},"application/atf":{"source":"iana"},"application/atfx":{"source":"iana"},"application/atom+xml":{"source":"iana","compressible":true,"extensions":["atom"]},"application/atomcat+xml":{"source":"iana","compressible":true,"extensions":["atomcat"]},"application/atomdeleted+xml":{"source":"iana","compressible":true,"extensions":["atomdeleted"]},"application/atomicmail":{"source":"iana"},"application/atomsvc+xml":{"source":"iana","compressible":true,"extensions":["atomsvc"]},"application/atsc-dwd+xml":{"source":"iana","compressible":true,"extensions":["dwd"]},"application/atsc-dynamic-event-message":{"source":"iana"},"application/atsc-held+xml":{"source":"iana","compressible":true,"extensions":["held"]},"application/atsc-rdt+json":{"source":"iana","compressible":true},"application/atsc-rsat+xml":{"source":"iana","compressible":true,"extensions":["rsat"]},"application/atxml":{"source":"iana"},"application/auth-policy+xml":{"source":"iana","compressible":true},"application/bacnet-xdd+zip":{"source":"iana","compressible":false},"application/batch-smtp":{"source":"iana"},"application/bdoc":{"compressible":false,"extensions":["bdoc"]},"application/beep+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/calendar+json":{"source":"iana","compressible":true},"application/calendar+xml":{"source":"iana","compressible":true,"extensions":["xcs"]},"application/call-completion":{"source":"iana"},"application/cals-1840":{"source":"iana"},"application/cap+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/cbor":{"source":"iana"},"application/cbor-seq":{"source":"iana"},"application/cccex":{"source":"iana"},"application/ccmp+xml":{"source":"iana","compressible":true},"application/ccxml+xml":{"source":"iana","compressible":true,"extensions":["ccxml"]},"application/cdfx+xml":{"source":"iana","compressible":true,"extensions":["cdfx"]},"application/cdmi-capability":{"source":"iana","extensions":["cdmia"]},"application/cdmi-container":{"source":"iana","extensions":["cdmic"]},"application/cdmi-domain":{"source":"iana","extensions":["cdmid"]},"application/cdmi-object":{"source":"iana","extensions":["cdmio"]},"application/cdmi-queue":{"source":"iana","extensions":["cdmiq"]},"application/cdni":{"source":"iana"},"application/cea":{"source":"iana"},"application/cea-2018+xml":{"source":"iana","compressible":true},"application/cellml+xml":{"source":"iana","compressible":true},"application/cfw":{"source":"iana"},"application/clue+xml":{"source":"iana","compressible":true},"application/clue_info+xml":{"source":"iana","compressible":true},"application/cms":{"source":"iana"},"application/cnrp+xml":{"source":"iana","compressible":true},"application/coap-group+json":{"source":"iana","compressible":true},"application/coap-payload":{"source":"iana"},"application/commonground":{"source":"iana"},"application/conference-info+xml":{"source":"iana","compressible":true},"application/cose":{"source":"iana"},"application/cose-key":{"source":"iana"},"application/cose-key-set":{"source":"iana"},"application/cpl+xml":{"source":"iana","compressible":true},"application/csrattrs":{"source":"iana"},"application/csta+xml":{"source":"iana","compressible":true},"application/cstadata+xml":{"source":"iana","compressible":true},"application/csvm+json":{"source":"iana","compressible":true},"application/cu-seeme":{"source":"apache","extensions":["cu"]},"application/cwt":{"source":"iana"},"application/cybercash":{"source":"iana"},"application/dart":{"compressible":true},"application/dash+xml":{"source":"iana","compressible":true,"extensions":["mpd"]},"application/dashdelta":{"source":"iana"},"application/davmount+xml":{"source":"iana","compressible":true,"extensions":["davmount"]},"application/dca-rft":{"source":"iana"},"application/dcd":{"source":"iana"},"application/dec-dx":{"source":"iana"},"application/dialog-info+xml":{"source":"iana","compressible":true},"application/dicom":{"source":"iana"},"application/dicom+json":{"source":"iana","compressible":true},"application/dicom+xml":{"source":"iana","compressible":true},"application/dii":{"source":"iana"},"application/dit":{"source":"iana"},"application/dns":{"source":"iana"},"application/dns+json":{"source":"iana","compressible":true},"application/dns-message":{"source":"iana"},"application/docbook+xml":{"source":"apache","compressible":true,"extensions":["dbk"]},"application/dots+cbor":{"source":"iana"},"application/dskpp+xml":{"source":"iana","compressible":true},"application/dssc+der":{"source":"iana","extensions":["dssc"]},"application/dssc+xml":{"source":"iana","compressible":true,"extensions":["xdssc"]},"application/dvcs":{"source":"iana"},"application/ecmascript":{"source":"iana","compressible":true,"extensions":["ecma","es"]},"application/edi-consent":{"source":"iana"},"application/edi-x12":{"source":"iana","compressible":false},"application/edifact":{"source":"iana","compressible":false},"application/efi":{"source":"iana"},"application/emergencycalldata.comment+xml":{"source":"iana","compressible":true},"application/emergencycalldata.control+xml":{"source":"iana","compressible":true},"application/emergencycalldata.deviceinfo+xml":{"source":"iana","compressible":true},"application/emergencycalldata.ecall.msd":{"source":"iana"},"application/emergencycalldata.providerinfo+xml":{"source":"iana","compressible":true},"application/emergencycalldata.serviceinfo+xml":{"source":"iana","compressible":true},"application/emergencycalldata.subscriberinfo+xml":{"source":"iana","compressible":true},"application/emergencycalldata.veds+xml":{"source":"iana","compressible":true},"application/emma+xml":{"source":"iana","compressible":true,"extensions":["emma"]},"application/emotionml+xml":{"source":"iana","compressible":true,"extensions":["emotionml"]},"application/encaprtp":{"source":"iana"},"application/epp+xml":{"source":"iana","compressible":true},"application/epub+zip":{"source":"iana","compressible":false,"extensions":["epub"]},"application/eshop":{"source":"iana"},"application/exi":{"source":"iana","extensions":["exi"]},"application/expect-ct-report+json":{"source":"iana","compressible":true},"application/fastinfoset":{"source":"iana"},"application/fastsoap":{"source":"iana"},"application/fdt+xml":{"source":"iana","compressible":true,"extensions":["fdt"]},"application/fhir+json":{"source":"iana","charset":"UTF-8","compressible":true},"application/fhir+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/fido.trusted-apps+json":{"compressible":true},"application/fits":{"source":"iana"},"application/flexfec":{"source":"iana"},"application/font-sfnt":{"source":"iana"},"application/font-tdpfr":{"source":"iana","extensions":["pfr"]},"application/font-woff":{"source":"iana","compressible":false},"application/framework-attributes+xml":{"source":"iana","compressible":true},"application/geo+json":{"source":"iana","compressible":true,"extensions":["geojson"]},"application/geo+json-seq":{"source":"iana"},"application/geopackage+sqlite3":{"source":"iana"},"application/geoxacml+xml":{"source":"iana","compressible":true},"application/gltf-buffer":{"source":"iana"},"application/gml+xml":{"source":"iana","compressible":true,"extensions":["gml"]},"application/gpx+xml":{"source":"apache","compressible":true,"extensions":["gpx"]},"application/gxf":{"source":"apache","extensions":["gxf"]},"application/gzip":{"source":"iana","compressible":false,"extensions":["gz"]},"application/h224":{"source":"iana"},"application/held+xml":{"source":"iana","compressible":true},"application/hjson":{"extensions":["hjson"]},"application/http":{"source":"iana"},"application/hyperstudio":{"source":"iana","extensions":["stk"]},"application/ibe-key-request+xml":{"source":"iana","compressible":true},"application/ibe-pkg-reply+xml":{"source":"iana","compressible":true},"application/ibe-pp-data":{"source":"iana"},"application/iges":{"source":"iana"},"application/im-iscomposing+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/index":{"source":"iana"},"application/index.cmd":{"source":"iana"},"application/index.obj":{"source":"iana"},"application/index.response":{"source":"iana"},"application/index.vnd":{"source":"iana"},"application/inkml+xml":{"source":"iana","compressible":true,"extensions":["ink","inkml"]},"application/iotp":{"source":"iana"},"application/ipfix":{"source":"iana","extensions":["ipfix"]},"application/ipp":{"source":"iana"},"application/isup":{"source":"iana"},"application/its+xml":{"source":"iana","compressible":true,"extensions":["its"]},"application/java-archive":{"source":"apache","compressible":false,"extensions":["jar","war","ear"]},"application/java-serialized-object":{"source":"apache","compressible":false,"extensions":["ser"]},"application/java-vm":{"source":"apache","compressible":false,"extensions":["class"]},"application/javascript":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["js","mjs"]},"application/jf2feed+json":{"source":"iana","compressible":true},"application/jose":{"source":"iana"},"application/jose+json":{"source":"iana","compressible":true},"application/jrd+json":{"source":"iana","compressible":true},"application/json":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["json","map"]},"application/json-patch+json":{"source":"iana","compressible":true},"application/json-seq":{"source":"iana"},"application/json5":{"extensions":["json5"]},"application/jsonml+json":{"source":"apache","compressible":true,"extensions":["jsonml"]},"application/jwk+json":{"source":"iana","compressible":true},"application/jwk-set+json":{"source":"iana","compressible":true},"application/jwt":{"source":"iana"},"application/kpml-request+xml":{"source":"iana","compressible":true},"application/kpml-response+xml":{"source":"iana","compressible":true},"application/ld+json":{"source":"iana","compressible":true,"extensions":["jsonld"]},"application/lgr+xml":{"source":"iana","compressible":true,"extensions":["lgr"]},"application/link-format":{"source":"iana"},"application/load-control+xml":{"source":"iana","compressible":true},"application/lost+xml":{"source":"iana","compressible":true,"extensions":["lostxml"]},"application/lostsync+xml":{"source":"iana","compressible":true},"application/lpf+zip":{"source":"iana","compressible":false},"application/lxf":{"source":"iana"},"application/mac-binhex40":{"source":"iana","extensions":["hqx"]},"application/mac-compactpro":{"source":"apache","extensions":["cpt"]},"application/macwriteii":{"source":"iana"},"application/mads+xml":{"source":"iana","compressible":true,"extensions":["mads"]},"application/manifest+json":{"charset":"UTF-8","compressible":true,"extensions":["webmanifest"]},"application/marc":{"source":"iana","extensions":["mrc"]},"application/marcxml+xml":{"source":"iana","compressible":true,"extensions":["mrcx"]},"application/mathematica":{"source":"iana","extensions":["ma","nb","mb"]},"application/mathml+xml":{"source":"iana","compressible":true,"extensions":["mathml"]},"application/mathml-content+xml":{"source":"iana","compressible":true},"application/mathml-presentation+xml":{"source":"iana","compressible":true},"application/mbms-associated-procedure-description+xml":{"source":"iana","compressible":true},"application/mbms-deregister+xml":{"source":"iana","compressible":true},"application/mbms-envelope+xml":{"source":"iana","compressible":true},"application/mbms-msk+xml":{"source":"iana","compressible":true},"application/mbms-msk-response+xml":{"source":"iana","compressible":true},"application/mbms-protection-description+xml":{"source":"iana","compressible":true},"application/mbms-reception-report+xml":{"source":"iana","compressible":true},"application/mbms-register+xml":{"source":"iana","compressible":true},"application/mbms-register-response+xml":{"source":"iana","compressible":true},"application/mbms-schedule+xml":{"source":"iana","compressible":true},"application/mbms-user-service-description+xml":{"source":"iana","compressible":true},"application/mbox":{"source":"iana","extensions":["mbox"]},"application/media-policy-dataset+xml":{"source":"iana","compressible":true},"application/media_control+xml":{"source":"iana","compressible":true},"application/mediaservercontrol+xml":{"source":"iana","compressible":true,"extensions":["mscml"]},"application/merge-patch+json":{"source":"iana","compressible":true},"application/metalink+xml":{"source":"apache","compressible":true,"extensions":["metalink"]},"application/metalink4+xml":{"source":"iana","compressible":true,"extensions":["meta4"]},"application/mets+xml":{"source":"iana","compressible":true,"extensions":["mets"]},"application/mf4":{"source":"iana"},"application/mikey":{"source":"iana"},"application/mipc":{"source":"iana"},"application/mmt-aei+xml":{"source":"iana","compressible":true,"extensions":["maei"]},"application/mmt-usd+xml":{"source":"iana","compressible":true,"extensions":["musd"]},"application/mods+xml":{"source":"iana","compressible":true,"extensions":["mods"]},"application/moss-keys":{"source":"iana"},"application/moss-signature":{"source":"iana"},"application/mosskey-data":{"source":"iana"},"application/mosskey-request":{"source":"iana"},"application/mp21":{"source":"iana","extensions":["m21","mp21"]},"application/mp4":{"source":"iana","extensions":["mp4s","m4p"]},"application/mpeg4-generic":{"source":"iana"},"application/mpeg4-iod":{"source":"iana"},"application/mpeg4-iod-xmt":{"source":"iana"},"application/mrb-consumer+xml":{"source":"iana","compressible":true,"extensions":["xdf"]},"application/mrb-publish+xml":{"source":"iana","compressible":true,"extensions":["xdf"]},"application/msc-ivr+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/msc-mixer+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/msword":{"source":"iana","compressible":false,"extensions":["doc","dot"]},"application/mud+json":{"source":"iana","compressible":true},"application/multipart-core":{"source":"iana"},"application/mxf":{"source":"iana","extensions":["mxf"]},"application/n-quads":{"source":"iana","extensions":["nq"]},"application/n-triples":{"source":"iana","extensions":["nt"]},"application/nasdata":{"source":"iana"},"application/news-checkgroups":{"source":"iana","charset":"US-ASCII"},"application/news-groupinfo":{"source":"iana","charset":"US-ASCII"},"application/news-transmission":{"source":"iana"},"application/nlsml+xml":{"source":"iana","compressible":true},"application/node":{"source":"iana","extensions":["cjs"]},"application/nss":{"source":"iana"},"application/ocsp-request":{"source":"iana"},"application/ocsp-response":{"source":"iana"},"application/octet-stream":{"source":"iana","compressible":false,"extensions":["bin","dms","lrf","mar","so","dist","distz","pkg","bpk","dump","elc","deploy","exe","dll","deb","dmg","iso","img","msi","msp","msm","buffer"]},"application/oda":{"source":"iana","extensions":["oda"]},"application/odm+xml":{"source":"iana","compressible":true},"application/odx":{"source":"iana"},"application/oebps-package+xml":{"source":"iana","compressible":true,"extensions":["opf"]},"application/ogg":{"source":"iana","compressible":false,"extensions":["ogx"]},"application/omdoc+xml":{"source":"apache","compressible":true,"extensions":["omdoc"]},"application/onenote":{"source":"apache","extensions":["onetoc","onetoc2","onetmp","onepkg"]},"application/oscore":{"source":"iana"},"application/oxps":{"source":"iana","extensions":["oxps"]},"application/p2p-overlay+xml":{"source":"iana","compressible":true,"extensions":["relo"]},"application/parityfec":{"source":"iana"},"application/passport":{"source":"iana"},"application/patch-ops-error+xml":{"source":"iana","compressible":true,"extensions":["xer"]},"application/pdf":{"source":"iana","compressible":false,"extensions":["pdf"]},"application/pdx":{"source":"iana"},"application/pem-certificate-chain":{"source":"iana"},"application/pgp-encrypted":{"source":"iana","compressible":false,"extensions":["pgp"]},"application/pgp-keys":{"source":"iana"},"application/pgp-signature":{"source":"iana","extensions":["asc","sig"]},"application/pics-rules":{"source":"apache","extensions":["prf"]},"application/pidf+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/pidf-diff+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/pkcs10":{"source":"iana","extensions":["p10"]},"application/pkcs12":{"source":"iana"},"application/pkcs7-mime":{"source":"iana","extensions":["p7m","p7c"]},"application/pkcs7-signature":{"source":"iana","extensions":["p7s"]},"application/pkcs8":{"source":"iana","extensions":["p8"]},"application/pkcs8-encrypted":{"source":"iana"},"application/pkix-attr-cert":{"source":"iana","extensions":["ac"]},"application/pkix-cert":{"source":"iana","extensions":["cer"]},"application/pkix-crl":{"source":"iana","extensions":["crl"]},"application/pkix-pkipath":{"source":"iana","extensions":["pkipath"]},"application/pkixcmp":{"source":"iana","extensions":["pki"]},"application/pls+xml":{"source":"iana","compressible":true,"extensions":["pls"]},"application/poc-settings+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/postscript":{"source":"iana","compressible":true,"extensions":["ai","eps","ps"]},"application/ppsp-tracker+json":{"source":"iana","compressible":true},"application/problem+json":{"source":"iana","compressible":true},"application/problem+xml":{"source":"iana","compressible":true},"application/provenance+xml":{"source":"iana","compressible":true,"extensions":["provx"]},"application/prs.alvestrand.titrax-sheet":{"source":"iana"},"application/prs.cww":{"source":"iana","extensions":["cww"]},"application/prs.hpub+zip":{"source":"iana","compressible":false},"application/prs.nprend":{"source":"iana"},"application/prs.plucker":{"source":"iana"},"application/prs.rdf-xml-crypt":{"source":"iana"},"application/prs.xsf+xml":{"source":"iana","compressible":true},"application/pskc+xml":{"source":"iana","compressible":true,"extensions":["pskcxml"]},"application/pvd+json":{"source":"iana","compressible":true},"application/qsig":{"source":"iana"},"application/raml+yaml":{"compressible":true,"extensions":["raml"]},"application/raptorfec":{"source":"iana"},"application/rdap+json":{"source":"iana","compressible":true},"application/rdf+xml":{"source":"iana","compressible":true,"extensions":["rdf","owl"]},"application/reginfo+xml":{"source":"iana","compressible":true,"extensions":["rif"]},"application/relax-ng-compact-syntax":{"source":"iana","extensions":["rnc"]},"application/remote-printing":{"source":"iana"},"application/reputon+json":{"source":"iana","compressible":true},"application/resource-lists+xml":{"source":"iana","compressible":true,"extensions":["rl"]},"application/resource-lists-diff+xml":{"source":"iana","compressible":true,"extensions":["rld"]},"application/rfc+xml":{"source":"iana","compressible":true},"application/riscos":{"source":"iana"},"application/rlmi+xml":{"source":"iana","compressible":true},"application/rls-services+xml":{"source":"iana","compressible":true,"extensions":["rs"]},"application/route-apd+xml":{"source":"iana","compressible":true,"extensions":["rapd"]},"application/route-s-tsid+xml":{"source":"iana","compressible":true,"extensions":["sls"]},"application/route-usd+xml":{"source":"iana","compressible":true,"extensions":["rusd"]},"application/rpki-ghostbusters":{"source":"iana","extensions":["gbr"]},"application/rpki-manifest":{"source":"iana","extensions":["mft"]},"application/rpki-publication":{"source":"iana"},"application/rpki-roa":{"source":"iana","extensions":["roa"]},"application/rpki-updown":{"source":"iana"},"application/rsd+xml":{"source":"apache","compressible":true,"extensions":["rsd"]},"application/rss+xml":{"source":"apache","compressible":true,"extensions":["rss"]},"application/rtf":{"source":"iana","compressible":true,"extensions":["rtf"]},"application/rtploopback":{"source":"iana"},"application/rtx":{"source":"iana"},"application/samlassertion+xml":{"source":"iana","compressible":true},"application/samlmetadata+xml":{"source":"iana","compressible":true},"application/sbe":{"source":"iana"},"application/sbml+xml":{"source":"iana","compressible":true,"extensions":["sbml"]},"application/scaip+xml":{"source":"iana","compressible":true},"application/scim+json":{"source":"iana","compressible":true},"application/scvp-cv-request":{"source":"iana","extensions":["scq"]},"application/scvp-cv-response":{"source":"iana","extensions":["scs"]},"application/scvp-vp-request":{"source":"iana","extensions":["spq"]},"application/scvp-vp-response":{"source":"iana","extensions":["spp"]},"application/sdp":{"source":"iana","extensions":["sdp"]},"application/secevent+jwt":{"source":"iana"},"application/senml+cbor":{"source":"iana"},"application/senml+json":{"source":"iana","compressible":true},"application/senml+xml":{"source":"iana","compressible":true,"extensions":["senmlx"]},"application/senml-etch+cbor":{"source":"iana"},"application/senml-etch+json":{"source":"iana","compressible":true},"application/senml-exi":{"source":"iana"},"application/sensml+cbor":{"source":"iana"},"application/sensml+json":{"source":"iana","compressible":true},"application/sensml+xml":{"source":"iana","compressible":true,"extensions":["sensmlx"]},"application/sensml-exi":{"source":"iana"},"application/sep+xml":{"source":"iana","compressible":true},"application/sep-exi":{"source":"iana"},"application/session-info":{"source":"iana"},"application/set-payment":{"source":"iana"},"application/set-payment-initiation":{"source":"iana","extensions":["setpay"]},"application/set-registration":{"source":"iana"},"application/set-registration-initiation":{"source":"iana","extensions":["setreg"]},"application/sgml":{"source":"iana"},"application/sgml-open-catalog":{"source":"iana"},"application/shf+xml":{"source":"iana","compressible":true,"extensions":["shf"]},"application/sieve":{"source":"iana","extensions":["siv","sieve"]},"application/simple-filter+xml":{"source":"iana","compressible":true},"application/simple-message-summary":{"source":"iana"},"application/simplesymbolcontainer":{"source":"iana"},"application/sipc":{"source":"iana"},"application/slate":{"source":"iana"},"application/smil":{"source":"iana"},"application/smil+xml":{"source":"iana","compressible":true,"extensions":["smi","smil"]},"application/smpte336m":{"source":"iana"},"application/soap+fastinfoset":{"source":"iana"},"application/soap+xml":{"source":"iana","compressible":true},"application/sparql-query":{"source":"iana","extensions":["rq"]},"application/sparql-results+xml":{"source":"iana","compressible":true,"extensions":["srx"]},"application/spirits-event+xml":{"source":"iana","compressible":true},"application/sql":{"source":"iana"},"application/srgs":{"source":"iana","extensions":["gram"]},"application/srgs+xml":{"source":"iana","compressible":true,"extensions":["grxml"]},"application/sru+xml":{"source":"iana","compressible":true,"extensions":["sru"]},"application/ssdl+xml":{"source":"apache","compressible":true,"extensions":["ssdl"]},"application/ssml+xml":{"source":"iana","compressible":true,"extensions":["ssml"]},"application/stix+json":{"source":"iana","compressible":true},"application/swid+xml":{"source":"iana","compressible":true,"extensions":["swidtag"]},"application/tamp-apex-update":{"source":"iana"},"application/tamp-apex-update-confirm":{"source":"iana"},"application/tamp-community-update":{"source":"iana"},"application/tamp-community-update-confirm":{"source":"iana"},"application/tamp-error":{"source":"iana"},"application/tamp-sequence-adjust":{"source":"iana"},"application/tamp-sequence-adjust-confirm":{"source":"iana"},"application/tamp-status-query":{"source":"iana"},"application/tamp-status-response":{"source":"iana"},"application/tamp-update":{"source":"iana"},"application/tamp-update-confirm":{"source":"iana"},"application/tar":{"compressible":true},"application/taxii+json":{"source":"iana","compressible":true},"application/td+json":{"source":"iana","compressible":true},"application/tei+xml":{"source":"iana","compressible":true,"extensions":["tei","teicorpus"]},"application/tetra_isi":{"source":"iana"},"application/thraud+xml":{"source":"iana","compressible":true,"extensions":["tfi"]},"application/timestamp-query":{"source":"iana"},"application/timestamp-reply":{"source":"iana"},"application/timestamped-data":{"source":"iana","extensions":["tsd"]},"application/tlsrpt+gzip":{"source":"iana"},"application/tlsrpt+json":{"source":"iana","compressible":true},"application/tnauthlist":{"source":"iana"},"application/toml":{"compressible":true,"extensions":["toml"]},"application/trickle-ice-sdpfrag":{"source":"iana"},"application/trig":{"source":"iana"},"application/ttml+xml":{"source":"iana","compressible":true,"extensions":["ttml"]},"application/tve-trigger":{"source":"iana"},"application/tzif":{"source":"iana"},"application/tzif-leap":{"source":"iana"},"application/ulpfec":{"source":"iana"},"application/urc-grpsheet+xml":{"source":"iana","compressible":true},"application/urc-ressheet+xml":{"source":"iana","compressible":true,"extensions":["rsheet"]},"application/urc-targetdesc+xml":{"source":"iana","compressible":true},"application/urc-uisocketdesc+xml":{"source":"iana","compressible":true},"application/vcard+json":{"source":"iana","compressible":true},"application/vcard+xml":{"source":"iana","compressible":true},"application/vemmi":{"source":"iana"},"application/vividence.scriptfile":{"source":"apache"},"application/vnd.1000minds.decision-model+xml":{"source":"iana","compressible":true,"extensions":["1km"]},"application/vnd.3gpp-prose+xml":{"source":"iana","compressible":true},"application/vnd.3gpp-prose-pc3ch+xml":{"source":"iana","compressible":true},"application/vnd.3gpp-v2x-local-service-information":{"source":"iana"},"application/vnd.3gpp.access-transfer-events+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.bsf+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.gmop+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mc-signalling-ear":{"source":"iana"},"application/vnd.3gpp.mcdata-affiliation-command+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcdata-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcdata-payload":{"source":"iana"},"application/vnd.3gpp.mcdata-service-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcdata-signalling":{"source":"iana"},"application/vnd.3gpp.mcdata-ue-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcdata-user-profile+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-affiliation-command+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-floor-request+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-location-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-mbms-usage-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-service-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-signed+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-ue-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-ue-init-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-user-profile+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-affiliation-command+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-affiliation-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-location-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-mbms-usage-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-service-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-transmission-request+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-ue-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-user-profile+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mid-call+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.pic-bw-large":{"source":"iana","extensions":["plb"]},"application/vnd.3gpp.pic-bw-small":{"source":"iana","extensions":["psb"]},"application/vnd.3gpp.pic-bw-var":{"source":"iana","extensions":["pvb"]},"application/vnd.3gpp.sms":{"source":"iana"},"application/vnd.3gpp.sms+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.srvcc-ext+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.srvcc-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.state-and-event-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.ussd+xml":{"source":"iana","compressible":true},"application/vnd.3gpp2.bcmcsinfo+xml":{"source":"iana","compressible":true},"application/vnd.3gpp2.sms":{"source":"iana"},"application/vnd.3gpp2.tcap":{"source":"iana","extensions":["tcap"]},"application/vnd.3lightssoftware.imagescal":{"source":"iana"},"application/vnd.3m.post-it-notes":{"source":"iana","extensions":["pwn"]},"application/vnd.accpac.simply.aso":{"source":"iana","extensions":["aso"]},"application/vnd.accpac.simply.imp":{"source":"iana","extensions":["imp"]},"application/vnd.acucobol":{"source":"iana","extensions":["acu"]},"application/vnd.acucorp":{"source":"iana","extensions":["atc","acutc"]},"application/vnd.adobe.air-application-installer-package+zip":{"source":"apache","compressible":false,"extensions":["air"]},"application/vnd.adobe.flash.movie":{"source":"iana"},"application/vnd.adobe.formscentral.fcdt":{"source":"iana","extensions":["fcdt"]},"application/vnd.adobe.fxp":{"source":"iana","extensions":["fxp","fxpl"]},"application/vnd.adobe.partial-upload":{"source":"iana"},"application/vnd.adobe.xdp+xml":{"source":"iana","compressible":true,"extensions":["xdp"]},"application/vnd.adobe.xfdf":{"source":"iana","extensions":["xfdf"]},"application/vnd.aether.imp":{"source":"iana"},"application/vnd.afpc.afplinedata":{"source":"iana"},"application/vnd.afpc.afplinedata-pagedef":{"source":"iana"},"application/vnd.afpc.foca-charset":{"source":"iana"},"application/vnd.afpc.foca-codedfont":{"source":"iana"},"application/vnd.afpc.foca-codepage":{"source":"iana"},"application/vnd.afpc.modca":{"source":"iana"},"application/vnd.afpc.modca-formdef":{"source":"iana"},"application/vnd.afpc.modca-mediummap":{"source":"iana"},"application/vnd.afpc.modca-objectcontainer":{"source":"iana"},"application/vnd.afpc.modca-overlay":{"source":"iana"},"application/vnd.afpc.modca-pagesegment":{"source":"iana"},"application/vnd.ah-barcode":{"source":"iana"},"application/vnd.ahead.space":{"source":"iana","extensions":["ahead"]},"application/vnd.airzip.filesecure.azf":{"source":"iana","extensions":["azf"]},"application/vnd.airzip.filesecure.azs":{"source":"iana","extensions":["azs"]},"application/vnd.amadeus+json":{"source":"iana","compressible":true},"application/vnd.amazon.ebook":{"source":"apache","extensions":["azw"]},"application/vnd.amazon.mobi8-ebook":{"source":"iana"},"application/vnd.americandynamics.acc":{"source":"iana","extensions":["acc"]},"application/vnd.amiga.ami":{"source":"iana","extensions":["ami"]},"application/vnd.amundsen.maze+xml":{"source":"iana","compressible":true},"application/vnd.android.ota":{"source":"iana"},"application/vnd.android.package-archive":{"source":"apache","compressible":false,"extensions":["apk"]},"application/vnd.anki":{"source":"iana"},"application/vnd.anser-web-certificate-issue-initiation":{"source":"iana","extensions":["cii"]},"application/vnd.anser-web-funds-transfer-initiation":{"source":"apache","extensions":["fti"]},"application/vnd.antix.game-component":{"source":"iana","extensions":["atx"]},"application/vnd.apache.thrift.binary":{"source":"iana"},"application/vnd.apache.thrift.compact":{"source":"iana"},"application/vnd.apache.thrift.json":{"source":"iana"},"application/vnd.api+json":{"source":"iana","compressible":true},"application/vnd.aplextor.warrp+json":{"source":"iana","compressible":true},"application/vnd.apothekende.reservation+json":{"source":"iana","compressible":true},"application/vnd.apple.installer+xml":{"source":"iana","compressible":true,"extensions":["mpkg"]},"application/vnd.apple.keynote":{"source":"iana","extensions":["keynote"]},"application/vnd.apple.mpegurl":{"source":"iana","extensions":["m3u8"]},"application/vnd.apple.numbers":{"source":"iana","extensions":["numbers"]},"application/vnd.apple.pages":{"source":"iana","extensions":["pages"]},"application/vnd.apple.pkpass":{"compressible":false,"extensions":["pkpass"]},"application/vnd.arastra.swi":{"source":"iana"},"application/vnd.aristanetworks.swi":{"source":"iana","extensions":["swi"]},"application/vnd.artisan+json":{"source":"iana","compressible":true},"application/vnd.artsquare":{"source":"iana"},"application/vnd.astraea-software.iota":{"source":"iana","extensions":["iota"]},"application/vnd.audiograph":{"source":"iana","extensions":["aep"]},"application/vnd.autopackage":{"source":"iana"},"application/vnd.avalon+json":{"source":"iana","compressible":true},"application/vnd.avistar+xml":{"source":"iana","compressible":true},"application/vnd.balsamiq.bmml+xml":{"source":"iana","compressible":true,"extensions":["bmml"]},"application/vnd.balsamiq.bmpr":{"source":"iana"},"application/vnd.banana-accounting":{"source":"iana"},"application/vnd.bbf.usp.error":{"source":"iana"},"application/vnd.bbf.usp.msg":{"source":"iana"},"application/vnd.bbf.usp.msg+json":{"source":"iana","compressible":true},"application/vnd.bekitzur-stech+json":{"source":"iana","compressible":true},"application/vnd.bint.med-content":{"source":"iana"},"application/vnd.biopax.rdf+xml":{"source":"iana","compressible":true},"application/vnd.blink-idb-value-wrapper":{"source":"iana"},"application/vnd.blueice.multipass":{"source":"iana","extensions":["mpm"]},"application/vnd.bluetooth.ep.oob":{"source":"iana"},"application/vnd.bluetooth.le.oob":{"source":"iana"},"application/vnd.bmi":{"source":"iana","extensions":["bmi"]},"application/vnd.bpf":{"source":"iana"},"application/vnd.bpf3":{"source":"iana"},"application/vnd.businessobjects":{"source":"iana","extensions":["rep"]},"application/vnd.byu.uapi+json":{"source":"iana","compressible":true},"application/vnd.cab-jscript":{"source":"iana"},"application/vnd.canon-cpdl":{"source":"iana"},"application/vnd.canon-lips":{"source":"iana"},"application/vnd.capasystems-pg+json":{"source":"iana","compressible":true},"application/vnd.cendio.thinlinc.clientconf":{"source":"iana"},"application/vnd.century-systems.tcp_stream":{"source":"iana"},"application/vnd.chemdraw+xml":{"source":"iana","compressible":true,"extensions":["cdxml"]},"application/vnd.chess-pgn":{"source":"iana"},"application/vnd.chipnuts.karaoke-mmd":{"source":"iana","extensions":["mmd"]},"application/vnd.ciedi":{"source":"iana"},"application/vnd.cinderella":{"source":"iana","extensions":["cdy"]},"application/vnd.cirpack.isdn-ext":{"source":"iana"},"application/vnd.citationstyles.style+xml":{"source":"iana","compressible":true,"extensions":["csl"]},"application/vnd.claymore":{"source":"iana","extensions":["cla"]},"application/vnd.cloanto.rp9":{"source":"iana","extensions":["rp9"]},"application/vnd.clonk.c4group":{"source":"iana","extensions":["c4g","c4d","c4f","c4p","c4u"]},"application/vnd.cluetrust.cartomobile-config":{"source":"iana","extensions":["c11amc"]},"application/vnd.cluetrust.cartomobile-config-pkg":{"source":"iana","extensions":["c11amz"]},"application/vnd.coffeescript":{"source":"iana"},"application/vnd.collabio.xodocuments.document":{"source":"iana"},"application/vnd.collabio.xodocuments.document-template":{"source":"iana"},"application/vnd.collabio.xodocuments.presentation":{"source":"iana"},"application/vnd.collabio.xodocuments.presentation-template":{"source":"iana"},"application/vnd.collabio.xodocuments.spreadsheet":{"source":"iana"},"application/vnd.collabio.xodocuments.spreadsheet-template":{"source":"iana"},"application/vnd.collection+json":{"source":"iana","compressible":true},"application/vnd.collection.doc+json":{"source":"iana","compressible":true},"application/vnd.collection.next+json":{"source":"iana","compressible":true},"application/vnd.comicbook+zip":{"source":"iana","compressible":false},"application/vnd.comicbook-rar":{"source":"iana"},"application/vnd.commerce-battelle":{"source":"iana"},"application/vnd.commonspace":{"source":"iana","extensions":["csp"]},"application/vnd.contact.cmsg":{"source":"iana","extensions":["cdbcmsg"]},"application/vnd.coreos.ignition+json":{"source":"iana","compressible":true},"application/vnd.cosmocaller":{"source":"iana","extensions":["cmc"]},"application/vnd.crick.clicker":{"source":"iana","extensions":["clkx"]},"application/vnd.crick.clicker.keyboard":{"source":"iana","extensions":["clkk"]},"application/vnd.crick.clicker.palette":{"source":"iana","extensions":["clkp"]},"application/vnd.crick.clicker.template":{"source":"iana","extensions":["clkt"]},"application/vnd.crick.clicker.wordbank":{"source":"iana","extensions":["clkw"]},"application/vnd.criticaltools.wbs+xml":{"source":"iana","compressible":true,"extensions":["wbs"]},"application/vnd.cryptii.pipe+json":{"source":"iana","compressible":true},"application/vnd.crypto-shade-file":{"source":"iana"},"application/vnd.ctc-posml":{"source":"iana","extensions":["pml"]},"application/vnd.ctct.ws+xml":{"source":"iana","compressible":true},"application/vnd.cups-pdf":{"source":"iana"},"application/vnd.cups-postscript":{"source":"iana"},"application/vnd.cups-ppd":{"source":"iana","extensions":["ppd"]},"application/vnd.cups-raster":{"source":"iana"},"application/vnd.cups-raw":{"source":"iana"},"application/vnd.curl":{"source":"iana"},"application/vnd.curl.car":{"source":"apache","extensions":["car"]},"application/vnd.curl.pcurl":{"source":"apache","extensions":["pcurl"]},"application/vnd.cyan.dean.root+xml":{"source":"iana","compressible":true},"application/vnd.cybank":{"source":"iana"},"application/vnd.d2l.coursepackage1p0+zip":{"source":"iana","compressible":false},"application/vnd.dart":{"source":"iana","compressible":true,"extensions":["dart"]},"application/vnd.data-vision.rdz":{"source":"iana","extensions":["rdz"]},"application/vnd.datapackage+json":{"source":"iana","compressible":true},"application/vnd.dataresource+json":{"source":"iana","compressible":true},"application/vnd.dbf":{"source":"iana"},"application/vnd.debian.binary-package":{"source":"iana"},"application/vnd.dece.data":{"source":"iana","extensions":["uvf","uvvf","uvd","uvvd"]},"application/vnd.dece.ttml+xml":{"source":"iana","compressible":true,"extensions":["uvt","uvvt"]},"application/vnd.dece.unspecified":{"source":"iana","extensions":["uvx","uvvx"]},"application/vnd.dece.zip":{"source":"iana","extensions":["uvz","uvvz"]},"application/vnd.denovo.fcselayout-link":{"source":"iana","extensions":["fe_launch"]},"application/vnd.desmume.movie":{"source":"iana"},"application/vnd.dir-bi.plate-dl-nosuffix":{"source":"iana"},"application/vnd.dm.delegation+xml":{"source":"iana","compressible":true},"application/vnd.dna":{"source":"iana","extensions":["dna"]},"application/vnd.document+json":{"source":"iana","compressible":true},"application/vnd.dolby.mlp":{"source":"apache","extensions":["mlp"]},"application/vnd.dolby.mobile.1":{"source":"iana"},"application/vnd.dolby.mobile.2":{"source":"iana"},"application/vnd.doremir.scorecloud-binary-document":{"source":"iana"},"application/vnd.dpgraph":{"source":"iana","extensions":["dpg"]},"application/vnd.dreamfactory":{"source":"iana","extensions":["dfac"]},"application/vnd.drive+json":{"source":"iana","compressible":true},"application/vnd.ds-keypoint":{"source":"apache","extensions":["kpxx"]},"application/vnd.dtg.local":{"source":"iana"},"application/vnd.dtg.local.flash":{"source":"iana"},"application/vnd.dtg.local.html":{"source":"iana"},"application/vnd.dvb.ait":{"source":"iana","extensions":["ait"]},"application/vnd.dvb.dvbisl+xml":{"source":"iana","compressible":true},"application/vnd.dvb.dvbj":{"source":"iana"},"application/vnd.dvb.esgcontainer":{"source":"iana"},"application/vnd.dvb.ipdcdftnotifaccess":{"source":"iana"},"application/vnd.dvb.ipdcesgaccess":{"source":"iana"},"application/vnd.dvb.ipdcesgaccess2":{"source":"iana"},"application/vnd.dvb.ipdcesgpdd":{"source":"iana"},"application/vnd.dvb.ipdcroaming":{"source":"iana"},"application/vnd.dvb.iptv.alfec-base":{"source":"iana"},"application/vnd.dvb.iptv.alfec-enhancement":{"source":"iana"},"application/vnd.dvb.notif-aggregate-root+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-container+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-generic+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-ia-msglist+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-ia-registration-request+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-ia-registration-response+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-init+xml":{"source":"iana","compressible":true},"application/vnd.dvb.pfr":{"source":"iana"},"application/vnd.dvb.service":{"source":"iana","extensions":["svc"]},"application/vnd.dxr":{"source":"iana"},"application/vnd.dynageo":{"source":"iana","extensions":["geo"]},"application/vnd.dzr":{"source":"iana"},"application/vnd.easykaraoke.cdgdownload":{"source":"iana"},"application/vnd.ecdis-update":{"source":"iana"},"application/vnd.ecip.rlp":{"source":"iana"},"application/vnd.ecowin.chart":{"source":"iana","extensions":["mag"]},"application/vnd.ecowin.filerequest":{"source":"iana"},"application/vnd.ecowin.fileupdate":{"source":"iana"},"application/vnd.ecowin.series":{"source":"iana"},"application/vnd.ecowin.seriesrequest":{"source":"iana"},"application/vnd.ecowin.seriesupdate":{"source":"iana"},"application/vnd.efi.img":{"source":"iana"},"application/vnd.efi.iso":{"source":"iana"},"application/vnd.emclient.accessrequest+xml":{"source":"iana","compressible":true},"application/vnd.enliven":{"source":"iana","extensions":["nml"]},"application/vnd.enphase.envoy":{"source":"iana"},"application/vnd.eprints.data+xml":{"source":"iana","compressible":true},"application/vnd.epson.esf":{"source":"iana","extensions":["esf"]},"application/vnd.epson.msf":{"source":"iana","extensions":["msf"]},"application/vnd.epson.quickanime":{"source":"iana","extensions":["qam"]},"application/vnd.epson.salt":{"source":"iana","extensions":["slt"]},"application/vnd.epson.ssf":{"source":"iana","extensions":["ssf"]},"application/vnd.ericsson.quickcall":{"source":"iana"},"application/vnd.espass-espass+zip":{"source":"iana","compressible":false},"application/vnd.eszigno3+xml":{"source":"iana","compressible":true,"extensions":["es3","et3"]},"application/vnd.etsi.aoc+xml":{"source":"iana","compressible":true},"application/vnd.etsi.asic-e+zip":{"source":"iana","compressible":false},"application/vnd.etsi.asic-s+zip":{"source":"iana","compressible":false},"application/vnd.etsi.cug+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvcommand+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvdiscovery+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvprofile+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvsad-bc+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvsad-cod+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvsad-npvr+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvservice+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvsync+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvueprofile+xml":{"source":"iana","compressible":true},"application/vnd.etsi.mcid+xml":{"source":"iana","compressible":true},"application/vnd.etsi.mheg5":{"source":"iana"},"application/vnd.etsi.overload-control-policy-dataset+xml":{"source":"iana","compressible":true},"application/vnd.etsi.pstn+xml":{"source":"iana","compressible":true},"application/vnd.etsi.sci+xml":{"source":"iana","compressible":true},"application/vnd.etsi.simservs+xml":{"source":"iana","compressible":true},"application/vnd.etsi.timestamp-token":{"source":"iana"},"application/vnd.etsi.tsl+xml":{"source":"iana","compressible":true},"application/vnd.etsi.tsl.der":{"source":"iana"},"application/vnd.eudora.data":{"source":"iana"},"application/vnd.evolv.ecig.profile":{"source":"iana"},"application/vnd.evolv.ecig.settings":{"source":"iana"},"application/vnd.evolv.ecig.theme":{"source":"iana"},"application/vnd.exstream-empower+zip":{"source":"iana","compressible":false},"application/vnd.exstream-package":{"source":"iana"},"application/vnd.ezpix-album":{"source":"iana","extensions":["ez2"]},"application/vnd.ezpix-package":{"source":"iana","extensions":["ez3"]},"application/vnd.f-secure.mobile":{"source":"iana"},"application/vnd.fastcopy-disk-image":{"source":"iana"},"application/vnd.fdf":{"source":"iana","extensions":["fdf"]},"application/vnd.fdsn.mseed":{"source":"iana","extensions":["mseed"]},"application/vnd.fdsn.seed":{"source":"iana","extensions":["seed","dataless"]},"application/vnd.ffsns":{"source":"iana"},"application/vnd.ficlab.flb+zip":{"source":"iana","compressible":false},"application/vnd.filmit.zfc":{"source":"iana"},"application/vnd.fints":{"source":"iana"},"application/vnd.firemonkeys.cloudcell":{"source":"iana"},"application/vnd.flographit":{"source":"iana","extensions":["gph"]},"application/vnd.fluxtime.clip":{"source":"iana","extensions":["ftc"]},"application/vnd.font-fontforge-sfd":{"source":"iana"},"application/vnd.framemaker":{"source":"iana","extensions":["fm","frame","maker","book"]},"application/vnd.frogans.fnc":{"source":"iana","extensions":["fnc"]},"application/vnd.frogans.ltf":{"source":"iana","extensions":["ltf"]},"application/vnd.fsc.weblaunch":{"source":"iana","extensions":["fsc"]},"application/vnd.fujitsu.oasys":{"source":"iana","extensions":["oas"]},"application/vnd.fujitsu.oasys2":{"source":"iana","extensions":["oa2"]},"application/vnd.fujitsu.oasys3":{"source":"iana","extensions":["oa3"]},"application/vnd.fujitsu.oasysgp":{"source":"iana","extensions":["fg5"]},"application/vnd.fujitsu.oasysprs":{"source":"iana","extensions":["bh2"]},"application/vnd.fujixerox.art-ex":{"source":"iana"},"application/vnd.fujixerox.art4":{"source":"iana"},"application/vnd.fujixerox.ddd":{"source":"iana","extensions":["ddd"]},"application/vnd.fujixerox.docuworks":{"source":"iana","extensions":["xdw"]},"application/vnd.fujixerox.docuworks.binder":{"source":"iana","extensions":["xbd"]},"application/vnd.fujixerox.docuworks.container":{"source":"iana"},"application/vnd.fujixerox.hbpl":{"source":"iana"},"application/vnd.fut-misnet":{"source":"iana"},"application/vnd.futoin+cbor":{"source":"iana"},"application/vnd.futoin+json":{"source":"iana","compressible":true},"application/vnd.fuzzysheet":{"source":"iana","extensions":["fzs"]},"application/vnd.genomatix.tuxedo":{"source":"iana","extensions":["txd"]},"application/vnd.gentics.grd+json":{"source":"iana","compressible":true},"application/vnd.geo+json":{"source":"iana","compressible":true},"application/vnd.geocube+xml":{"source":"iana","compressible":true},"application/vnd.geogebra.file":{"source":"iana","extensions":["ggb"]},"application/vnd.geogebra.tool":{"source":"iana","extensions":["ggt"]},"application/vnd.geometry-explorer":{"source":"iana","extensions":["gex","gre"]},"application/vnd.geonext":{"source":"iana","extensions":["gxt"]},"application/vnd.geoplan":{"source":"iana","extensions":["g2w"]},"application/vnd.geospace":{"source":"iana","extensions":["g3w"]},"application/vnd.gerber":{"source":"iana"},"application/vnd.globalplatform.card-content-mgt":{"source":"iana"},"application/vnd.globalplatform.card-content-mgt-response":{"source":"iana"},"application/vnd.gmx":{"source":"iana","extensions":["gmx"]},"application/vnd.google-apps.document":{"compressible":false,"extensions":["gdoc"]},"application/vnd.google-apps.presentation":{"compressible":false,"extensions":["gslides"]},"application/vnd.google-apps.spreadsheet":{"compressible":false,"extensions":["gsheet"]},"application/vnd.google-earth.kml+xml":{"source":"iana","compressible":true,"extensions":["kml"]},"application/vnd.google-earth.kmz":{"source":"iana","compressible":false,"extensions":["kmz"]},"application/vnd.gov.sk.e-form+xml":{"source":"iana","compressible":true},"application/vnd.gov.sk.e-form+zip":{"source":"iana","compressible":false},"application/vnd.gov.sk.xmldatacontainer+xml":{"source":"iana","compressible":true},"application/vnd.grafeq":{"source":"iana","extensions":["gqf","gqs"]},"application/vnd.gridmp":{"source":"iana"},"application/vnd.groove-account":{"source":"iana","extensions":["gac"]},"application/vnd.groove-help":{"source":"iana","extensions":["ghf"]},"application/vnd.groove-identity-message":{"source":"iana","extensions":["gim"]},"application/vnd.groove-injector":{"source":"iana","extensions":["grv"]},"application/vnd.groove-tool-message":{"source":"iana","extensions":["gtm"]},"application/vnd.groove-tool-template":{"source":"iana","extensions":["tpl"]},"application/vnd.groove-vcard":{"source":"iana","extensions":["vcg"]},"application/vnd.hal+json":{"source":"iana","compressible":true},"application/vnd.hal+xml":{"source":"iana","compressible":true,"extensions":["hal"]},"application/vnd.handheld-entertainment+xml":{"source":"iana","compressible":true,"extensions":["zmm"]},"application/vnd.hbci":{"source":"iana","extensions":["hbci"]},"application/vnd.hc+json":{"source":"iana","compressible":true},"application/vnd.hcl-bireports":{"source":"iana"},"application/vnd.hdt":{"source":"iana"},"application/vnd.heroku+json":{"source":"iana","compressible":true},"application/vnd.hhe.lesson-player":{"source":"iana","extensions":["les"]},"application/vnd.hp-hpgl":{"source":"iana","extensions":["hpgl"]},"application/vnd.hp-hpid":{"source":"iana","extensions":["hpid"]},"application/vnd.hp-hps":{"source":"iana","extensions":["hps"]},"application/vnd.hp-jlyt":{"source":"iana","extensions":["jlt"]},"application/vnd.hp-pcl":{"source":"iana","extensions":["pcl"]},"application/vnd.hp-pclxl":{"source":"iana","extensions":["pclxl"]},"application/vnd.httphone":{"source":"iana"},"application/vnd.hydrostatix.sof-data":{"source":"iana","extensions":["sfd-hdstx"]},"application/vnd.hyper+json":{"source":"iana","compressible":true},"application/vnd.hyper-item+json":{"source":"iana","compressible":true},"application/vnd.hyperdrive+json":{"source":"iana","compressible":true},"application/vnd.hzn-3d-crossword":{"source":"iana"},"application/vnd.ibm.afplinedata":{"source":"iana"},"application/vnd.ibm.electronic-media":{"source":"iana"},"application/vnd.ibm.minipay":{"source":"iana","extensions":["mpy"]},"application/vnd.ibm.modcap":{"source":"iana","extensions":["afp","listafp","list3820"]},"application/vnd.ibm.rights-management":{"source":"iana","extensions":["irm"]},"application/vnd.ibm.secure-container":{"source":"iana","extensions":["sc"]},"application/vnd.iccprofile":{"source":"iana","extensions":["icc","icm"]},"application/vnd.ieee.1905":{"source":"iana"},"application/vnd.igloader":{"source":"iana","extensions":["igl"]},"application/vnd.imagemeter.folder+zip":{"source":"iana","compressible":false},"application/vnd.imagemeter.image+zip":{"source":"iana","compressible":false},"application/vnd.immervision-ivp":{"source":"iana","extensions":["ivp"]},"application/vnd.immervision-ivu":{"source":"iana","extensions":["ivu"]},"application/vnd.ims.imsccv1p1":{"source":"iana"},"application/vnd.ims.imsccv1p2":{"source":"iana"},"application/vnd.ims.imsccv1p3":{"source":"iana"},"application/vnd.ims.lis.v2.result+json":{"source":"iana","compressible":true},"application/vnd.ims.lti.v2.toolconsumerprofile+json":{"source":"iana","compressible":true},"application/vnd.ims.lti.v2.toolproxy+json":{"source":"iana","compressible":true},"application/vnd.ims.lti.v2.toolproxy.id+json":{"source":"iana","compressible":true},"application/vnd.ims.lti.v2.toolsettings+json":{"source":"iana","compressible":true},"application/vnd.ims.lti.v2.toolsettings.simple+json":{"source":"iana","compressible":true},"application/vnd.informedcontrol.rms+xml":{"source":"iana","compressible":true},"application/vnd.informix-visionary":{"source":"iana"},"application/vnd.infotech.project":{"source":"iana"},"application/vnd.infotech.project+xml":{"source":"iana","compressible":true},"application/vnd.innopath.wamp.notification":{"source":"iana"},"application/vnd.insors.igm":{"source":"iana","extensions":["igm"]},"application/vnd.intercon.formnet":{"source":"iana","extensions":["xpw","xpx"]},"application/vnd.intergeo":{"source":"iana","extensions":["i2g"]},"application/vnd.intertrust.digibox":{"source":"iana"},"application/vnd.intertrust.nncp":{"source":"iana"},"application/vnd.intu.qbo":{"source":"iana","extensions":["qbo"]},"application/vnd.intu.qfx":{"source":"iana","extensions":["qfx"]},"application/vnd.iptc.g2.catalogitem+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.conceptitem+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.knowledgeitem+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.newsitem+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.newsmessage+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.packageitem+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.planningitem+xml":{"source":"iana","compressible":true},"application/vnd.ipunplugged.rcprofile":{"source":"iana","extensions":["rcprofile"]},"application/vnd.irepository.package+xml":{"source":"iana","compressible":true,"extensions":["irp"]},"application/vnd.is-xpr":{"source":"iana","extensions":["xpr"]},"application/vnd.isac.fcs":{"source":"iana","extensions":["fcs"]},"application/vnd.iso11783-10+zip":{"source":"iana","compressible":false},"application/vnd.jam":{"source":"iana","extensions":["jam"]},"application/vnd.japannet-directory-service":{"source":"iana"},"application/vnd.japannet-jpnstore-wakeup":{"source":"iana"},"application/vnd.japannet-payment-wakeup":{"source":"iana"},"application/vnd.japannet-registration":{"source":"iana"},"application/vnd.japannet-registration-wakeup":{"source":"iana"},"application/vnd.japannet-setstore-wakeup":{"source":"iana"},"application/vnd.japannet-verification":{"source":"iana"},"application/vnd.japannet-verification-wakeup":{"source":"iana"},"application/vnd.jcp.javame.midlet-rms":{"source":"iana","extensions":["rms"]},"application/vnd.jisp":{"source":"iana","extensions":["jisp"]},"application/vnd.joost.joda-archive":{"source":"iana","extensions":["joda"]},"application/vnd.jsk.isdn-ngn":{"source":"iana"},"application/vnd.kahootz":{"source":"iana","extensions":["ktz","ktr"]},"application/vnd.kde.karbon":{"source":"iana","extensions":["karbon"]},"application/vnd.kde.kchart":{"source":"iana","extensions":["chrt"]},"application/vnd.kde.kformula":{"source":"iana","extensions":["kfo"]},"application/vnd.kde.kivio":{"source":"iana","extensions":["flw"]},"application/vnd.kde.kontour":{"source":"iana","extensions":["kon"]},"application/vnd.kde.kpresenter":{"source":"iana","extensions":["kpr","kpt"]},"application/vnd.kde.kspread":{"source":"iana","extensions":["ksp"]},"application/vnd.kde.kword":{"source":"iana","extensions":["kwd","kwt"]},"application/vnd.kenameaapp":{"source":"iana","extensions":["htke"]},"application/vnd.kidspiration":{"source":"iana","extensions":["kia"]},"application/vnd.kinar":{"source":"iana","extensions":["kne","knp"]},"application/vnd.koan":{"source":"iana","extensions":["skp","skd","skt","skm"]},"application/vnd.kodak-descriptor":{"source":"iana","extensions":["sse"]},"application/vnd.las":{"source":"iana"},"application/vnd.las.las+json":{"source":"iana","compressible":true},"application/vnd.las.las+xml":{"source":"iana","compressible":true,"extensions":["lasxml"]},"application/vnd.laszip":{"source":"iana"},"application/vnd.leap+json":{"source":"iana","compressible":true},"application/vnd.liberty-request+xml":{"source":"iana","compressible":true},"application/vnd.llamagraphics.life-balance.desktop":{"source":"iana","extensions":["lbd"]},"application/vnd.llamagraphics.life-balance.exchange+xml":{"source":"iana","compressible":true,"extensions":["lbe"]},"application/vnd.logipipe.circuit+zip":{"source":"iana","compressible":false},"application/vnd.loom":{"source":"iana"},"application/vnd.lotus-1-2-3":{"source":"iana","extensions":["123"]},"application/vnd.lotus-approach":{"source":"iana","extensions":["apr"]},"application/vnd.lotus-freelance":{"source":"iana","extensions":["pre"]},"application/vnd.lotus-notes":{"source":"iana","extensions":["nsf"]},"application/vnd.lotus-organizer":{"source":"iana","extensions":["org"]},"application/vnd.lotus-screencam":{"source":"iana","extensions":["scm"]},"application/vnd.lotus-wordpro":{"source":"iana","extensions":["lwp"]},"application/vnd.macports.portpkg":{"source":"iana","extensions":["portpkg"]},"application/vnd.mapbox-vector-tile":{"source":"iana"},"application/vnd.marlin.drm.actiontoken+xml":{"source":"iana","compressible":true},"application/vnd.marlin.drm.conftoken+xml":{"source":"iana","compressible":true},"application/vnd.marlin.drm.license+xml":{"source":"iana","compressible":true},"application/vnd.marlin.drm.mdcf":{"source":"iana"},"application/vnd.mason+json":{"source":"iana","compressible":true},"application/vnd.maxmind.maxmind-db":{"source":"iana"},"application/vnd.mcd":{"source":"iana","extensions":["mcd"]},"application/vnd.medcalcdata":{"source":"iana","extensions":["mc1"]},"application/vnd.mediastation.cdkey":{"source":"iana","extensions":["cdkey"]},"application/vnd.meridian-slingshot":{"source":"iana"},"application/vnd.mfer":{"source":"iana","extensions":["mwf"]},"application/vnd.mfmp":{"source":"iana","extensions":["mfm"]},"application/vnd.micro+json":{"source":"iana","compressible":true},"application/vnd.micrografx.flo":{"source":"iana","extensions":["flo"]},"application/vnd.micrografx.igx":{"source":"iana","extensions":["igx"]},"application/vnd.microsoft.portable-executable":{"source":"iana"},"application/vnd.microsoft.windows.thumbnail-cache":{"source":"iana"},"application/vnd.miele+json":{"source":"iana","compressible":true},"application/vnd.mif":{"source":"iana","extensions":["mif"]},"application/vnd.minisoft-hp3000-save":{"source":"iana"},"application/vnd.mitsubishi.misty-guard.trustweb":{"source":"iana"},"application/vnd.mobius.daf":{"source":"iana","extensions":["daf"]},"application/vnd.mobius.dis":{"source":"iana","extensions":["dis"]},"application/vnd.mobius.mbk":{"source":"iana","extensions":["mbk"]},"application/vnd.mobius.mqy":{"source":"iana","extensions":["mqy"]},"application/vnd.mobius.msl":{"source":"iana","extensions":["msl"]},"application/vnd.mobius.plc":{"source":"iana","extensions":["plc"]},"application/vnd.mobius.txf":{"source":"iana","extensions":["txf"]},"application/vnd.mophun.application":{"source":"iana","extensions":["mpn"]},"application/vnd.mophun.certificate":{"source":"iana","extensions":["mpc"]},"application/vnd.motorola.flexsuite":{"source":"iana"},"application/vnd.motorola.flexsuite.adsi":{"source":"iana"},"application/vnd.motorola.flexsuite.fis":{"source":"iana"},"application/vnd.motorola.flexsuite.gotap":{"source":"iana"},"application/vnd.motorola.flexsuite.kmr":{"source":"iana"},"application/vnd.motorola.flexsuite.ttc":{"source":"iana"},"application/vnd.motorola.flexsuite.wem":{"source":"iana"},"application/vnd.motorola.iprm":{"source":"iana"},"application/vnd.mozilla.xul+xml":{"source":"iana","compressible":true,"extensions":["xul"]},"application/vnd.ms-3mfdocument":{"source":"iana"},"application/vnd.ms-artgalry":{"source":"iana","extensions":["cil"]},"application/vnd.ms-asf":{"source":"iana"},"application/vnd.ms-cab-compressed":{"source":"iana","extensions":["cab"]},"application/vnd.ms-color.iccprofile":{"source":"apache"},"application/vnd.ms-excel":{"source":"iana","compressible":false,"extensions":["xls","xlm","xla","xlc","xlt","xlw"]},"application/vnd.ms-excel.addin.macroenabled.12":{"source":"iana","extensions":["xlam"]},"application/vnd.ms-excel.sheet.binary.macroenabled.12":{"source":"iana","extensions":["xlsb"]},"application/vnd.ms-excel.sheet.macroenabled.12":{"source":"iana","extensions":["xlsm"]},"application/vnd.ms-excel.template.macroenabled.12":{"source":"iana","extensions":["xltm"]},"application/vnd.ms-fontobject":{"source":"iana","compressible":true,"extensions":["eot"]},"application/vnd.ms-htmlhelp":{"source":"iana","extensions":["chm"]},"application/vnd.ms-ims":{"source":"iana","extensions":["ims"]},"application/vnd.ms-lrm":{"source":"iana","extensions":["lrm"]},"application/vnd.ms-office.activex+xml":{"source":"iana","compressible":true},"application/vnd.ms-officetheme":{"source":"iana","extensions":["thmx"]},"application/vnd.ms-opentype":{"source":"apache","compressible":true},"application/vnd.ms-outlook":{"compressible":false,"extensions":["msg"]},"application/vnd.ms-package.obfuscated-opentype":{"source":"apache"},"application/vnd.ms-pki.seccat":{"source":"apache","extensions":["cat"]},"application/vnd.ms-pki.stl":{"source":"apache","extensions":["stl"]},"application/vnd.ms-playready.initiator+xml":{"source":"iana","compressible":true},"application/vnd.ms-powerpoint":{"source":"iana","compressible":false,"extensions":["ppt","pps","pot"]},"application/vnd.ms-powerpoint.addin.macroenabled.12":{"source":"iana","extensions":["ppam"]},"application/vnd.ms-powerpoint.presentation.macroenabled.12":{"source":"iana","extensions":["pptm"]},"application/vnd.ms-powerpoint.slide.macroenabled.12":{"source":"iana","extensions":["sldm"]},"application/vnd.ms-powerpoint.slideshow.macroenabled.12":{"source":"iana","extensions":["ppsm"]},"application/vnd.ms-powerpoint.template.macroenabled.12":{"source":"iana","extensions":["potm"]},"application/vnd.ms-printdevicecapabilities+xml":{"source":"iana","compressible":true},"application/vnd.ms-printing.printticket+xml":{"source":"apache","compressible":true},"application/vnd.ms-printschematicket+xml":{"source":"iana","compressible":true},"application/vnd.ms-project":{"source":"iana","extensions":["mpp","mpt"]},"application/vnd.ms-tnef":{"source":"iana"},"application/vnd.ms-windows.devicepairing":{"source":"iana"},"application/vnd.ms-windows.nwprinting.oob":{"source":"iana"},"application/vnd.ms-windows.printerpairing":{"source":"iana"},"application/vnd.ms-windows.wsd.oob":{"source":"iana"},"application/vnd.ms-wmdrm.lic-chlg-req":{"source":"iana"},"application/vnd.ms-wmdrm.lic-resp":{"source":"iana"},"application/vnd.ms-wmdrm.meter-chlg-req":{"source":"iana"},"application/vnd.ms-wmdrm.meter-resp":{"source":"iana"},"application/vnd.ms-word.document.macroenabled.12":{"source":"iana","extensions":["docm"]},"application/vnd.ms-word.template.macroenabled.12":{"source":"iana","extensions":["dotm"]},"application/vnd.ms-works":{"source":"iana","extensions":["wps","wks","wcm","wdb"]},"application/vnd.ms-wpl":{"source":"iana","extensions":["wpl"]},"application/vnd.ms-xpsdocument":{"source":"iana","compressible":false,"extensions":["xps"]},"application/vnd.msa-disk-image":{"source":"iana"},"application/vnd.mseq":{"source":"iana","extensions":["mseq"]},"application/vnd.msign":{"source":"iana"},"application/vnd.multiad.creator":{"source":"iana"},"application/vnd.multiad.creator.cif":{"source":"iana"},"application/vnd.music-niff":{"source":"iana"},"application/vnd.musician":{"source":"iana","extensions":["mus"]},"application/vnd.muvee.style":{"source":"iana","extensions":["msty"]},"application/vnd.mynfc":{"source":"iana","extensions":["taglet"]},"application/vnd.ncd.control":{"source":"iana"},"application/vnd.ncd.reference":{"source":"iana"},"application/vnd.nearst.inv+json":{"source":"iana","compressible":true},"application/vnd.nervana":{"source":"iana"},"application/vnd.netfpx":{"source":"iana"},"application/vnd.neurolanguage.nlu":{"source":"iana","extensions":["nlu"]},"application/vnd.nimn":{"source":"iana"},"application/vnd.nintendo.nitro.rom":{"source":"iana"},"application/vnd.nintendo.snes.rom":{"source":"iana"},"application/vnd.nitf":{"source":"iana","extensions":["ntf","nitf"]},"application/vnd.noblenet-directory":{"source":"iana","extensions":["nnd"]},"application/vnd.noblenet-sealer":{"source":"iana","extensions":["nns"]},"application/vnd.noblenet-web":{"source":"iana","extensions":["nnw"]},"application/vnd.nokia.catalogs":{"source":"iana"},"application/vnd.nokia.conml+wbxml":{"source":"iana"},"application/vnd.nokia.conml+xml":{"source":"iana","compressible":true},"application/vnd.nokia.iptv.config+xml":{"source":"iana","compressible":true},"application/vnd.nokia.isds-radio-presets":{"source":"iana"},"application/vnd.nokia.landmark+wbxml":{"source":"iana"},"application/vnd.nokia.landmark+xml":{"source":"iana","compressible":true},"application/vnd.nokia.landmarkcollection+xml":{"source":"iana","compressible":true},"application/vnd.nokia.n-gage.ac+xml":{"source":"iana","compressible":true,"extensions":["ac"]},"application/vnd.nokia.n-gage.data":{"source":"iana","extensions":["ngdat"]},"application/vnd.nokia.n-gage.symbian.install":{"source":"iana","extensions":["n-gage"]},"application/vnd.nokia.ncd":{"source":"iana"},"application/vnd.nokia.pcd+wbxml":{"source":"iana"},"application/vnd.nokia.pcd+xml":{"source":"iana","compressible":true},"application/vnd.nokia.radio-preset":{"source":"iana","extensions":["rpst"]},"application/vnd.nokia.radio-presets":{"source":"iana","extensions":["rpss"]},"application/vnd.novadigm.edm":{"source":"iana","extensions":["edm"]},"application/vnd.novadigm.edx":{"source":"iana","extensions":["edx"]},"application/vnd.novadigm.ext":{"source":"iana","extensions":["ext"]},"application/vnd.ntt-local.content-share":{"source":"iana"},"application/vnd.ntt-local.file-transfer":{"source":"iana"},"application/vnd.ntt-local.ogw_remote-access":{"source":"iana"},"application/vnd.ntt-local.sip-ta_remote":{"source":"iana"},"application/vnd.ntt-local.sip-ta_tcp_stream":{"source":"iana"},"application/vnd.oasis.opendocument.chart":{"source":"iana","extensions":["odc"]},"application/vnd.oasis.opendocument.chart-template":{"source":"iana","extensions":["otc"]},"application/vnd.oasis.opendocument.database":{"source":"iana","extensions":["odb"]},"application/vnd.oasis.opendocument.formula":{"source":"iana","extensions":["odf"]},"application/vnd.oasis.opendocument.formula-template":{"source":"iana","extensions":["odft"]},"application/vnd.oasis.opendocument.graphics":{"source":"iana","compressible":false,"extensions":["odg"]},"application/vnd.oasis.opendocument.graphics-template":{"source":"iana","extensions":["otg"]},"application/vnd.oasis.opendocument.image":{"source":"iana","extensions":["odi"]},"application/vnd.oasis.opendocument.image-template":{"source":"iana","extensions":["oti"]},"application/vnd.oasis.opendocument.presentation":{"source":"iana","compressible":false,"extensions":["odp"]},"application/vnd.oasis.opendocument.presentation-template":{"source":"iana","extensions":["otp"]},"application/vnd.oasis.opendocument.spreadsheet":{"source":"iana","compressible":false,"extensions":["ods"]},"application/vnd.oasis.opendocument.spreadsheet-template":{"source":"iana","extensions":["ots"]},"application/vnd.oasis.opendocument.text":{"source":"iana","compressible":false,"extensions":["odt"]},"application/vnd.oasis.opendocument.text-master":{"source":"iana","extensions":["odm"]},"application/vnd.oasis.opendocument.text-template":{"source":"iana","extensions":["ott"]},"application/vnd.oasis.opendocument.text-web":{"source":"iana","extensions":["oth"]},"application/vnd.obn":{"source":"iana"},"application/vnd.ocf+cbor":{"source":"iana"},"application/vnd.oci.image.manifest.v1+json":{"source":"iana","compressible":true},"application/vnd.oftn.l10n+json":{"source":"iana","compressible":true},"application/vnd.oipf.contentaccessdownload+xml":{"source":"iana","compressible":true},"application/vnd.oipf.contentaccessstreaming+xml":{"source":"iana","compressible":true},"application/vnd.oipf.cspg-hexbinary":{"source":"iana"},"application/vnd.oipf.dae.svg+xml":{"source":"iana","compressible":true},"application/vnd.oipf.dae.xhtml+xml":{"source":"iana","compressible":true},"application/vnd.oipf.mippvcontrolmessage+xml":{"source":"iana","compressible":true},"application/vnd.oipf.pae.gem":{"source":"iana"},"application/vnd.oipf.spdiscovery+xml":{"source":"iana","compressible":true},"application/vnd.oipf.spdlist+xml":{"source":"iana","compressible":true},"application/vnd.oipf.ueprofile+xml":{"source":"iana","compressible":true},"application/vnd.oipf.userprofile+xml":{"source":"iana","compressible":true},"application/vnd.olpc-sugar":{"source":"iana","extensions":["xo"]},"application/vnd.oma-scws-config":{"source":"iana"},"application/vnd.oma-scws-http-request":{"source":"iana"},"application/vnd.oma-scws-http-response":{"source":"iana"},"application/vnd.oma.bcast.associated-procedure-parameter+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.drm-trigger+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.imd+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.ltkm":{"source":"iana"},"application/vnd.oma.bcast.notification+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.provisioningtrigger":{"source":"iana"},"application/vnd.oma.bcast.sgboot":{"source":"iana"},"application/vnd.oma.bcast.sgdd+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.sgdu":{"source":"iana"},"application/vnd.oma.bcast.simple-symbol-container":{"source":"iana"},"application/vnd.oma.bcast.smartcard-trigger+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.sprov+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.stkm":{"source":"iana"},"application/vnd.oma.cab-address-book+xml":{"source":"iana","compressible":true},"application/vnd.oma.cab-feature-handler+xml":{"source":"iana","compressible":true},"application/vnd.oma.cab-pcc+xml":{"source":"iana","compressible":true},"application/vnd.oma.cab-subs-invite+xml":{"source":"iana","compressible":true},"application/vnd.oma.cab-user-prefs+xml":{"source":"iana","compressible":true},"application/vnd.oma.dcd":{"source":"iana"},"application/vnd.oma.dcdc":{"source":"iana"},"application/vnd.oma.dd2+xml":{"source":"iana","compressible":true,"extensions":["dd2"]},"application/vnd.oma.drm.risd+xml":{"source":"iana","compressible":true},"application/vnd.oma.group-usage-list+xml":{"source":"iana","compressible":true},"application/vnd.oma.lwm2m+json":{"source":"iana","compressible":true},"application/vnd.oma.lwm2m+tlv":{"source":"iana"},"application/vnd.oma.pal+xml":{"source":"iana","compressible":true},"application/vnd.oma.poc.detailed-progress-report+xml":{"source":"iana","compressible":true},"application/vnd.oma.poc.final-report+xml":{"source":"iana","compressible":true},"application/vnd.oma.poc.groups+xml":{"source":"iana","compressible":true},"application/vnd.oma.poc.invocation-descriptor+xml":{"source":"iana","compressible":true},"application/vnd.oma.poc.optimized-progress-report+xml":{"source":"iana","compressible":true},"application/vnd.oma.push":{"source":"iana"},"application/vnd.oma.scidm.messages+xml":{"source":"iana","compressible":true},"application/vnd.oma.xcap-directory+xml":{"source":"iana","compressible":true},"application/vnd.omads-email+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/vnd.omads-file+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/vnd.omads-folder+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/vnd.omaloc-supl-init":{"source":"iana"},"application/vnd.onepager":{"source":"iana"},"application/vnd.onepagertamp":{"source":"iana"},"application/vnd.onepagertamx":{"source":"iana"},"application/vnd.onepagertat":{"source":"iana"},"application/vnd.onepagertatp":{"source":"iana"},"application/vnd.onepagertatx":{"source":"iana"},"application/vnd.openblox.game+xml":{"source":"iana","compressible":true,"extensions":["obgx"]},"application/vnd.openblox.game-binary":{"source":"iana"},"application/vnd.openeye.oeb":{"source":"iana"},"application/vnd.openofficeorg.extension":{"source":"apache","extensions":["oxt"]},"application/vnd.openstreetmap.data+xml":{"source":"iana","compressible":true,"extensions":["osm"]},"application/vnd.openxmlformats-officedocument.custom-properties+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.customxmlproperties+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawing+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.chart+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.extended-properties+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.comments+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.presentation":{"source":"iana","compressible":false,"extensions":["pptx"]},"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.presprops+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.slide":{"source":"iana","extensions":["sldx"]},"application/vnd.openxmlformats-officedocument.presentationml.slide+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.slideshow":{"source":"iana","extensions":["ppsx"]},"application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.tags+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.template":{"source":"iana","extensions":["potx"]},"application/vnd.openxmlformats-officedocument.presentationml.template.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"source":"iana","compressible":false,"extensions":["xlsx"]},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.template":{"source":"iana","extensions":["xltx"]},"application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.theme+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.themeoverride+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.vmldrawing":{"source":"iana"},"application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.document":{"source":"iana","compressible":false,"extensions":["docx"]},"application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.template":{"source":"iana","extensions":["dotx"]},"application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-package.core-properties+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-package.relationships+xml":{"source":"iana","compressible":true},"application/vnd.oracle.resource+json":{"source":"iana","compressible":true},"application/vnd.orange.indata":{"source":"iana"},"application/vnd.osa.netdeploy":{"source":"iana"},"application/vnd.osgeo.mapguide.package":{"source":"iana","extensions":["mgp"]},"application/vnd.osgi.bundle":{"source":"iana"},"application/vnd.osgi.dp":{"source":"iana","extensions":["dp"]},"application/vnd.osgi.subsystem":{"source":"iana","extensions":["esa"]},"application/vnd.otps.ct-kip+xml":{"source":"iana","compressible":true},"application/vnd.oxli.countgraph":{"source":"iana"},"application/vnd.pagerduty+json":{"source":"iana","compressible":true},"application/vnd.palm":{"source":"iana","extensions":["pdb","pqa","oprc"]},"application/vnd.panoply":{"source":"iana"},"application/vnd.paos.xml":{"source":"iana"},"application/vnd.patentdive":{"source":"iana"},"application/vnd.patientecommsdoc":{"source":"iana"},"application/vnd.pawaafile":{"source":"iana","extensions":["paw"]},"application/vnd.pcos":{"source":"iana"},"application/vnd.pg.format":{"source":"iana","extensions":["str"]},"application/vnd.pg.osasli":{"source":"iana","extensions":["ei6"]},"application/vnd.piaccess.application-licence":{"source":"iana"},"application/vnd.picsel":{"source":"iana","extensions":["efif"]},"application/vnd.pmi.widget":{"source":"iana","extensions":["wg"]},"application/vnd.poc.group-advertisement+xml":{"source":"iana","compressible":true},"application/vnd.pocketlearn":{"source":"iana","extensions":["plf"]},"application/vnd.powerbuilder6":{"source":"iana","extensions":["pbd"]},"application/vnd.powerbuilder6-s":{"source":"iana"},"application/vnd.powerbuilder7":{"source":"iana"},"application/vnd.powerbuilder7-s":{"source":"iana"},"application/vnd.powerbuilder75":{"source":"iana"},"application/vnd.powerbuilder75-s":{"source":"iana"},"application/vnd.preminet":{"source":"iana"},"application/vnd.previewsystems.box":{"source":"iana","extensions":["box"]},"application/vnd.proteus.magazine":{"source":"iana","extensions":["mgz"]},"application/vnd.psfs":{"source":"iana"},"application/vnd.publishare-delta-tree":{"source":"iana","extensions":["qps"]},"application/vnd.pvi.ptid1":{"source":"iana","extensions":["ptid"]},"application/vnd.pwg-multiplexed":{"source":"iana"},"application/vnd.pwg-xhtml-print+xml":{"source":"iana","compressible":true},"application/vnd.qualcomm.brew-app-res":{"source":"iana"},"application/vnd.quarantainenet":{"source":"iana"},"application/vnd.quark.quarkxpress":{"source":"iana","extensions":["qxd","qxt","qwd","qwt","qxl","qxb"]},"application/vnd.quobject-quoxdocument":{"source":"iana"},"application/vnd.radisys.moml+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-audit+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-audit-conf+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-audit-conn+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-audit-dialog+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-audit-stream+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-conf+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-base+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-fax-detect+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-fax-sendrecv+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-group+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-speech+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-transform+xml":{"source":"iana","compressible":true},"application/vnd.rainstor.data":{"source":"iana"},"application/vnd.rapid":{"source":"iana"},"application/vnd.rar":{"source":"iana"},"application/vnd.realvnc.bed":{"source":"iana","extensions":["bed"]},"application/vnd.recordare.musicxml":{"source":"iana","extensions":["mxl"]},"application/vnd.recordare.musicxml+xml":{"source":"iana","compressible":true,"extensions":["musicxml"]},"application/vnd.renlearn.rlprint":{"source":"iana"},"application/vnd.restful+json":{"source":"iana","compressible":true},"application/vnd.rig.cryptonote":{"source":"iana","extensions":["cryptonote"]},"application/vnd.rim.cod":{"source":"apache","extensions":["cod"]},"application/vnd.rn-realmedia":{"source":"apache","extensions":["rm"]},"application/vnd.rn-realmedia-vbr":{"source":"apache","extensions":["rmvb"]},"application/vnd.route66.link66+xml":{"source":"iana","compressible":true,"extensions":["link66"]},"application/vnd.rs-274x":{"source":"iana"},"application/vnd.ruckus.download":{"source":"iana"},"application/vnd.s3sms":{"source":"iana"},"application/vnd.sailingtracker.track":{"source":"iana","extensions":["st"]},"application/vnd.sar":{"source":"iana"},"application/vnd.sbm.cid":{"source":"iana"},"application/vnd.sbm.mid2":{"source":"iana"},"application/vnd.scribus":{"source":"iana"},"application/vnd.sealed.3df":{"source":"iana"},"application/vnd.sealed.csf":{"source":"iana"},"application/vnd.sealed.doc":{"source":"iana"},"application/vnd.sealed.eml":{"source":"iana"},"application/vnd.sealed.mht":{"source":"iana"},"application/vnd.sealed.net":{"source":"iana"},"application/vnd.sealed.ppt":{"source":"iana"},"application/vnd.sealed.tiff":{"source":"iana"},"application/vnd.sealed.xls":{"source":"iana"},"application/vnd.sealedmedia.softseal.html":{"source":"iana"},"application/vnd.sealedmedia.softseal.pdf":{"source":"iana"},"application/vnd.seemail":{"source":"iana","extensions":["see"]},"application/vnd.sema":{"source":"iana","extensions":["sema"]},"application/vnd.semd":{"source":"iana","extensions":["semd"]},"application/vnd.semf":{"source":"iana","extensions":["semf"]},"application/vnd.shade-save-file":{"source":"iana"},"application/vnd.shana.informed.formdata":{"source":"iana","extensions":["ifm"]},"application/vnd.shana.informed.formtemplate":{"source":"iana","extensions":["itp"]},"application/vnd.shana.informed.interchange":{"source":"iana","extensions":["iif"]},"application/vnd.shana.informed.package":{"source":"iana","extensions":["ipk"]},"application/vnd.shootproof+json":{"source":"iana","compressible":true},"application/vnd.shopkick+json":{"source":"iana","compressible":true},"application/vnd.shp":{"source":"iana"},"application/vnd.shx":{"source":"iana"},"application/vnd.sigrok.session":{"source":"iana"},"application/vnd.simtech-mindmapper":{"source":"iana","extensions":["twd","twds"]},"application/vnd.siren+json":{"source":"iana","compressible":true},"application/vnd.smaf":{"source":"iana","extensions":["mmf"]},"application/vnd.smart.notebook":{"source":"iana"},"application/vnd.smart.teacher":{"source":"iana","extensions":["teacher"]},"application/vnd.snesdev-page-table":{"source":"iana"},"application/vnd.software602.filler.form+xml":{"source":"iana","compressible":true,"extensions":["fo"]},"application/vnd.software602.filler.form-xml-zip":{"source":"iana"},"application/vnd.solent.sdkm+xml":{"source":"iana","compressible":true,"extensions":["sdkm","sdkd"]},"application/vnd.spotfire.dxp":{"source":"iana","extensions":["dxp"]},"application/vnd.spotfire.sfs":{"source":"iana","extensions":["sfs"]},"application/vnd.sqlite3":{"source":"iana"},"application/vnd.sss-cod":{"source":"iana"},"application/vnd.sss-dtf":{"source":"iana"},"application/vnd.sss-ntf":{"source":"iana"},"application/vnd.stardivision.calc":{"source":"apache","extensions":["sdc"]},"application/vnd.stardivision.draw":{"source":"apache","extensions":["sda"]},"application/vnd.stardivision.impress":{"source":"apache","extensions":["sdd"]},"application/vnd.stardivision.math":{"source":"apache","extensions":["smf"]},"application/vnd.stardivision.writer":{"source":"apache","extensions":["sdw","vor"]},"application/vnd.stardivision.writer-global":{"source":"apache","extensions":["sgl"]},"application/vnd.stepmania.package":{"source":"iana","extensions":["smzip"]},"application/vnd.stepmania.stepchart":{"source":"iana","extensions":["sm"]},"application/vnd.street-stream":{"source":"iana"},"application/vnd.sun.wadl+xml":{"source":"iana","compressible":true,"extensions":["wadl"]},"application/vnd.sun.xml.calc":{"source":"apache","extensions":["sxc"]},"application/vnd.sun.xml.calc.template":{"source":"apache","extensions":["stc"]},"application/vnd.sun.xml.draw":{"source":"apache","extensions":["sxd"]},"application/vnd.sun.xml.draw.template":{"source":"apache","extensions":["std"]},"application/vnd.sun.xml.impress":{"source":"apache","extensions":["sxi"]},"application/vnd.sun.xml.impress.template":{"source":"apache","extensions":["sti"]},"application/vnd.sun.xml.math":{"source":"apache","extensions":["sxm"]},"application/vnd.sun.xml.writer":{"source":"apache","extensions":["sxw"]},"application/vnd.sun.xml.writer.global":{"source":"apache","extensions":["sxg"]},"application/vnd.sun.xml.writer.template":{"source":"apache","extensions":["stw"]},"application/vnd.sus-calendar":{"source":"iana","extensions":["sus","susp"]},"application/vnd.svd":{"source":"iana","extensions":["svd"]},"application/vnd.swiftview-ics":{"source":"iana"},"application/vnd.symbian.install":{"source":"apache","extensions":["sis","sisx"]},"application/vnd.syncml+xml":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["xsm"]},"application/vnd.syncml.dm+wbxml":{"source":"iana","charset":"UTF-8","extensions":["bdm"]},"application/vnd.syncml.dm+xml":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["xdm"]},"application/vnd.syncml.dm.notification":{"source":"iana"},"application/vnd.syncml.dmddf+wbxml":{"source":"iana"},"application/vnd.syncml.dmddf+xml":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["ddf"]},"application/vnd.syncml.dmtnds+wbxml":{"source":"iana"},"application/vnd.syncml.dmtnds+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/vnd.syncml.ds.notification":{"source":"iana"},"application/vnd.tableschema+json":{"source":"iana","compressible":true},"application/vnd.tao.intent-module-archive":{"source":"iana","extensions":["tao"]},"application/vnd.tcpdump.pcap":{"source":"iana","extensions":["pcap","cap","dmp"]},"application/vnd.think-cell.ppttc+json":{"source":"iana","compressible":true},"application/vnd.tmd.mediaflex.api+xml":{"source":"iana","compressible":true},"application/vnd.tml":{"source":"iana"},"application/vnd.tmobile-livetv":{"source":"iana","extensions":["tmo"]},"application/vnd.tri.onesource":{"source":"iana"},"application/vnd.trid.tpt":{"source":"iana","extensions":["tpt"]},"application/vnd.triscape.mxs":{"source":"iana","extensions":["mxs"]},"application/vnd.trueapp":{"source":"iana","extensions":["tra"]},"application/vnd.truedoc":{"source":"iana"},"application/vnd.ubisoft.webplayer":{"source":"iana"},"application/vnd.ufdl":{"source":"iana","extensions":["ufd","ufdl"]},"application/vnd.uiq.theme":{"source":"iana","extensions":["utz"]},"application/vnd.umajin":{"source":"iana","extensions":["umj"]},"application/vnd.unity":{"source":"iana","extensions":["unityweb"]},"application/vnd.uoml+xml":{"source":"iana","compressible":true,"extensions":["uoml"]},"application/vnd.uplanet.alert":{"source":"iana"},"application/vnd.uplanet.alert-wbxml":{"source":"iana"},"application/vnd.uplanet.bearer-choice":{"source":"iana"},"application/vnd.uplanet.bearer-choice-wbxml":{"source":"iana"},"application/vnd.uplanet.cacheop":{"source":"iana"},"application/vnd.uplanet.cacheop-wbxml":{"source":"iana"},"application/vnd.uplanet.channel":{"source":"iana"},"application/vnd.uplanet.channel-wbxml":{"source":"iana"},"application/vnd.uplanet.list":{"source":"iana"},"application/vnd.uplanet.list-wbxml":{"source":"iana"},"application/vnd.uplanet.listcmd":{"source":"iana"},"application/vnd.uplanet.listcmd-wbxml":{"source":"iana"},"application/vnd.uplanet.signal":{"source":"iana"},"application/vnd.uri-map":{"source":"iana"},"application/vnd.valve.source.material":{"source":"iana"},"application/vnd.vcx":{"source":"iana","extensions":["vcx"]},"application/vnd.vd-study":{"source":"iana"},"application/vnd.vectorworks":{"source":"iana"},"application/vnd.vel+json":{"source":"iana","compressible":true},"application/vnd.verimatrix.vcas":{"source":"iana"},"application/vnd.veryant.thin":{"source":"iana"},"application/vnd.ves.encrypted":{"source":"iana"},"application/vnd.vidsoft.vidconference":{"source":"iana"},"application/vnd.visio":{"source":"iana","extensions":["vsd","vst","vss","vsw"]},"application/vnd.visionary":{"source":"iana","extensions":["vis"]},"application/vnd.vividence.scriptfile":{"source":"iana"},"application/vnd.vsf":{"source":"iana","extensions":["vsf"]},"application/vnd.wap.sic":{"source":"iana"},"application/vnd.wap.slc":{"source":"iana"},"application/vnd.wap.wbxml":{"source":"iana","charset":"UTF-8","extensions":["wbxml"]},"application/vnd.wap.wmlc":{"source":"iana","extensions":["wmlc"]},"application/vnd.wap.wmlscriptc":{"source":"iana","extensions":["wmlsc"]},"application/vnd.webturbo":{"source":"iana","extensions":["wtb"]},"application/vnd.wfa.p2p":{"source":"iana"},"application/vnd.wfa.wsc":{"source":"iana"},"application/vnd.windows.devicepairing":{"source":"iana"},"application/vnd.wmc":{"source":"iana"},"application/vnd.wmf.bootstrap":{"source":"iana"},"application/vnd.wolfram.mathematica":{"source":"iana"},"application/vnd.wolfram.mathematica.package":{"source":"iana"},"application/vnd.wolfram.player":{"source":"iana","extensions":["nbp"]},"application/vnd.wordperfect":{"source":"iana","extensions":["wpd"]},"application/vnd.wqd":{"source":"iana","extensions":["wqd"]},"application/vnd.wrq-hp3000-labelled":{"source":"iana"},"application/vnd.wt.stf":{"source":"iana","extensions":["stf"]},"application/vnd.wv.csp+wbxml":{"source":"iana"},"application/vnd.wv.csp+xml":{"source":"iana","compressible":true},"application/vnd.wv.ssp+xml":{"source":"iana","compressible":true},"application/vnd.xacml+json":{"source":"iana","compressible":true},"application/vnd.xara":{"source":"iana","extensions":["xar"]},"application/vnd.xfdl":{"source":"iana","extensions":["xfdl"]},"application/vnd.xfdl.webform":{"source":"iana"},"application/vnd.xmi+xml":{"source":"iana","compressible":true},"application/vnd.xmpie.cpkg":{"source":"iana"},"application/vnd.xmpie.dpkg":{"source":"iana"},"application/vnd.xmpie.plan":{"source":"iana"},"application/vnd.xmpie.ppkg":{"source":"iana"},"application/vnd.xmpie.xlim":{"source":"iana"},"application/vnd.yamaha.hv-dic":{"source":"iana","extensions":["hvd"]},"application/vnd.yamaha.hv-script":{"source":"iana","extensions":["hvs"]},"application/vnd.yamaha.hv-voice":{"source":"iana","extensions":["hvp"]},"application/vnd.yamaha.openscoreformat":{"source":"iana","extensions":["osf"]},"application/vnd.yamaha.openscoreformat.osfpvg+xml":{"source":"iana","compressible":true,"extensions":["osfpvg"]},"application/vnd.yamaha.remote-setup":{"source":"iana"},"application/vnd.yamaha.smaf-audio":{"source":"iana","extensions":["saf"]},"application/vnd.yamaha.smaf-phrase":{"source":"iana","extensions":["spf"]},"application/vnd.yamaha.through-ngn":{"source":"iana"},"application/vnd.yamaha.tunnel-udpencap":{"source":"iana"},"application/vnd.yaoweme":{"source":"iana"},"application/vnd.yellowriver-custom-menu":{"source":"iana","extensions":["cmp"]},"application/vnd.youtube.yt":{"source":"iana"},"application/vnd.zul":{"source":"iana","extensions":["zir","zirz"]},"application/vnd.zzazz.deck+xml":{"source":"iana","compressible":true,"extensions":["zaz"]},"application/voicexml+xml":{"source":"iana","compressible":true,"extensions":["vxml"]},"application/voucher-cms+json":{"source":"iana","compressible":true},"application/vq-rtcpxr":{"source":"iana"},"application/wasm":{"compressible":true,"extensions":["wasm"]},"application/watcherinfo+xml":{"source":"iana","compressible":true},"application/webpush-options+json":{"source":"iana","compressible":true},"application/whoispp-query":{"source":"iana"},"application/whoispp-response":{"source":"iana"},"application/widget":{"source":"iana","extensions":["wgt"]},"application/winhlp":{"source":"apache","extensions":["hlp"]},"application/wita":{"source":"iana"},"application/wordperfect5.1":{"source":"iana"},"application/wsdl+xml":{"source":"iana","compressible":true,"extensions":["wsdl"]},"application/wspolicy+xml":{"source":"iana","compressible":true,"extensions":["wspolicy"]},"application/x-7z-compressed":{"source":"apache","compressible":false,"extensions":["7z"]},"application/x-abiword":{"source":"apache","extensions":["abw"]},"application/x-ace-compressed":{"source":"apache","extensions":["ace"]},"application/x-amf":{"source":"apache"},"application/x-apple-diskimage":{"source":"apache","extensions":["dmg"]},"application/x-arj":{"compressible":false,"extensions":["arj"]},"application/x-authorware-bin":{"source":"apache","extensions":["aab","x32","u32","vox"]},"application/x-authorware-map":{"source":"apache","extensions":["aam"]},"application/x-authorware-seg":{"source":"apache","extensions":["aas"]},"application/x-bcpio":{"source":"apache","extensions":["bcpio"]},"application/x-bdoc":{"compressible":false,"extensions":["bdoc"]},"application/x-bittorrent":{"source":"apache","extensions":["torrent"]},"application/x-blorb":{"source":"apache","extensions":["blb","blorb"]},"application/x-bzip":{"source":"apache","compressible":false,"extensions":["bz"]},"application/x-bzip2":{"source":"apache","compressible":false,"extensions":["bz2","boz"]},"application/x-cbr":{"source":"apache","extensions":["cbr","cba","cbt","cbz","cb7"]},"application/x-cdlink":{"source":"apache","extensions":["vcd"]},"application/x-cfs-compressed":{"source":"apache","extensions":["cfs"]},"application/x-chat":{"source":"apache","extensions":["chat"]},"application/x-chess-pgn":{"source":"apache","extensions":["pgn"]},"application/x-chrome-extension":{"extensions":["crx"]},"application/x-cocoa":{"source":"nginx","extensions":["cco"]},"application/x-compress":{"source":"apache"},"application/x-conference":{"source":"apache","extensions":["nsc"]},"application/x-cpio":{"source":"apache","extensions":["cpio"]},"application/x-csh":{"source":"apache","extensions":["csh"]},"application/x-deb":{"compressible":false},"application/x-debian-package":{"source":"apache","extensions":["deb","udeb"]},"application/x-dgc-compressed":{"source":"apache","extensions":["dgc"]},"application/x-director":{"source":"apache","extensions":["dir","dcr","dxr","cst","cct","cxt","w3d","fgd","swa"]},"application/x-doom":{"source":"apache","extensions":["wad"]},"application/x-dtbncx+xml":{"source":"apache","compressible":true,"extensions":["ncx"]},"application/x-dtbook+xml":{"source":"apache","compressible":true,"extensions":["dtb"]},"application/x-dtbresource+xml":{"source":"apache","compressible":true,"extensions":["res"]},"application/x-dvi":{"source":"apache","compressible":false,"extensions":["dvi"]},"application/x-envoy":{"source":"apache","extensions":["evy"]},"application/x-eva":{"source":"apache","extensions":["eva"]},"application/x-font-bdf":{"source":"apache","extensions":["bdf"]},"application/x-font-dos":{"source":"apache"},"application/x-font-framemaker":{"source":"apache"},"application/x-font-ghostscript":{"source":"apache","extensions":["gsf"]},"application/x-font-libgrx":{"source":"apache"},"application/x-font-linux-psf":{"source":"apache","extensions":["psf"]},"application/x-font-pcf":{"source":"apache","extensions":["pcf"]},"application/x-font-snf":{"source":"apache","extensions":["snf"]},"application/x-font-speedo":{"source":"apache"},"application/x-font-sunos-news":{"source":"apache"},"application/x-font-type1":{"source":"apache","extensions":["pfa","pfb","pfm","afm"]},"application/x-font-vfont":{"source":"apache"},"application/x-freearc":{"source":"apache","extensions":["arc"]},"application/x-futuresplash":{"source":"apache","extensions":["spl"]},"application/x-gca-compressed":{"source":"apache","extensions":["gca"]},"application/x-glulx":{"source":"apache","extensions":["ulx"]},"application/x-gnumeric":{"source":"apache","extensions":["gnumeric"]},"application/x-gramps-xml":{"source":"apache","extensions":["gramps"]},"application/x-gtar":{"source":"apache","extensions":["gtar"]},"application/x-gzip":{"source":"apache"},"application/x-hdf":{"source":"apache","extensions":["hdf"]},"application/x-httpd-php":{"compressible":true,"extensions":["php"]},"application/x-install-instructions":{"source":"apache","extensions":["install"]},"application/x-iso9660-image":{"source":"apache","extensions":["iso"]},"application/x-java-archive-diff":{"source":"nginx","extensions":["jardiff"]},"application/x-java-jnlp-file":{"source":"apache","compressible":false,"extensions":["jnlp"]},"application/x-javascript":{"compressible":true},"application/x-keepass2":{"extensions":["kdbx"]},"application/x-latex":{"source":"apache","compressible":false,"extensions":["latex"]},"application/x-lua-bytecode":{"extensions":["luac"]},"application/x-lzh-compressed":{"source":"apache","extensions":["lzh","lha"]},"application/x-makeself":{"source":"nginx","extensions":["run"]},"application/x-mie":{"source":"apache","extensions":["mie"]},"application/x-mobipocket-ebook":{"source":"apache","extensions":["prc","mobi"]},"application/x-mpegurl":{"compressible":false},"application/x-ms-application":{"source":"apache","extensions":["application"]},"application/x-ms-shortcut":{"source":"apache","extensions":["lnk"]},"application/x-ms-wmd":{"source":"apache","extensions":["wmd"]},"application/x-ms-wmz":{"source":"apache","extensions":["wmz"]},"application/x-ms-xbap":{"source":"apache","extensions":["xbap"]},"application/x-msaccess":{"source":"apache","extensions":["mdb"]},"application/x-msbinder":{"source":"apache","extensions":["obd"]},"application/x-mscardfile":{"source":"apache","extensions":["crd"]},"application/x-msclip":{"source":"apache","extensions":["clp"]},"application/x-msdos-program":{"extensions":["exe"]},"application/x-msdownload":{"source":"apache","extensions":["exe","dll","com","bat","msi"]},"application/x-msmediaview":{"source":"apache","extensions":["mvb","m13","m14"]},"application/x-msmetafile":{"source":"apache","extensions":["wmf","wmz","emf","emz"]},"application/x-msmoney":{"source":"apache","extensions":["mny"]},"application/x-mspublisher":{"source":"apache","extensions":["pub"]},"application/x-msschedule":{"source":"apache","extensions":["scd"]},"application/x-msterminal":{"source":"apache","extensions":["trm"]},"application/x-mswrite":{"source":"apache","extensions":["wri"]},"application/x-netcdf":{"source":"apache","extensions":["nc","cdf"]},"application/x-ns-proxy-autoconfig":{"compressible":true,"extensions":["pac"]},"application/x-nzb":{"source":"apache","extensions":["nzb"]},"application/x-perl":{"source":"nginx","extensions":["pl","pm"]},"application/x-pilot":{"source":"nginx","extensions":["prc","pdb"]},"application/x-pkcs12":{"source":"apache","compressible":false,"extensions":["p12","pfx"]},"application/x-pkcs7-certificates":{"source":"apache","extensions":["p7b","spc"]},"application/x-pkcs7-certreqresp":{"source":"apache","extensions":["p7r"]},"application/x-pki-message":{"source":"iana"},"application/x-rar-compressed":{"source":"apache","compressible":false,"extensions":["rar"]},"application/x-redhat-package-manager":{"source":"nginx","extensions":["rpm"]},"application/x-research-info-systems":{"source":"apache","extensions":["ris"]},"application/x-sea":{"source":"nginx","extensions":["sea"]},"application/x-sh":{"source":"apache","compressible":true,"extensions":["sh"]},"application/x-shar":{"source":"apache","extensions":["shar"]},"application/x-shockwave-flash":{"source":"apache","compressible":false,"extensions":["swf"]},"application/x-silverlight-app":{"source":"apache","extensions":["xap"]},"application/x-sql":{"source":"apache","extensions":["sql"]},"application/x-stuffit":{"source":"apache","compressible":false,"extensions":["sit"]},"application/x-stuffitx":{"source":"apache","extensions":["sitx"]},"application/x-subrip":{"source":"apache","extensions":["srt"]},"application/x-sv4cpio":{"source":"apache","extensions":["sv4cpio"]},"application/x-sv4crc":{"source":"apache","extensions":["sv4crc"]},"application/x-t3vm-image":{"source":"apache","extensions":["t3"]},"application/x-tads":{"source":"apache","extensions":["gam"]},"application/x-tar":{"source":"apache","compressible":true,"extensions":["tar"]},"application/x-tcl":{"source":"apache","extensions":["tcl","tk"]},"application/x-tex":{"source":"apache","extensions":["tex"]},"application/x-tex-tfm":{"source":"apache","extensions":["tfm"]},"application/x-texinfo":{"source":"apache","extensions":["texinfo","texi"]},"application/x-tgif":{"source":"apache","extensions":["obj"]},"application/x-ustar":{"source":"apache","extensions":["ustar"]},"application/x-virtualbox-hdd":{"compressible":true,"extensions":["hdd"]},"application/x-virtualbox-ova":{"compressible":true,"extensions":["ova"]},"application/x-virtualbox-ovf":{"compressible":true,"extensions":["ovf"]},"application/x-virtualbox-vbox":{"compressible":true,"extensions":["vbox"]},"application/x-virtualbox-vbox-extpack":{"compressible":false,"extensions":["vbox-extpack"]},"application/x-virtualbox-vdi":{"compressible":true,"extensions":["vdi"]},"application/x-virtualbox-vhd":{"compressible":true,"extensions":["vhd"]},"application/x-virtualbox-vmdk":{"compressible":true,"extensions":["vmdk"]},"application/x-wais-source":{"source":"apache","extensions":["src"]},"application/x-web-app-manifest+json":{"compressible":true,"extensions":["webapp"]},"application/x-www-form-urlencoded":{"source":"iana","compressible":true},"application/x-x509-ca-cert":{"source":"iana","extensions":["der","crt","pem"]},"application/x-x509-ca-ra-cert":{"source":"iana"},"application/x-x509-next-ca-cert":{"source":"iana"},"application/x-xfig":{"source":"apache","extensions":["fig"]},"application/x-xliff+xml":{"source":"apache","compressible":true,"extensions":["xlf"]},"application/x-xpinstall":{"source":"apache","compressible":false,"extensions":["xpi"]},"application/x-xz":{"source":"apache","extensions":["xz"]},"application/x-zmachine":{"source":"apache","extensions":["z1","z2","z3","z4","z5","z6","z7","z8"]},"application/x400-bp":{"source":"iana"},"application/xacml+xml":{"source":"iana","compressible":true},"application/xaml+xml":{"source":"apache","compressible":true,"extensions":["xaml"]},"application/xcap-att+xml":{"source":"iana","compressible":true,"extensions":["xav"]},"application/xcap-caps+xml":{"source":"iana","compressible":true,"extensions":["xca"]},"application/xcap-diff+xml":{"source":"iana","compressible":true,"extensions":["xdf"]},"application/xcap-el+xml":{"source":"iana","compressible":true,"extensions":["xel"]},"application/xcap-error+xml":{"source":"iana","compressible":true,"extensions":["xer"]},"application/xcap-ns+xml":{"source":"iana","compressible":true,"extensions":["xns"]},"application/xcon-conference-info+xml":{"source":"iana","compressible":true},"application/xcon-conference-info-diff+xml":{"source":"iana","compressible":true},"application/xenc+xml":{"source":"iana","compressible":true,"extensions":["xenc"]},"application/xhtml+xml":{"source":"iana","compressible":true,"extensions":["xhtml","xht"]},"application/xhtml-voice+xml":{"source":"apache","compressible":true},"application/xliff+xml":{"source":"iana","compressible":true,"extensions":["xlf"]},"application/xml":{"source":"iana","compressible":true,"extensions":["xml","xsl","xsd","rng"]},"application/xml-dtd":{"source":"iana","compressible":true,"extensions":["dtd"]},"application/xml-external-parsed-entity":{"source":"iana"},"application/xml-patch+xml":{"source":"iana","compressible":true},"application/xmpp+xml":{"source":"iana","compressible":true},"application/xop+xml":{"source":"iana","compressible":true,"extensions":["xop"]},"application/xproc+xml":{"source":"apache","compressible":true,"extensions":["xpl"]},"application/xslt+xml":{"source":"iana","compressible":true,"extensions":["xslt"]},"application/xspf+xml":{"source":"apache","compressible":true,"extensions":["xspf"]},"application/xv+xml":{"source":"iana","compressible":true,"extensions":["mxml","xhvml","xvml","xvm"]},"application/yang":{"source":"iana","extensions":["yang"]},"application/yang-data+json":{"source":"iana","compressible":true},"application/yang-data+xml":{"source":"iana","compressible":true},"application/yang-patch+json":{"source":"iana","compressible":true},"application/yang-patch+xml":{"source":"iana","compressible":true},"application/yin+xml":{"source":"iana","compressible":true,"extensions":["yin"]},"application/zip":{"source":"iana","compressible":false,"extensions":["zip"]},"application/zlib":{"source":"iana"},"application/zstd":{"source":"iana"},"audio/1d-interleaved-parityfec":{"source":"iana"},"audio/32kadpcm":{"source":"iana"},"audio/3gpp":{"source":"iana","compressible":false,"extensions":["3gpp"]},"audio/3gpp2":{"source":"iana"},"audio/aac":{"source":"iana"},"audio/ac3":{"source":"iana"},"audio/adpcm":{"source":"apache","extensions":["adp"]},"audio/amr":{"source":"iana"},"audio/amr-wb":{"source":"iana"},"audio/amr-wb+":{"source":"iana"},"audio/aptx":{"source":"iana"},"audio/asc":{"source":"iana"},"audio/atrac-advanced-lossless":{"source":"iana"},"audio/atrac-x":{"source":"iana"},"audio/atrac3":{"source":"iana"},"audio/basic":{"source":"iana","compressible":false,"extensions":["au","snd"]},"audio/bv16":{"source":"iana"},"audio/bv32":{"source":"iana"},"audio/clearmode":{"source":"iana"},"audio/cn":{"source":"iana"},"audio/dat12":{"source":"iana"},"audio/dls":{"source":"iana"},"audio/dsr-es201108":{"source":"iana"},"audio/dsr-es202050":{"source":"iana"},"audio/dsr-es202211":{"source":"iana"},"audio/dsr-es202212":{"source":"iana"},"audio/dv":{"source":"iana"},"audio/dvi4":{"source":"iana"},"audio/eac3":{"source":"iana"},"audio/encaprtp":{"source":"iana"},"audio/evrc":{"source":"iana"},"audio/evrc-qcp":{"source":"iana"},"audio/evrc0":{"source":"iana"},"audio/evrc1":{"source":"iana"},"audio/evrcb":{"source":"iana"},"audio/evrcb0":{"source":"iana"},"audio/evrcb1":{"source":"iana"},"audio/evrcnw":{"source":"iana"},"audio/evrcnw0":{"source":"iana"},"audio/evrcnw1":{"source":"iana"},"audio/evrcwb":{"source":"iana"},"audio/evrcwb0":{"source":"iana"},"audio/evrcwb1":{"source":"iana"},"audio/evs":{"source":"iana"},"audio/flexfec":{"source":"iana"},"audio/fwdred":{"source":"iana"},"audio/g711-0":{"source":"iana"},"audio/g719":{"source":"iana"},"audio/g722":{"source":"iana"},"audio/g7221":{"source":"iana"},"audio/g723":{"source":"iana"},"audio/g726-16":{"source":"iana"},"audio/g726-24":{"source":"iana"},"audio/g726-32":{"source":"iana"},"audio/g726-40":{"source":"iana"},"audio/g728":{"source":"iana"},"audio/g729":{"source":"iana"},"audio/g7291":{"source":"iana"},"audio/g729d":{"source":"iana"},"audio/g729e":{"source":"iana"},"audio/gsm":{"source":"iana"},"audio/gsm-efr":{"source":"iana"},"audio/gsm-hr-08":{"source":"iana"},"audio/ilbc":{"source":"iana"},"audio/ip-mr_v2.5":{"source":"iana"},"audio/isac":{"source":"apache"},"audio/l16":{"source":"iana"},"audio/l20":{"source":"iana"},"audio/l24":{"source":"iana","compressible":false},"audio/l8":{"source":"iana"},"audio/lpc":{"source":"iana"},"audio/melp":{"source":"iana"},"audio/melp1200":{"source":"iana"},"audio/melp2400":{"source":"iana"},"audio/melp600":{"source":"iana"},"audio/mhas":{"source":"iana"},"audio/midi":{"source":"apache","extensions":["mid","midi","kar","rmi"]},"audio/mobile-xmf":{"source":"iana","extensions":["mxmf"]},"audio/mp3":{"compressible":false,"extensions":["mp3"]},"audio/mp4":{"source":"iana","compressible":false,"extensions":["m4a","mp4a"]},"audio/mp4a-latm":{"source":"iana"},"audio/mpa":{"source":"iana"},"audio/mpa-robust":{"source":"iana"},"audio/mpeg":{"source":"iana","compressible":false,"extensions":["mpga","mp2","mp2a","mp3","m2a","m3a"]},"audio/mpeg4-generic":{"source":"iana"},"audio/musepack":{"source":"apache"},"audio/ogg":{"source":"iana","compressible":false,"extensions":["oga","ogg","spx"]},"audio/opus":{"source":"iana"},"audio/parityfec":{"source":"iana"},"audio/pcma":{"source":"iana"},"audio/pcma-wb":{"source":"iana"},"audio/pcmu":{"source":"iana"},"audio/pcmu-wb":{"source":"iana"},"audio/prs.sid":{"source":"iana"},"audio/qcelp":{"source":"iana"},"audio/raptorfec":{"source":"iana"},"audio/red":{"source":"iana"},"audio/rtp-enc-aescm128":{"source":"iana"},"audio/rtp-midi":{"source":"iana"},"audio/rtploopback":{"source":"iana"},"audio/rtx":{"source":"iana"},"audio/s3m":{"source":"apache","extensions":["s3m"]},"audio/silk":{"source":"apache","extensions":["sil"]},"audio/smv":{"source":"iana"},"audio/smv-qcp":{"source":"iana"},"audio/smv0":{"source":"iana"},"audio/sp-midi":{"source":"iana"},"audio/speex":{"source":"iana"},"audio/t140c":{"source":"iana"},"audio/t38":{"source":"iana"},"audio/telephone-event":{"source":"iana"},"audio/tetra_acelp":{"source":"iana"},"audio/tetra_acelp_bb":{"source":"iana"},"audio/tone":{"source":"iana"},"audio/uemclip":{"source":"iana"},"audio/ulpfec":{"source":"iana"},"audio/usac":{"source":"iana"},"audio/vdvi":{"source":"iana"},"audio/vmr-wb":{"source":"iana"},"audio/vnd.3gpp.iufp":{"source":"iana"},"audio/vnd.4sb":{"source":"iana"},"audio/vnd.audiokoz":{"source":"iana"},"audio/vnd.celp":{"source":"iana"},"audio/vnd.cisco.nse":{"source":"iana"},"audio/vnd.cmles.radio-events":{"source":"iana"},"audio/vnd.cns.anp1":{"source":"iana"},"audio/vnd.cns.inf1":{"source":"iana"},"audio/vnd.dece.audio":{"source":"iana","extensions":["uva","uvva"]},"audio/vnd.digital-winds":{"source":"iana","extensions":["eol"]},"audio/vnd.dlna.adts":{"source":"iana"},"audio/vnd.dolby.heaac.1":{"source":"iana"},"audio/vnd.dolby.heaac.2":{"source":"iana"},"audio/vnd.dolby.mlp":{"source":"iana"},"audio/vnd.dolby.mps":{"source":"iana"},"audio/vnd.dolby.pl2":{"source":"iana"},"audio/vnd.dolby.pl2x":{"source":"iana"},"audio/vnd.dolby.pl2z":{"source":"iana"},"audio/vnd.dolby.pulse.1":{"source":"iana"},"audio/vnd.dra":{"source":"iana","extensions":["dra"]},"audio/vnd.dts":{"source":"iana","extensions":["dts"]},"audio/vnd.dts.hd":{"source":"iana","extensions":["dtshd"]},"audio/vnd.dts.uhd":{"source":"iana"},"audio/vnd.dvb.file":{"source":"iana"},"audio/vnd.everad.plj":{"source":"iana"},"audio/vnd.hns.audio":{"source":"iana"},"audio/vnd.lucent.voice":{"source":"iana","extensions":["lvp"]},"audio/vnd.ms-playready.media.pya":{"source":"iana","extensions":["pya"]},"audio/vnd.nokia.mobile-xmf":{"source":"iana"},"audio/vnd.nortel.vbk":{"source":"iana"},"audio/vnd.nuera.ecelp4800":{"source":"iana","extensions":["ecelp4800"]},"audio/vnd.nuera.ecelp7470":{"source":"iana","extensions":["ecelp7470"]},"audio/vnd.nuera.ecelp9600":{"source":"iana","extensions":["ecelp9600"]},"audio/vnd.octel.sbc":{"source":"iana"},"audio/vnd.presonus.multitrack":{"source":"iana"},"audio/vnd.qcelp":{"source":"iana"},"audio/vnd.rhetorex.32kadpcm":{"source":"iana"},"audio/vnd.rip":{"source":"iana","extensions":["rip"]},"audio/vnd.rn-realaudio":{"compressible":false},"audio/vnd.sealedmedia.softseal.mpeg":{"source":"iana"},"audio/vnd.vmx.cvsd":{"source":"iana"},"audio/vnd.wave":{"compressible":false},"audio/vorbis":{"source":"iana","compressible":false},"audio/vorbis-config":{"source":"iana"},"audio/wav":{"compressible":false,"extensions":["wav"]},"audio/wave":{"compressible":false,"extensions":["wav"]},"audio/webm":{"source":"apache","compressible":false,"extensions":["weba"]},"audio/x-aac":{"source":"apache","compressible":false,"extensions":["aac"]},"audio/x-aiff":{"source":"apache","extensions":["aif","aiff","aifc"]},"audio/x-caf":{"source":"apache","compressible":false,"extensions":["caf"]},"audio/x-flac":{"source":"apache","extensions":["flac"]},"audio/x-m4a":{"source":"nginx","extensions":["m4a"]},"audio/x-matroska":{"source":"apache","extensions":["mka"]},"audio/x-mpegurl":{"source":"apache","extensions":["m3u"]},"audio/x-ms-wax":{"source":"apache","extensions":["wax"]},"audio/x-ms-wma":{"source":"apache","extensions":["wma"]},"audio/x-pn-realaudio":{"source":"apache","extensions":["ram","ra"]},"audio/x-pn-realaudio-plugin":{"source":"apache","extensions":["rmp"]},"audio/x-realaudio":{"source":"nginx","extensions":["ra"]},"audio/x-tta":{"source":"apache"},"audio/x-wav":{"source":"apache","extensions":["wav"]},"audio/xm":{"source":"apache","extensions":["xm"]},"chemical/x-cdx":{"source":"apache","extensions":["cdx"]},"chemical/x-cif":{"source":"apache","extensions":["cif"]},"chemical/x-cmdf":{"source":"apache","extensions":["cmdf"]},"chemical/x-cml":{"source":"apache","extensions":["cml"]},"chemical/x-csml":{"source":"apache","extensions":["csml"]},"chemical/x-pdb":{"source":"apache"},"chemical/x-xyz":{"source":"apache","extensions":["xyz"]},"font/collection":{"source":"iana","extensions":["ttc"]},"font/otf":{"source":"iana","compressible":true,"extensions":["otf"]},"font/sfnt":{"source":"iana"},"font/ttf":{"source":"iana","compressible":true,"extensions":["ttf"]},"font/woff":{"source":"iana","extensions":["woff"]},"font/woff2":{"source":"iana","extensions":["woff2"]},"image/aces":{"source":"iana","extensions":["exr"]},"image/apng":{"compressible":false,"extensions":["apng"]},"image/avci":{"source":"iana"},"image/avcs":{"source":"iana"},"image/bmp":{"source":"iana","compressible":true,"extensions":["bmp"]},"image/cgm":{"source":"iana","extensions":["cgm"]},"image/dicom-rle":{"source":"iana","extensions":["drle"]},"image/emf":{"source":"iana","extensions":["emf"]},"image/fits":{"source":"iana","extensions":["fits"]},"image/g3fax":{"source":"iana","extensions":["g3"]},"image/gif":{"source":"iana","compressible":false,"extensions":["gif"]},"image/heic":{"source":"iana","extensions":["heic"]},"image/heic-sequence":{"source":"iana","extensions":["heics"]},"image/heif":{"source":"iana","extensions":["heif"]},"image/heif-sequence":{"source":"iana","extensions":["heifs"]},"image/hej2k":{"source":"iana","extensions":["hej2"]},"image/hsj2":{"source":"iana","extensions":["hsj2"]},"image/ief":{"source":"iana","extensions":["ief"]},"image/jls":{"source":"iana","extensions":["jls"]},"image/jp2":{"source":"iana","compressible":false,"extensions":["jp2","jpg2"]},"image/jpeg":{"source":"iana","compressible":false,"extensions":["jpeg","jpg","jpe"]},"image/jph":{"source":"iana","extensions":["jph"]},"image/jphc":{"source":"iana","extensions":["jhc"]},"image/jpm":{"source":"iana","compressible":false,"extensions":["jpm"]},"image/jpx":{"source":"iana","compressible":false,"extensions":["jpx","jpf"]},"image/jxr":{"source":"iana","extensions":["jxr"]},"image/jxra":{"source":"iana","extensions":["jxra"]},"image/jxrs":{"source":"iana","extensions":["jxrs"]},"image/jxs":{"source":"iana","extensions":["jxs"]},"image/jxsc":{"source":"iana","extensions":["jxsc"]},"image/jxsi":{"source":"iana","extensions":["jxsi"]},"image/jxss":{"source":"iana","extensions":["jxss"]},"image/ktx":{"source":"iana","extensions":["ktx"]},"image/naplps":{"source":"iana"},"image/pjpeg":{"compressible":false},"image/png":{"source":"iana","compressible":false,"extensions":["png"]},"image/prs.btif":{"source":"iana","extensions":["btif"]},"image/prs.pti":{"source":"iana","extensions":["pti"]},"image/pwg-raster":{"source":"iana"},"image/sgi":{"source":"apache","extensions":["sgi"]},"image/svg+xml":{"source":"iana","compressible":true,"extensions":["svg","svgz"]},"image/t38":{"source":"iana","extensions":["t38"]},"image/tiff":{"source":"iana","compressible":false,"extensions":["tif","tiff"]},"image/tiff-fx":{"source":"iana","extensions":["tfx"]},"image/vnd.adobe.photoshop":{"source":"iana","compressible":true,"extensions":["psd"]},"image/vnd.airzip.accelerator.azv":{"source":"iana","extensions":["azv"]},"image/vnd.cns.inf2":{"source":"iana"},"image/vnd.dece.graphic":{"source":"iana","extensions":["uvi","uvvi","uvg","uvvg"]},"image/vnd.djvu":{"source":"iana","extensions":["djvu","djv"]},"image/vnd.dvb.subtitle":{"source":"iana","extensions":["sub"]},"image/vnd.dwg":{"source":"iana","extensions":["dwg"]},"image/vnd.dxf":{"source":"iana","extensions":["dxf"]},"image/vnd.fastbidsheet":{"source":"iana","extensions":["fbs"]},"image/vnd.fpx":{"source":"iana","extensions":["fpx"]},"image/vnd.fst":{"source":"iana","extensions":["fst"]},"image/vnd.fujixerox.edmics-mmr":{"source":"iana","extensions":["mmr"]},"image/vnd.fujixerox.edmics-rlc":{"source":"iana","extensions":["rlc"]},"image/vnd.globalgraphics.pgb":{"source":"iana"},"image/vnd.microsoft.icon":{"source":"iana","extensions":["ico"]},"image/vnd.mix":{"source":"iana"},"image/vnd.mozilla.apng":{"source":"iana"},"image/vnd.ms-dds":{"extensions":["dds"]},"image/vnd.ms-modi":{"source":"iana","extensions":["mdi"]},"image/vnd.ms-photo":{"source":"apache","extensions":["wdp"]},"image/vnd.net-fpx":{"source":"iana","extensions":["npx"]},"image/vnd.radiance":{"source":"iana"},"image/vnd.sealed.png":{"source":"iana"},"image/vnd.sealedmedia.softseal.gif":{"source":"iana"},"image/vnd.sealedmedia.softseal.jpg":{"source":"iana"},"image/vnd.svf":{"source":"iana"},"image/vnd.tencent.tap":{"source":"iana","extensions":["tap"]},"image/vnd.valve.source.texture":{"source":"iana","extensions":["vtf"]},"image/vnd.wap.wbmp":{"source":"iana","extensions":["wbmp"]},"image/vnd.xiff":{"source":"iana","extensions":["xif"]},"image/vnd.zbrush.pcx":{"source":"iana","extensions":["pcx"]},"image/webp":{"source":"apache","extensions":["webp"]},"image/wmf":{"source":"iana","extensions":["wmf"]},"image/x-3ds":{"source":"apache","extensions":["3ds"]},"image/x-cmu-raster":{"source":"apache","extensions":["ras"]},"image/x-cmx":{"source":"apache","extensions":["cmx"]},"image/x-freehand":{"source":"apache","extensions":["fh","fhc","fh4","fh5","fh7"]},"image/x-icon":{"source":"apache","compressible":true,"extensions":["ico"]},"image/x-jng":{"source":"nginx","extensions":["jng"]},"image/x-mrsid-image":{"source":"apache","extensions":["sid"]},"image/x-ms-bmp":{"source":"nginx","compressible":true,"extensions":["bmp"]},"image/x-pcx":{"source":"apache","extensions":["pcx"]},"image/x-pict":{"source":"apache","extensions":["pic","pct"]},"image/x-portable-anymap":{"source":"apache","extensions":["pnm"]},"image/x-portable-bitmap":{"source":"apache","extensions":["pbm"]},"image/x-portable-graymap":{"source":"apache","extensions":["pgm"]},"image/x-portable-pixmap":{"source":"apache","extensions":["ppm"]},"image/x-rgb":{"source":"apache","extensions":["rgb"]},"image/x-tga":{"source":"apache","extensions":["tga"]},"image/x-xbitmap":{"source":"apache","extensions":["xbm"]},"image/x-xcf":{"compressible":false},"image/x-xpixmap":{"source":"apache","extensions":["xpm"]},"image/x-xwindowdump":{"source":"apache","extensions":["xwd"]},"message/cpim":{"source":"iana"},"message/delivery-status":{"source":"iana"},"message/disposition-notification":{"source":"iana","extensions":["disposition-notification"]},"message/external-body":{"source":"iana"},"message/feedback-report":{"source":"iana"},"message/global":{"source":"iana","extensions":["u8msg"]},"message/global-delivery-status":{"source":"iana","extensions":["u8dsn"]},"message/global-disposition-notification":{"source":"iana","extensions":["u8mdn"]},"message/global-headers":{"source":"iana","extensions":["u8hdr"]},"message/http":{"source":"iana","compressible":false},"message/imdn+xml":{"source":"iana","compressible":true},"message/news":{"source":"iana"},"message/partial":{"source":"iana","compressible":false},"message/rfc822":{"source":"iana","compressible":true,"extensions":["eml","mime"]},"message/s-http":{"source":"iana"},"message/sip":{"source":"iana"},"message/sipfrag":{"source":"iana"},"message/tracking-status":{"source":"iana"},"message/vnd.si.simp":{"source":"iana"},"message/vnd.wfa.wsc":{"source":"iana","extensions":["wsc"]},"model/3mf":{"source":"iana","extensions":["3mf"]},"model/gltf+json":{"source":"iana","compressible":true,"extensions":["gltf"]},"model/gltf-binary":{"source":"iana","compressible":true,"extensions":["glb"]},"model/iges":{"source":"iana","compressible":false,"extensions":["igs","iges"]},"model/mesh":{"source":"iana","compressible":false,"extensions":["msh","mesh","silo"]},"model/mtl":{"source":"iana","extensions":["mtl"]},"model/obj":{"source":"iana","extensions":["obj"]},"model/stl":{"source":"iana","extensions":["stl"]},"model/vnd.collada+xml":{"source":"iana","compressible":true,"extensions":["dae"]},"model/vnd.dwf":{"source":"iana","extensions":["dwf"]},"model/vnd.flatland.3dml":{"source":"iana"},"model/vnd.gdl":{"source":"iana","extensions":["gdl"]},"model/vnd.gs-gdl":{"source":"apache"},"model/vnd.gs.gdl":{"source":"iana"},"model/vnd.gtw":{"source":"iana","extensions":["gtw"]},"model/vnd.moml+xml":{"source":"iana","compressible":true},"model/vnd.mts":{"source":"iana","extensions":["mts"]},"model/vnd.opengex":{"source":"iana","extensions":["ogex"]},"model/vnd.parasolid.transmit.binary":{"source":"iana","extensions":["x_b"]},"model/vnd.parasolid.transmit.text":{"source":"iana","extensions":["x_t"]},"model/vnd.rosette.annotated-data-model":{"source":"iana"},"model/vnd.usdz+zip":{"source":"iana","compressible":false,"extensions":["usdz"]},"model/vnd.valve.source.compiled-map":{"source":"iana","extensions":["bsp"]},"model/vnd.vtu":{"source":"iana","extensions":["vtu"]},"model/vrml":{"source":"iana","compressible":false,"extensions":["wrl","vrml"]},"model/x3d+binary":{"source":"apache","compressible":false,"extensions":["x3db","x3dbz"]},"model/x3d+fastinfoset":{"source":"iana","extensions":["x3db"]},"model/x3d+vrml":{"source":"apache","compressible":false,"extensions":["x3dv","x3dvz"]},"model/x3d+xml":{"source":"iana","compressible":true,"extensions":["x3d","x3dz"]},"model/x3d-vrml":{"source":"iana","extensions":["x3dv"]},"multipart/alternative":{"source":"iana","compressible":false},"multipart/appledouble":{"source":"iana"},"multipart/byteranges":{"source":"iana"},"multipart/digest":{"source":"iana"},"multipart/encrypted":{"source":"iana","compressible":false},"multipart/form-data":{"source":"iana","compressible":false},"multipart/header-set":{"source":"iana"},"multipart/mixed":{"source":"iana"},"multipart/multilingual":{"source":"iana"},"multipart/parallel":{"source":"iana"},"multipart/related":{"source":"iana","compressible":false},"multipart/report":{"source":"iana"},"multipart/signed":{"source":"iana","compressible":false},"multipart/vnd.bint.med-plus":{"source":"iana"},"multipart/voice-message":{"source":"iana"},"multipart/x-mixed-replace":{"source":"iana"},"text/1d-interleaved-parityfec":{"source":"iana"},"text/cache-manifest":{"source":"iana","compressible":true,"extensions":["appcache","manifest"]},"text/calendar":{"source":"iana","extensions":["ics","ifb"]},"text/calender":{"compressible":true},"text/cmd":{"compressible":true},"text/coffeescript":{"extensions":["coffee","litcoffee"]},"text/css":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["css"]},"text/csv":{"source":"iana","compressible":true,"extensions":["csv"]},"text/csv-schema":{"source":"iana"},"text/directory":{"source":"iana"},"text/dns":{"source":"iana"},"text/ecmascript":{"source":"iana"},"text/encaprtp":{"source":"iana"},"text/enriched":{"source":"iana"},"text/flexfec":{"source":"iana"},"text/fwdred":{"source":"iana"},"text/grammar-ref-list":{"source":"iana"},"text/html":{"source":"iana","compressible":true,"extensions":["html","htm","shtml"]},"text/jade":{"extensions":["jade"]},"text/javascript":{"source":"iana","compressible":true},"text/jcr-cnd":{"source":"iana"},"text/jsx":{"compressible":true,"extensions":["jsx"]},"text/less":{"compressible":true,"extensions":["less"]},"text/markdown":{"source":"iana","compressible":true,"extensions":["markdown","md"]},"text/mathml":{"source":"nginx","extensions":["mml"]},"text/mdx":{"compressible":true,"extensions":["mdx"]},"text/mizar":{"source":"iana"},"text/n3":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["n3"]},"text/parameters":{"source":"iana","charset":"UTF-8"},"text/parityfec":{"source":"iana"},"text/plain":{"source":"iana","compressible":true,"extensions":["txt","text","conf","def","list","log","in","ini"]},"text/provenance-notation":{"source":"iana","charset":"UTF-8"},"text/prs.fallenstein.rst":{"source":"iana"},"text/prs.lines.tag":{"source":"iana","extensions":["dsc"]},"text/prs.prop.logic":{"source":"iana"},"text/raptorfec":{"source":"iana"},"text/red":{"source":"iana"},"text/rfc822-headers":{"source":"iana"},"text/richtext":{"source":"iana","compressible":true,"extensions":["rtx"]},"text/rtf":{"source":"iana","compressible":true,"extensions":["rtf"]},"text/rtp-enc-aescm128":{"source":"iana"},"text/rtploopback":{"source":"iana"},"text/rtx":{"source":"iana"},"text/sgml":{"source":"iana","extensions":["sgml","sgm"]},"text/shex":{"extensions":["shex"]},"text/slim":{"extensions":["slim","slm"]},"text/strings":{"source":"iana"},"text/stylus":{"extensions":["stylus","styl"]},"text/t140":{"source":"iana"},"text/tab-separated-values":{"source":"iana","compressible":true,"extensions":["tsv"]},"text/troff":{"source":"iana","extensions":["t","tr","roff","man","me","ms"]},"text/turtle":{"source":"iana","charset":"UTF-8","extensions":["ttl"]},"text/ulpfec":{"source":"iana"},"text/uri-list":{"source":"iana","compressible":true,"extensions":["uri","uris","urls"]},"text/vcard":{"source":"iana","compressible":true,"extensions":["vcard"]},"text/vnd.a":{"source":"iana"},"text/vnd.abc":{"source":"iana"},"text/vnd.ascii-art":{"source":"iana"},"text/vnd.curl":{"source":"iana","extensions":["curl"]},"text/vnd.curl.dcurl":{"source":"apache","extensions":["dcurl"]},"text/vnd.curl.mcurl":{"source":"apache","extensions":["mcurl"]},"text/vnd.curl.scurl":{"source":"apache","extensions":["scurl"]},"text/vnd.debian.copyright":{"source":"iana","charset":"UTF-8"},"text/vnd.dmclientscript":{"source":"iana"},"text/vnd.dvb.subtitle":{"source":"iana","extensions":["sub"]},"text/vnd.esmertec.theme-descriptor":{"source":"iana","charset":"UTF-8"},"text/vnd.ficlab.flt":{"source":"iana"},"text/vnd.fly":{"source":"iana","extensions":["fly"]},"text/vnd.fmi.flexstor":{"source":"iana","extensions":["flx"]},"text/vnd.gml":{"source":"iana"},"text/vnd.graphviz":{"source":"iana","extensions":["gv"]},"text/vnd.hgl":{"source":"iana"},"text/vnd.in3d.3dml":{"source":"iana","extensions":["3dml"]},"text/vnd.in3d.spot":{"source":"iana","extensions":["spot"]},"text/vnd.iptc.newsml":{"source":"iana"},"text/vnd.iptc.nitf":{"source":"iana"},"text/vnd.latex-z":{"source":"iana"},"text/vnd.motorola.reflex":{"source":"iana"},"text/vnd.ms-mediapackage":{"source":"iana"},"text/vnd.net2phone.commcenter.command":{"source":"iana"},"text/vnd.radisys.msml-basic-layout":{"source":"iana"},"text/vnd.senx.warpscript":{"source":"iana"},"text/vnd.si.uricatalogue":{"source":"iana"},"text/vnd.sosi":{"source":"iana"},"text/vnd.sun.j2me.app-descriptor":{"source":"iana","charset":"UTF-8","extensions":["jad"]},"text/vnd.trolltech.linguist":{"source":"iana","charset":"UTF-8"},"text/vnd.wap.si":{"source":"iana"},"text/vnd.wap.sl":{"source":"iana"},"text/vnd.wap.wml":{"source":"iana","extensions":["wml"]},"text/vnd.wap.wmlscript":{"source":"iana","extensions":["wmls"]},"text/vtt":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["vtt"]},"text/x-asm":{"source":"apache","extensions":["s","asm"]},"text/x-c":{"source":"apache","extensions":["c","cc","cxx","cpp","h","hh","dic"]},"text/x-component":{"source":"nginx","extensions":["htc"]},"text/x-fortran":{"source":"apache","extensions":["f","for","f77","f90"]},"text/x-gwt-rpc":{"compressible":true},"text/x-handlebars-template":{"extensions":["hbs"]},"text/x-java-source":{"source":"apache","extensions":["java"]},"text/x-jquery-tmpl":{"compressible":true},"text/x-lua":{"extensions":["lua"]},"text/x-markdown":{"compressible":true,"extensions":["mkd"]},"text/x-nfo":{"source":"apache","extensions":["nfo"]},"text/x-opml":{"source":"apache","extensions":["opml"]},"text/x-org":{"compressible":true,"extensions":["org"]},"text/x-pascal":{"source":"apache","extensions":["p","pas"]},"text/x-processing":{"compressible":true,"extensions":["pde"]},"text/x-sass":{"extensions":["sass"]},"text/x-scss":{"extensions":["scss"]},"text/x-setext":{"source":"apache","extensions":["etx"]},"text/x-sfv":{"source":"apache","extensions":["sfv"]},"text/x-suse-ymp":{"compressible":true,"extensions":["ymp"]},"text/x-uuencode":{"source":"apache","extensions":["uu"]},"text/x-vcalendar":{"source":"apache","extensions":["vcs"]},"text/x-vcard":{"source":"apache","extensions":["vcf"]},"text/xml":{"source":"iana","compressible":true,"extensions":["xml"]},"text/xml-external-parsed-entity":{"source":"iana"},"text/yaml":{"extensions":["yaml","yml"]},"video/1d-interleaved-parityfec":{"source":"iana"},"video/3gpp":{"source":"iana","extensions":["3gp","3gpp"]},"video/3gpp-tt":{"source":"iana"},"video/3gpp2":{"source":"iana","extensions":["3g2"]},"video/bmpeg":{"source":"iana"},"video/bt656":{"source":"iana"},"video/celb":{"source":"iana"},"video/dv":{"source":"iana"},"video/encaprtp":{"source":"iana"},"video/flexfec":{"source":"iana"},"video/h261":{"source":"iana","extensions":["h261"]},"video/h263":{"source":"iana","extensions":["h263"]},"video/h263-1998":{"source":"iana"},"video/h263-2000":{"source":"iana"},"video/h264":{"source":"iana","extensions":["h264"]},"video/h264-rcdo":{"source":"iana"},"video/h264-svc":{"source":"iana"},"video/h265":{"source":"iana"},"video/iso.segment":{"source":"iana"},"video/jpeg":{"source":"iana","extensions":["jpgv"]},"video/jpeg2000":{"source":"iana"},"video/jpm":{"source":"apache","extensions":["jpm","jpgm"]},"video/mj2":{"source":"iana","extensions":["mj2","mjp2"]},"video/mp1s":{"source":"iana"},"video/mp2p":{"source":"iana"},"video/mp2t":{"source":"iana","extensions":["ts"]},"video/mp4":{"source":"iana","compressible":false,"extensions":["mp4","mp4v","mpg4"]},"video/mp4v-es":{"source":"iana"},"video/mpeg":{"source":"iana","compressible":false,"extensions":["mpeg","mpg","mpe","m1v","m2v"]},"video/mpeg4-generic":{"source":"iana"},"video/mpv":{"source":"iana"},"video/nv":{"source":"iana"},"video/ogg":{"source":"iana","compressible":false,"extensions":["ogv"]},"video/parityfec":{"source":"iana"},"video/pointer":{"source":"iana"},"video/quicktime":{"source":"iana","compressible":false,"extensions":["qt","mov"]},"video/raptorfec":{"source":"iana"},"video/raw":{"source":"iana"},"video/rtp-enc-aescm128":{"source":"iana"},"video/rtploopback":{"source":"iana"},"video/rtx":{"source":"iana"},"video/smpte291":{"source":"iana"},"video/smpte292m":{"source":"iana"},"video/ulpfec":{"source":"iana"},"video/vc1":{"source":"iana"},"video/vc2":{"source":"iana"},"video/vnd.cctv":{"source":"iana"},"video/vnd.dece.hd":{"source":"iana","extensions":["uvh","uvvh"]},"video/vnd.dece.mobile":{"source":"iana","extensions":["uvm","uvvm"]},"video/vnd.dece.mp4":{"source":"iana"},"video/vnd.dece.pd":{"source":"iana","extensions":["uvp","uvvp"]},"video/vnd.dece.sd":{"source":"iana","extensions":["uvs","uvvs"]},"video/vnd.dece.video":{"source":"iana","extensions":["uvv","uvvv"]},"video/vnd.directv.mpeg":{"source":"iana"},"video/vnd.directv.mpeg-tts":{"source":"iana"},"video/vnd.dlna.mpeg-tts":{"source":"iana"},"video/vnd.dvb.file":{"source":"iana","extensions":["dvb"]},"video/vnd.fvt":{"source":"iana","extensions":["fvt"]},"video/vnd.hns.video":{"source":"iana"},"video/vnd.iptvforum.1dparityfec-1010":{"source":"iana"},"video/vnd.iptvforum.1dparityfec-2005":{"source":"iana"},"video/vnd.iptvforum.2dparityfec-1010":{"source":"iana"},"video/vnd.iptvforum.2dparityfec-2005":{"source":"iana"},"video/vnd.iptvforum.ttsavc":{"source":"iana"},"video/vnd.iptvforum.ttsmpeg2":{"source":"iana"},"video/vnd.motorola.video":{"source":"iana"},"video/vnd.motorola.videop":{"source":"iana"},"video/vnd.mpegurl":{"source":"iana","extensions":["mxu","m4u"]},"video/vnd.ms-playready.media.pyv":{"source":"iana","extensions":["pyv"]},"video/vnd.nokia.interleaved-multimedia":{"source":"iana"},"video/vnd.nokia.mp4vr":{"source":"iana"},"video/vnd.nokia.videovoip":{"source":"iana"},"video/vnd.objectvideo":{"source":"iana"},"video/vnd.radgamettools.bink":{"source":"iana"},"video/vnd.radgamettools.smacker":{"source":"iana"},"video/vnd.sealed.mpeg1":{"source":"iana"},"video/vnd.sealed.mpeg4":{"source":"iana"},"video/vnd.sealed.swf":{"source":"iana"},"video/vnd.sealedmedia.softseal.mov":{"source":"iana"},"video/vnd.uvvu.mp4":{"source":"iana","extensions":["uvu","uvvu"]},"video/vnd.vivo":{"source":"iana","extensions":["viv"]},"video/vnd.youtube.yt":{"source":"iana"},"video/vp8":{"source":"iana"},"video/webm":{"source":"apache","compressible":false,"extensions":["webm"]},"video/x-f4v":{"source":"apache","extensions":["f4v"]},"video/x-fli":{"source":"apache","extensions":["fli"]},"video/x-flv":{"source":"apache","compressible":false,"extensions":["flv"]},"video/x-m4v":{"source":"apache","extensions":["m4v"]},"video/x-matroska":{"source":"apache","compressible":false,"extensions":["mkv","mk3d","mks"]},"video/x-mng":{"source":"apache","extensions":["mng"]},"video/x-ms-asf":{"source":"apache","extensions":["asf","asx"]},"video/x-ms-vob":{"source":"apache","extensions":["vob"]},"video/x-ms-wm":{"source":"apache","extensions":["wm"]},"video/x-ms-wmv":{"source":"apache","compressible":false,"extensions":["wmv"]},"video/x-ms-wmx":{"source":"apache","extensions":["wmx"]},"video/x-ms-wvx":{"source":"apache","extensions":["wvx"]},"video/x-msvideo":{"source":"apache","extensions":["avi"]},"video/x-sgi-movie":{"source":"apache","extensions":["movie"]},"video/x-smv":{"source":"apache","extensions":["smv"]},"x-conference/x-cooltalk":{"source":"apache","extensions":["ice"]},"x-shader/x-fragment":{"compressible":true},"x-shader/x-vertex":{"compressible":true}}; + +/***/ }), + +/***/ 125: +/***/ (function(module) { + +// API +module.exports = state; + +/** + * Creates initial state object + * for iteration over list + * + * @param {array|object} list - list to iterate over + * @param {function|null} sortMethod - function to use for keys sort, + * or `null` to keep them as is + * @returns {object} - initial state object + */ +function state(list, sortMethod) +{ + var isNamedList = !Array.isArray(list) + , initState = + { + index : 0, + keyedList: isNamedList || sortMethod ? Object.keys(list) : null, + jobs : {}, + results : isNamedList ? {} : [], + size : isNamedList ? Object.keys(list).length : list.length + } + ; + + if (sortMethod) + { + // sort array keys based on it's values + // sort object's keys just on own merit + initState.keyedList.sort(isNamedList ? sortMethod : function(a, b) + { + return sortMethod(list[a], list[b]); + }); + } + + return initState; +} + + +/***/ }), + +/***/ 128: +/***/ (function(module, __unusedexports, __webpack_require__) { + +/*! + * mime-db + * Copyright(c) 2014 Jonathan Ong + * MIT Licensed + */ + +/** + * Module exports. + */ + +module.exports = __webpack_require__(118) + + +/***/ }), + +/***/ 154: +/***/ (function(module) { + +module.exports = defer; + +/** + * Runs provided function on next iteration of the event loop + * + * @param {function} fn - function to run + */ +function defer(fn) +{ + var nextTick = typeof setImmediate == 'function' + ? setImmediate + : ( + typeof process == 'object' && typeof process.nextTick == 'function' + ? process.nextTick + : null + ); + + if (nextTick) + { + nextTick(fn); + } + else + { + setTimeout(fn, 0); + } +} + + +/***/ }), + +/***/ 164: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var Stream = __webpack_require__(413).Stream; +var util = __webpack_require__(669); + +module.exports = DelayedStream; +function DelayedStream() { + this.source = null; + this.dataSize = 0; + this.maxDataSize = 1024 * 1024; + this.pauseStream = true; + + this._maxDataSizeExceeded = false; + this._released = false; + this._bufferedEvents = []; +} +util.inherits(DelayedStream, Stream); + +DelayedStream.create = function(source, options) { + var delayedStream = new this(); + + options = options || {}; + for (var option in options) { + delayedStream[option] = options[option]; + } + + delayedStream.source = source; + + var realEmit = source.emit; + source.emit = function() { + delayedStream._handleEmit(arguments); + return realEmit.apply(source, arguments); + }; + + source.on('error', function() {}); + if (delayedStream.pauseStream) { + source.pause(); + } + + return delayedStream; +}; + +Object.defineProperty(DelayedStream.prototype, 'readable', { + configurable: true, + enumerable: true, + get: function() { + return this.source.readable; + } +}); + +DelayedStream.prototype.setEncoding = function() { + return this.source.setEncoding.apply(this.source, arguments); +}; + +DelayedStream.prototype.resume = function() { + if (!this._released) { + this.release(); + } + + this.source.resume(); +}; + +DelayedStream.prototype.pause = function() { + this.source.pause(); +}; + +DelayedStream.prototype.release = function() { + this._released = true; + + this._bufferedEvents.forEach(function(args) { + this.emit.apply(this, args); + }.bind(this)); + this._bufferedEvents = []; +}; + +DelayedStream.prototype.pipe = function() { + var r = Stream.prototype.pipe.apply(this, arguments); + this.resume(); + return r; +}; + +DelayedStream.prototype._handleEmit = function(args) { + if (this._released) { + this.emit.apply(this, args); + return; + } + + if (args[0] === 'data') { + this.dataSize += args[1].length; + this._checkIfMaxDataSizeExceeded(); + } + + this._bufferedEvents.push(args); +}; + +DelayedStream.prototype._checkIfMaxDataSizeExceeded = function() { + if (this._maxDataSizeExceeded) { + return; + } + + if (this.dataSize <= this.maxDataSize) { + return; + } + + this._maxDataSizeExceeded = true; + var message = + 'DelayedStream#maxDataSize of ' + this.maxDataSize + ' bytes exceeded.' + this.emit('error', new Error(message)); +}; + + +/***/ }), + +/***/ 176: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var serialOrdered = __webpack_require__(275); + +// Public API +module.exports = serial; + +/** + * Runs iterator over provided array elements in series + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function serial(list, iterator, callback) +{ + return serialOrdered(list, iterator, null, callback); +} + + +/***/ }), + +/***/ 211: +/***/ (function(module) { + +module.exports = require("https"); + +/***/ }), + +/***/ 213: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var util = __webpack_require__(669); +var Stream = __webpack_require__(413).Stream; +var DelayedStream = __webpack_require__(164); + +module.exports = CombinedStream; +function CombinedStream() { + this.writable = false; + this.readable = true; + this.dataSize = 0; + this.maxDataSize = 2 * 1024 * 1024; + this.pauseStreams = true; + + this._released = false; + this._streams = []; + this._currentStream = null; + this._insideLoop = false; + this._pendingNext = false; +} +util.inherits(CombinedStream, Stream); + +CombinedStream.create = function(options) { + var combinedStream = new this(); + + options = options || {}; + for (var option in options) { + combinedStream[option] = options[option]; + } + + return combinedStream; +}; + +CombinedStream.isStreamLike = function(stream) { + return (typeof stream !== 'function') + && (typeof stream !== 'string') + && (typeof stream !== 'boolean') + && (typeof stream !== 'number') + && (!Buffer.isBuffer(stream)); +}; + +CombinedStream.prototype.append = function(stream) { + var isStreamLike = CombinedStream.isStreamLike(stream); + + if (isStreamLike) { + if (!(stream instanceof DelayedStream)) { + var newStream = DelayedStream.create(stream, { + maxDataSize: Infinity, + pauseStream: this.pauseStreams, + }); + stream.on('data', this._checkDataSize.bind(this)); + stream = newStream; + } + + this._handleErrors(stream); + + if (this.pauseStreams) { + stream.pause(); + } + } + + this._streams.push(stream); + return this; +}; + +CombinedStream.prototype.pipe = function(dest, options) { + Stream.prototype.pipe.call(this, dest, options); + this.resume(); + return dest; +}; + +CombinedStream.prototype._getNext = function() { + this._currentStream = null; + + if (this._insideLoop) { + this._pendingNext = true; + return; // defer call + } + + this._insideLoop = true; + try { + do { + this._pendingNext = false; + this._realGetNext(); + } while (this._pendingNext); + } finally { + this._insideLoop = false; + } +}; + +CombinedStream.prototype._realGetNext = function() { + var stream = this._streams.shift(); + + + if (typeof stream == 'undefined') { + this.end(); + return; + } + + if (typeof stream !== 'function') { + this._pipeNext(stream); + return; + } + + var getStream = stream; + getStream(function(stream) { + var isStreamLike = CombinedStream.isStreamLike(stream); + if (isStreamLike) { + stream.on('data', this._checkDataSize.bind(this)); + this._handleErrors(stream); + } + + this._pipeNext(stream); + }.bind(this)); +}; + +CombinedStream.prototype._pipeNext = function(stream) { + this._currentStream = stream; + + var isStreamLike = CombinedStream.isStreamLike(stream); + if (isStreamLike) { + stream.on('end', this._getNext.bind(this)); + stream.pipe(this, {end: false}); + return; + } + + var value = stream; + this.write(value); + this._getNext(); +}; + +CombinedStream.prototype._handleErrors = function(stream) { + var self = this; + stream.on('error', function(err) { + self._emitError(err); + }); +}; + +CombinedStream.prototype.write = function(data) { + this.emit('data', data); +}; + +CombinedStream.prototype.pause = function() { + if (!this.pauseStreams) { + return; + } + + if(this.pauseStreams && this._currentStream && typeof(this._currentStream.pause) == 'function') this._currentStream.pause(); + this.emit('pause'); +}; + +CombinedStream.prototype.resume = function() { + if (!this._released) { + this._released = true; + this.writable = true; + this._getNext(); + } + + if(this.pauseStreams && this._currentStream && typeof(this._currentStream.resume) == 'function') this._currentStream.resume(); + this.emit('resume'); +}; + +CombinedStream.prototype.end = function() { + this._reset(); + this.emit('end'); +}; + +CombinedStream.prototype.destroy = function() { + this._reset(); + this.emit('close'); +}; + +CombinedStream.prototype._reset = function() { + this.writable = false; + this._streams = []; + this._currentStream = null; +}; + +CombinedStream.prototype._checkDataSize = function() { + this._updateDataSize(); + if (this.dataSize <= this.maxDataSize) { + return; + } + + var message = + 'DelayedStream#maxDataSize of ' + this.maxDataSize + ' bytes exceeded.'; + this._emitError(new Error(message)); +}; + +CombinedStream.prototype._updateDataSize = function() { + this.dataSize = 0; + + var self = this; + this._streams.forEach(function(stream) { + if (!stream.dataSize) { + return; + } + + self.dataSize += stream.dataSize; + }); + + if (this._currentStream && this._currentStream.dataSize) { + this.dataSize += this._currentStream.dataSize; + } +}; + +CombinedStream.prototype._emitError = function(err) { + this._reset(); + this.emit('error', err); +}; + + +/***/ }), + +/***/ 258: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var async = __webpack_require__(792) + , abort = __webpack_require__(762) + ; + +// API +module.exports = iterate; + +/** + * Iterates over each job object + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {object} state - current job status + * @param {function} callback - invoked when all elements processed + */ +function iterate(list, iterator, state, callback) +{ + // store current index + var key = state['keyedList'] ? state['keyedList'][state.index] : state.index; + + state.jobs[key] = runJob(iterator, key, list[key], function(error, output) + { + // don't repeat yourself + // skip secondary callbacks + if (!(key in state.jobs)) + { + return; + } + + // clean up jobs + delete state.jobs[key]; + + if (error) + { + // don't process rest of the results + // stop still active jobs + // and reset the list + abort(state); + } + else + { + state.results[key] = output; + } + + // return salvaged results + callback(error, state.results); + }); +} + +/** + * Runs iterator over provided job element + * + * @param {function} iterator - iterator to invoke + * @param {string|number} key - key/index of the element in the list of jobs + * @param {mixed} item - job description + * @param {function} callback - invoked after iterator is done with the job + * @returns {function|mixed} - job abort function or something else + */ +function runJob(iterator, key, item, callback) +{ + var aborter; + + // allow shortcut if iterator expects only two arguments + if (iterator.length == 2) + { + aborter = iterator(item, async(callback)); + } + // otherwise go with full three arguments + else + { + aborter = iterator(item, key, async(callback)); + } + + return aborter; +} + + +/***/ }), + +/***/ 275: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var iterate = __webpack_require__(258) + , initState = __webpack_require__(125) + , terminator = __webpack_require__(2) + ; + +// Public API +module.exports = serialOrdered; +// sorting helpers +module.exports.ascending = ascending; +module.exports.descending = descending; + +/** + * Runs iterator over provided sorted array elements in series + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} sortMethod - custom sort function + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function serialOrdered(list, iterator, sortMethod, callback) +{ + var state = initState(list, sortMethod); + + iterate(list, iterator, state, function iteratorHandler(error, result) + { + if (error) + { + callback(error, result); + return; + } + + state.index++; + + // are we there yet? + if (state.index < (state['keyedList'] || list).length) + { + iterate(list, iterator, state, iteratorHandler); + return; + } + + // done here + callback(null, state.results); + }); + + return terminator.bind(state, callback); +} + +/* + * -- Sort methods + */ + +/** + * sort helper to sort array elements in ascending order + * + * @param {mixed} a - an item to compare + * @param {mixed} b - an item to compare + * @returns {number} - comparison result + */ +function ascending(a, b) +{ + return a < b ? -1 : a > b ? 1 : 0; +} + +/** + * sort helper to sort array elements in descending order + * + * @param {mixed} a - an item to compare + * @param {mixed} b - an item to compare + * @returns {number} - comparison result + */ +function descending(a, b) +{ + return -1 * ascending(a, b); +} + + +/***/ }), + +/***/ 294: +/***/ (function(module, __unusedexports, __webpack_require__) { + +module.exports = +{ + parallel : __webpack_require__(89), + serial : __webpack_require__(176), + serialOrdered : __webpack_require__(275) +}; + + +/***/ }), + +/***/ 413: +/***/ (function(module) { + +module.exports = require("stream"); + +/***/ }), + +/***/ 416: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var CombinedStream = __webpack_require__(213); +var util = __webpack_require__(669); +var path = __webpack_require__(622); +var http = __webpack_require__(605); +var https = __webpack_require__(211); +var parseUrl = __webpack_require__(835).parse; +var fs = __webpack_require__(747); +var mime = __webpack_require__(769); +var asynckit = __webpack_require__(294); +var populate = __webpack_require__(70); + +// Public API +module.exports = FormData; + +// make it a Stream +util.inherits(FormData, CombinedStream); + +/** + * Create readable "multipart/form-data" streams. + * Can be used to submit forms + * and file uploads to other web applications. + * + * @constructor + * @param {Object} options - Properties to be added/overriden for FormData and CombinedStream + */ +function FormData(options) { + if (!(this instanceof FormData)) { + return new FormData(); + } + + this._overheadLength = 0; + this._valueLength = 0; + this._valuesToMeasure = []; + + CombinedStream.call(this); + + options = options || {}; + for (var option in options) { + this[option] = options[option]; + } +} + +FormData.LINE_BREAK = '\r\n'; +FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream'; + +FormData.prototype.append = function(field, value, options) { + + options = options || {}; + + // allow filename as single option + if (typeof options == 'string') { + options = {filename: options}; + } + + var append = CombinedStream.prototype.append.bind(this); + + // all that streamy business can't handle numbers + if (typeof value == 'number') { + value = '' + value; + } + + // https://github.com/felixge/node-form-data/issues/38 + if (util.isArray(value)) { + // Please convert your array into string + // the way web server expects it + this._error(new Error('Arrays are not supported.')); + return; + } + + var header = this._multiPartHeader(field, value, options); + var footer = this._multiPartFooter(); + + append(header); + append(value); + append(footer); + + // pass along options.knownLength + this._trackLength(header, value, options); +}; + +FormData.prototype._trackLength = function(header, value, options) { + var valueLength = 0; + + // used w/ getLengthSync(), when length is known. + // e.g. for streaming directly from a remote server, + // w/ a known file a size, and not wanting to wait for + // incoming file to finish to get its size. + if (options.knownLength != null) { + valueLength += +options.knownLength; + } else if (Buffer.isBuffer(value)) { + valueLength = value.length; + } else if (typeof value === 'string') { + valueLength = Buffer.byteLength(value); + } + + this._valueLength += valueLength; + + // @check why add CRLF? does this account for custom/multiple CRLFs? + this._overheadLength += + Buffer.byteLength(header) + + FormData.LINE_BREAK.length; + + // empty or either doesn't have path or not an http response + if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) { + return; + } + + // no need to bother with the length + if (!options.knownLength) { + this._valuesToMeasure.push(value); + } +}; + +FormData.prototype._lengthRetriever = function(value, callback) { + + if (value.hasOwnProperty('fd')) { + + // take read range into a account + // `end` = Infinity –> read file till the end + // + // TODO: Looks like there is bug in Node fs.createReadStream + // it doesn't respect `end` options without `start` options + // Fix it when node fixes it. + // https://github.com/joyent/node/issues/7819 + if (value.end != undefined && value.end != Infinity && value.start != undefined) { + + // when end specified + // no need to calculate range + // inclusive, starts with 0 + callback(null, value.end + 1 - (value.start ? value.start : 0)); + + // not that fast snoopy + } else { + // still need to fetch file size from fs + fs.stat(value.path, function(err, stat) { + + var fileSize; + + if (err) { + callback(err); + return; + } + + // update final size based on the range options + fileSize = stat.size - (value.start ? value.start : 0); + callback(null, fileSize); + }); + } + + // or http response + } else if (value.hasOwnProperty('httpVersion')) { + callback(null, +value.headers['content-length']); + + // or request stream http://github.com/mikeal/request + } else if (value.hasOwnProperty('httpModule')) { + // wait till response come back + value.on('response', function(response) { + value.pause(); + callback(null, +response.headers['content-length']); + }); + value.resume(); + + // something else + } else { + callback('Unknown stream'); + } +}; + +FormData.prototype._multiPartHeader = function(field, value, options) { + // custom header specified (as string)? + // it becomes responsible for boundary + // (e.g. to handle extra CRLFs on .NET servers) + if (typeof options.header == 'string') { + return options.header; + } + + var contentDisposition = this._getContentDisposition(value, options); + var contentType = this._getContentType(value, options); + + var contents = ''; + var headers = { + // add custom disposition as third element or keep it two elements if not + 'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []), + // if no content type. allow it to be empty array + 'Content-Type': [].concat(contentType || []) + }; + + // allow custom headers. + if (typeof options.header == 'object') { + populate(headers, options.header); + } + + var header; + for (var prop in headers) { + if (!headers.hasOwnProperty(prop)) continue; + header = headers[prop]; + + // skip nullish headers. + if (header == null) { + continue; + } + + // convert all headers to arrays. + if (!Array.isArray(header)) { + header = [header]; + } + + // add non-empty headers. + if (header.length) { + contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK; + } + } + + return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK; +}; + +FormData.prototype._getContentDisposition = function(value, options) { + + var filename + , contentDisposition + ; + + if (typeof options.filepath === 'string') { + // custom filepath for relative paths + filename = path.normalize(options.filepath).replace(/\\/g, '/'); + } else if (options.filename || value.name || value.path) { + // custom filename take precedence + // formidable and the browser add a name property + // fs- and request- streams have path property + filename = path.basename(options.filename || value.name || value.path); + } else if (value.readable && value.hasOwnProperty('httpVersion')) { + // or try http response + filename = path.basename(value.client._httpMessage.path || ''); + } + + if (filename) { + contentDisposition = 'filename="' + filename + '"'; + } + + return contentDisposition; +}; + +FormData.prototype._getContentType = function(value, options) { + + // use custom content-type above all + var contentType = options.contentType; + + // or try `name` from formidable, browser + if (!contentType && value.name) { + contentType = mime.lookup(value.name); + } + + // or try `path` from fs-, request- streams + if (!contentType && value.path) { + contentType = mime.lookup(value.path); + } + + // or if it's http-reponse + if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) { + contentType = value.headers['content-type']; + } + + // or guess it from the filepath or filename + if (!contentType && (options.filepath || options.filename)) { + contentType = mime.lookup(options.filepath || options.filename); + } + + // fallback to the default content type if `value` is not simple value + if (!contentType && typeof value == 'object') { + contentType = FormData.DEFAULT_CONTENT_TYPE; + } + + return contentType; +}; + +FormData.prototype._multiPartFooter = function() { + return function(next) { + var footer = FormData.LINE_BREAK; + + var lastPart = (this._streams.length === 0); + if (lastPart) { + footer += this._lastBoundary(); + } + + next(footer); + }.bind(this); +}; + +FormData.prototype._lastBoundary = function() { + return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK; +}; + +FormData.prototype.getHeaders = function(userHeaders) { + var header; + var formHeaders = { + 'content-type': 'multipart/form-data; boundary=' + this.getBoundary() + }; + + for (header in userHeaders) { + if (userHeaders.hasOwnProperty(header)) { + formHeaders[header.toLowerCase()] = userHeaders[header]; + } + } + + return formHeaders; +}; + +FormData.prototype.getBoundary = function() { + if (!this._boundary) { + this._generateBoundary(); + } + + return this._boundary; +}; + +FormData.prototype.getBuffer = function() { + var dataBuffer = new Buffer.alloc( 0 ); + var boundary = this.getBoundary(); + + // Create the form content. Add Line breaks to the end of data. + for (var i = 0, len = this._streams.length; i < len; i++) { + if (typeof this._streams[i] !== 'function') { + + // Add content to the buffer. + if(Buffer.isBuffer(this._streams[i])) { + dataBuffer = Buffer.concat( [dataBuffer, this._streams[i]]); + }else { + dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(this._streams[i])]); + } + + // Add break after content. + if (typeof this._streams[i] !== 'string' || this._streams[i].substring( 2, boundary.length + 2 ) !== boundary) { + dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(FormData.LINE_BREAK)] ); + } + } + } + + // Add the footer and return the Buffer object. + return Buffer.concat( [dataBuffer, Buffer.from(this._lastBoundary())] ); +}; + +FormData.prototype._generateBoundary = function() { + // This generates a 50 character boundary similar to those used by Firefox. + // They are optimized for boyer-moore parsing. + var boundary = '--------------------------'; + for (var i = 0; i < 24; i++) { + boundary += Math.floor(Math.random() * 10).toString(16); + } + + this._boundary = boundary; +}; + +// Note: getLengthSync DOESN'T calculate streams length +// As workaround one can calculate file size manually +// and add it as knownLength option +FormData.prototype.getLengthSync = function() { + var knownLength = this._overheadLength + this._valueLength; + + // Don't get confused, there are 3 "internal" streams for each keyval pair + // so it basically checks if there is any value added to the form + if (this._streams.length) { + knownLength += this._lastBoundary().length; + } + + // https://github.com/form-data/form-data/issues/40 + if (!this.hasKnownLength()) { + // Some async length retrievers are present + // therefore synchronous length calculation is false. + // Please use getLength(callback) to get proper length + this._error(new Error('Cannot calculate proper length in synchronous way.')); + } + + return knownLength; +}; + +// Public API to check if length of added values is known +// https://github.com/form-data/form-data/issues/196 +// https://github.com/form-data/form-data/issues/262 +FormData.prototype.hasKnownLength = function() { + var hasKnownLength = true; + + if (this._valuesToMeasure.length) { + hasKnownLength = false; + } + + return hasKnownLength; +}; + +FormData.prototype.getLength = function(cb) { + var knownLength = this._overheadLength + this._valueLength; + + if (this._streams.length) { + knownLength += this._lastBoundary().length; + } + + if (!this._valuesToMeasure.length) { + process.nextTick(cb.bind(this, null, knownLength)); + return; + } + + asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) { + if (err) { + cb(err); + return; + } + + values.forEach(function(length) { + knownLength += length; + }); + + cb(null, knownLength); + }); +}; + +FormData.prototype.submit = function(params, cb) { + var request + , options + , defaults = {method: 'post'} + ; + + // parse provided url if it's string + // or treat it as options object + if (typeof params == 'string') { + + params = parseUrl(params); + options = populate({ + port: params.port, + path: params.pathname, + host: params.hostname, + protocol: params.protocol + }, defaults); + + // use custom params + } else { + + options = populate(params, defaults); + // if no port provided use default one + if (!options.port) { + options.port = options.protocol == 'https:' ? 443 : 80; + } + } + + // put that good code in getHeaders to some use + options.headers = this.getHeaders(params.headers); + + // https if specified, fallback to http in any other case + if (options.protocol == 'https:') { + request = https.request(options); + } else { + request = http.request(options); + } + + // get content length and fire away + this.getLength(function(err, length) { + if (err) { + this._error(err); + return; + } + + // add content length + request.setHeader('Content-Length', length); + + this.pipe(request); + if (cb) { + request.on('error', cb); + request.on('response', cb.bind(this, null)); + } + }.bind(this)); + + return request; +}; + +FormData.prototype._error = function(err) { + if (!this.error) { + this.error = err; + this.pause(); + this.emit('error', err); + } +}; + +FormData.prototype.toString = function () { + return '[object FormData]'; +}; + + +/***/ }), + +/***/ 417: +/***/ (function(module) { + +module.exports = require("crypto"); + +/***/ }), + +/***/ 431: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const os = __importStar(__webpack_require__(87)); +const utils_1 = __webpack_require__(82); +/** + * Commands + * + * Command Format: + * ::name key=value,key=value::message + * + * Examples: + * ::warning::This is the message + * ::set-env name=MY_VAR::some value + */ +function issueCommand(command, properties, message) { + const cmd = new Command(command, properties, message); + process.stdout.write(cmd.toString() + os.EOL); +} +exports.issueCommand = issueCommand; +function issue(name, message = '') { + issueCommand(name, {}, message); +} +exports.issue = issue; +const CMD_STRING = '::'; +class Command { + constructor(command, properties, message) { + if (!command) { + command = 'missing.command'; + } + this.command = command; + this.properties = properties; + this.message = message; + } + toString() { + let cmdStr = CMD_STRING + this.command; + if (this.properties && Object.keys(this.properties).length > 0) { + cmdStr += ' '; + let first = true; + for (const key in this.properties) { + if (this.properties.hasOwnProperty(key)) { + const val = this.properties[key]; + if (val) { + if (first) { + first = false; + } + else { + cmdStr += ','; + } + cmdStr += `${key}=${escapeProperty(val)}`; + } + } + } + } + cmdStr += `${CMD_STRING}${escapeData(this.message)}`; + return cmdStr; + } +} +function escapeData(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} +function escapeProperty(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C'); +} +//# sourceMappingURL=command.js.map + +/***/ }), + +/***/ 470: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const command_1 = __webpack_require__(431); +const file_command_1 = __webpack_require__(102); +const utils_1 = __webpack_require__(82); +const os = __importStar(__webpack_require__(87)); +const path = __importStar(__webpack_require__(622)); +/** + * The code to exit an action + */ +var ExitCode; +(function (ExitCode) { + /** + * A code indicating that the action was successful + */ + ExitCode[ExitCode["Success"] = 0] = "Success"; + /** + * A code indicating that the action was a failure + */ + ExitCode[ExitCode["Failure"] = 1] = "Failure"; +})(ExitCode = exports.ExitCode || (exports.ExitCode = {})); +//----------------------------------------------------------------------- +// Variables +//----------------------------------------------------------------------- +/** + * Sets env variable for this action and future actions in the job + * @param name the name of the variable to set + * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function exportVariable(name, val) { + const convertedVal = utils_1.toCommandValue(val); + process.env[name] = convertedVal; + const filePath = process.env['GITHUB_ENV'] || ''; + if (filePath) { + const delimiter = '_GitHubActionsFileCommandDelimeter_'; + const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; + file_command_1.issueCommand('ENV', commandValue); + } + else { + command_1.issueCommand('set-env', { name }, convertedVal); + } +} +exports.exportVariable = exportVariable; +/** + * Registers a secret which will get masked from logs + * @param secret value of the secret + */ +function setSecret(secret) { + command_1.issueCommand('add-mask', {}, secret); +} +exports.setSecret = setSecret; +/** + * Prepends inputPath to the PATH (for this action and future actions) + * @param inputPath + */ +function addPath(inputPath) { + const filePath = process.env['GITHUB_PATH'] || ''; + if (filePath) { + file_command_1.issueCommand('PATH', inputPath); + } + else { + command_1.issueCommand('add-path', {}, inputPath); + } + process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; +} +exports.addPath = addPath; +/** + * Gets the value of an input. The value is also trimmed. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string + */ +function getInput(name, options) { + const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; + if (options && options.required && !val) { + throw new Error(`Input required and not supplied: ${name}`); + } + return val.trim(); +} +exports.getInput = getInput; +/** + * Sets the value of an output. + * + * @param name name of the output to set + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setOutput(name, value) { + command_1.issueCommand('set-output', { name }, value); +} +exports.setOutput = setOutput; +/** + * Enables or disables the echoing of commands into stdout for the rest of the step. + * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. + * + */ +function setCommandEcho(enabled) { + command_1.issue('echo', enabled ? 'on' : 'off'); +} +exports.setCommandEcho = setCommandEcho; +//----------------------------------------------------------------------- +// Results +//----------------------------------------------------------------------- +/** + * Sets the action status to failed. + * When the action exits it will be with an exit code of 1 + * @param message add error issue message + */ +function setFailed(message) { + process.exitCode = ExitCode.Failure; + error(message); +} +exports.setFailed = setFailed; +//----------------------------------------------------------------------- +// Logging Commands +//----------------------------------------------------------------------- +/** + * Gets whether Actions Step Debug is on or not + */ +function isDebug() { + return process.env['RUNNER_DEBUG'] === '1'; +} +exports.isDebug = isDebug; +/** + * Writes debug message to user log + * @param message debug message + */ +function debug(message) { + command_1.issueCommand('debug', {}, message); +} +exports.debug = debug; +/** + * Adds an error issue + * @param message error issue message. Errors will be converted to string via toString() + */ +function error(message) { + command_1.issue('error', message instanceof Error ? message.toString() : message); +} +exports.error = error; +/** + * Adds an warning issue + * @param message warning issue message. Errors will be converted to string via toString() + */ +function warning(message) { + command_1.issue('warning', message instanceof Error ? message.toString() : message); +} +exports.warning = warning; +/** + * Writes info to log with console.log. + * @param message info message + */ +function info(message) { + process.stdout.write(message + os.EOL); +} +exports.info = info; +/** + * Begin an output group. + * + * Output until the next `groupEnd` will be foldable in this group + * + * @param name The name of the output group + */ +function startGroup(name) { + command_1.issue('group', name); +} +exports.startGroup = startGroup; +/** + * End an output group. + */ +function endGroup() { + command_1.issue('endgroup'); +} +exports.endGroup = endGroup; +/** + * Wrap an asynchronous function call in a group. + * + * Returns the same type as the function itself. + * + * @param name The name of the group + * @param fn The function to wrap in the group + */ +function group(name, fn) { + return __awaiter(this, void 0, void 0, function* () { + startGroup(name); + let result; + try { + result = yield fn(); + } + finally { + endGroup(); + } + return result; + }); +} +exports.group = group; +//----------------------------------------------------------------------- +// Wrapper action state +//----------------------------------------------------------------------- +/** + * Saves state for current action, the state can only be retrieved by this action's post job execution. + * + * @param name name of the state to store + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function saveState(name, value) { + command_1.issueCommand('save-state', { name }, value); +} +exports.saveState = saveState; +/** + * Gets the value of an state set by this action's main execution. + * + * @param name name of the state to get + * @returns string + */ +function getState(name) { + return process.env[`STATE_${name}`] || ''; +} +exports.getState = getState; +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 605: +/***/ (function(module) { + +module.exports = require("http"); + +/***/ }), + +/***/ 618: +/***/ (function(module, __unusedexports, __webpack_require__) { + +/*! For license information please see mailgun.js.LICENSE.txt */ +!function(e,t){ true?module.exports=t():undefined}(this,(function(){return(()=>{var e={271:(e,t,r)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=r(185);class n extends o.EventTarget{constructor(){throw super(),new TypeError("AbortSignal cannot be constructed directly")}get aborted(){const e=s.get(this);if("boolean"!=typeof e)throw new TypeError("Expected 'this' to be an 'AbortSignal' object, but got "+(null===this?"null":typeof this));return e}}o.defineEventAttribute(n.prototype,"abort");const s=new WeakMap;Object.defineProperties(n.prototype,{aborted:{enumerable:!0}}),"function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(n.prototype,Symbol.toStringTag,{configurable:!0,value:"AbortSignal"});class i{constructor(){a.set(this,function(){const e=Object.create(n.prototype);return o.EventTarget.call(e),s.set(e,!1),e}())}get signal(){return u(this)}abort(){var e;e=u(this),!1===s.get(e)&&(s.set(e,!0),e.dispatchEvent({type:"abort"}))}}const a=new WeakMap;function u(e){const t=a.get(e);if(null==t)throw new TypeError("Expected 'this' to be an 'AbortController' object, but got "+(null===e?"null":typeof e));return t}Object.defineProperties(i.prototype,{signal:{enumerable:!0},abort:{enumerable:!0}}),"function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(i.prototype,Symbol.toStringTag,{configurable:!0,value:"AbortController"}),t.AbortController=i,t.AbortSignal=n,t.default=i,e.exports=i,e.exports.AbortController=e.exports.default=i,e.exports.AbortSignal=n},990:function(e,t,r){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var n=o(r(765)),s=function(){function e(e){this.formData=e}return e.prototype.client=function(e){return new n.default(e,this.formData)},e}();t.default=s},765:function(e,t,r){"use strict";var o=this&&this.__assign||function(){return(o=Object.assign||function(e){for(var t,r=1,o=arguments.length;r{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=r(78),n=(r(955),function(){function e(e){this.request=e}return e.prototype._parsePageNumber=function(e){return e.split("/").pop()},e.prototype._parsePage=function(e,t){return{id:e,number:this._parsePageNumber(t),url:t}},e.prototype._parsePageLinks=function(e){var t=this;return Object.entries(e.body.paging).reduce((function(e,r){var o=r[0],n=r[1];return e[o]=t._parsePage(o,n),e}),{})},e.prototype._parseEventList=function(e){return{items:e.body.items,pages:this._parsePageLinks(e)}},e.prototype.get=function(e,t){var r,n=this;return t&&t.page?(r=o("/v2",e,"events",t.page),delete t.page):r=o("/v2",e,"events"),this.request.get(r,t).then((function(e){return n._parseEventList(e)}))},e}());t.default=n},853:(e,t,r)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),r(955);var o=function(){function e(e){this.request=e}return e.prototype.list=function(e){var t=this;return this.request.get("/v1/ip_pools",e).then((function(e){return t.parseIpPoolsResponse(e)}))},e.prototype.create=function(e){return this.request.post("/v1/ip_pools",e).then((function(e){return null==e?void 0:e.body}))},e.prototype.update=function(e,t){return this.request.patch("/v1/ip_pools/"+e,t).then((function(e){return null==e?void 0:e.body}))},e.prototype.delete=function(e,t){return this.request.delete("/v1/ip_pools/"+e,t).then((function(e){return null==e?void 0:e.body}))},e.prototype.parseIpPoolsResponse=function(e){return e.body.ip_pools},e}();t.default=o},580:(e,t,r)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),r(955);var o=function(){function e(e){this.request=e}return e.prototype.list=function(e){var t=this;return this.request.get("/v3/ips",e).then((function(e){return t.parseIpsResponse(e)}))},e.prototype.get=function(e){var t=this;return this.request.get("/v3/ips/"+e).then((function(e){return t.parseIpsResponse(e)}))},e.prototype.parseIpsResponse=function(e){return e.body},e}();t.default=o},616:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e){this.request=e}return e.prototype._parseResponse=function(e){return e.body?e.body:e},e.prototype.create=function(e,t){return t.message?this.request.postMulti("/v3/"+e+"/messages.mime",t).then(this._parseResponse):this.request.postMulti("/v3/"+e+"/messages",t).then(this._parseResponse)},e}();t.default=r},726:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e){this.request=e}return e.prototype.get=function(e,t){var r={};return Array.isArray(e)&&(e=e.join(",")),r.addresses=e,t&&(r.syntax_only=!1),this.request.get("/v3/address/parse",r).then((function(e){return e.body}))},e}();t.default=r},955:function(e,t,r){"use strict";var o=this&&this.__assign||function(){return(o=Object.assign||function(e){for(var t,r=1,o=arguments.length;r0&&n[n.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!n||s[1]>n[0]&&s[1]0&&(f.searchParams=r.query,delete f.query),[4,l.default(u.default(this.url,t),o({method:e.toLocaleUpperCase(),headers:i,throwHttpErrors:!1},f))];case 1:return(null==(p=s.sent())?void 0:p.ok)?[3,6]:(null==p?void 0:p.body)&&d(p.body)?[4,(m=p.body,_=[],new Promise((function(e,t){m.on("data",(function(e){return _.push(e)})),m.on("error",t),m.on("end",(function(){return e(Buffer.concat(_).toString("utf8"))}))})))]:[3,3];case 2:return y=s.sent(),[3,5];case 3:return[4,null==p?void 0:p.json()];case 4:y=s.sent(),s.label=5;case 5:throw h=y,new c.default({status:null==p?void 0:p.status,statusText:null==p?void 0:p.statusText,body:{message:h}});case 6:return b={},[4,null==p?void 0:p.json()];case 7:return[2,(b.body=s.sent(),b.status=null==p?void 0:p.status,b)]}var m,_}))}))},e.prototype.query=function(e,t,r,n){return this.request(e,t,o({query:r},n))},e.prototype.command=function(e,t,r,n){return this.request(e,t,o({headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r},n))},e.prototype.get=function(e,t,r){return this.query("get",e,t,r)},e.prototype.head=function(e,t,r){return this.query("head",e,t,r)},e.prototype.options=function(e,t,r){return this.query("options",e,t,r)},e.prototype.post=function(e,t,r){return this.command("post",e,t,r)},e.prototype.postMulti=function(e,t){var r=new this.formData;return Object.keys(t).filter((function(e){return t[e]})).forEach((function(e){if("attachment"!==e)Array.isArray(t[e])?t[e].forEach((function(t){r.append(e,t)})):r.append(e,t[e]);else{var o=t.attachment;if(Array.isArray(o))o.forEach((function(t){var o=t.data?t.data:t,n=f(t);r.append(e,o,n)}));else{var n=d(o)?o:o.data,s=f(o);r.append(e,n,s)}}})),this.command("post",e,r,{headers:{"Content-Type":null}})},e.prototype.put=function(e,t,r){return this.command("put",e,t,r)},e.prototype.patch=function(e,t,r){return this.command("patch",e,t,r)},e.prototype.delete=function(e,t,r){return this.command("delete",e,t,r)},e}();t.default=p},893:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e){this.request=e}return e.prototype.list=function(e){return this.request.get("/v3/routes",e).then((function(e){return e.body.items}))},e.prototype.get=function(e){return this.request.get("/v3/routes/"+e).then((function(e){return e.body.route}))},e.prototype.create=function(e){return this.request.post("/v3/routes",e).then((function(e){return e.body.route}))},e.prototype.update=function(e,t){return this.request.put("/v3/routes/"+e,t).then((function(e){return e.body}))},e.prototype.destroy=function(e){return this.request.delete("/v3/routes/"+e).then((function(e){return e.body}))},e}();t.default=r},154:function(e,t,r){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var n=o(r(78)),s=function(e){this.start=new Date(e.start),this.end=new Date(e.end),this.resolution=e.resolution,this.stats=e.stats.map((function(e){return e.time=new Date(e.time),e}))},i=function(){function e(e){this.request=e}return e.prototype._parseStats=function(e){return new s(e.body)},e.prototype.getDomain=function(e,t){return this.request.get(n.default("/v3",e,"stats/total"),t).then(this._parseStats)},e.prototype.getAccount=function(e){return this.request.get("/v3/stats/total",e).then(this._parseStats)},e}();t.default=i},526:function(e,t,r){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var n=o(r(835)),s=o(r(78)),i={headers:{"Content-Type":"application/json"}},a=function(e){this.type="bounces",this.address=e.address,this.code=+e.code,this.error=e.error,this.created_at=new Date(e.created_at)},u=function(e){this.type="complaints",this.address=e.address,this.created_at=new Date(e.created_at)},l=function(e){this.type="unsubscribes",this.address=e.address,this.tags=e.tags,this.created_at=new Date(e.created_at)},c=function(){function e(e){this.request=e,this.models={bounces:a,complaints:u,unsubscribes:l}}return e.prototype._parsePage=function(e,t){var r=n.default.parse(t,!0).query;return{id:e,page:r.page,address:r.address,url:t}},e.prototype._parsePageLinks=function(e){var t=this;return Object.entries(e.body.paging).reduce((function(e,r){var o=r[0],n=r[1];return e[o]=t._parsePage(o,n),e}),{})},e.prototype._parseList=function(e,t){var r={};return r.items=e.body.items.map((function(e){return new t(e)})),r.pages=this._parsePageLinks(e),r},e.prototype._parseItem=function(e,t){return new t(e.body)},e.prototype.list=function(e,t,r){var o=this,n=this.models[t];return this.request.get(s.default("v3",e,t),r).then((function(e){return o._parseList(e,n)}))},e.prototype.get=function(e,t,r){var o=this,n=this.models[t];return this.request.get(s.default("v3",e,t,encodeURIComponent(r))).then((function(e){return o._parseItem(e,n)}))},e.prototype.create=function(e,t,r){return Array.isArray(r)||(r=[r]),this.request.post(s.default("v3",e,t),r,i).then((function(e){return e.body}))},e.prototype.destroy=function(e,t,r){return this.request.delete(s.default("v3",e,t,encodeURIComponent(r))).then((function(e){return e.body}))},e}();t.default=c,e.exports=c},335:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e){this.request=e}return e.prototype.get=function(e){return this.request.get("/v3/address/validate",{address:e}).then((function(e){return e.body}))},e}();t.default=r},632:function(e,t,r){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var n=o(r(78)),s=function(e,t){this.id=e,this.url=t.url},i=function(){function e(e){this.request=e}return e.prototype._parseWebhookList=function(e){return e.body.webhooks},e.prototype._parseWebhookWithID=function(e){return function(t){return new s(e,t.body.webhook)}},e.prototype._parseWebhookTest=function(e){return{code:e.body.code,message:e.body.message}},e.prototype.list=function(e,t){return this.request.get(n.default("/v2/domains",e,"webhooks"),t).then(this._parseWebhookList)},e.prototype.get=function(e,t){return this.request.get(n.default("/v2/domains",e,"webhooks",t)).then(this._parseWebhookWithID(t))},e.prototype.create=function(e,t,r,o){return o?this.request.put(n.default("/v2/domains",e,"webhooks",t,"test"),{url:r}).then(this._parseWebhookTest):this.request.post(n.default("/v2/domains",e,"webhooks"),{id:t,url:r}).then(this._parseWebhookWithID(t))},e.prototype.update=function(e,t,r){return this.request.put(n.default("/v2/domains",e,"webhooks",t),{url:r}).then(this._parseWebhookWithID(t))},e.prototype.destroy=function(e,t){return this.request.delete(n.default("/v2/domains",e,"webhooks",t)).then(this._parseWebhookWithID(t))},e}();t.default=i},706:e=>{!function(){"use strict";e.exports=function(e){return(e instanceof Buffer?e:Buffer.from(e.toString(),"binary")).toString("base64")}}()},175:e=>{"use strict";e.exports=function(e){if(!/^data:/i.test(e))throw new TypeError('`uri` does not appear to be a Data URI (must begin with "data:")');const t=(e=e.replace(/\r?\n/g,"")).indexOf(",");if(-1===t||t<=4)throw new TypeError("malformed data: URI");const r=e.substring(5,t).split(";");let o="",n=!1;const s=r[0]||"text/plain";let i=s;for(let e=1;e{"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=new WeakMap,o=new WeakMap;function n(e){const t=r.get(e);return console.assert(null!=t,"'this' is expected an Event object, but got",e),t}function s(e){null==e.passiveListener?e.event.cancelable&&(e.canceled=!0,"function"==typeof e.event.preventDefault&&e.event.preventDefault()):"undefined"!=typeof console&&"function"==typeof console.error&&console.error("Unable to preventDefault inside passive event listener invocation.",e.passiveListener)}function i(e,t){r.set(this,{eventTarget:e,event:t,eventPhase:2,currentTarget:e,canceled:!1,stopped:!1,immediateStopped:!1,passiveListener:null,timeStamp:t.timeStamp||Date.now()}),Object.defineProperty(this,"isTrusted",{value:!1,enumerable:!0});const o=Object.keys(t);for(let e=0;e0){const e=new Array(arguments.length);for(let t=0;t{const{Readable:o}=r(413),n=new WeakMap;class s{constructor(e=[],t={type:""}){let r=0;const o=e.map((e=>{let t;return t=e instanceof Buffer?e:ArrayBuffer.isView(e)?Buffer.from(e.buffer,e.byteOffset,e.byteLength):e instanceof ArrayBuffer?Buffer.from(e):e instanceof s?e:Buffer.from("string"==typeof e?e:String(e)),r+=t.length||t.size||0,t})),i=void 0===t.type?"":String(t.type).toLowerCase();n.set(this,{type:/[^\u0020-\u007E]/.test(i)?"":i,size:r,parts:o})}get size(){return n.get(this).size}get type(){return n.get(this).type}async text(){return Buffer.from(await this.arrayBuffer()).toString()}async arrayBuffer(){const e=new Uint8Array(this.size);let t=0;for await(const r of this.stream())e.set(r,t),t+=r.length;return e.buffer}stream(){return o.from(async function*(e){for(const t of e)"stream"in t?yield*t.stream():yield t}(n.get(this).parts))}slice(e=0,t=this.size,r=""){const{size:o}=this;let i=e<0?Math.max(o+e,0):Math.min(e,o),a=t<0?Math.max(o+t,0):Math.min(t,o);const u=Math.max(a-i,0),l=n.get(this).parts.values(),c=[];let d=0;for(const e of l){const t=ArrayBuffer.isView(e)?e.byteLength:e.size;if(i&&t<=i)i-=t,a-=t;else{const r=e.slice(i,Math.min(t,a));if(c.push(r),d+=ArrayBuffer.isView(r)?r.byteLength:r.size,i=0,d>=u)break}}const f=new s([],{type:r});return Object.assign(n.get(f),{size:u,parts:c}),f}get[Symbol.toStringTag](){return"Blob"}static[Symbol.hasInstance](e){return"object"==typeof e&&"function"==typeof e.stream&&0===e.stream.length&&"function"==typeof e.constructor&&/^(Blob|File)$/.test(e[Symbol.toStringTag])}}Object.defineProperties(s.prototype,{size:{enumerable:!0},type:{enumerable:!0},slice:{enumerable:!0}}),e.exports=s},556:(e,t,r)=>{"use strict";const o=r(711),n=r(271);if(global.fetch||(global.fetch=(e,t)=>o(e,{highWaterMark:1e7,...t})),global.Headers||(global.Headers=o.Headers),global.Request||(global.Request=o.Request),global.Response||(global.Response=o.Response),global.AbortController||(global.AbortController=n),!global.ReadableStream)try{global.ReadableStream=r(377)}catch(e){}e.exports=r(721)},721:function(e){var t;t=function(){"use strict";const e={},t=e=>"undefined"!=typeof self&&self&&e in self?self:"undefined"!=typeof window&&window&&e in window?window:"undefined"!=typeof global&&global&&e in global?global:"undefined"!=typeof globalThis&&globalThis?globalThis:void 0,r=["Headers","Request","Response","ReadableStream","fetch","AbortController","FormData"];for(const o of r)Object.defineProperty(e,o,{get(){const e=t(o),r=e&&e[o];return"function"==typeof r?r.bind(e):r}});const o=e=>null!==e&&"object"==typeof e,n="function"==typeof e.AbortController,s="function"==typeof e.ReadableStream,i="function"==typeof e.FormData,a=(t,r)=>{const o=new e.Headers(t||{}),n=r instanceof e.Headers,s=new e.Headers(r||{});for(const[e,t]of s)n&&"undefined"===t||void 0===t?o.delete(e):o.set(e,t);return o},u=(...e)=>{let t={},r={};for(const n of e){if(Array.isArray(n))Array.isArray(t)||(t=[]),t=[...t,...n];else if(o(n)){for(let[e,r]of Object.entries(n))o(r)&&e in t&&(r=u(t[e],r)),t={...t,[e]:r};o(n.headers)&&(r=a(r,n.headers))}t.headers=r}return t},l=["get","post","put","patch","head","delete"],c={json:"application/json",text:"text/*",formData:"multipart/form-data",arrayBuffer:"*/*",blob:"*/*"},d=[413,429,503],f=Symbol("stop");class p extends Error{constructor(e){super(e.statusText||String(0===e.status||e.status?e.status:"Unknown response error")),this.name="HTTPError",this.response=e}}class h extends Error{constructor(e){super("Request timed out"),this.name="TimeoutError",this.request=e}}const y=e=>new Promise((t=>setTimeout(t,e))),b=e=>l.includes(e)?e.toUpperCase():e,m={limit:2,methods:["get","put","head","delete","options","trace"],statusCodes:[408,413,429,500,502,503,504],afterStatusCodes:d},_=(e={})=>{if("number"==typeof e)return{...m,limit:e};if(e.methods&&!Array.isArray(e.methods))throw new Error("retry.methods must be an array");if(e.statusCodes&&!Array.isArray(e.statusCodes))throw new Error("retry.statusCodes must be an array");return{...m,...e,afterStatusCodes:d}},g=2147483647;class w{constructor(t,r={}){if(this._retryCount=0,this._input=t,this._options={credentials:this._input.credentials||"same-origin",...r,headers:a(this._input.headers,r.headers),hooks:u({beforeRequest:[],beforeRetry:[],afterResponse:[]},r.hooks),method:b(r.method||this._input.method),prefixUrl:String(r.prefixUrl||""),retry:_(r.retry),throwHttpErrors:!1!==r.throwHttpErrors,timeout:void 0===r.timeout?1e4:r.timeout,fetch:r.fetch||e.fetch},"string"!=typeof this._input&&!(this._input instanceof URL||this._input instanceof e.Request))throw new TypeError("`input` must be a string, URL, or Request");if(this._options.prefixUrl&&"string"==typeof this._input){if(this._input.startsWith("/"))throw new Error("`input` must not begin with a slash when using `prefixUrl`");this._options.prefixUrl.endsWith("/")||(this._options.prefixUrl+="/"),this._input=this._options.prefixUrl+this._input}if(n&&(this.abortController=new e.AbortController,this._options.signal&&this._options.signal.addEventListener("abort",(()=>{this.abortController.abort()})),this._options.signal=this.abortController.signal),this.request=new e.Request(this._input,this._options),this._options.searchParams){const t="?"+new URLSearchParams(this._options.searchParams).toString(),r=this.request.url.replace(/(?:\?.*?)?(?=#|$)/,t);!(i&&this._options.body instanceof e.FormData||this._options.body instanceof URLSearchParams)||this._options.headers&&this._options.headers["content-type"]||this.request.headers.delete("content-type"),this.request=new e.Request(new e.Request(r,this.request),this._options)}void 0!==this._options.json&&(this._options.body=JSON.stringify(this._options.json),this.request.headers.set("content-type","application/json"),this.request=new e.Request(this.request,{body:this._options.body}));const o=async()=>{if(this._options.timeout>g)throw new RangeError("The `timeout` option cannot be greater than 2147483647");await y(1);let t=await this._fetch();for(const r of this._options.hooks.afterResponse){const o=await r(this.request,this._options,this._decorateResponse(t.clone()));o instanceof e.Response&&(t=o)}if(this._decorateResponse(t),!t.ok&&this._options.throwHttpErrors)throw new p(t);if(this._options.onDownloadProgress){if("function"!=typeof this._options.onDownloadProgress)throw new TypeError("The `onDownloadProgress` option must be a function");if(!s)throw new Error("Streams are not supported in your environment. `ReadableStream` is missing.");return this._stream(t.clone(),this._options.onDownloadProgress)}return t},l=this._options.retry.methods.includes(this.request.method.toLowerCase())?this._retry(o):o();for(const[e,t]of Object.entries(c))l[e]=async()=>{this.request.headers.set("accept",this.request.headers.get("accept")||t);const o=(await l).clone();if("json"===e){if(204===o.status)return"";if(r.parseJson)return r.parseJson(await o.text())}return o[e]()};return l}_calculateRetryDelay(e){if(this._retryCount++,this._retryCountthis._options.retry.maxRetryAfter?0:e}if(413===e.response.status)return 0}return.3*2**(this._retryCount-1)*1e3}return 0}_decorateResponse(e){return this._options.parseJson&&(e.json=async()=>this._options.parseJson(await e.text())),e}async _retry(e){try{return await e()}catch(t){const r=Math.min(this._calculateRetryDelay(t),g);if(0!==r&&this._retryCount>0){await y(r);for(const e of this._options.hooks.beforeRetry)if(await e({request:this.request,options:this._options,error:t,retryCount:this._retryCount})===f)return;return this._retry(e)}if(this._options.throwHttpErrors)throw t}}async _fetch(){for(const e of this._options.hooks.beforeRequest){const t=await e(this.request,this._options);if(t instanceof Request){this.request=t;break}if(t instanceof Response)return t}return!1===this._options.timeout?this._options.fetch(this.request.clone()):(e=this.request.clone(),t=this.abortController,r=this._options,new Promise(((o,n)=>{const s=setTimeout((()=>{t&&t.abort(),n(new h(e))}),r.timeout);r.fetch(e).then(o).catch(n).then((()=>{clearTimeout(s)}))})));var e,t,r}_stream(t,r){const o=Number(t.headers.get("content-length"))||0;let n=0;return new e.Response(new e.ReadableStream({start(e){const s=t.body.getReader();r&&r({percent:0,transferredBytes:0,totalBytes:o},new Uint8Array),async function t(){const{done:i,value:a}=await s.read();i?e.close():(r&&(n+=a.byteLength,r({percent:0===o?0:n/o,transferredBytes:n,totalBytes:o},a)),e.enqueue(a),t())}()}}))}}const v=(...e)=>{for(const t of e)if((!o(t)||Array.isArray(t))&&void 0!==t)throw new TypeError("The `options` argument must be an object");return u({},...e)},S=e=>{const t=(t,r)=>new w(t,v(e,r));for(const r of l)t[r]=(t,o)=>new w(t,v(e,o,{method:r}));return t.HTTPError=p,t.TimeoutError=h,t.create=e=>S(v(e)),t.extend=t=>S(v(e,t)),t.stop=f,t};return S()},e.exports=t()},711:(e,t,r)=>{"use strict";t=e.exports=I;const o=r(605),n=r(211),s=r(761),i=r(413),a=r(175),u=r(669),l=r(30),c=r(417),d=r(835);class f extends Error{constructor(e,t){super(e),Error.captureStackTrace(this,this.constructor),this.type=t}get name(){return this.constructor.name}get[Symbol.toStringTag](){return this.constructor.name}}class p extends f{constructor(e,t,r){super(e,t),r&&(this.code=this.errno=r.code,this.erroredSysCall=r.syscall)}}const h=Symbol.toStringTag,y=e=>"object"==typeof e&&"function"==typeof e.append&&"function"==typeof e.delete&&"function"==typeof e.get&&"function"==typeof e.getAll&&"function"==typeof e.has&&"function"==typeof e.set&&"function"==typeof e.sort&&"URLSearchParams"===e[h],b=e=>"object"==typeof e&&"function"==typeof e.arrayBuffer&&"string"==typeof e.type&&"function"==typeof e.stream&&"function"==typeof e.constructor&&/^(Blob|File)$/.test(e[h]);function m(e){return"object"==typeof e&&"function"==typeof e.append&&"function"==typeof e.set&&"function"==typeof e.get&&"function"==typeof e.getAll&&"function"==typeof e.delete&&"function"==typeof e.keys&&"function"==typeof e.values&&"function"==typeof e.entries&&"function"==typeof e.constructor&&"FormData"===e[h]}const _="\r\n",g="-".repeat(2),w=Buffer.byteLength(_),v=e=>`${g}${e}${g}${_.repeat(2)}`;function S(e,t,r){let o="";return o+=`${g}${e}\r\n`,o+=`Content-Disposition: form-data; name="${t}"`,b(r)&&(o+=`; filename="${r.name}"\r\n`,o+=`Content-Type: ${r.type||"application/octet-stream"}`),`${o}${_.repeat(2)}`}const T=Symbol("Body internals");class R{constructor(e,{size:t=0}={}){let r=null;null===e?e=null:y(e)?e=Buffer.from(e.toString()):b(e)||Buffer.isBuffer(e)||(u.types.isAnyArrayBuffer(e)?e=Buffer.from(e):ArrayBuffer.isView(e)?e=Buffer.from(e.buffer,e.byteOffset,e.byteLength):e instanceof i||(m(e)?(r=`NodeFetchFormDataBoundary${c.randomBytes(8).toString("hex")}`,e=i.Readable.from(async function*(e,t){for(const[r,o]of e)yield S(t,r,o),b(o)?yield*o.stream():yield o,yield _;yield v(t)}(e,r))):e=Buffer.from(String(e)))),this[T]={body:e,boundary:r,disturbed:!1,error:null},this.size=t,e instanceof i&&e.on("error",(e=>{const t=e instanceof f?e:new p(`Invalid response body while trying to fetch ${this.url}: ${e.message}`,"system",e);this[T].error=t}))}get body(){return this[T].body}get bodyUsed(){return this[T].disturbed}async arrayBuffer(){const{buffer:e,byteOffset:t,byteLength:r}=await q(this);return e.slice(t,t+r)}async blob(){const e=this.headers&&this.headers.get("content-type")||this[T].body&&this[T].body.type||"",t=await this.buffer();return new l([t],{type:e})}async json(){const e=await q(this);return JSON.parse(e.toString())}async text(){return(await q(this)).toString()}buffer(){return q(this)}}async function q(e){if(e[T].disturbed)throw new TypeError(`body used already for: ${e.url}`);if(e[T].disturbed=!0,e[T].error)throw e[T].error;let{body:t}=e;if(null===t)return Buffer.alloc(0);if(b(t)&&(t=t.stream()),Buffer.isBuffer(t))return t;if(!(t instanceof i))return Buffer.alloc(0);const r=[];let o=0;try{for await(const n of t){if(e.size>0&&o+n.length>e.size){const r=new p(`content size at ${e.url} over limit: ${e.size}`,"max-size");throw t.destroy(r),r}o+=n.length,r.push(n)}}catch(t){throw t instanceof f?t:new p(`Invalid response body while trying to fetch ${e.url}: ${t.message}`,"system",t)}if(!0!==t.readableEnded&&!0!==t._readableState.ended)throw new p(`Premature close of server response while trying to fetch ${e.url}`);try{return r.every((e=>"string"==typeof e))?Buffer.from(r.join("")):Buffer.concat(r,o)}catch(t){throw new p(`Could not create Buffer from response body for ${e.url}: ${t.message}`,"system",t)}}Object.defineProperties(R.prototype,{body:{enumerable:!0},bodyUsed:{enumerable:!0},arrayBuffer:{enumerable:!0},blob:{enumerable:!0},json:{enumerable:!0},text:{enumerable:!0}});const P=(e,t)=>{let r,o,{body:n}=e;if(e.bodyUsed)throw new Error("cannot clone body after it is used");return n instanceof i&&"function"!=typeof n.getBoundary&&(r=new i.PassThrough({highWaterMark:t}),o=new i.PassThrough({highWaterMark:t}),n.pipe(r),n.pipe(o),e[T].body=r,n=o),n},E=(e,t)=>null===e?null:"string"==typeof e?"text/plain;charset=UTF-8":y(e)?"application/x-www-form-urlencoded;charset=UTF-8":b(e)?e.type||null:Buffer.isBuffer(e)||u.types.isAnyArrayBuffer(e)||ArrayBuffer.isView(e)?null:e&&"function"==typeof e.getBoundary?`multipart/form-data;boundary=${e.getBoundary()}`:m(e)?`multipart/form-data; boundary=${t[T].boundary}`:e instanceof i?null:"text/plain;charset=UTF-8",j="function"==typeof o.validateHeaderName?o.validateHeaderName:e=>{if(!/^[\^`\-\w!#$%&'*+.|~]+$/.test(e)){const t=new TypeError(`Header name must be a valid HTTP token [${e}]`);throw Object.defineProperty(t,"code",{value:"ERR_INVALID_HTTP_TOKEN"}),t}},k="function"==typeof o.validateHeaderValue?o.validateHeaderValue:(e,t)=>{if(/[^\t\u0020-\u007E\u0080-\u00FF]/.test(t)){const t=new TypeError(`Invalid character in header content ["${e}"]`);throw Object.defineProperty(t,"code",{value:"ERR_INVALID_CHAR"}),t}};class O extends URLSearchParams{constructor(e){let t=[];if(e instanceof O){const r=e.raw();for(const[e,o]of Object.entries(r))t.push(...o.map((t=>[e,t])))}else if(null==e);else{if("object"!=typeof e||u.types.isBoxedPrimitive(e))throw new TypeError("Failed to construct 'Headers': The provided value is not of type '(sequence> or record)");{const r=e[Symbol.iterator];if(null==r)t.push(...Object.entries(e));else{if("function"!=typeof r)throw new TypeError("Header pairs must be iterable");t=[...e].map((e=>{if("object"!=typeof e||u.types.isBoxedPrimitive(e))throw new TypeError("Each header pair must be an iterable object");return[...e]})).map((e=>{if(2!==e.length)throw new TypeError("Each header pair must be a name/value tuple");return[...e]}))}}}return t=t.length>0?t.map((([e,t])=>(j(e),k(e,String(t)),[String(e).toLowerCase(),String(t)]))):void 0,super(t),new Proxy(this,{get(e,t,r){switch(t){case"append":case"set":return(e,o)=>(j(e),k(e,String(o)),URLSearchParams.prototype[t].call(r,String(e).toLowerCase(),String(o)));case"delete":case"has":case"getAll":return e=>(j(e),URLSearchParams.prototype[t].call(r,String(e).toLowerCase()));case"keys":return()=>(e.sort(),new Set(URLSearchParams.prototype.keys.call(e)).keys());default:return Reflect.get(e,t,r)}}})}get[Symbol.toStringTag](){return this.constructor.name}toString(){return Object.prototype.toString.call(this)}get(e){const t=this.getAll(e);if(0===t.length)return null;let r=t.join(", ");return/^content-encoding$/i.test(e)&&(r=r.toLowerCase()),r}forEach(e){for(const t of this.keys())e(this.get(t),t)}*values(){for(const e of this.keys())yield this.get(e)}*entries(){for(const e of this.keys())yield[e,this.get(e)]}[Symbol.iterator](){return this.entries()}raw(){return[...this.keys()].reduce(((e,t)=>(e[t]=this.getAll(t),e)),{})}[Symbol.for("nodejs.util.inspect.custom")](){return[...this.keys()].reduce(((e,t)=>{const r=this.getAll(t);return e[t]="host"===t?r[0]:r.length>1?r:r[0],e}),{})}}Object.defineProperties(O.prototype,["get","entries","forEach","values"].reduce(((e,t)=>(e[t]={enumerable:!0},e)),{}));const C=new Set([301,302,303,307,308]),A=e=>C.has(e),B=Symbol("Response internals");class x extends R{constructor(e=null,t={}){super(e,t);const r=t.status||200,o=new O(t.headers);if(null!==e&&!o.has("Content-Type")){const t=E(e);t&&o.append("Content-Type",t)}this[B]={url:t.url,status:r,statusText:t.statusText||"",headers:o,counter:t.counter,highWaterMark:t.highWaterMark}}get url(){return this[B].url||""}get status(){return this[B].status}get ok(){return this[B].status>=200&&this[B].status<300}get redirected(){return this[B].counter>0}get statusText(){return this[B].statusText}get headers(){return this[B].headers}get highWaterMark(){return this[B].highWaterMark}clone(){return new x(P(this,this.highWaterMark),{url:this.url,status:this.status,statusText:this.statusText,headers:this.headers,ok:this.ok,redirected:this.redirected,size:this.size})}static redirect(e,t=302){if(!A(t))throw new RangeError('Failed to execute "redirect" on "response": Invalid status code');return new x(null,{headers:{location:new URL(e).toString()},status:t})}get[Symbol.toStringTag](){return"Response"}}Object.defineProperties(x.prototype,{url:{enumerable:!0},status:{enumerable:!0},ok:{enumerable:!0},redirected:{enumerable:!0},statusText:{enumerable:!0},headers:{enumerable:!0},clone:{enumerable:!0}});const W=Symbol("Request internals"),L=e=>"object"==typeof e&&"object"==typeof e[W];class z extends R{constructor(e,t={}){let r;L(e)?r=new URL(e.url):(r=new URL(e),e={});let o=t.method||e.method||"GET";if(o=o.toUpperCase(),(null!=t.body||L(e))&&null!==e.body&&("GET"===o||"HEAD"===o))throw new TypeError("Request with GET/HEAD method cannot have body");const n=t.body?t.body:L(e)&&null!==e.body?P(e):null;super(n,{size:t.size||e.size||0});const s=new O(t.headers||e.headers||{});if(null!==n&&!s.has("Content-Type")){const e=E(n,this);e&&s.append("Content-Type",e)}let i=L(e)?e.signal:null;if("signal"in t&&(i=t.signal),null!==i&&("object"!=typeof(a=i)||"AbortSignal"!==a[h]))throw new TypeError("Expected signal to be an instanceof AbortSignal");var a;this[W]={method:o,redirect:t.redirect||e.redirect||"follow",headers:s,parsedURL:r,signal:i},this.follow=void 0===t.follow?void 0===e.follow?20:e.follow:t.follow,this.compress=void 0===t.compress?void 0===e.compress||e.compress:t.compress,this.counter=t.counter||e.counter||0,this.agent=t.agent||e.agent,this.highWaterMark=t.highWaterMark||e.highWaterMark||16384,this.insecureHTTPParser=t.insecureHTTPParser||e.insecureHTTPParser||!1}get method(){return this[W].method}get url(){return d.format(this[W].parsedURL)}get headers(){return this[W].headers}get redirect(){return this[W].redirect}get signal(){return this[W].signal}clone(){return new z(this)}get[Symbol.toStringTag](){return"Request"}}Object.defineProperties(z.prototype,{method:{enumerable:!0},url:{enumerable:!0},headers:{enumerable:!0},redirect:{enumerable:!0},clone:{enumerable:!0},signal:{enumerable:!0}});class M extends f{constructor(e,t="aborted"){super(e,t)}}const $=new Set(["data:","http:","https:"]);async function I(e,t){return new Promise(((r,u)=>{const l=new z(e,t),c=(e=>{const{parsedURL:t}=e[W],r=new O(e[W].headers);r.has("Accept")||r.set("Accept","*/*");let o=null;if(null===e.body&&/^(post|put)$/i.test(e.method)&&(o="0"),null!==e.body){const t=(e=>{const{body:t}=e;return null===t?0:b(t)?t.size:Buffer.isBuffer(t)?t.length:t&&"function"==typeof t.getLengthSync?t.hasKnownLength&&t.hasKnownLength()?t.getLengthSync():null:m(t)?function(e,t){let r=0;for(const[o,n]of e)r+=Buffer.byteLength(S(t,o,n)),b(n)?r+=n.size:r+=Buffer.byteLength(String(n)),r+=w;return r+=Buffer.byteLength(v(t)),r}(e[T].boundary):null})(e);"number"!=typeof t||Number.isNaN(t)||(o=String(t))}o&&r.set("Content-Length",o),r.has("User-Agent")||r.set("User-Agent","node-fetch"),e.compress&&!r.has("Accept-Encoding")&&r.set("Accept-Encoding","gzip,deflate,br");let{agent:n}=e;"function"==typeof n&&(n=n(t)),r.has("Connection")||n||r.set("Connection","close");const s=(e=>{if(e.search)return e.search;const t=e.href.length-1,r=e.hash||("#"===e.href[t]?"#":"");return"?"===e.href[t-r.length]?"?":""})(t);return{path:t.pathname+s,pathname:t.pathname,hostname:t.hostname,protocol:t.protocol,port:t.port,hash:t.hash,search:t.search,query:t.query,href:t.href,method:e.method,headers:r[Symbol.for("nodejs.util.inspect.custom")](),insecureHTTPParser:e.insecureHTTPParser,agent:n}})(l);if(!$.has(c.protocol))throw new TypeError(`node-fetch cannot load ${e}. URL scheme "${c.protocol.replace(/:$/,"")}" is not supported.`);if("data:"===c.protocol){const e=a(l.url),t=new x(e,{headers:{"Content-Type":e.typeFull}});return void r(t)}const d=("https:"===c.protocol?n:o).request,{signal:f}=l;let h=null;const y=()=>{const e=new M("The operation was aborted.");u(e),l.body&&l.body instanceof i.Readable&&l.body.destroy(e),h&&h.body&&h.body.emit("error",e)};if(f&&f.aborted)return void y();const _=()=>{y(),R()},g=d(c);f&&f.addEventListener("abort",_);const R=()=>{g.abort(),f&&f.removeEventListener("abort",_)};g.on("error",(e=>{u(new p(`request to ${l.url} failed, reason: ${e.message}`,"system",e)),R()})),g.on("response",(e=>{g.setTimeout(0);const o=function(e=[]){return new O(e.reduce(((e,t,r,o)=>(r%2==0&&e.push(o.slice(r,r+2)),e)),[]).filter((([e,t])=>{try{return j(e),k(e,String(t)),!0}catch{return!1}})))}(e.rawHeaders);if(A(e.statusCode)){const n=o.get("Location"),s=null===n?null:new URL(n,l.url);switch(l.redirect){case"error":return u(new p(`uri requested responds with a redirect, redirect mode is set to error: ${l.url}`,"no-redirect")),void R();case"manual":if(null!==s)try{o.set("Location",s)}catch(e){u(e)}break;case"follow":{if(null===s)break;if(l.counter>=l.follow)return u(new p(`maximum redirect reached at: ${l.url}`,"max-redirect")),void R();const o={headers:new O(l.headers),follow:l.follow,counter:l.counter+1,agent:l.agent,compress:l.compress,method:l.method,body:l.body,signal:l.signal,size:l.size};return 303!==e.statusCode&&l.body&&t.body instanceof i.Readable?(u(new p("Cannot follow redirect with body being a readable stream","unsupported-redirect")),void R()):(303!==e.statusCode&&(301!==e.statusCode&&302!==e.statusCode||"POST"!==l.method)||(o.method="GET",o.body=void 0,o.headers.delete("content-length")),r(I(new z(s,o))),void R())}}}e.once("end",(()=>{f&&f.removeEventListener("abort",_)}));let n=i.pipeline(e,new i.PassThrough,(e=>{u(e)}));process.version<"v12.10"&&e.on("aborted",_);const a={url:l.url,status:e.statusCode,statusText:e.statusMessage,headers:o,size:l.size,counter:l.counter,highWaterMark:l.highWaterMark},c=o.get("Content-Encoding");if(!l.compress||"HEAD"===l.method||null===c||204===e.statusCode||304===e.statusCode)return h=new x(n,a),void r(h);const d={flush:s.Z_SYNC_FLUSH,finishFlush:s.Z_SYNC_FLUSH};if("gzip"===c||"x-gzip"===c)return n=i.pipeline(n,s.createGunzip(d),(e=>{u(e)})),h=new x(n,a),void r(h);if("deflate"!==c&&"x-deflate"!==c){if("br"===c)return n=i.pipeline(n,s.createBrotliDecompress(),(e=>{u(e)})),h=new x(n,a),void r(h);h=new x(n,a),r(h)}else i.pipeline(e,new i.PassThrough,(e=>{u(e)})).once("data",(e=>{n=8==(15&e[0])?i.pipeline(n,s.createInflate(),(e=>{u(e)})):i.pipeline(n,s.createInflateRaw(),(e=>{u(e)})),h=new x(n,a),r(h)}))})),((e,{body:t})=>{null===t?e.end():b(t)?t.stream().pipe(e):Buffer.isBuffer(t)?(e.write(t),e.end()):t.pipe(e)})(g,l)}))}t.AbortError=M,t.FetchError=p,t.Headers=O,t.Request=z,t.Response=x,t.default=I,t.isRedirect=A},78:e=>{function t(e){return e.replace(/[\/]+/g,"/").replace(/\/\?/g,"?").replace(/\/\#/g,"#").replace(/\:\//g,"://")}e.exports=function(){var e=[].slice.call(arguments,0).join("/");return t(e)}},377:(e,t,r)=>{"use strict";r.r(t),r.d(t,{ByteLengthQueuingStrategy:()=>pr,CountQueuingStrategy:()=>mr,ReadableByteStreamController:()=>ye,ReadableStream:()=>rr,ReadableStreamBYOBReader:()=>ze,ReadableStreamBYOBRequest:()=>he,ReadableStreamDefaultController:()=>Mt,ReadableStreamDefaultReader:()=>X,TransformStream:()=>Tr,TransformStreamDefaultController:()=>jr,WritableStream:()=>Ge,WritableStreamDefaultController:()=>yt,WritableStreamDefaultWriter:()=>ut});const o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol:e=>`Symbol(${e})`;function n(){}const s="undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:void 0;function i(e){return"object"==typeof e&&null!==e||"function"==typeof e}const a=n,u=Promise,l=Promise.prototype.then,c=Promise.resolve.bind(u),d=Promise.reject.bind(u);function f(e){return new u(e)}function p(e){return c(e)}function h(e){return d(e)}function y(e,t,r){return l.call(e,t,r)}function b(e,t,r){y(y(e,t,r),void 0,a)}function m(e,t){b(e,t)}function _(e,t){b(e,void 0,t)}function g(e,t,r){return y(e,t,r)}function w(e){y(e,void 0,a)}const v=(()=>{const e=s&&s.queueMicrotask;if("function"==typeof e)return e;const t=p(void 0);return e=>y(t,e)})();function S(e,t,r){if("function"!=typeof e)throw new TypeError("Argument is not a function");return Function.prototype.apply.call(e,t,r)}function T(e,t,r){try{return p(S(e,t,r))}catch(e){return h(e)}}class R{constructor(){this._cursor=0,this._size=0,this._front={_elements:[],_next:void 0},this._back=this._front,this._cursor=0,this._size=0}get length(){return this._size}push(e){const t=this._back;let r=t;16383===t._elements.length&&(r={_elements:[],_next:void 0}),t._elements.push(e),r!==t&&(this._back=r,t._next=r),++this._size}shift(){const e=this._front;let t=e;const r=this._cursor;let o=r+1;const n=e._elements,s=n[r];return 16384===o&&(t=e._next,o=0),--this._size,this._cursor=o,e!==t&&(this._front=t),n[r]=void 0,s}forEach(e){let t=this._cursor,r=this._front,o=r._elements;for(;!(t===o.length&&void 0===r._next||t===o.length&&(r=r._next,o=r._elements,t=0,0===o.length));)e(o[t]),++t}peek(){const e=this._front,t=this._cursor;return e._elements[t]}}function q(e,t){e._ownerReadableStream=t,t._reader=e,"readable"===t._state?k(e):"closed"===t._state?function(e){k(e),A(e)}(e):O(e,t._storedError)}function P(e,t){return ar(e._ownerReadableStream,t)}function E(e){"readable"===e._ownerReadableStream._state?C(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness")):function(e,t){O(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness"))}(e),e._ownerReadableStream._reader=void 0,e._ownerReadableStream=void 0}function j(e){return new TypeError("Cannot "+e+" a stream using a released reader")}function k(e){e._closedPromise=f(((t,r)=>{e._closedPromise_resolve=t,e._closedPromise_reject=r}))}function O(e,t){k(e),C(e,t)}function C(e,t){void 0!==e._closedPromise_reject&&(w(e._closedPromise),e._closedPromise_reject(t),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}function A(e){void 0!==e._closedPromise_resolve&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}const B=o("[[AbortSteps]]"),x=o("[[ErrorSteps]]"),W=o("[[CancelSteps]]"),L=o("[[PullSteps]]"),z=Number.isFinite||function(e){return"number"==typeof e&&isFinite(e)},M=Math.trunc||function(e){return e<0?Math.ceil(e):Math.floor(e)};function $(e,t){if(void 0!==e&&"object"!=typeof(r=e)&&"function"!=typeof r)throw new TypeError(`${t} is not an object.`);var r}function I(e,t){if("function"!=typeof e)throw new TypeError(`${t} is not a function.`)}function D(e,t){if(!function(e){return"object"==typeof e&&null!==e||"function"==typeof e}(e))throw new TypeError(`${t} is not an object.`)}function F(e,t,r){if(void 0===e)throw new TypeError(`Parameter ${t} is required in '${r}'.`)}function U(e,t,r){if(void 0===e)throw new TypeError(`${t} is required in '${r}'.`)}function H(e){return Number(e)}function N(e){return 0===e?0:e}function V(e,t){const r=Number.MAX_SAFE_INTEGER;let o=Number(e);if(o=N(o),!z(o))throw new TypeError(`${t} is not a finite number`);if(o=function(e){return N(M(e))}(o),o<0||o>r)throw new TypeError(`${t} is outside the accepted range of 0 to ${r}, inclusive`);return z(o)&&0!==o?o:0}function Q(e,t){if(!sr(e))throw new TypeError(`${t} is not a ReadableStream.`)}function Y(e){return new X(e)}function G(e,t){e._reader._readRequests.push(t)}function J(e,t,r){const o=e._reader._readRequests.shift();r?o._closeSteps():o._chunkSteps(t)}function K(e){return e._reader._readRequests.length}function Z(e){const t=e._reader;return void 0!==t&&!!ee(t)}class X{constructor(e){if(F(e,1,"ReadableStreamDefaultReader"),Q(e,"First parameter"),ir(e))throw new TypeError("This stream has already been locked for exclusive reading by another reader");q(this,e),this._readRequests=new R}get closed(){return ee(this)?this._closedPromise:h(re("closed"))}cancel(e){return ee(this)?void 0===this._ownerReadableStream?h(j("cancel")):P(this,e):h(re("cancel"))}read(){if(!ee(this))return h(re("read"));if(void 0===this._ownerReadableStream)return h(j("read from"));let e,t;const r=f(((r,o)=>{e=r,t=o}));return te(this,{_chunkSteps:t=>e({value:t,done:!1}),_closeSteps:()=>e({value:void 0,done:!0}),_errorSteps:e=>t(e)}),r}releaseLock(){if(!ee(this))throw re("releaseLock");if(void 0!==this._ownerReadableStream){if(this._readRequests.length>0)throw new TypeError("Tried to release a reader lock when that reader has pending read() calls un-settled");E(this)}}}function ee(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_readRequests")}function te(e,t){const r=e._ownerReadableStream;r._disturbed=!0,"closed"===r._state?t._closeSteps():"errored"===r._state?t._errorSteps(r._storedError):r._readableStreamController[L](t)}function re(e){return new TypeError(`ReadableStreamDefaultReader.prototype.${e} can only be used on a ReadableStreamDefaultReader`)}Object.defineProperties(X.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(X.prototype,o.toStringTag,{value:"ReadableStreamDefaultReader",configurable:!0});const oe=Object.getPrototypeOf(Object.getPrototypeOf((async function*(){})).prototype);class ne{constructor(e,t){this._ongoingPromise=void 0,this._isFinished=!1,this._reader=e,this._preventCancel=t}next(){const e=()=>this._nextSteps();return this._ongoingPromise=this._ongoingPromise?g(this._ongoingPromise,e,e):e(),this._ongoingPromise}return(e){const t=()=>this._returnSteps(e);return this._ongoingPromise?g(this._ongoingPromise,t,t):t()}_nextSteps(){if(this._isFinished)return Promise.resolve({value:void 0,done:!0});const e=this._reader;if(void 0===e._ownerReadableStream)return h(j("iterate"));let t,r;const o=f(((e,o)=>{t=e,r=o}));return te(e,{_chunkSteps:e=>{this._ongoingPromise=void 0,v((()=>t({value:e,done:!1})))},_closeSteps:()=>{this._ongoingPromise=void 0,this._isFinished=!0,E(e),t({value:void 0,done:!0})},_errorSteps:t=>{this._ongoingPromise=void 0,this._isFinished=!0,E(e),r(t)}}),o}_returnSteps(e){if(this._isFinished)return Promise.resolve({value:e,done:!0});this._isFinished=!0;const t=this._reader;if(void 0===t._ownerReadableStream)return h(j("finish iterating"));if(!this._preventCancel){const r=P(t,e);return E(t),g(r,(()=>({value:e,done:!0})))}return E(t),p({value:e,done:!0})}}const se={next(){return ie(this)?this._asyncIteratorImpl.next():h(ae("next"))},return(e){return ie(this)?this._asyncIteratorImpl.return(e):h(ae("return"))}};function ie(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_asyncIteratorImpl")}function ae(e){return new TypeError(`ReadableStreamAsyncIterator.${e} can only be used on a ReadableSteamAsyncIterator`)}void 0!==oe&&Object.setPrototypeOf(se,oe);const ue=Number.isNaN||function(e){return e!=e};function le(e){return!!function(e){return"number"==typeof e&&(!ue(e)&&!(e<0))}(e)&&e!==1/0}function ce(e){const t=e._queue.shift();return e._queueTotalSize-=t.size,e._queueTotalSize<0&&(e._queueTotalSize=0),t.value}function de(e,t,r){if(!le(r=Number(r)))throw new RangeError("Size must be a finite, non-NaN, non-negative number.");e._queue.push({value:t,size:r}),e._queueTotalSize+=r}function fe(e){e._queue=new R,e._queueTotalSize=0}function pe(e){return e.slice()}class he{constructor(){throw new TypeError("Illegal constructor")}get view(){if(!me(this))throw Ae("view");return this._view}respond(e){if(!me(this))throw Ae("respond");if(F(e,1,"respond"),e=V(e,"First parameter"),void 0===this._associatedReadableByteStreamController)throw new TypeError("This BYOB request has been invalidated");this._view.buffer,function(e,t){if(!le(t=Number(t)))throw new RangeError("bytesWritten must be a finite");Ee(e,t)}(this._associatedReadableByteStreamController,e)}respondWithNewView(e){if(!me(this))throw Ae("respondWithNewView");if(F(e,1,"respondWithNewView"),!ArrayBuffer.isView(e))throw new TypeError("You can only respond with array buffer views");if(0===e.byteLength)throw new TypeError("chunk must have non-zero byteLength");if(0===e.buffer.byteLength)throw new TypeError("chunk's buffer must have non-zero byteLength");if(void 0===this._associatedReadableByteStreamController)throw new TypeError("This BYOB request has been invalidated");!function(e,t){const r=e._pendingPullIntos.peek();if(r.byteOffset+r.bytesFilled!==t.byteOffset)throw new RangeError("The region specified by view does not match byobRequest");if(r.byteLength!==t.byteLength)throw new RangeError("The buffer of view has different capacity than byobRequest");r.buffer=t.buffer,Ee(e,t.byteLength)}(this._associatedReadableByteStreamController,e)}}Object.defineProperties(he.prototype,{respond:{enumerable:!0},respondWithNewView:{enumerable:!0},view:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(he.prototype,o.toStringTag,{value:"ReadableStreamBYOBRequest",configurable:!0});class ye{constructor(){throw new TypeError("Illegal constructor")}get byobRequest(){if(!be(this))throw Be("byobRequest");if(null===this._byobRequest&&this._pendingPullIntos.length>0){const e=this._pendingPullIntos.peek(),t=new Uint8Array(e.buffer,e.byteOffset+e.bytesFilled,e.byteLength-e.bytesFilled),r=Object.create(he.prototype);!function(e,t,r){e._associatedReadableByteStreamController=t,e._view=r}(r,this,t),this._byobRequest=r}return this._byobRequest}get desiredSize(){if(!be(this))throw Be("desiredSize");return Ce(this)}close(){if(!be(this))throw Be("close");if(this._closeRequested)throw new TypeError("The stream has already been closed; do not close it again!");const e=this._controlledReadableByteStream._state;if("readable"!==e)throw new TypeError(`The stream (in ${e} state) is not in the readable state and cannot be closed`);!function(e){const t=e._controlledReadableByteStream;if(!e._closeRequested&&"readable"===t._state)if(e._queueTotalSize>0)e._closeRequested=!0;else{if(e._pendingPullIntos.length>0&&e._pendingPullIntos.peek().bytesFilled>0){const t=new TypeError("Insufficient bytes to fill elements in the given buffer");throw Oe(e,t),t}ke(e),ur(t)}}(this)}enqueue(e){if(!be(this))throw Be("enqueue");if(F(e,1,"enqueue"),!ArrayBuffer.isView(e))throw new TypeError("chunk must be an array buffer view");if(0===e.byteLength)throw new TypeError("chunk must have non-zero byteLength");if(0===e.buffer.byteLength)throw new TypeError("chunk's buffer must have non-zero byteLength");if(this._closeRequested)throw new TypeError("stream is closed or draining");const t=this._controlledReadableByteStream._state;if("readable"!==t)throw new TypeError(`The stream (in ${t} state) is not in the readable state and cannot be enqueued to`);!function(e,t){const r=e._controlledReadableByteStream;if(e._closeRequested||"readable"!==r._state)return;const o=t.buffer,n=t.byteOffset,s=t.byteLength,i=o;Z(r)?0===K(r)?ve(e,i,n,s):J(r,new Uint8Array(i,n,s),!1):Le(r)?(ve(e,i,n,s),Pe(e)):ve(e,i,n,s),_e(e)}(this,e)}error(e){if(!be(this))throw Be("error");Oe(this,e)}[W](e){this._pendingPullIntos.length>0&&(this._pendingPullIntos.peek().bytesFilled=0),fe(this);const t=this._cancelAlgorithm(e);return ke(this),t}[L](e){const t=this._controlledReadableByteStream;if(this._queueTotalSize>0){const t=this._queue.shift();this._queueTotalSize-=t.byteLength,Re(this);const r=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return void e._chunkSteps(r)}const r=this._autoAllocateChunkSize;if(void 0!==r){let t;try{t=new ArrayBuffer(r)}catch(t){return void e._errorSteps(t)}const o={buffer:t,byteOffset:0,byteLength:r,bytesFilled:0,elementSize:1,viewConstructor:Uint8Array,readerType:"default"};this._pendingPullIntos.push(o)}G(t,e),_e(this)}}function be(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_controlledReadableByteStream")}function me(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_associatedReadableByteStreamController")}function _e(e){(function(e){const t=e._controlledReadableByteStream;return"readable"===t._state&&(!e._closeRequested&&(!!e._started&&(!!(Z(t)&&K(t)>0)||(!!(Le(t)&&We(t)>0)||Ce(e)>0))))})(e)&&(e._pulling?e._pullAgain=!0:(e._pulling=!0,b(e._pullAlgorithm(),(()=>{e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,_e(e))}),(t=>{Oe(e,t)}))))}function ge(e,t){let r=!1;"closed"===e._state&&(r=!0);const o=we(t);"default"===t.readerType?J(e,o,r):function(e,t,r){const o=e._reader._readIntoRequests.shift();r?o._closeSteps(t):o._chunkSteps(t)}(e,o,r)}function we(e){const t=e.bytesFilled,r=e.elementSize;return new e.viewConstructor(e.buffer,e.byteOffset,t/r)}function ve(e,t,r,o){e._queue.push({buffer:t,byteOffset:r,byteLength:o}),e._queueTotalSize+=o}function Se(e,t){const r=t.elementSize,o=t.bytesFilled-t.bytesFilled%r,n=Math.min(e._queueTotalSize,t.byteLength-t.bytesFilled),s=t.bytesFilled+n,i=s-s%r;let a=n,u=!1;i>o&&(a=i-t.bytesFilled,u=!0);const l=e._queue;for(;a>0;){const r=l.peek(),o=Math.min(a,r.byteLength),n=t.byteOffset+t.bytesFilled;c=t.buffer,d=n,f=r.buffer,p=r.byteOffset,h=o,new Uint8Array(c).set(new Uint8Array(f,p,h),d),r.byteLength===o?l.shift():(r.byteOffset+=o,r.byteLength-=o),e._queueTotalSize-=o,Te(e,o,t),a-=o}var c,d,f,p,h;return u}function Te(e,t,r){qe(e),r.bytesFilled+=t}function Re(e){0===e._queueTotalSize&&e._closeRequested?(ke(e),ur(e._controlledReadableByteStream)):_e(e)}function qe(e){null!==e._byobRequest&&(e._byobRequest._associatedReadableByteStreamController=void 0,e._byobRequest._view=null,e._byobRequest=null)}function Pe(e){for(;e._pendingPullIntos.length>0;){if(0===e._queueTotalSize)return;const t=e._pendingPullIntos.peek();Se(e,t)&&(je(e),ge(e._controlledReadableByteStream,t))}}function Ee(e,t){const r=e._pendingPullIntos.peek();if("closed"===e._controlledReadableByteStream._state){if(0!==t)throw new TypeError("bytesWritten must be 0 when calling respond() on a closed stream");!function(e,t){t.buffer=t.buffer;const r=e._controlledReadableByteStream;if(Le(r))for(;We(r)>0;)ge(r,je(e))}(e,r)}else!function(e,t,r){if(r.bytesFilled+t>r.byteLength)throw new RangeError("bytesWritten out of range");if(Te(e,t,r),r.bytesFilled0){const t=r.byteOffset+r.bytesFilled,n=r.buffer.slice(t-o,t);ve(e,n,0,n.byteLength)}r.buffer=r.buffer,r.bytesFilled-=o,ge(e._controlledReadableByteStream,r),Pe(e)}(e,t,r);_e(e)}function je(e){const t=e._pendingPullIntos.shift();return qe(e),t}function ke(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0}function Oe(e,t){const r=e._controlledReadableByteStream;"readable"===r._state&&(function(e){qe(e),e._pendingPullIntos=new R}(e),fe(e),ke(e),lr(r,t))}function Ce(e){const t=e._controlledReadableByteStream._state;return"errored"===t?null:"closed"===t?0:e._strategyHWM-e._queueTotalSize}function Ae(e){return new TypeError(`ReadableStreamBYOBRequest.prototype.${e} can only be used on a ReadableStreamBYOBRequest`)}function Be(e){return new TypeError(`ReadableByteStreamController.prototype.${e} can only be used on a ReadableByteStreamController`)}function xe(e,t){e._reader._readIntoRequests.push(t)}function We(e){return e._reader._readIntoRequests.length}function Le(e){const t=e._reader;return void 0!==t&&!!Me(t)}Object.defineProperties(ye.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},byobRequest:{enumerable:!0},desiredSize:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(ye.prototype,o.toStringTag,{value:"ReadableByteStreamController",configurable:!0});class ze{constructor(e){if(F(e,1,"ReadableStreamBYOBReader"),Q(e,"First parameter"),ir(e))throw new TypeError("This stream has already been locked for exclusive reading by another reader");if(!be(e._readableStreamController))throw new TypeError("Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source");q(this,e),this._readIntoRequests=new R}get closed(){return Me(this)?this._closedPromise:h($e("closed"))}cancel(e){return Me(this)?void 0===this._ownerReadableStream?h(j("cancel")):P(this,e):h($e("cancel"))}read(e){if(!Me(this))return h($e("read"));if(!ArrayBuffer.isView(e))return h(new TypeError("view must be an array buffer view"));if(0===e.byteLength)return h(new TypeError("view must have non-zero byteLength"));if(0===e.buffer.byteLength)return h(new TypeError("view's buffer must have non-zero byteLength"));if(void 0===this._ownerReadableStream)return h(j("read from"));let t,r;const o=f(((e,o)=>{t=e,r=o}));return function(e,t,r){const o=e._ownerReadableStream;o._disturbed=!0,"errored"===o._state?r._errorSteps(o._storedError):function(e,t,r){const o=e._controlledReadableByteStream;let n=1;t.constructor!==DataView&&(n=t.constructor.BYTES_PER_ELEMENT);const s=t.constructor,i={buffer:t.buffer,byteOffset:t.byteOffset,byteLength:t.byteLength,bytesFilled:0,elementSize:n,viewConstructor:s,readerType:"byob"};if(e._pendingPullIntos.length>0)return e._pendingPullIntos.push(i),void xe(o,r);if("closed"!==o._state){if(e._queueTotalSize>0){if(Se(e,i)){const t=we(i);return Re(e),void r._chunkSteps(t)}if(e._closeRequested){const t=new TypeError("Insufficient bytes to fill elements in the given buffer");return Oe(e,t),void r._errorSteps(t)}}e._pendingPullIntos.push(i),xe(o,r),_e(e)}else{const e=new s(i.buffer,i.byteOffset,0);r._closeSteps(e)}}(o._readableStreamController,t,r)}(this,e,{_chunkSteps:e=>t({value:e,done:!1}),_closeSteps:e=>t({value:e,done:!0}),_errorSteps:e=>r(e)}),o}releaseLock(){if(!Me(this))throw $e("releaseLock");if(void 0!==this._ownerReadableStream){if(this._readIntoRequests.length>0)throw new TypeError("Tried to release a reader lock when that reader has pending read() calls un-settled");E(this)}}}function Me(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_readIntoRequests")}function $e(e){return new TypeError(`ReadableStreamBYOBReader.prototype.${e} can only be used on a ReadableStreamBYOBReader`)}function Ie(e,t){const{highWaterMark:r}=e;if(void 0===r)return t;if(ue(r)||r<0)throw new RangeError("Invalid highWaterMark");return r}function De(e){const{size:t}=e;return t||(()=>1)}function Fe(e,t){$(e,t);const r=null==e?void 0:e.highWaterMark,o=null==e?void 0:e.size;return{highWaterMark:void 0===r?void 0:H(r),size:void 0===o?void 0:Ue(o,`${t} has member 'size' that`)}}function Ue(e,t){return I(e,t),t=>H(e(t))}function He(e,t,r){return I(e,r),r=>T(e,t,[r])}function Ne(e,t,r){return I(e,r),()=>T(e,t,[])}function Ve(e,t,r){return I(e,r),r=>S(e,t,[r])}function Qe(e,t,r){return I(e,r),(r,o)=>T(e,t,[r,o])}function Ye(e,t){if(!Ze(e))throw new TypeError(`${t} is not a WritableStream.`)}Object.defineProperties(ze.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(ze.prototype,o.toStringTag,{value:"ReadableStreamBYOBReader",configurable:!0});class Ge{constructor(e={},t={}){void 0===e?e=null:D(e,"First parameter");const r=Fe(t,"Second parameter"),o=function(e,t){$(e,t);const r=null==e?void 0:e.abort,o=null==e?void 0:e.close,n=null==e?void 0:e.start,s=null==e?void 0:e.type,i=null==e?void 0:e.write;return{abort:void 0===r?void 0:He(r,e,`${t} has member 'abort' that`),close:void 0===o?void 0:Ne(o,e,`${t} has member 'close' that`),start:void 0===n?void 0:Ve(n,e,`${t} has member 'start' that`),write:void 0===i?void 0:Qe(i,e,`${t} has member 'write' that`),type:s}}(e,"First parameter");if(Ke(this),void 0!==o.type)throw new RangeError("Invalid type is specified");const n=De(r);!function(e,t,r,o){const n=Object.create(yt.prototype);let s=()=>{},i=()=>p(void 0),a=()=>p(void 0),u=()=>p(void 0);void 0!==t.start&&(s=()=>t.start(n)),void 0!==t.write&&(i=e=>t.write(e,n)),void 0!==t.close&&(a=()=>t.close()),void 0!==t.abort&&(u=e=>t.abort(e)),bt(e,n,s,i,a,u,r,o)}(this,o,Ie(r,1),n)}get locked(){if(!Ze(this))throw Tt("locked");return Xe(this)}abort(e){return Ze(this)?Xe(this)?h(new TypeError("Cannot abort a stream that already has a writer")):et(this,e):h(Tt("abort"))}close(){return Ze(this)?Xe(this)?h(new TypeError("Cannot close a stream that already has a writer")):st(this)?h(new TypeError("Cannot close an already-closing stream")):tt(this):h(Tt("close"))}getWriter(){if(!Ze(this))throw Tt("getWriter");return Je(this)}}function Je(e){return new ut(e)}function Ke(e){e._state="writable",e._storedError=void 0,e._writer=void 0,e._writableStreamController=void 0,e._writeRequests=new R,e._inFlightWriteRequest=void 0,e._closeRequest=void 0,e._inFlightCloseRequest=void 0,e._pendingAbortRequest=void 0,e._backpressure=!1}function Ze(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_writableStreamController")}function Xe(e){return void 0!==e._writer}function et(e,t){const r=e._state;if("closed"===r||"errored"===r)return p(void 0);if(void 0!==e._pendingAbortRequest)return e._pendingAbortRequest._promise;let o=!1;"erroring"===r&&(o=!0,t=void 0);const n=f(((r,n)=>{e._pendingAbortRequest={_promise:void 0,_resolve:r,_reject:n,_reason:t,_wasAlreadyErroring:o}}));return e._pendingAbortRequest._promise=n,o||ot(e,t),n}function tt(e){const t=e._state;if("closed"===t||"errored"===t)return h(new TypeError(`The stream (in ${t} state) is not in the writable state and cannot be closed`));const r=f(((t,r)=>{const o={_resolve:t,_reject:r};e._closeRequest=o})),o=e._writer;var n;return void 0!==o&&e._backpressure&&"writable"===t&&xt(o),de(n=e._writableStreamController,ht,0),gt(n),r}function rt(e,t){"writable"!==e._state?nt(e):ot(e,t)}function ot(e,t){const r=e._writableStreamController;e._state="erroring",e._storedError=t;const o=e._writer;void 0!==o&&dt(o,t),!function(e){return void 0!==e._inFlightWriteRequest||void 0!==e._inFlightCloseRequest}(e)&&r._started&&nt(e)}function nt(e){e._state="errored",e._writableStreamController[x]();const t=e._storedError;if(e._writeRequests.forEach((e=>{e._reject(t)})),e._writeRequests=new R,void 0===e._pendingAbortRequest)return void it(e);const r=e._pendingAbortRequest;if(e._pendingAbortRequest=void 0,r._wasAlreadyErroring)return r._reject(t),void it(e);b(e._writableStreamController[B](r._reason),(()=>{r._resolve(),it(e)}),(t=>{r._reject(t),it(e)}))}function st(e){return void 0!==e._closeRequest||void 0!==e._inFlightCloseRequest}function it(e){void 0!==e._closeRequest&&(e._closeRequest._reject(e._storedError),e._closeRequest=void 0);const t=e._writer;void 0!==t&&jt(t,e._storedError)}function at(e,t){const r=e._writer;void 0!==r&&t!==e._backpressure&&(t?function(e){Ot(e)}(r):xt(r)),e._backpressure=t}Object.defineProperties(Ge.prototype,{abort:{enumerable:!0},close:{enumerable:!0},getWriter:{enumerable:!0},locked:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(Ge.prototype,o.toStringTag,{value:"WritableStream",configurable:!0});class ut{constructor(e){if(F(e,1,"WritableStreamDefaultWriter"),Ye(e,"First parameter"),Xe(e))throw new TypeError("This stream has already been locked for exclusive writing by another writer");this._ownerWritableStream=e,e._writer=this;const t=e._state;if("writable"===t)!st(e)&&e._backpressure?Ot(this):At(this),Pt(this);else if("erroring"===t)Ct(this,e._storedError),Pt(this);else if("closed"===t)At(this),Pt(this),kt(this);else{const t=e._storedError;Ct(this,t),Et(this,t)}}get closed(){return lt(this)?this._closedPromise:h(Rt("closed"))}get desiredSize(){if(!lt(this))throw Rt("desiredSize");if(void 0===this._ownerWritableStream)throw qt("desiredSize");return function(e){const t=e._ownerWritableStream,r=t._state;return"errored"===r||"erroring"===r?null:"closed"===r?0:_t(t._writableStreamController)}(this)}get ready(){return lt(this)?this._readyPromise:h(Rt("ready"))}abort(e){return lt(this)?void 0===this._ownerWritableStream?h(qt("abort")):function(e,t){return et(e._ownerWritableStream,t)}(this,e):h(Rt("abort"))}close(){if(!lt(this))return h(Rt("close"));const e=this._ownerWritableStream;return void 0===e?h(qt("close")):st(e)?h(new TypeError("Cannot close an already-closing stream")):ct(this)}releaseLock(){if(!lt(this))throw Rt("releaseLock");void 0!==this._ownerWritableStream&&ft(this)}write(e){return lt(this)?void 0===this._ownerWritableStream?h(qt("write to")):pt(this,e):h(Rt("write"))}}function lt(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_ownerWritableStream")}function ct(e){return tt(e._ownerWritableStream)}function dt(e,t){"pending"===e._readyPromiseState?Bt(e,t):function(e,t){Ct(e,t)}(e,t)}function ft(e){const t=e._ownerWritableStream,r=new TypeError("Writer was released and can no longer be used to monitor the stream's closedness");dt(e,r),function(e,t){"pending"===e._closedPromiseState?jt(e,t):function(e,t){Et(e,t)}(e,t)}(e,r),t._writer=void 0,e._ownerWritableStream=void 0}function pt(e,t){const r=e._ownerWritableStream,o=r._writableStreamController,n=function(e,t){try{return e._strategySizeAlgorithm(t)}catch(t){return wt(e,t),1}}(o,t);if(r!==e._ownerWritableStream)return h(qt("write to"));const s=r._state;if("errored"===s)return h(r._storedError);if(st(r)||"closed"===s)return h(new TypeError("The stream is closing or closed and cannot be written to"));if("erroring"===s)return h(r._storedError);const i=function(e){return f(((t,r)=>{const o={_resolve:t,_reject:r};e._writeRequests.push(o)}))}(r);return function(e,t,r){try{de(e,t,r)}catch(t){return void wt(e,t)}const o=e._controlledWritableStream;st(o)||"writable"!==o._state||at(o,vt(e)),gt(e)}(o,t,n),i}Object.defineProperties(ut.prototype,{abort:{enumerable:!0},close:{enumerable:!0},releaseLock:{enumerable:!0},write:{enumerable:!0},closed:{enumerable:!0},desiredSize:{enumerable:!0},ready:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(ut.prototype,o.toStringTag,{value:"WritableStreamDefaultWriter",configurable:!0});const ht={};class yt{constructor(){throw new TypeError("Illegal constructor")}error(e){if(!i(t=this)||!Object.prototype.hasOwnProperty.call(t,"_controlledWritableStream"))throw new TypeError("WritableStreamDefaultController.prototype.error can only be used on a WritableStreamDefaultController");var t;"writable"===this._controlledWritableStream._state&&St(this,e)}[B](e){const t=this._abortAlgorithm(e);return mt(this),t}[x](){fe(this)}}function bt(e,t,r,o,n,s,i,a){t._controlledWritableStream=e,e._writableStreamController=t,t._queue=void 0,t._queueTotalSize=void 0,fe(t),t._started=!1,t._strategySizeAlgorithm=a,t._strategyHWM=i,t._writeAlgorithm=o,t._closeAlgorithm=n,t._abortAlgorithm=s;const u=vt(t);at(e,u),b(p(r()),(()=>{t._started=!0,gt(t)}),(r=>{t._started=!0,rt(e,r)}))}function mt(e){e._writeAlgorithm=void 0,e._closeAlgorithm=void 0,e._abortAlgorithm=void 0,e._strategySizeAlgorithm=void 0}function _t(e){return e._strategyHWM-e._queueTotalSize}function gt(e){const t=e._controlledWritableStream;if(!e._started)return;if(void 0!==t._inFlightWriteRequest)return;if("erroring"===t._state)return void nt(t);if(0===e._queue.length)return;const r=e._queue.peek().value;r===ht?function(e){const t=e._controlledWritableStream;(function(e){e._inFlightCloseRequest=e._closeRequest,e._closeRequest=void 0})(t),ce(e);const r=e._closeAlgorithm();mt(e),b(r,(()=>{!function(e){e._inFlightCloseRequest._resolve(void 0),e._inFlightCloseRequest=void 0,"erroring"===e._state&&(e._storedError=void 0,void 0!==e._pendingAbortRequest&&(e._pendingAbortRequest._resolve(),e._pendingAbortRequest=void 0)),e._state="closed";const t=e._writer;void 0!==t&&kt(t)}(t)}),(e=>{!function(e,t){e._inFlightCloseRequest._reject(t),e._inFlightCloseRequest=void 0,void 0!==e._pendingAbortRequest&&(e._pendingAbortRequest._reject(t),e._pendingAbortRequest=void 0),rt(e,t)}(t,e)}))}(e):function(e,t){const r=e._controlledWritableStream;!function(e){e._inFlightWriteRequest=e._writeRequests.shift()}(r),b(e._writeAlgorithm(t),(()=>{!function(e){e._inFlightWriteRequest._resolve(void 0),e._inFlightWriteRequest=void 0}(r);const t=r._state;if(ce(e),!st(r)&&"writable"===t){const t=vt(e);at(r,t)}gt(e)}),(t=>{"writable"===r._state&&mt(e),function(e,t){e._inFlightWriteRequest._reject(t),e._inFlightWriteRequest=void 0,rt(e,t)}(r,t)}))}(e,r)}function wt(e,t){"writable"===e._controlledWritableStream._state&&St(e,t)}function vt(e){return _t(e)<=0}function St(e,t){const r=e._controlledWritableStream;mt(e),ot(r,t)}function Tt(e){return new TypeError(`WritableStream.prototype.${e} can only be used on a WritableStream`)}function Rt(e){return new TypeError(`WritableStreamDefaultWriter.prototype.${e} can only be used on a WritableStreamDefaultWriter`)}function qt(e){return new TypeError("Cannot "+e+" a stream using a released writer")}function Pt(e){e._closedPromise=f(((t,r)=>{e._closedPromise_resolve=t,e._closedPromise_reject=r,e._closedPromiseState="pending"}))}function Et(e,t){Pt(e),jt(e,t)}function jt(e,t){void 0!==e._closedPromise_reject&&(w(e._closedPromise),e._closedPromise_reject(t),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="rejected")}function kt(e){void 0!==e._closedPromise_resolve&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="resolved")}function Ot(e){e._readyPromise=f(((t,r)=>{e._readyPromise_resolve=t,e._readyPromise_reject=r})),e._readyPromiseState="pending"}function Ct(e,t){Ot(e),Bt(e,t)}function At(e){Ot(e),xt(e)}function Bt(e,t){void 0!==e._readyPromise_reject&&(w(e._readyPromise),e._readyPromise_reject(t),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="rejected")}function xt(e){void 0!==e._readyPromise_resolve&&(e._readyPromise_resolve(void 0),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="fulfilled")}Object.defineProperties(yt.prototype,{error:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(yt.prototype,o.toStringTag,{value:"WritableStreamDefaultController",configurable:!0});const Wt="undefined"!=typeof DOMException?DOMException:void 0,Lt=function(e){if("function"!=typeof e&&"object"!=typeof e)return!1;try{return new e,!0}catch(e){return!1}}(Wt)?Wt:function(){const e=function(e,t){this.message=e||"",this.name=t||"Error",Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)};return e.prototype=Object.create(Error.prototype),Object.defineProperty(e.prototype,"constructor",{value:e,writable:!0,configurable:!0}),e}();function zt(e,t,r,o,s,i){const a=Y(e),u=Je(t);e._disturbed=!0;let l=!1,c=p(void 0);return f(((d,g)=>{let v;if(void 0!==i){if(v=()=>{const r=new Lt("Aborted","AbortError"),n=[];o||n.push((()=>"writable"===t._state?et(t,r):p(void 0))),s||n.push((()=>"readable"===e._state?ar(e,r):p(void 0))),j((()=>Promise.all(n.map((e=>e())))),!0,r)},i.aborted)return void v();i.addEventListener("abort",v)}var S,T,R;if(P(e,a._closedPromise,(e=>{o?k(!0,e):j((()=>et(t,e)),!0,e)})),P(t,u._closedPromise,(t=>{s?k(!0,t):j((()=>ar(e,t)),!0,t)})),S=e,T=a._closedPromise,R=()=>{r?k():j((()=>function(e){const t=e._ownerWritableStream,r=t._state;return st(t)||"closed"===r?p(void 0):"errored"===r?h(t._storedError):ct(e)}(u)))},"closed"===S._state?R():m(T,R),st(t)||"closed"===t._state){const t=new TypeError("the destination writable stream closed before all data could be piped to it");s?k(!0,t):j((()=>ar(e,t)),!0,t)}function q(){const e=c;return y(c,(()=>e!==c?q():void 0))}function P(e,t,r){"errored"===e._state?r(e._storedError):_(t,r)}function j(e,r,o){function n(){b(e(),(()=>O(r,o)),(e=>O(!0,e)))}l||(l=!0,"writable"!==t._state||st(t)?n():m(q(),n))}function k(e,r){l||(l=!0,"writable"!==t._state||st(t)?O(e,r):m(q(),(()=>O(e,r))))}function O(e,t){ft(u),E(a),void 0!==i&&i.removeEventListener("abort",v),e?g(t):d(void 0)}w(f(((e,t)=>{!function r(o){o?e():y(l?p(!0):y(u._readyPromise,(()=>f(((e,t)=>{te(a,{_chunkSteps:t=>{c=y(pt(u,t),void 0,n),e(!1)},_closeSteps:()=>e(!0),_errorSteps:t})})))),r,t)}(!1)})))}))}class Mt{constructor(){throw new TypeError("Illegal constructor")}get desiredSize(){if(!$t(this))throw Gt("desiredSize");return Vt(this)}close(){if(!$t(this))throw Gt("close");if(!Qt(this))throw new TypeError("The stream is not in a state that permits close");Ut(this)}enqueue(e){if(!$t(this))throw Gt("enqueue");if(!Qt(this))throw new TypeError("The stream is not in a state that permits enqueue");return Ht(this,e)}error(e){if(!$t(this))throw Gt("error");Nt(this,e)}[W](e){fe(this);const t=this._cancelAlgorithm(e);return Ft(this),t}[L](e){const t=this._controlledReadableStream;if(this._queue.length>0){const r=ce(this);this._closeRequested&&0===this._queue.length?(Ft(this),ur(t)):It(this),e._chunkSteps(r)}else G(t,e),It(this)}}function $t(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_controlledReadableStream")}function It(e){Dt(e)&&(e._pulling?e._pullAgain=!0:(e._pulling=!0,b(e._pullAlgorithm(),(()=>{e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,It(e))}),(t=>{Nt(e,t)}))))}function Dt(e){const t=e._controlledReadableStream;return!!Qt(e)&&(!!e._started&&(!!(ir(t)&&K(t)>0)||Vt(e)>0))}function Ft(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0,e._strategySizeAlgorithm=void 0}function Ut(e){if(!Qt(e))return;const t=e._controlledReadableStream;e._closeRequested=!0,0===e._queue.length&&(Ft(e),ur(t))}function Ht(e,t){if(!Qt(e))return;const r=e._controlledReadableStream;if(ir(r)&&K(r)>0)J(r,t,!1);else{let r;try{r=e._strategySizeAlgorithm(t)}catch(t){throw Nt(e,t),t}try{de(e,t,r)}catch(t){throw Nt(e,t),t}}It(e)}function Nt(e,t){const r=e._controlledReadableStream;"readable"===r._state&&(fe(e),Ft(e),lr(r,t))}function Vt(e){const t=e._controlledReadableStream._state;return"errored"===t?null:"closed"===t?0:e._strategyHWM-e._queueTotalSize}function Qt(e){const t=e._controlledReadableStream._state;return!e._closeRequested&&"readable"===t}function Yt(e,t,r,o,n,s,i){t._controlledReadableStream=e,t._queue=void 0,t._queueTotalSize=void 0,fe(t),t._started=!1,t._closeRequested=!1,t._pullAgain=!1,t._pulling=!1,t._strategySizeAlgorithm=i,t._strategyHWM=s,t._pullAlgorithm=o,t._cancelAlgorithm=n,e._readableStreamController=t,b(p(r()),(()=>{t._started=!0,It(t)}),(e=>{Nt(t,e)}))}function Gt(e){return new TypeError(`ReadableStreamDefaultController.prototype.${e} can only be used on a ReadableStreamDefaultController`)}function Jt(e,t,r){return I(e,r),r=>T(e,t,[r])}function Kt(e,t,r){return I(e,r),r=>T(e,t,[r])}function Zt(e,t,r){return I(e,r),r=>S(e,t,[r])}function Xt(e,t){if("bytes"!=(e=`${e}`))throw new TypeError(`${t} '${e}' is not a valid enumeration value for ReadableStreamType`);return e}function er(e,t){if("byob"!=(e=`${e}`))throw new TypeError(`${t} '${e}' is not a valid enumeration value for ReadableStreamReaderMode`);return e}function tr(e,t){$(e,t);const r=null==e?void 0:e.preventAbort,o=null==e?void 0:e.preventCancel,n=null==e?void 0:e.preventClose,s=null==e?void 0:e.signal;return void 0!==s&&function(e,t){if(!function(e){if("object"!=typeof e||null===e)return!1;try{return"boolean"==typeof e.aborted}catch(e){return!1}}(e))throw new TypeError(`${t} is not an AbortSignal.`)}(s,`${t} has member 'signal' that`),{preventAbort:Boolean(r),preventCancel:Boolean(o),preventClose:Boolean(n),signal:s}}Object.defineProperties(Mt.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},desiredSize:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(Mt.prototype,o.toStringTag,{value:"ReadableStreamDefaultController",configurable:!0});class rr{constructor(e={},t={}){void 0===e?e=null:D(e,"First parameter");const r=Fe(t,"Second parameter"),o=function(e,t){$(e,t);const r=e,o=null==r?void 0:r.autoAllocateChunkSize,n=null==r?void 0:r.cancel,s=null==r?void 0:r.pull,i=null==r?void 0:r.start,a=null==r?void 0:r.type;return{autoAllocateChunkSize:void 0===o?void 0:V(o,`${t} has member 'autoAllocateChunkSize' that`),cancel:void 0===n?void 0:Jt(n,r,`${t} has member 'cancel' that`),pull:void 0===s?void 0:Kt(s,r,`${t} has member 'pull' that`),start:void 0===i?void 0:Zt(i,r,`${t} has member 'start' that`),type:void 0===a?void 0:Xt(a,`${t} has member 'type' that`)}}(e,"First parameter");if(nr(this),"bytes"===o.type){if(void 0!==r.size)throw new RangeError("The strategy for a byte stream cannot have a size function");!function(e,t,r){const o=Object.create(ye.prototype);let n=()=>{},s=()=>p(void 0),i=()=>p(void 0);void 0!==t.start&&(n=()=>t.start(o)),void 0!==t.pull&&(s=()=>t.pull(o)),void 0!==t.cancel&&(i=e=>t.cancel(e));const a=t.autoAllocateChunkSize;!function(e,t,r,o,n,s,i){t._controlledReadableByteStream=e,t._pullAgain=!1,t._pulling=!1,t._byobRequest=null,t._queue=t._queueTotalSize=void 0,fe(t),t._closeRequested=!1,t._started=!1,t._strategyHWM=s,t._pullAlgorithm=o,t._cancelAlgorithm=n,t._autoAllocateChunkSize=i,t._pendingPullIntos=new R,e._readableStreamController=t,b(p(r()),(()=>{t._started=!0,_e(t)}),(e=>{Oe(t,e)}))}(e,o,n,s,i,r,a)}(this,o,Ie(r,0))}else{const e=De(r);!function(e,t,r,o){const n=Object.create(Mt.prototype);let s=()=>{},i=()=>p(void 0),a=()=>p(void 0);void 0!==t.start&&(s=()=>t.start(n)),void 0!==t.pull&&(i=()=>t.pull(n)),void 0!==t.cancel&&(a=e=>t.cancel(e)),Yt(e,n,s,i,a,r,o)}(this,o,Ie(r,1),e)}}get locked(){if(!sr(this))throw cr("locked");return ir(this)}cancel(e){return sr(this)?ir(this)?h(new TypeError("Cannot cancel a stream that already has a reader")):ar(this,e):h(cr("cancel"))}getReader(e){if(!sr(this))throw cr("getReader");return void 0===function(e,t){$(e,t);const r=null==e?void 0:e.mode;return{mode:void 0===r?void 0:er(r,`${t} has member 'mode' that`)}}(e,"First parameter").mode?Y(this):new ze(this)}pipeThrough(e,t={}){if(!sr(this))throw cr("pipeThrough");F(e,1,"pipeThrough");const r=function(e,t){$(e,t);const r=null==e?void 0:e.readable;U(r,"readable","ReadableWritablePair"),Q(r,`${t} has member 'readable' that`);const o=null==e?void 0:e.writable;return U(o,"writable","ReadableWritablePair"),Ye(o,`${t} has member 'writable' that`),{readable:r,writable:o}}(e,"First parameter"),o=tr(t,"Second parameter");if(ir(this))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream");if(Xe(r.writable))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream");return w(zt(this,r.writable,o.preventClose,o.preventAbort,o.preventCancel,o.signal)),r.readable}pipeTo(e,t={}){if(!sr(this))return h(cr("pipeTo"));if(void 0===e)return h("Parameter 1 is required in 'pipeTo'.");if(!Ze(e))return h(new TypeError("ReadableStream.prototype.pipeTo's first argument must be a WritableStream"));let r;try{r=tr(t,"Second parameter")}catch(e){return h(e)}return ir(this)?h(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream")):Xe(e)?h(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream")):zt(this,e,r.preventClose,r.preventAbort,r.preventCancel,r.signal)}tee(){if(!sr(this))throw cr("tee");const e=function(e,t){const r=Y(e);let o,n,s,i,a,u=!1,l=!1,c=!1;const d=f((e=>{a=e}));function h(){return u||(u=!0,te(r,{_chunkSteps:e=>{v((()=>{u=!1;const t=e,r=e;l||Ht(s._readableStreamController,t),c||Ht(i._readableStreamController,r),a(void 0)}))},_closeSteps:()=>{u=!1,l||Ut(s._readableStreamController),c||Ut(i._readableStreamController)},_errorSteps:()=>{u=!1}})),p(void 0)}function y(){}return s=or(y,h,(function(t){if(l=!0,o=t,c){const t=pe([o,n]),r=ar(e,t);a(r)}return d})),i=or(y,h,(function(t){if(c=!0,n=t,l){const t=pe([o,n]),r=ar(e,t);a(r)}return d})),_(r._closedPromise,(e=>{Nt(s._readableStreamController,e),Nt(i._readableStreamController,e),a(void 0)})),[s,i]}(this);return pe(e)}values(e){if(!sr(this))throw cr("values");return function(e,t){const r=Y(e),o=new ne(r,t),n=Object.create(se);return n._asyncIteratorImpl=o,n}(this,function(e,t){$(e,"First parameter");const r=null==e?void 0:e.preventCancel;return{preventCancel:Boolean(r)}}(e).preventCancel)}}function or(e,t,r,o=1,n=(()=>1)){const s=Object.create(rr.prototype);return nr(s),Yt(s,Object.create(Mt.prototype),e,t,r,o,n),s}function nr(e){e._state="readable",e._reader=void 0,e._storedError=void 0,e._disturbed=!1}function sr(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_readableStreamController")}function ir(e){return void 0!==e._reader}function ar(e,t){return e._disturbed=!0,"closed"===e._state?p(void 0):"errored"===e._state?h(e._storedError):(ur(e),g(e._readableStreamController[W](t),n))}function ur(e){e._state="closed";const t=e._reader;void 0!==t&&(ee(t)&&(t._readRequests.forEach((e=>{e._closeSteps()})),t._readRequests=new R),A(t))}function lr(e,t){e._state="errored",e._storedError=t;const r=e._reader;void 0!==r&&(ee(r)?(r._readRequests.forEach((e=>{e._errorSteps(t)})),r._readRequests=new R):(r._readIntoRequests.forEach((e=>{e._errorSteps(t)})),r._readIntoRequests=new R),C(r,t))}function cr(e){return new TypeError(`ReadableStream.prototype.${e} can only be used on a ReadableStream`)}function dr(e,t){$(e,t);const r=null==e?void 0:e.highWaterMark;return U(r,"highWaterMark","QueuingStrategyInit"),{highWaterMark:H(r)}}Object.defineProperties(rr.prototype,{cancel:{enumerable:!0},getReader:{enumerable:!0},pipeThrough:{enumerable:!0},pipeTo:{enumerable:!0},tee:{enumerable:!0},values:{enumerable:!0},locked:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(rr.prototype,o.toStringTag,{value:"ReadableStream",configurable:!0}),"symbol"==typeof o.asyncIterator&&Object.defineProperty(rr.prototype,o.asyncIterator,{value:rr.prototype.values,writable:!0,configurable:!0});const fr=function(e){return e.byteLength};class pr{constructor(e){F(e,1,"ByteLengthQueuingStrategy"),e=dr(e,"First parameter"),this._byteLengthQueuingStrategyHighWaterMark=e.highWaterMark}get highWaterMark(){if(!yr(this))throw hr("highWaterMark");return this._byteLengthQueuingStrategyHighWaterMark}get size(){if(!yr(this))throw hr("size");return fr}}function hr(e){return new TypeError(`ByteLengthQueuingStrategy.prototype.${e} can only be used on a ByteLengthQueuingStrategy`)}function yr(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_byteLengthQueuingStrategyHighWaterMark")}Object.defineProperties(pr.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(pr.prototype,o.toStringTag,{value:"ByteLengthQueuingStrategy",configurable:!0});const br=function(){return 1};class mr{constructor(e){F(e,1,"CountQueuingStrategy"),e=dr(e,"First parameter"),this._countQueuingStrategyHighWaterMark=e.highWaterMark}get highWaterMark(){if(!gr(this))throw _r("highWaterMark");return this._countQueuingStrategyHighWaterMark}get size(){if(!gr(this))throw _r("size");return br}}function _r(e){return new TypeError(`CountQueuingStrategy.prototype.${e} can only be used on a CountQueuingStrategy`)}function gr(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_countQueuingStrategyHighWaterMark")}function wr(e,t,r){return I(e,r),r=>T(e,t,[r])}function vr(e,t,r){return I(e,r),r=>S(e,t,[r])}function Sr(e,t,r){return I(e,r),(r,o)=>T(e,t,[r,o])}Object.defineProperties(mr.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(mr.prototype,o.toStringTag,{value:"CountQueuingStrategy",configurable:!0});class Tr{constructor(e={},t={},r={}){void 0===e&&(e=null);const o=Fe(t,"Second parameter"),n=Fe(r,"Third parameter"),s=function(e,t){$(e,t);const r=null==e?void 0:e.flush,o=null==e?void 0:e.readableType,n=null==e?void 0:e.start,s=null==e?void 0:e.transform,i=null==e?void 0:e.writableType;return{flush:void 0===r?void 0:wr(r,e,`${t} has member 'flush' that`),readableType:o,start:void 0===n?void 0:vr(n,e,`${t} has member 'start' that`),transform:void 0===s?void 0:Sr(s,e,`${t} has member 'transform' that`),writableType:i}}(e,"First parameter");if(void 0!==s.readableType)throw new RangeError("Invalid readableType specified");if(void 0!==s.writableType)throw new RangeError("Invalid writableType specified");const i=Ie(n,0),a=De(n),u=Ie(o,1),l=De(o);let c;!function(e,t,r,o,n,s){function i(){return t}e._writable=function(e,t,r,o,n=1,s=(()=>1)){const i=Object.create(Ge.prototype);return Ke(i),bt(i,Object.create(yt.prototype),e,t,r,o,n,s),i}(i,(function(t){return function(e,t){const r=e._transformStreamController;return e._backpressure?g(e._backpressureChangePromise,(()=>{const o=e._writable;if("erroring"===o._state)throw o._storedError;return Ar(r,t)})):Ar(r,t)}(e,t)}),(function(){return function(e){const t=e._readable,r=e._transformStreamController,o=r._flushAlgorithm();return Or(r),g(o,(()=>{if("errored"===t._state)throw t._storedError;Ut(t._readableStreamController)}),(r=>{throw qr(e,r),t._storedError}))}(e)}),(function(t){return function(e,t){return qr(e,t),p(void 0)}(e,t)}),r,o),e._readable=or(i,(function(){return function(e){return Er(e,!1),e._backpressureChangePromise}(e)}),(function(t){return Pr(e,t),p(void 0)}),n,s),e._backpressure=void 0,e._backpressureChangePromise=void 0,e._backpressureChangePromise_resolve=void 0,Er(e,!0),e._transformStreamController=void 0}(this,f((e=>{c=e})),u,l,i,a),function(e,t){const r=Object.create(jr.prototype);let o=e=>{try{return Cr(r,e),p(void 0)}catch(e){return h(e)}},n=()=>p(void 0);void 0!==t.transform&&(o=e=>t.transform(e,r)),void 0!==t.flush&&(n=()=>t.flush(r)),function(e,t,r,o){t._controlledTransformStream=e,e._transformStreamController=t,t._transformAlgorithm=r,t._flushAlgorithm=o}(e,r,o,n)}(this,s),void 0!==s.start?c(s.start(this._transformStreamController)):c(void 0)}get readable(){if(!Rr(this))throw xr("readable");return this._readable}get writable(){if(!Rr(this))throw xr("writable");return this._writable}}function Rr(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_transformStreamController")}function qr(e,t){Nt(e._readable._readableStreamController,t),Pr(e,t)}function Pr(e,t){Or(e._transformStreamController),wt(e._writable._writableStreamController,t),e._backpressure&&Er(e,!1)}function Er(e,t){void 0!==e._backpressureChangePromise&&e._backpressureChangePromise_resolve(),e._backpressureChangePromise=f((t=>{e._backpressureChangePromise_resolve=t})),e._backpressure=t}Object.defineProperties(Tr.prototype,{readable:{enumerable:!0},writable:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(Tr.prototype,o.toStringTag,{value:"TransformStream",configurable:!0});class jr{constructor(){throw new TypeError("Illegal constructor")}get desiredSize(){if(!kr(this))throw Br("desiredSize");return Vt(this._controlledTransformStream._readable._readableStreamController)}enqueue(e){if(!kr(this))throw Br("enqueue");Cr(this,e)}error(e){if(!kr(this))throw Br("error");var t;t=e,qr(this._controlledTransformStream,t)}terminate(){if(!kr(this))throw Br("terminate");!function(e){const t=e._controlledTransformStream;Ut(t._readable._readableStreamController);Pr(t,new TypeError("TransformStream terminated"))}(this)}}function kr(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_controlledTransformStream")}function Or(e){e._transformAlgorithm=void 0,e._flushAlgorithm=void 0}function Cr(e,t){const r=e._controlledTransformStream,o=r._readable._readableStreamController;if(!Qt(o))throw new TypeError("Readable side is not in a state that permits enqueue");try{Ht(o,t)}catch(e){throw Pr(r,e),r._readable._storedError}(function(e){return!Dt(e)})(o)!==r._backpressure&&Er(r,!0)}function Ar(e,t){return g(e._transformAlgorithm(t),void 0,(t=>{throw qr(e._controlledTransformStream,t),t}))}function Br(e){return new TypeError(`TransformStreamDefaultController.prototype.${e} can only be used on a TransformStreamDefaultController`)}function xr(e){return new TypeError(`TransformStream.prototype.${e} can only be used on a TransformStream`)}Object.defineProperties(jr.prototype,{enqueue:{enumerable:!0},error:{enumerable:!0},terminate:{enumerable:!0},desiredSize:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(jr.prototype,o.toStringTag,{value:"TransformStreamDefaultController",configurable:!0})},417:e=>{"use strict";e.exports=__webpack_require__(417)},605:e=>{"use strict";e.exports=__webpack_require__(605)},211:e=>{"use strict";e.exports=__webpack_require__(211)},413:e=>{"use strict";e.exports=__webpack_require__(413)},835:e=>{"use strict";e.exports=__webpack_require__(835)},669:e=>{"use strict";e.exports=__webpack_require__(669)},761:e=>{"use strict";e.exports=__webpack_require__(761)}},t={};function r(o){if(t[o])return t[o].exports;var n=t[o]={exports:{}};return e[o].call(n.exports,n,n.exports,r),n.exports}return r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r(990)})().default})); +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9tYWlsZ3VuL3dlYnBhY2svdW5pdmVyc2FsTW9kdWxlRGVmaW5pdGlvbiIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2Fib3J0LWNvbnRyb2xsZXIvZGlzdC9hYm9ydC1jb250cm9sbGVyLmpzIiwid2VicGFjazovL21haWxndW4vLi9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL2NsaWVudC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL2RvbWFpbnMudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9lcnJvci50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL2V2ZW50cy50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL2lwLXBvb2xzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvaXBzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvbWVzc2FnZXMudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9wYXJzZS50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL3JlcXVlc3QudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9yb3V0ZXMudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9zdGF0cy50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL3N1cHByZXNzaW9ucy50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL3ZhbGlkYXRlLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvd2ViaG9va3MudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9idG9hL2luZGV4LmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvZGF0YS11cmktdG8tYnVmZmVyL2Rpc3Qvc3JjL2luZGV4LmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvZXZlbnQtdGFyZ2V0LXNoaW0vZGlzdC9ldmVudC10YXJnZXQtc2hpbS5qcyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2ZldGNoLWJsb2IvaW5kZXguanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9reS11bml2ZXJzYWwvaW5kZXguanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9reS91bWQuanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9ub2RlLWZldGNoL2Rpc3QvaW5kZXguY2pzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvdXJsLWpvaW4vbGliL3VybC1qb2luLmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvd2ViLXN0cmVhbXMtcG9seWZpbGwvZGlzdC9wb255ZmlsbC5lczIwMTgubWpzIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgXCJjcnlwdG9cIiIsIndlYnBhY2s6Ly9tYWlsZ3VuL2V4dGVybmFsIFwiaHR0cFwiIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgXCJodHRwc1wiIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgXCJzdHJlYW1cIiIsIndlYnBhY2s6Ly9tYWlsZ3VuL2V4dGVybmFsIFwidXJsXCIiLCJ3ZWJwYWNrOi8vbWFpbGd1bi9leHRlcm5hbCBcInV0aWxcIiIsIndlYnBhY2s6Ly9tYWlsZ3VuL2V4dGVybmFsIFwiemxpYlwiIiwid2VicGFjazovL21haWxndW4vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vbWFpbGd1bi93ZWJwYWNrL3N0YXJ0dXAiLCJ3ZWJwYWNrOi8vbWFpbGd1bi93ZWJwYWNrL3J1bnRpbWUvZGVmaW5lIHByb3BlcnR5IGdldHRlcnMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi93ZWJwYWNrL3J1bnRpbWUvaGFzT3duUHJvcGVydHkgc2hvcnRoYW5kIiwid2VicGFjazovL21haWxndW4vd2VicGFjay9ydW50aW1lL21ha2UgbmFtZXNwYWNlIG9iamVjdCJdLCJuYW1lcyI6WyJyb290IiwiZmFjdG9yeSIsImV4cG9ydHMiLCJtb2R1bGUiLCJkZWZpbmUiLCJhbWQiLCJ0aGlzIiwiT2JqZWN0IiwiZGVmaW5lUHJvcGVydHkiLCJ2YWx1ZSIsImV2ZW50VGFyZ2V0U2hpbSIsIkFib3J0U2lnbmFsIiwiRXZlbnRUYXJnZXQiLCJzdXBlciIsIlR5cGVFcnJvciIsImFib3J0ZWQiLCJhYm9ydGVkRmxhZ3MiLCJnZXQiLCJkZWZpbmVFdmVudEF0dHJpYnV0ZSIsInByb3RvdHlwZSIsIldlYWtNYXAiLCJkZWZpbmVQcm9wZXJ0aWVzIiwiZW51bWVyYWJsZSIsIlN5bWJvbCIsInRvU3RyaW5nVGFnIiwiY29uZmlndXJhYmxlIiwiQWJvcnRDb250cm9sbGVyIiwic2lnbmFscyIsInNldCIsInNpZ25hbCIsImNyZWF0ZSIsImNhbGwiLCJjcmVhdGVBYm9ydFNpZ25hbCIsImdldFNpZ25hbCIsImRpc3BhdGNoRXZlbnQiLCJ0eXBlIiwiY29udHJvbGxlciIsImFib3J0IiwiZGVmYXVsdCIsIkZvcm1EYXRhIiwiZm9ybURhdGEiLCJjbGllbnQiLCJvcHRpb25zIiwiY29uZmlnIiwidXJsIiwidXNlcm5hbWUiLCJFcnJvciIsImtleSIsInJlcXVlc3QiLCJkb21haW5zIiwid2ViaG9va3MiLCJldmVudHMiLCJzdGF0cyIsInN1cHByZXNzaW9ucyIsIm1lc3NhZ2VzIiwicm91dGVzIiwiaXBzIiwiaXBfcG9vbHMiLCJwdWJsaWNfa2V5IiwicHVibGljX3JlcXVlc3QiLCJ2YWxpZGF0ZSIsInBhcnNlIiwiZGF0YSIsInJlY2VpdmluZyIsInNlbmRpbmciLCJuYW1lIiwicmVxdWlyZV90bHMiLCJza2lwX3ZlcmlmaWNhdGlvbiIsInN0YXRlIiwid2lsZGNhcmQiLCJzcGFtX2FjdGlvbiIsImNyZWF0ZWRfYXQiLCJzbXRwX3Bhc3N3b3JkIiwic210cF9sb2dpbiIsInJlY2VpdmluZ19kbnNfcmVjb3JkcyIsInNlbmRpbmdfZG5zX3JlY29yZHMiLCJfcGFyc2VNZXNzYWdlIiwicmVzcG9uc2UiLCJib2R5IiwiX3BhcnNlRG9tYWluTGlzdCIsIml0ZW1zIiwibWFwIiwiaXRlbSIsIkRvbWFpbiIsIl9wYXJzZURvbWFpbiIsImRvbWFpbiIsIl9wYXJzZVRyYWNraW5nU2V0dGluZ3MiLCJ0cmFja2luZyIsIl9wYXJzZVRyYWNraW5nVXBkYXRlIiwibGlzdCIsInF1ZXJ5IiwidGhlbiIsInBvc3QiLCJkZXN0cm95IiwiZGVsZXRlIiwiZ2V0VHJhY2tpbmciLCJ1cGRhdGVUcmFja2luZyIsInB1dCIsImdldElwcyIsImFzc2lnbklwIiwiaXAiLCJkZWxldGVJcCIsImxpbmtJcFBvb2wiLCJwb29sX2lkIiwidW5saW5rSXBQb2xsIiwic3RhdHVzIiwic3RhdHVzVGV4dCIsIm1lc3NhZ2UiLCJib2R5TWVzc2FnZSIsImVycm9yIiwic3RhY2siLCJkZXRhaWxzIiwidXJsam9pbiIsIl9wYXJzZVBhZ2VOdW1iZXIiLCJzcGxpdCIsInBvcCIsIl9wYXJzZVBhZ2UiLCJpZCIsIm51bWJlciIsIl9wYXJzZVBhZ2VMaW5rcyIsImVudHJpZXMiLCJwYWdpbmciLCJyZWR1Y2UiLCJhY2MiLCJfcGFyc2VFdmVudExpc3QiLCJwYWdlcyIsInBhZ2UiLCJwYXJzZUlwUG9vbHNSZXNwb25zZSIsInVwZGF0ZSIsInBvb2xJZCIsInBhdGNoIiwicGFyc2VJcHNSZXNwb25zZSIsIl9wYXJzZVJlc3BvbnNlIiwicG9zdE11bHRpIiwiYWRkcmVzc2VzIiwiZW5hYmxlRG5zRXNwQ2hlY2tzIiwiQXJyYXkiLCJpc0FycmF5Iiwiam9pbiIsInN5bnRheF9vbmx5IiwiaXNTdHJlYW0iLCJhdHRhY2htZW50IiwicGlwZSIsImdldEF0dGFjaG1lbnRPcHRpb25zIiwiZmlsZW5hbWUiLCJjb250ZW50VHlwZSIsImtub3duTGVuZ3RoIiwiaGVhZGVycyIsIm1ldGhvZCIsImJhc2ljIiwiQXV0aG9yaXphdGlvbiIsInBhcmFtcyIsImdldE93blByb3BlcnR5TmFtZXMiLCJsZW5ndGgiLCJzZWFyY2hQYXJhbXMiLCJ0b0xvY2FsZVVwcGVyQ2FzZSIsInRocm93SHR0cEVycm9ycyIsIm9rIiwic3RyZWFtIiwiY2h1bmtzIiwiUHJvbWlzZSIsInJlc29sdmUiLCJyZWplY3QiLCJvbiIsImNodW5rIiwicHVzaCIsIkJ1ZmZlciIsImNvbmNhdCIsInRvU3RyaW5nIiwianNvbiIsImNvbW1hbmQiLCJoZWFkIiwia2V5cyIsImZpbHRlciIsImZvckVhY2giLCJhcHBlbmQiLCJvYmoiLCJSZXF1ZXN0Iiwic3RhcnQiLCJEYXRlIiwiZW5kIiwicmVzb2x1dGlvbiIsInN0YXQiLCJ0aW1lIiwiX3BhcnNlU3RhdHMiLCJTdGF0cyIsImdldERvbWFpbiIsImdldEFjY291bnQiLCJjcmVhdGVPcHRpb25zIiwiYWRkcmVzcyIsImNvZGUiLCJ0YWdzIiwibW9kZWxzIiwiYm91bmNlcyIsIkJvdW5jZSIsImNvbXBsYWludHMiLCJDb21wbGFpbnQiLCJ1bnN1YnNjcmliZXMiLCJVbnN1YnNjcmliZSIsInBhZ2VVcmwiLCJfcGFyc2VMaXN0IiwiTW9kZWwiLCJkIiwiX3BhcnNlSXRlbSIsIm1vZGVsIiwiZW5jb2RlVVJJQ29tcG9uZW50IiwiU3VwcHJlc3Npb25DbGllbnQiLCJfcGFyc2VXZWJob29rTGlzdCIsIl9wYXJzZVdlYmhvb2tXaXRoSUQiLCJXZWJob29rIiwid2ViaG9vayIsIl9wYXJzZVdlYmhvb2tUZXN0IiwidGVzdCIsInN0ciIsImZyb20iLCJ1cmkiLCJmaXJzdENvbW1hIiwicmVwbGFjZSIsImluZGV4T2YiLCJtZXRhIiwic3Vic3RyaW5nIiwiY2hhcnNldCIsImJhc2U2NCIsInR5cGVGdWxsIiwiaSIsImVuY29kaW5nIiwidW5lc2NhcGUiLCJidWZmZXIiLCJwcml2YXRlRGF0YSIsIndyYXBwZXJzIiwicGQiLCJldmVudCIsInJldHYiLCJjb25zb2xlIiwiYXNzZXJ0Iiwic2V0Q2FuY2VsRmxhZyIsInBhc3NpdmVMaXN0ZW5lciIsImNhbmNlbGFibGUiLCJjYW5jZWxlZCIsInByZXZlbnREZWZhdWx0IiwiRXZlbnQiLCJldmVudFRhcmdldCIsImV2ZW50UGhhc2UiLCJjdXJyZW50VGFyZ2V0Iiwic3RvcHBlZCIsImltbWVkaWF0ZVN0b3BwZWQiLCJ0aW1lU3RhbXAiLCJub3ciLCJkZWZpbmVSZWRpcmVjdERlc2NyaXB0b3IiLCJkZWZpbmVDYWxsRGVzY3JpcHRvciIsImFwcGx5IiwiYXJndW1lbnRzIiwiZ2V0V3JhcHBlciIsInByb3RvIiwid3JhcHBlciIsIkJhc2VFdmVudCIsIkN1c3RvbUV2ZW50IiwiY29uc3RydWN0b3IiLCJ3cml0YWJsZSIsImlzRnVuYyIsImdldE93blByb3BlcnR5RGVzY3JpcHRvciIsImRlZmluZVdyYXBwZXIiLCJnZXRQcm90b3R5cGVPZiIsImlzU3RvcHBlZCIsInNldFBhc3NpdmVMaXN0ZW5lciIsInN0b3BQcm9wYWdhdGlvbiIsInN0b3BJbW1lZGlhdGVQcm9wYWdhdGlvbiIsIkJvb2xlYW4iLCJidWJibGVzIiwiY29tcG9zZWQiLCJjYW5jZWxCdWJibGUiLCJ3aW5kb3ciLCJzZXRQcm90b3R5cGVPZiIsImxpc3RlbmVyc01hcCIsImlzT2JqZWN0IiwieCIsImdldExpc3RlbmVycyIsImxpc3RlbmVycyIsImV2ZW50VGFyZ2V0UHJvdG90eXBlIiwiZXZlbnROYW1lIiwibm9kZSIsImxpc3RlbmVyVHlwZSIsImxpc3RlbmVyIiwibmV4dCIsInByZXYiLCJuZXdOb2RlIiwicGFzc2l2ZSIsIm9uY2UiLCJkZWZpbmVFdmVudEF0dHJpYnV0ZURlc2NyaXB0b3IiLCJkZWZpbmVDdXN0b21FdmVudFRhcmdldCIsImV2ZW50TmFtZXMiLCJDdXN0b21FdmVudFRhcmdldCIsInR5cGVzIiwiTWFwIiwib3B0aW9uc0lzT2JqIiwiY2FwdHVyZSIsInVuZGVmaW5lZCIsIndyYXBwZWRFdmVudCIsIndyYXBFdmVudCIsImVyciIsImhhbmRsZUV2ZW50Iiwic2V0RXZlbnRQaGFzZSIsInNldEN1cnJlbnRUYXJnZXQiLCJkZWZhdWx0UHJldmVudGVkIiwiUmVhZGFibGUiLCJ3bSIsIkJsb2IiLCJibG9iUGFydHMiLCJzaXplIiwicGFydHMiLCJlbGVtZW50IiwiQXJyYXlCdWZmZXIiLCJpc1ZpZXciLCJieXRlT2Zmc2V0IiwiYnl0ZUxlbmd0aCIsIlN0cmluZyIsInRvTG93ZXJDYXNlIiwiYXJyYXlCdWZmZXIiLCJVaW50OEFycmF5Iiwib2Zmc2V0IiwiYXN5bmMiLCJwYXJ0IiwicmVhZCIsInJlbGF0aXZlU3RhcnQiLCJNYXRoIiwibWF4IiwibWluIiwicmVsYXRpdmVFbmQiLCJzcGFuIiwidmFsdWVzIiwiYWRkZWQiLCJzbGljZSIsImJsb2IiLCJhc3NpZ24iLCJoYXNJbnN0YW5jZSIsIm9iamVjdCIsImZldGNoIiwiZ2xvYmFsIiwiaGlnaFdhdGVyTWFyayIsIkhlYWRlcnMiLCJSZXNwb25zZSIsIlJlYWRhYmxlU3RyZWFtIiwiXyIsImdsb2JhbHMiLCJnZXRHbG9iYWwiLCJwcm9wZXJ0eSIsInNlbGYiLCJnbG9iYWxUaGlzIiwiZ2xvYmFsUHJvcGVydGllcyIsImdsb2JhbE9iamVjdCIsImJpbmQiLCJzdXBwb3J0c0Fib3J0Q29udHJvbGxlciIsInN1cHBvcnRzU3RyZWFtcyIsInN1cHBvcnRzRm9ybURhdGEiLCJtZXJnZUhlYWRlcnMiLCJzb3VyY2UxIiwic291cmNlMiIsInJlc3VsdCIsImlzSGVhZGVyc0luc3RhbmNlIiwic291cmNlIiwiZGVlcE1lcmdlIiwic291cmNlcyIsInJldHVyblZhbHVlIiwicmVxdWVzdE1ldGhvZHMiLCJyZXNwb25zZVR5cGVzIiwidGV4dCIsInJldHJ5QWZ0ZXJTdGF0dXNDb2RlcyIsInN0b3AiLCJIVFRQRXJyb3IiLCJUaW1lb3V0RXJyb3IiLCJkZWxheSIsIm1zIiwic2V0VGltZW91dCIsIm5vcm1hbGl6ZVJlcXVlc3RNZXRob2QiLCJpbnB1dCIsImluY2x1ZGVzIiwidG9VcHBlckNhc2UiLCJkZWZhdWx0UmV0cnlPcHRpb25zIiwibGltaXQiLCJtZXRob2RzIiwic3RhdHVzQ29kZXMiLCJhZnRlclN0YXR1c0NvZGVzIiwibm9ybWFsaXplUmV0cnlPcHRpb25zIiwicmV0cnkiLCJtYXhTYWZlVGltZW91dCIsIkt5IiwiX3JldHJ5Q291bnQiLCJfaW5wdXQiLCJfb3B0aW9ucyIsImNyZWRlbnRpYWxzIiwiaG9va3MiLCJiZWZvcmVSZXF1ZXN0IiwiYmVmb3JlUmV0cnkiLCJhZnRlclJlc3BvbnNlIiwicHJlZml4VXJsIiwidGltZW91dCIsIlVSTCIsInN0YXJ0c1dpdGgiLCJlbmRzV2l0aCIsImFib3J0Q29udHJvbGxlciIsImFkZEV2ZW50TGlzdGVuZXIiLCJVUkxTZWFyY2hQYXJhbXMiLCJKU09OIiwic3RyaW5naWZ5IiwiZm4iLCJSYW5nZUVycm9yIiwiX2ZldGNoIiwiaG9vayIsIm1vZGlmaWVkUmVzcG9uc2UiLCJfZGVjb3JhdGVSZXNwb25zZSIsImNsb25lIiwib25Eb3dubG9hZFByb2dyZXNzIiwiX3N0cmVhbSIsIl9yZXRyeSIsIm1pbWVUeXBlIiwicGFyc2VKc29uIiwicmV0cnlBZnRlciIsImFmdGVyIiwiTnVtYmVyIiwiaXNOYU4iLCJtYXhSZXRyeUFmdGVyIiwiX2NhbGN1bGF0ZVJldHJ5RGVsYXkiLCJyZXRyeUNvdW50IiwidGltZW91dElEIiwiY2F0Y2giLCJjbGVhclRpbWVvdXQiLCJ0b3RhbEJ5dGVzIiwidHJhbnNmZXJyZWRCeXRlcyIsInJlYWRlciIsImdldFJlYWRlciIsInBlcmNlbnQiLCJkb25lIiwiY2xvc2UiLCJlbnF1ZXVlIiwidmFsaWRhdGVBbmRNZXJnZSIsImNyZWF0ZUluc3RhbmNlIiwiZGVmYXVsdHMiLCJreSIsIm5ld0RlZmF1bHRzIiwiZXh0ZW5kIiwiaHR0cCIsImh0dHBzIiwiemxpYiIsIlN0cmVhbSIsImRhdGFVcmlUb0J1ZmZlciIsInV0aWwiLCJjcnlwdG8iLCJGZXRjaEJhc2VFcnJvciIsImNhcHR1cmVTdGFja1RyYWNlIiwiRmV0Y2hFcnJvciIsInN5c3RlbUVycm9yIiwiZXJybm8iLCJlcnJvcmVkU3lzQ2FsbCIsInN5c2NhbGwiLCJOQU1FIiwiaXNVUkxTZWFyY2hQYXJhbWV0ZXJzIiwiZ2V0QWxsIiwiaGFzIiwic29ydCIsImlzQmxvYiIsImlzRm9ybURhdGEiLCJjYXJyaWFnZSIsImRhc2hlcyIsInJlcGVhdCIsImNhcnJpYWdlTGVuZ3RoIiwiZ2V0Rm9vdGVyIiwiYm91bmRhcnkiLCJnZXRIZWFkZXIiLCJmaWVsZCIsImhlYWRlciIsIklOVEVSTkFMUyIsIkJvZHkiLCJpc0J1ZmZlciIsImlzQW55QXJyYXlCdWZmZXIiLCJyYW5kb21CeXRlcyIsImZvcm0iLCJmb3JtRGF0YUl0ZXJhdG9yIiwiZGlzdHVyYmVkIiwiY29uc3VtZUJvZHkiLCJjdCIsImJ1ZiIsImFsbG9jIiwiYWNjdW0iLCJhY2N1bUJ5dGVzIiwicmVhZGFibGVFbmRlZCIsIl9yZWFkYWJsZVN0YXRlIiwiZW5kZWQiLCJldmVyeSIsImMiLCJib2R5VXNlZCIsImluc3RhbmNlIiwicDEiLCJwMiIsImdldEJvdW5kYXJ5IiwiUGFzc1Rocm91Z2giLCJleHRyYWN0Q29udGVudFR5cGUiLCJ2YWxpZGF0ZUhlYWRlck5hbWUiLCJ2YWxpZGF0ZUhlYWRlclZhbHVlIiwiaW5pdCIsInJhdyIsImlzQm94ZWRQcmltaXRpdmUiLCJpdGVyYXRvciIsInBhaXIiLCJQcm94eSIsInRhcmdldCIsInAiLCJyZWNlaXZlciIsIlNldCIsIlJlZmxlY3QiLCJjYWxsYmFjayIsImZvciIsInJlZGlyZWN0U3RhdHVzIiwiaXNSZWRpcmVjdCIsIklOVEVSTkFMUyQxIiwiY291bnRlciIsInJlZGlyZWN0ZWQiLCJsb2NhdGlvbiIsIklOVEVSTkFMUyQyIiwiaXNSZXF1ZXN0IiwicGFyc2VkVVJMIiwiaW5wdXRCb2R5IiwicmVkaXJlY3QiLCJmb2xsb3ciLCJjb21wcmVzcyIsImFnZW50IiwiaW5zZWN1cmVIVFRQUGFyc2VyIiwiZm9ybWF0IiwiQWJvcnRFcnJvciIsInN1cHBvcnRlZFNjaGVtYXMiLCJvcHRpb25zXyIsImNvbnRlbnRMZW5ndGhWYWx1ZSIsImdldExlbmd0aFN5bmMiLCJoYXNLbm93bkxlbmd0aCIsImdldEZvcm1EYXRhTGVuZ3RoIiwiZ2V0VG90YWxCeXRlcyIsInNlYXJjaCIsImxhc3RPZmZzZXQiLCJocmVmIiwiaGFzaCIsImdldFNlYXJjaCIsInBhdGgiLCJwYXRobmFtZSIsImhvc3RuYW1lIiwicHJvdG9jb2wiLCJwb3J0IiwiZ2V0Tm9kZVJlcXVlc3RPcHRpb25zIiwic2VuZCIsImVtaXQiLCJhYm9ydEFuZEZpbmFsaXplIiwiZmluYWxpemUiLCJyZXF1ZXN0XyIsInJlbW92ZUV2ZW50TGlzdGVuZXIiLCJyZXNwb25zZV8iLCJpbmRleCIsImFycmF5IiwiZnJvbVJhd0hlYWRlcnMiLCJyYXdIZWFkZXJzIiwic3RhdHVzQ29kZSIsImxvY2F0aW9uVVJMIiwicmVxdWVzdE9wdGlvbnMiLCJwaXBlbGluZSIsInByb2Nlc3MiLCJ2ZXJzaW9uIiwicmVzcG9uc2VPcHRpb25zIiwic3RhdHVzTWVzc2FnZSIsImNvZGluZ3MiLCJ6bGliT3B0aW9ucyIsImZsdXNoIiwiWl9TWU5DX0ZMVVNIIiwiZmluaXNoRmx1c2giLCJjcmVhdGVHdW56aXAiLCJjcmVhdGVCcm90bGlEZWNvbXByZXNzIiwiY3JlYXRlSW5mbGF0ZSIsImNyZWF0ZUluZmxhdGVSYXciLCJkZXN0Iiwid3JpdGUiLCJ3cml0ZVRvU3RyZWFtIiwibm9ybWFsaXplIiwiam9pbmVkIiwiU3ltYm9sUG9seWZpbGwiLCJkZXNjcmlwdGlvbiIsIm5vb3AiLCJ0eXBlSXNPYmplY3QiLCJyZXRocm93QXNzZXJ0aW9uRXJyb3JSZWplY3Rpb24iLCJvcmlnaW5hbFByb21pc2UiLCJvcmlnaW5hbFByb21pc2VUaGVuIiwib3JpZ2luYWxQcm9taXNlUmVzb2x2ZSIsIm9yaWdpbmFsUHJvbWlzZVJlamVjdCIsIm5ld1Byb21pc2UiLCJleGVjdXRvciIsInByb21pc2VSZXNvbHZlZFdpdGgiLCJwcm9taXNlUmVqZWN0ZWRXaXRoIiwicmVhc29uIiwiUGVyZm9ybVByb21pc2VUaGVuIiwicHJvbWlzZSIsIm9uRnVsZmlsbGVkIiwib25SZWplY3RlZCIsInVwb25Qcm9taXNlIiwidXBvbkZ1bGZpbGxtZW50IiwidXBvblJlamVjdGlvbiIsInRyYW5zZm9ybVByb21pc2VXaXRoIiwiZnVsZmlsbG1lbnRIYW5kbGVyIiwicmVqZWN0aW9uSGFuZGxlciIsInNldFByb21pc2VJc0hhbmRsZWRUb1RydWUiLCJxdWV1ZU1pY3JvdGFzayIsImdsb2JhbFF1ZXVlTWljcm90YXNrIiwicmVzb2x2ZWRQcm9taXNlIiwicmVmbGVjdENhbGwiLCJGIiwiViIsImFyZ3MiLCJGdW5jdGlvbiIsInByb21pc2VDYWxsIiwiU2ltcGxlUXVldWUiLCJfY3Vyc29yIiwiX3NpemUiLCJfZnJvbnQiLCJfZWxlbWVudHMiLCJfbmV4dCIsIl9iYWNrIiwib2xkQmFjayIsIm5ld0JhY2siLCJRVUVVRV9NQVhfQVJSQVlfU0laRSIsIm9sZEZyb250IiwibmV3RnJvbnQiLCJvbGRDdXJzb3IiLCJuZXdDdXJzb3IiLCJlbGVtZW50cyIsImZyb250IiwiY3Vyc29yIiwiUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljSW5pdGlhbGl6ZSIsIl9vd25lclJlYWRhYmxlU3RyZWFtIiwiX3JlYWRlciIsIl9zdGF0ZSIsImRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZSIsImRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlUmVzb2x2ZSIsImRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZUFzUmVzb2x2ZWQiLCJkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemVBc1JlamVjdGVkIiwiX3N0b3JlZEVycm9yIiwiUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljQ2FuY2VsIiwiUmVhZGFibGVTdHJlYW1DYW5jZWwiLCJSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNSZWxlYXNlIiwiZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VSZWplY3QiLCJkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZVJlc2V0VG9SZWplY3RlZCIsInJlYWRlckxvY2tFeGNlcHRpb24iLCJfY2xvc2VkUHJvbWlzZSIsIl9jbG9zZWRQcm9taXNlX3Jlc29sdmUiLCJfY2xvc2VkUHJvbWlzZV9yZWplY3QiLCJBYm9ydFN0ZXBzIiwiRXJyb3JTdGVwcyIsIkNhbmNlbFN0ZXBzIiwiUHVsbFN0ZXBzIiwiTnVtYmVySXNGaW5pdGUiLCJpc0Zpbml0ZSIsIk1hdGhUcnVuYyIsInRydW5jIiwidiIsImNlaWwiLCJmbG9vciIsImFzc2VydERpY3Rpb25hcnkiLCJjb250ZXh0IiwiYXNzZXJ0RnVuY3Rpb24iLCJhc3NlcnRPYmplY3QiLCJhc3NlcnRSZXF1aXJlZEFyZ3VtZW50IiwicG9zaXRpb24iLCJhc3NlcnRSZXF1aXJlZEZpZWxkIiwiY29udmVydFVucmVzdHJpY3RlZERvdWJsZSIsImNlbnNvck5lZ2F0aXZlWmVybyIsImNvbnZlcnRVbnNpZ25lZExvbmdMb25nV2l0aEVuZm9yY2VSYW5nZSIsInVwcGVyQm91bmQiLCJNQVhfU0FGRV9JTlRFR0VSIiwiaW50ZWdlclBhcnQiLCJhc3NlcnRSZWFkYWJsZVN0cmVhbSIsIklzUmVhZGFibGVTdHJlYW0iLCJBY3F1aXJlUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyIiwiUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyIiwiUmVhZGFibGVTdHJlYW1BZGRSZWFkUmVxdWVzdCIsInJlYWRSZXF1ZXN0IiwiX3JlYWRSZXF1ZXN0cyIsIlJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRSZXF1ZXN0Iiwic2hpZnQiLCJfY2xvc2VTdGVwcyIsIl9jaHVua1N0ZXBzIiwiUmVhZGFibGVTdHJlYW1HZXROdW1SZWFkUmVxdWVzdHMiLCJSZWFkYWJsZVN0cmVhbUhhc0RlZmF1bHRSZWFkZXIiLCJJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlciIsIklzUmVhZGFibGVTdHJlYW1Mb2NrZWQiLCJkZWZhdWx0UmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbiIsInJlc29sdmVQcm9taXNlIiwicmVqZWN0UHJvbWlzZSIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlclJlYWQiLCJfZXJyb3JTdGVwcyIsImUiLCJoYXNPd25Qcm9wZXJ0eSIsIl9kaXN0dXJiZWQiLCJfcmVhZGFibGVTdHJlYW1Db250cm9sbGVyIiwiY2FuY2VsIiwicmVsZWFzZUxvY2siLCJjbG9zZWQiLCJBc3luY0l0ZXJhdG9yUHJvdG90eXBlIiwiUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9ySW1wbCIsInByZXZlbnRDYW5jZWwiLCJfb25nb2luZ1Byb21pc2UiLCJfaXNGaW5pc2hlZCIsIl9wcmV2ZW50Q2FuY2VsIiwibmV4dFN0ZXBzIiwiX25leHRTdGVwcyIsInJldHVyblN0ZXBzIiwiX3JldHVyblN0ZXBzIiwiUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9yUHJvdG90eXBlIiwiSXNSZWFkYWJsZVN0cmVhbUFzeW5jSXRlcmF0b3IiLCJfYXN5bmNJdGVyYXRvckltcGwiLCJzdHJlYW1Bc3luY0l0ZXJhdG9yQnJhbmRDaGVja0V4Y2VwdGlvbiIsInJldHVybiIsIk51bWJlcklzTmFOIiwiSXNGaW5pdGVOb25OZWdhdGl2ZU51bWJlciIsIklzTm9uTmVnYXRpdmVOdW1iZXIiLCJJbmZpbml0eSIsIkRlcXVldWVWYWx1ZSIsImNvbnRhaW5lciIsIl9xdWV1ZSIsIl9xdWV1ZVRvdGFsU2l6ZSIsIkVucXVldWVWYWx1ZVdpdGhTaXplIiwiUmVzZXRRdWV1ZSIsIkNyZWF0ZUFycmF5RnJvbUxpc3QiLCJSZWFkYWJsZVN0cmVhbUJZT0JSZXF1ZXN0IiwiSXNSZWFkYWJsZVN0cmVhbUJZT0JSZXF1ZXN0IiwiYnlvYlJlcXVlc3RCcmFuZENoZWNrRXhjZXB0aW9uIiwiX3ZpZXciLCJieXRlc1dyaXR0ZW4iLCJfYXNzb2NpYXRlZFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZEludGVybmFsIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmQiLCJ2aWV3IiwiZmlyc3REZXNjcmlwdG9yIiwiX3BlbmRpbmdQdWxsSW50b3MiLCJwZWVrIiwiYnl0ZXNGaWxsZWQiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZFdpdGhOZXdWaWV3IiwicmVzcG9uZCIsInJlc3BvbmRXaXRoTmV3VmlldyIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIiLCJJc1JlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIiLCJieXRlU3RyZWFtQ29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24iLCJfYnlvYlJlcXVlc3QiLCJieW9iUmVxdWVzdCIsIlNldFVwUmVhZGFibGVTdHJlYW1CWU9CUmVxdWVzdCIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJHZXREZXNpcmVkU2l6ZSIsIl9jbG9zZVJlcXVlc3RlZCIsIl9jb250cm9sbGVkUmVhZGFibGVCeXRlU3RyZWFtIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVycm9yIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyIsIlJlYWRhYmxlU3RyZWFtQ2xvc2UiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ2xvc2UiLCJ0cmFuc2ZlcnJlZEJ1ZmZlciIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFbnF1ZXVlQ2h1bmtUb1F1ZXVlIiwiUmVhZGFibGVTdHJlYW1IYXNCWU9CUmVhZGVyIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclByb2Nlc3NQdWxsSW50b0Rlc2NyaXB0b3JzVXNpbmdRdWV1ZSIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDYWxsUHVsbElmTmVlZGVkIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVucXVldWUiLCJfY2FuY2VsQWxnb3JpdGhtIiwiZW50cnkiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVySGFuZGxlUXVldWVEcmFpbiIsImF1dG9BbGxvY2F0ZUNodW5rU2l6ZSIsIl9hdXRvQWxsb2NhdGVDaHVua1NpemUiLCJidWZmZXJFIiwicHVsbEludG9EZXNjcmlwdG9yIiwiZWxlbWVudFNpemUiLCJ2aWV3Q29uc3RydWN0b3IiLCJyZWFkZXJUeXBlIiwiX3N0YXJ0ZWQiLCJSZWFkYWJsZVN0cmVhbUdldE51bVJlYWRJbnRvUmVxdWVzdHMiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyU2hvdWxkQ2FsbFB1bGwiLCJfcHVsbGluZyIsIl9wdWxsQWdhaW4iLCJfcHVsbEFsZ29yaXRobSIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDb21taXRQdWxsSW50b0Rlc2NyaXB0b3IiLCJmaWxsZWRWaWV3IiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNvbnZlcnRQdWxsSW50b0Rlc2NyaXB0b3IiLCJyZWFkSW50b1JlcXVlc3QiLCJfcmVhZEludG9SZXF1ZXN0cyIsIlJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRJbnRvUmVxdWVzdCIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJGaWxsUHVsbEludG9EZXNjcmlwdG9yRnJvbVF1ZXVlIiwiY3VycmVudEFsaWduZWRCeXRlcyIsIm1heEJ5dGVzVG9Db3B5IiwibWF4Qnl0ZXNGaWxsZWQiLCJtYXhBbGlnbmVkQnl0ZXMiLCJ0b3RhbEJ5dGVzVG9Db3B5UmVtYWluaW5nIiwicmVhZHkiLCJxdWV1ZSIsImhlYWRPZlF1ZXVlIiwiYnl0ZXNUb0NvcHkiLCJkZXN0U3RhcnQiLCJkZXN0T2Zmc2V0Iiwic3JjIiwic3JjT2Zmc2V0IiwibiIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJGaWxsSGVhZFB1bGxJbnRvRGVzY3JpcHRvciIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJJbnZhbGlkYXRlQllPQlJlcXVlc3QiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyU2hpZnRQZW5kaW5nUHVsbEludG8iLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZEluQ2xvc2VkU3RhdGUiLCJyZW1haW5kZXJTaXplIiwicmVtYWluZGVyIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRJblJlYWRhYmxlU3RhdGUiLCJkZXNjcmlwdG9yIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyUGVuZGluZ1B1bGxJbnRvcyIsIlJlYWRhYmxlU3RyZWFtRXJyb3IiLCJfc3RyYXRlZ3lIV00iLCJSZWFkYWJsZVN0cmVhbUFkZFJlYWRJbnRvUmVxdWVzdCIsIklzUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyIiwiZGVzaXJlZFNpemUiLCJSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIiLCJieW9iUmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbiIsIkRhdGFWaWV3IiwiQllURVNfUEVSX0VMRU1FTlQiLCJjdG9yIiwiZW1wdHlWaWV3IiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclB1bGxJbnRvIiwiUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyUmVhZCIsIkV4dHJhY3RIaWdoV2F0ZXJNYXJrIiwic3RyYXRlZ3kiLCJkZWZhdWx0SFdNIiwiRXh0cmFjdFNpemVBbGdvcml0aG0iLCJjb252ZXJ0UXVldWluZ1N0cmF0ZWd5IiwiY29udmVydFF1ZXVpbmdTdHJhdGVneVNpemUiLCJjb252ZXJ0VW5kZXJseWluZ1NpbmtBYm9ydENhbGxiYWNrIiwib3JpZ2luYWwiLCJjb252ZXJ0VW5kZXJseWluZ1NpbmtDbG9zZUNhbGxiYWNrIiwiY29udmVydFVuZGVybHlpbmdTaW5rU3RhcnRDYWxsYmFjayIsImNvbnZlcnRVbmRlcmx5aW5nU2lua1dyaXRlQ2FsbGJhY2siLCJhc3NlcnRXcml0YWJsZVN0cmVhbSIsIklzV3JpdGFibGVTdHJlYW0iLCJXcml0YWJsZVN0cmVhbSIsInJhd1VuZGVybHlpbmdTaW5rIiwicmF3U3RyYXRlZ3kiLCJ1bmRlcmx5aW5nU2luayIsImNvbnZlcnRVbmRlcmx5aW5nU2luayIsIkluaXRpYWxpemVXcml0YWJsZVN0cmVhbSIsInNpemVBbGdvcml0aG0iLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyIiwic3RhcnRBbGdvcml0aG0iLCJ3cml0ZUFsZ29yaXRobSIsImNsb3NlQWxnb3JpdGhtIiwiYWJvcnRBbGdvcml0aG0iLCJTZXRVcFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIiLCJTZXRVcFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJGcm9tVW5kZXJseWluZ1NpbmsiLCJzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uIiwiSXNXcml0YWJsZVN0cmVhbUxvY2tlZCIsIldyaXRhYmxlU3RyZWFtQWJvcnQiLCJXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodCIsIldyaXRhYmxlU3RyZWFtQ2xvc2UiLCJBY3F1aXJlV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyIiwiX3dyaXRlciIsIl93cml0YWJsZVN0cmVhbUNvbnRyb2xsZXIiLCJfd3JpdGVSZXF1ZXN0cyIsIl9pbkZsaWdodFdyaXRlUmVxdWVzdCIsIl9jbG9zZVJlcXVlc3QiLCJfaW5GbGlnaHRDbG9zZVJlcXVlc3QiLCJfcGVuZGluZ0Fib3J0UmVxdWVzdCIsIl9iYWNrcHJlc3N1cmUiLCJfcHJvbWlzZSIsIndhc0FscmVhZHlFcnJvcmluZyIsIl9yZXNvbHZlIiwiX3JlamVjdCIsIl9yZWFzb24iLCJfd2FzQWxyZWFkeUVycm9yaW5nIiwiV3JpdGFibGVTdHJlYW1TdGFydEVycm9yaW5nIiwiY2xvc2VSZXF1ZXN0Iiwid3JpdGVyIiwiZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZVJlc29sdmUiLCJjbG9zZVNlbnRpbmVsIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckFkdmFuY2VRdWV1ZUlmTmVlZGVkIiwiV3JpdGFibGVTdHJlYW1EZWFsV2l0aFJlamVjdGlvbiIsIldyaXRhYmxlU3RyZWFtRmluaXNoRXJyb3JpbmciLCJXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJFbnN1cmVSZWFkeVByb21pc2VSZWplY3RlZCIsIldyaXRhYmxlU3RyZWFtSGFzT3BlcmF0aW9uTWFya2VkSW5GbGlnaHQiLCJzdG9yZWRFcnJvciIsIndyaXRlUmVxdWVzdCIsIldyaXRhYmxlU3RyZWFtUmVqZWN0Q2xvc2VBbmRDbG9zZWRQcm9taXNlSWZOZWVkZWQiLCJhYm9ydFJlcXVlc3QiLCJkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZVJlamVjdCIsIldyaXRhYmxlU3RyZWFtVXBkYXRlQmFja3ByZXNzdXJlIiwiYmFja3ByZXNzdXJlIiwiZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemUiLCJkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVzZXQiLCJnZXRXcml0ZXIiLCJsb2NrZWQiLCJfb3duZXJXcml0YWJsZVN0cmVhbSIsImRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VJbml0aWFsaXplQXNSZXNvbHZlZCIsImRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZSIsImRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VJbml0aWFsaXplQXNSZWplY3RlZCIsImRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVzb2x2ZSIsImRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZUFzUmVqZWN0ZWQiLCJJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlciIsImRlZmF1bHRXcml0ZXJCcmFuZENoZWNrRXhjZXB0aW9uIiwiZGVmYXVsdFdyaXRlckxvY2tFeGNlcHRpb24iLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0RGVzaXJlZFNpemUiLCJXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJHZXREZXNpcmVkU2l6ZSIsIl9yZWFkeVByb21pc2UiLCJXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJBYm9ydCIsIldyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckNsb3NlIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyUmVsZWFzZSIsIldyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcldyaXRlIiwiX3JlYWR5UHJvbWlzZVN0YXRlIiwiZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZVJlamVjdCIsImRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VSZXNldFRvUmVqZWN0ZWQiLCJyZWxlYXNlZEVycm9yIiwiX2Nsb3NlZFByb21pc2VTdGF0ZSIsImRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVzZXRUb1JlamVjdGVkIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyRW5zdXJlQ2xvc2VkUHJvbWlzZVJlamVjdGVkIiwiY2h1bmtTaXplIiwiX3N0cmF0ZWd5U2l6ZUFsZ29yaXRobSIsImNodW5rU2l6ZUUiLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3JJZk5lZWRlZCIsIldyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXRDaHVua1NpemUiLCJXcml0YWJsZVN0cmVhbUFkZFdyaXRlUmVxdWVzdCIsImVucXVldWVFIiwiX2NvbnRyb2xsZWRXcml0YWJsZVN0cmVhbSIsIldyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXRCYWNrcHJlc3N1cmUiLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyV3JpdGUiLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3IiLCJfYWJvcnRBbGdvcml0aG0iLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zIiwiX3dyaXRlQWxnb3JpdGhtIiwiX2Nsb3NlQWxnb3JpdGhtIiwiciIsIldyaXRhYmxlU3RyZWFtTWFya0Nsb3NlUmVxdWVzdEluRmxpZ2h0Iiwic2lua0Nsb3NlUHJvbWlzZSIsIldyaXRhYmxlU3RyZWFtRmluaXNoSW5GbGlnaHRDbG9zZSIsIldyaXRhYmxlU3RyZWFtRmluaXNoSW5GbGlnaHRDbG9zZVdpdGhFcnJvciIsIldyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJQcm9jZXNzQ2xvc2UiLCJXcml0YWJsZVN0cmVhbU1hcmtGaXJzdFdyaXRlUmVxdWVzdEluRmxpZ2h0IiwiV3JpdGFibGVTdHJlYW1GaW5pc2hJbkZsaWdodFdyaXRlIiwiV3JpdGFibGVTdHJlYW1GaW5pc2hJbkZsaWdodFdyaXRlV2l0aEVycm9yIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlclByb2Nlc3NXcml0ZSIsIl9yZWFkeVByb21pc2VfcmVzb2x2ZSIsIl9yZWFkeVByb21pc2VfcmVqZWN0IiwiTmF0aXZlRE9NRXhjZXB0aW9uIiwiRE9NRXhjZXB0aW9uIiwiRE9NRXhjZXB0aW9uJDEiLCJfYSIsImlzRE9NRXhjZXB0aW9uQ29uc3RydWN0b3IiLCJjcmVhdGVET01FeGNlcHRpb25Qb2x5ZmlsbCIsIlJlYWRhYmxlU3RyZWFtUGlwZVRvIiwicHJldmVudENsb3NlIiwicHJldmVudEFib3J0Iiwic2h1dHRpbmdEb3duIiwiY3VycmVudFdyaXRlIiwiYWN0aW9ucyIsInNodXRkb3duV2l0aEFjdGlvbiIsImFsbCIsImFjdGlvbiIsImlzT3JCZWNvbWVzRXJyb3JlZCIsInNodXRkb3duIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyQ2xvc2VXaXRoRXJyb3JQcm9wYWdhdGlvbiIsImRlc3RDbG9zZWQiLCJ3YWl0Rm9yV3JpdGVzVG9GaW5pc2giLCJvbGRDdXJyZW50V3JpdGUiLCJvcmlnaW5hbElzRXJyb3IiLCJvcmlnaW5hbEVycm9yIiwiZG9UaGVSZXN0IiwibmV3RXJyb3IiLCJpc0Vycm9yIiwicmVzb2x2ZUxvb3AiLCJyZWplY3RMb29wIiwicmVzb2x2ZVJlYWQiLCJyZWplY3RSZWFkIiwiUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlciIsIklzUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlciIsImRlZmF1bHRDb250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbiIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXREZXNpcmVkU2l6ZSIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYW5DbG9zZU9yRW5xdWV1ZSIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbG9zZSIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlIiwiUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVycm9yIiwiUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyIsIl9jb250cm9sbGVkUmVhZGFibGVTdHJlYW0iLCJSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZCIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJTaG91bGRDYWxsUHVsbCIsIlNldFVwUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlciIsInB1bGxBbGdvcml0aG0iLCJjYW5jZWxBbGdvcml0aG0iLCJjb252ZXJ0VW5kZXJseWluZ1NvdXJjZUNhbmNlbENhbGxiYWNrIiwiY29udmVydFVuZGVybHlpbmdTb3VyY2VQdWxsQ2FsbGJhY2siLCJjb252ZXJ0VW5kZXJseWluZ1NvdXJjZVN0YXJ0Q2FsbGJhY2siLCJjb252ZXJ0UmVhZGFibGVTdHJlYW1UeXBlIiwiY29udmVydFJlYWRhYmxlU3RyZWFtUmVhZGVyTW9kZSIsIm1vZGUiLCJjb252ZXJ0UGlwZU9wdGlvbnMiLCJpc0Fib3J0U2lnbmFsIiwiYXNzZXJ0QWJvcnRTaWduYWwiLCJyYXdVbmRlcmx5aW5nU291cmNlIiwidW5kZXJseWluZ1NvdXJjZSIsInB1bGwiLCJjb252ZXJ0VW5kZXJseWluZ0RlZmF1bHRPckJ5dGVTb3VyY2UiLCJJbml0aWFsaXplUmVhZGFibGVTdHJlYW0iLCJ1bmRlcmx5aW5nQnl0ZVNvdXJjZSIsIlNldFVwUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlciIsIlNldFVwUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckZyb21VbmRlcmx5aW5nU291cmNlIiwiU2V0VXBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRnJvbVVuZGVybHlpbmdTb3VyY2UiLCJzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uJDEiLCJyYXdPcHRpb25zIiwiY29udmVydFJlYWRlck9wdGlvbnMiLCJyYXdUcmFuc2Zvcm0iLCJ0cmFuc2Zvcm0iLCJyZWFkYWJsZSIsImNvbnZlcnRSZWFkYWJsZVdyaXRhYmxlUGFpciIsImRlc3RpbmF0aW9uIiwiYnJhbmNoZXMiLCJjbG9uZUZvckJyYW5jaDIiLCJyZWFzb24xIiwicmVhc29uMiIsImJyYW5jaDEiLCJicmFuY2gyIiwicmVzb2x2ZUNhbmNlbFByb21pc2UiLCJyZWFkaW5nIiwiY2FuY2VsZWQxIiwiY2FuY2VsZWQyIiwiY2FuY2VsUHJvbWlzZSIsInZhbHVlMSIsInZhbHVlMiIsIkNyZWF0ZVJlYWRhYmxlU3RyZWFtIiwiY29tcG9zaXRlUmVhc29uIiwiY2FuY2VsUmVzdWx0IiwiUmVhZGFibGVTdHJlYW1UZWUiLCJpbXBsIiwiQWNxdWlyZVJlYWRhYmxlU3RyZWFtQXN5bmNJdGVyYXRvciIsImNvbnZlcnRJdGVyYXRvck9wdGlvbnMiLCJjb252ZXJ0UXVldWluZ1N0cmF0ZWd5SW5pdCIsInBpcGVUaHJvdWdoIiwicGlwZVRvIiwidGVlIiwiYXN5bmNJdGVyYXRvciIsImJ5dGVMZW5ndGhTaXplRnVuY3Rpb24iLCJCeXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5IiwiX2J5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3lIaWdoV2F0ZXJNYXJrIiwiSXNCeXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5IiwiYnl0ZUxlbmd0aEJyYW5kQ2hlY2tFeGNlcHRpb24iLCJjb3VudFNpemVGdW5jdGlvbiIsIkNvdW50UXVldWluZ1N0cmF0ZWd5IiwiX2NvdW50UXVldWluZ1N0cmF0ZWd5SGlnaFdhdGVyTWFyayIsIklzQ291bnRRdWV1aW5nU3RyYXRlZ3kiLCJjb3VudEJyYW5kQ2hlY2tFeGNlcHRpb24iLCJjb252ZXJ0VHJhbnNmb3JtZXJGbHVzaENhbGxiYWNrIiwiY29udmVydFRyYW5zZm9ybWVyU3RhcnRDYWxsYmFjayIsImNvbnZlcnRUcmFuc2Zvcm1lclRyYW5zZm9ybUNhbGxiYWNrIiwiVHJhbnNmb3JtU3RyZWFtIiwicmF3VHJhbnNmb3JtZXIiLCJyYXdXcml0YWJsZVN0cmF0ZWd5IiwicmF3UmVhZGFibGVTdHJhdGVneSIsIndyaXRhYmxlU3RyYXRlZ3kiLCJyZWFkYWJsZVN0cmF0ZWd5IiwidHJhbnNmb3JtZXIiLCJyZWFkYWJsZVR5cGUiLCJ3cml0YWJsZVR5cGUiLCJjb252ZXJ0VHJhbnNmb3JtZXIiLCJyZWFkYWJsZUhpZ2hXYXRlck1hcmsiLCJyZWFkYWJsZVNpemVBbGdvcml0aG0iLCJ3cml0YWJsZUhpZ2hXYXRlck1hcmsiLCJ3cml0YWJsZVNpemVBbGdvcml0aG0iLCJzdGFydFByb21pc2VfcmVzb2x2ZSIsInN0YXJ0UHJvbWlzZSIsIl93cml0YWJsZSIsIkNyZWF0ZVdyaXRhYmxlU3RyZWFtIiwiX3RyYW5zZm9ybVN0cmVhbUNvbnRyb2xsZXIiLCJfYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZSIsIlRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyUGVyZm9ybVRyYW5zZm9ybSIsIlRyYW5zZm9ybVN0cmVhbURlZmF1bHRTaW5rV3JpdGVBbGdvcml0aG0iLCJfcmVhZGFibGUiLCJmbHVzaFByb21pc2UiLCJfZmx1c2hBbGdvcml0aG0iLCJUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyIsIlRyYW5zZm9ybVN0cmVhbUVycm9yIiwiVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNpbmtDbG9zZUFsZ29yaXRobSIsIlRyYW5zZm9ybVN0cmVhbURlZmF1bHRTaW5rQWJvcnRBbGdvcml0aG0iLCJUcmFuc2Zvcm1TdHJlYW1TZXRCYWNrcHJlc3N1cmUiLCJUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0U291cmNlUHVsbEFsZ29yaXRobSIsIlRyYW5zZm9ybVN0cmVhbUVycm9yV3JpdGFibGVBbmRVbmJsb2NrV3JpdGUiLCJfYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZV9yZXNvbHZlIiwiSW5pdGlhbGl6ZVRyYW5zZm9ybVN0cmVhbSIsIlRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyIiwidHJhbnNmb3JtQWxnb3JpdGhtIiwiVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlIiwidHJhbnNmb3JtUmVzdWx0RSIsImZsdXNoQWxnb3JpdGhtIiwiX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW0iLCJfdHJhbnNmb3JtQWxnb3JpdGhtIiwiU2V0VXBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlciIsIlNldFVwVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJGcm9tVHJhbnNmb3JtZXIiLCJJc1RyYW5zZm9ybVN0cmVhbSIsInN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24kMiIsIklzVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIiLCJkZWZhdWx0Q29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24kMSIsIlRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyVGVybWluYXRlIiwicmVhZGFibGVDb250cm9sbGVyIiwiUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckhhc0JhY2twcmVzc3VyZSIsInRlcm1pbmF0ZSIsInJlcXVpcmUiLCJfX3dlYnBhY2tfbW9kdWxlX2NhY2hlX18iLCJfX3dlYnBhY2tfcmVxdWlyZV9fIiwibW9kdWxlSWQiLCJfX3dlYnBhY2tfbW9kdWxlc19fIiwiZGVmaW5pdGlvbiIsIm8iLCJwcm9wIl0sIm1hcHBpbmdzIjoiO0NBQUEsU0FBMkNBLEVBQU1DLEdBQzFCLGlCQUFaQyxTQUEwQyxpQkFBWEMsT0FDeENBLE9BQU9ELFFBQVVELElBQ1EsbUJBQVhHLFFBQXlCQSxPQUFPQyxJQUM5Q0QsT0FBTyxHQUFJSCxHQUNlLGlCQUFaQyxRQUNkQSxRQUFpQixRQUFJRCxJQUVyQkQsRUFBYyxRQUFJQyxJQVJwQixDQVNHSyxNQUFNLFdBQ1QsTSx3Q0NKQUMsT0FBT0MsZUFBZU4sRUFBUyxhQUEvQixDQUErQ08sT0FBTyxJQUV0RCxJQUFJQyxFQUFrQixFQUFRLEtBTTlCLE1BQU1DLFVBQW9CRCxFQUFnQkUsWUFJdEMsY0FFSSxNQURBQyxRQUNNLElBQUlDLFVBQVUsOENBS3hCLGNBQ0ksTUFBTUMsRUFBVUMsRUFBYUMsSUFBSVgsTUFDakMsR0FBdUIsa0JBQVpTLEVBQ1AsTUFBTSxJQUFJRCxVQUFVLDJEQUFtRSxPQUFUUixLQUFnQixjQUFnQkEsT0FFbEgsT0FBT1MsR0FHZkwsRUFBZ0JRLHFCQUFxQlAsRUFBWVEsVUFBVyxTQXVCNUQsTUFBTUgsRUFBZSxJQUFJSSxRQUV6QmIsT0FBT2MsaUJBQWlCVixFQUFZUSxVQUFXLENBQzNDSixRQUFTLENBQUVPLFlBQVksS0FHTCxtQkFBWEMsUUFBdUQsaUJBQXZCQSxPQUFPQyxhQUM5Q2pCLE9BQU9DLGVBQWVHLEVBQVlRLFVBQVdJLE9BQU9DLFlBQWEsQ0FDN0RDLGNBQWMsRUFDZGhCLE1BQU8sZ0JBUWYsTUFBTWlCLEVBSUYsY0FDSUMsRUFBUUMsSUFBSXRCLEtBekNwQixXQUNJLE1BQU11QixFQUFTdEIsT0FBT3VCLE9BQU9uQixFQUFZUSxXQUd6QyxPQUZBVCxFQUFnQkUsWUFBWW1CLEtBQUtGLEdBQ2pDYixFQUFhWSxJQUFJQyxHQUFRLEdBQ2xCQSxFQXFDZUcsSUFLdEIsYUFDSSxPQUFPQyxFQUFVM0IsTUFLckIsUUEzQ0osSUFBcUJ1QixJQTRDREksRUFBVTNCLE9BM0NPLElBQTdCVSxFQUFhQyxJQUFJWSxLQUdyQmIsRUFBYVksSUFBSUMsR0FBUSxHQUN6QkEsRUFBT0ssY0FBYyxDQUFFQyxLQUFNLFlBNkNqQyxNQUFNUixFQUFVLElBQUlQLFFBSXBCLFNBQVNhLEVBQVVHLEdBQ2YsTUFBTVAsRUFBU0YsRUFBUVYsSUFBSW1CLEdBQzNCLEdBQWMsTUFBVlAsRUFDQSxNQUFNLElBQUlmLFVBQVUsK0RBQTZFLE9BQWZzQixFQUFzQixjQUFnQkEsSUFFNUgsT0FBT1AsRUFHWHRCLE9BQU9jLGlCQUFpQkssRUFBZ0JQLFVBQVcsQ0FDL0NVLE9BQVEsQ0FBRVAsWUFBWSxHQUN0QmUsTUFBTyxDQUFFZixZQUFZLEtBRUgsbUJBQVhDLFFBQXVELGlCQUF2QkEsT0FBT0MsYUFDOUNqQixPQUFPQyxlQUFla0IsRUFBZ0JQLFVBQVdJLE9BQU9DLFlBQWEsQ0FDakVDLGNBQWMsRUFDZGhCLE1BQU8sb0JBSWZQLEVBQVF3QixnQkFBa0JBLEVBQzFCeEIsRUFBUVMsWUFBY0EsRUFDdEJULEVBQVFvQyxRQUFVWixFQUVsQnZCLEVBQU9ELFFBQVV3QixFQUNqQnZCLEVBQU9ELFFBQVF3QixnQkFBa0J2QixFQUFPRCxRQUFQLFFBQTRCd0IsRUFDN0R2QixFQUFPRCxRQUFRUyxZQUFjQSxHLHNLQzdIN0IsZ0JBR0EsYUFHRSxXQUFZNEIsR0FDVmpDLEtBQUtrQyxTQUFXRCxFQU1wQixPQUhFLFlBQUFFLE9BQUEsU0FBT0MsR0FDTCxPQUFPLElBQUksVUFBT0EsRUFBU3BDLEtBQUtrQyxXQUVwQyxFQVZBLEcsdVpDSEEsZ0JBSUEsV0FDQSxZQUNBLFlBQ0EsWUFDQSxZQUNBLFlBQ0EsWUFDQSxZQUNBLFlBQ0EsWUFDQSxZLFVBa0JFLFNBQVlFLEVBQWtCRixHQUM1QixJQUFNRyxFQUF5QixLQUFLRCxHQU1wQyxHQUpLQyxFQUFPQyxNQUNWRCxFQUFPQyxJQUFNLDRCQUdWRCxFQUFPRSxTQUNWLE1BQU0sSUFBSUMsTUFBTSxvQ0FHbEIsSUFBS0gsRUFBT0ksSUFDVixNQUFNLElBQUlELE1BQU0sK0JBSWxCeEMsS0FBSzBDLFFBQVUsSUFBSSxVQUFRTCxFQUFRSCxHQUVuQ2xDLEtBQUsyQyxRQUFVLElBQUksVUFBYTNDLEtBQUswQyxTQUNyQzFDLEtBQUs0QyxTQUFXLElBQUksVUFBYzVDLEtBQUswQyxTQUN2QzFDLEtBQUs2QyxPQUFTLElBQUksVUFBWTdDLEtBQUswQyxTQUNuQzFDLEtBQUs4QyxNQUFRLElBQUksVUFBWTlDLEtBQUswQyxTQUNsQzFDLEtBQUsrQyxhQUFlLElBQUksVUFBa0IvQyxLQUFLMEMsU0FDL0MxQyxLQUFLZ0QsU0FBVyxJQUFJLFVBQWVoRCxLQUFLMEMsU0FDeEMxQyxLQUFLaUQsT0FBUyxJQUFJLFVBQWFqRCxLQUFLMEMsU0FDcEMxQyxLQUFLa0QsSUFBTSxJQUFJLFVBQVVsRCxLQUFLMEMsU0FDOUIxQyxLQUFLbUQsU0FBVyxJQUFJLFVBQWNuRCxLQUFLMEMsU0FFbkNMLEVBQU9lLGFBQ1RmLEVBQU9JLElBQU1KLEVBQU9lLFdBRXBCcEQsS0FBS3FELGVBQWlCLElBQUksVUFBUWhCLEVBQVFILEdBQzFDbEMsS0FBS3NELFNBQVcsSUFBSSxVQUFldEQsS0FBS3FELGdCQUN4Q3JELEtBQUt1RCxNQUFRLElBQUksVUFBWXZELEtBQUtxRCxtQixxS0NqRXhDLGVBZ0JBLEVBY0UsU0FBWUcsRUFBa0JDLEVBQWlCQyxHQUM3QzFELEtBQUsyRCxLQUFPSCxFQUFLRyxLQUNqQjNELEtBQUs0RCxZQUFjSixFQUFLSSxZQUN4QjVELEtBQUs2RCxrQkFBb0JMLEVBQUtLLGtCQUM5QjdELEtBQUs4RCxNQUFRTixFQUFLTSxNQUNsQjlELEtBQUsrRCxTQUFXUCxFQUFLTyxTQUNyQi9ELEtBQUtnRSxZQUFjUixFQUFLUSxZQUN4QmhFLEtBQUtpRSxXQUFhVCxFQUFLUyxXQUN2QmpFLEtBQUtrRSxjQUFnQlYsRUFBS1UsY0FDMUJsRSxLQUFLbUUsV0FBYVgsRUFBS1csV0FDdkJuRSxLQUFLNkIsS0FBTzJCLEVBQUszQixLQUVqQjdCLEtBQUtvRSxzQkFBd0JYLEdBQWEsS0FDMUN6RCxLQUFLcUUsb0JBQXNCWCxHQUFXLE1BSTFDLGFBR0UsV0FBWWhCLEdBQ1YxQyxLQUFLMEMsUUFBVUEsRUF5Rm5CLE9BdEZFLFlBQUE0QixjQUFBLFNBQWNDLEdBQ1osT0FBT0EsRUFBU0MsTUFHbEIsWUFBQUMsaUJBQUEsU0FBaUJGLEdBQ2YsT0FBT0EsRUFBU0MsS0FBS0UsTUFBTUMsS0FBSSxTQUFVQyxHQUN2QyxPQUFPLElBQUlDLEVBQU9ELE9BSXRCLFlBQUFFLGFBQUEsU0FBYVAsR0FPWCxPQUFPLElBQUlNLEVBQ1ROLEVBQVNDLEtBQUtPLE9BQ2RSLEVBQVNDLEtBQUtKLHNCQUNkRyxFQUFTQyxLQUFLSCxzQkFJbEIsWUFBQVcsdUJBQUEsU0FBdUJULEdBQ3JCLE9BQU9BLEVBQVNDLEtBQUtTLFVBR3ZCLFlBQUFDLHFCQUFBLFNBQXFCWCxHQUNuQixPQUFPQSxFQUFTQyxNQUdsQixZQUFBVyxLQUFBLFNBQUtDLEdBQ0gsT0FBT3BGLEtBQUswQyxRQUFRL0IsSUFBSSxjQUFleUUsR0FDcENDLEtBQUtyRixLQUFLeUUsbUJBR2YsWUFBQTlELElBQUEsU0FBSW9FLEdBQ0YsT0FBTy9FLEtBQUswQyxRQUFRL0IsSUFBSSxlQUFlb0UsR0FDcENNLEtBQUtyRixLQUFLOEUsZUFHZixZQUFBdEQsT0FBQSxTQUFPZ0MsR0FDTCxPQUFPeEQsS0FBSzBDLFFBQVE0QyxLQUFLLGNBQWU5QixHQUNyQzZCLEtBQUtyRixLQUFLOEUsZUFHZixZQUFBUyxRQUFBLFNBQVFSLEdBQ04sT0FBTy9FLEtBQUswQyxRQUFROEMsT0FBTyxlQUFlVCxHQUN2Q00sS0FBS3JGLEtBQUtzRSxnQkFLZixZQUFBbUIsWUFBQSxTQUFZVixHQUNWLE9BQU8vRSxLQUFLMEMsUUFBUS9CLElBQUksVUFBUSxjQUFlb0UsRUFBUSxhQUNwRE0sS0FBS3JGLEtBQUtnRix5QkFHZixZQUFBVSxlQUFBLFNBQWVYLEVBQWdCbEQsRUFBYzJCLEdBQzNDLE9BQU94RCxLQUFLMEMsUUFBUWlELElBQUksVUFBUSxjQUFlWixFQUFRLFdBQVlsRCxHQUFPMkIsR0FDdkU2QixLQUFLckYsS0FBS2tGLHVCQUtmLFlBQUFVLE9BQUEsU0FBT2IsR0FDTCxPQUFPL0UsS0FBSzBDLFFBQVEvQixJQUFJLFVBQVEsY0FBZW9FLEVBQVEsUUFDcERNLE1BQUssU0FBQ2QsR0FBdUMsYUFBbUIsUUFBbkIsRUFBS0EsYUFBUSxFQUFSQSxFQUFVQyxZQUFJLGVBQUVFLFVBR3ZFLFlBQUFtQixTQUFBLFNBQVNkLEVBQWdCZSxHQUN2QixPQUFPOUYsS0FBSzBDLFFBQVE0QyxLQUFLLFVBQVEsY0FBZVAsRUFBUSxPQUFRLENBQUVlLEdBQUUsS0FHdEUsWUFBQUMsU0FBQSxTQUFTaEIsRUFBZ0JlLEdBQ3ZCLE9BQU85RixLQUFLMEMsUUFBUThDLE9BQU8sVUFBUSxjQUFlVCxFQUFRLE1BQU9lLEtBR25FLFlBQUFFLFdBQUEsU0FBV2pCLEVBQWdCa0IsR0FDekIsT0FBT2pHLEtBQUswQyxRQUFRNEMsS0FBSyxVQUFRLGNBQWVQLEVBQVEsT0FBUSxDQUFFa0IsUUFBTyxLQUczRSxZQUFBQyxhQUFBLFNBQWFuQixFQUFnQmtCLEVBQWlCSCxHQUM1QyxPQUFPOUYsS0FBSzBDLFFBQVE4QyxPQUFPLFVBQVEsY0FBZVQsRUFBUSxNQUFPLFdBQVksQ0FBRWtCLFFBQU8sRUFBRUgsR0FBRSxLQUU5RixFQTdGQSxHLG1jQzdDQSxrQkFLRSxXQUFZLEcsSUFDVkssRUFBTSxTQUNOQyxFQUFVLGFBQ1ZDLEVBQU8sVUFDUCxJQUFBN0IsWUFBSSxJQUFHLEtBQUUsRUFKWCxPQU1tQjhCLEVBQXVCOUIsRUFBWixRQUFFK0IsRUFBVS9CLEVBQUwsTSxPQUNuQyxnQkFBTyxNQUVGZ0MsTUFBUSxLQUNiLEVBQUtMLE9BQVNBLEVBQ2QsRUFBS0UsUUFBVUEsR0FBV0UsR0FBU0gsRUFDbkMsRUFBS0ssUUFBVUgsRSxFQUVuQixPQW5Cc0MsT0FtQnRDLEVBbkJBLENBQXNDOUQsTyx5RkNGdEMsSUFBTWtFLEVBQVUsRUFBUSxJQUl4QixHQUZrQixFQUFRLEtBRTFCLFdBR0UsV0FBWWhFLEdBQ1YxQyxLQUFLMEMsUUFBVUEsRUF3Q25CLE9BckNFLFlBQUFpRSxpQkFBQSxTQUFpQnJFLEdBQ2YsT0FBT0EsRUFBSXNFLE1BQU0sS0FBS0MsT0FHeEIsWUFBQUMsV0FBQSxTQUFXQyxFQUFZekUsR0FDckIsTUFBTyxDQUFFeUUsR0FBRSxFQUFFQyxPQUFRaEgsS0FBSzJHLGlCQUFpQnJFLEdBQU1BLElBQUcsSUFHdEQsWUFBQTJFLGdCQUFBLFNBQWdCMUMsR0FBaEIsV0FFRSxPQURjdEUsT0FBT2lILFFBQVEzQyxFQUFTQyxLQUFLMkMsUUFDOUJDLFFBQ1gsU0FBQ0MsRUFBVSxHLElBQUNOLEVBQUUsS0FBRXpFLEVBQUcsS0FFakIsT0FEQStFLEVBQUlOLEdBQU0sRUFBS0QsV0FBV0MsRUFBSXpFLEdBQ3ZCK0UsSUFDTixLQUdQLFlBQUFDLGdCQUFBLFNBQWdCL0MsR0FDZCxNQUFPLENBQ0xHLE1BQU9ILEVBQVNDLEtBQUtFLE1BQ3JCNkMsTUFBT3ZILEtBQUtpSCxnQkFBZ0IxQyxLQUloQyxZQUFBNUQsSUFBQSxTQUFJb0UsRUFBZ0JLLEdBQXBCLElBQ005QyxFQUROLE9BVUUsT0FQSThDLEdBQVNBLEVBQU1vQyxNQUNqQmxGLEVBQU1vRSxFQUFRLE1BQU8zQixFQUFRLFNBQVVLLEVBQU1vQyxhQUN0Q3BDLEVBQU1vQyxNQUVibEYsRUFBTW9FLEVBQVEsTUFBTzNCLEVBQVEsVUFHeEIvRSxLQUFLMEMsUUFBUS9CLElBQUkyQixFQUFLOEMsR0FDMUJDLE1BQUssU0FBQ2QsR0FBb0QsU0FBSytDLGdCQUFMLE9BRWpFLEVBNUNBLEkseUZDSmtCLEVBQVEsS0FBMUIsSUFJQSxhQUdFLFdBQVk1RSxHQUNWMUMsS0FBSzBDLFFBQVVBLEVBMEJuQixPQXZCRSxZQUFBeUMsS0FBQSxTQUFLQyxHQUFMLFdBQ0UsT0FBT3BGLEtBQUswQyxRQUFRL0IsSUFBSSxlQUFnQnlFLEdBQ3JDQyxNQUFLLFNBQUNkLEdBQThELFNBQUtrRCxxQkFBTCxPQUd6RSxZQUFBakcsT0FBQSxTQUFPZ0MsR0FDTCxPQUFPeEQsS0FBSzBDLFFBQVE0QyxLQUFLLGVBQWdCOUIsR0FDdEM2QixNQUFLLFNBQUNkLEdBQXdELE9BQUtBLGFBQVEsRUFBUkEsRUFBVUMsU0FHbEYsWUFBQWtELE9BQUEsU0FBT0MsRUFBZ0JuRSxHQUNyQixPQUFPeEQsS0FBSzBDLFFBQVFrRixNQUFNLGdCQUFnQkQsRUFBVW5FLEdBQ2pENkIsTUFBSyxTQUFDZCxHQUF1QixPQUFLQSxhQUFRLEVBQVJBLEVBQVVDLFNBR2pELFlBQUFnQixPQUFBLFNBQU9tQyxFQUFnQm5FLEdBQ3JCLE9BQU94RCxLQUFLMEMsUUFBUThDLE9BQU8sZ0JBQWdCbUMsRUFBVW5FLEdBQ2xENkIsTUFBSyxTQUFDZCxHQUF1QixPQUFLQSxhQUFRLEVBQVJBLEVBQVVDLFNBR3pDLFlBQUFpRCxxQkFBUixTQUE2QmxELEdBQzNCLE9BQU9BLEVBQVNDLEtBQUtyQixVQUV6QixFQTlCQSxHLHlGQ0prQixFQUFRLEtBQTFCLElBR0EsYUFHRSxXQUFZVCxHQUNWMUMsS0FBSzBDLFFBQVVBLEVBZ0JuQixPQWJFLFlBQUF5QyxLQUFBLFNBQUtDLEdBQUwsV0FDRSxPQUFPcEYsS0FBSzBDLFFBQVEvQixJQUFJLFVBQVd5RSxHQUNoQ0MsTUFBSyxTQUFDZCxHQUE0QyxTQUFLc0QsaUJBQUwsT0FHdkQsWUFBQWxILElBQUEsU0FBSW1GLEdBQUosV0FDRSxPQUFPOUYsS0FBSzBDLFFBQVEvQixJQUFJLFdBQVdtRixHQUNoQ1QsTUFBSyxTQUFDZCxHQUErQixTQUFLc0QsaUJBQUwsT0FHbEMsWUFBQUEsaUJBQVIsU0FBeUJ0RCxHQUN2QixPQUFPQSxFQUFTQyxNQUVwQixFQXBCQSxHLHVGQ0RBLGlCQUdFLFdBQVk5QixHQUNWMUMsS0FBSzBDLFFBQVVBLEVBb0JuQixPQWpCRSxZQUFBb0YsZUFBQSxTQUFldkQsR0FDYixPQUFJQSxFQUFTQyxLQUNKRCxFQUFTQyxLQUdYRCxHQUdULFlBQUEvQyxPQUFBLFNBQU91RCxFQUFnQnZCLEdBQ3JCLE9BQUlBLEVBQUs2QyxRQUNBckcsS0FBSzBDLFFBQVFxRixVQUFVLE9BQU9oRCxFQUFNLGlCQUFrQnZCLEdBQzVENkIsS0FBS3JGLEtBQUs4SCxnQkFHTjlILEtBQUswQyxRQUFRcUYsVUFBVSxPQUFPaEQsRUFBTSxZQUFhdkIsR0FDckQ2QixLQUFLckYsS0FBSzhILGlCQUVqQixFQXhCQSxHLHVGQ0FBLGlCQUdFLFdBQVlwRixHQUNWMUMsS0FBSzBDLFFBQVVBLEVBbUJuQixPQWhCRSxZQUFBL0IsSUFBQSxTQUFJcUgsRUFBOEJDLEdBQ2hDLElBQU03QyxFQUFRLEdBWWQsT0FWSThDLE1BQU1DLFFBQVFILEtBQ2hCQSxFQUFZQSxFQUFVSSxLQUFLLE1BRzdCaEQsRUFBTTRDLFVBQVlBLEVBRWRDLElBQ0Y3QyxFQUFNaUQsYUFBYyxHQUdmckksS0FBSzBDLFFBQVEvQixJQUFJLG9CQUFxQnlFLEdBQzFDQyxNQUFLLFNBQUNkLEdBQWEsT0FBQUEsRUFBQSxTQUUxQixFQXZCQSxHLGt4RENEQSxnQkFDQSxXQUNBLFlBRUEsWUFJTStELEVBQVcsU0FBQ0MsR0FBb0IsTUFBc0IsaUJBQWZBLEdBQVAsbUJBQXlDQSxFQUFXQyxNQUVwRkMsRUFBdUIsU0FBQzdELEdBQzVCLEdBQW9CLGlCQUFUQSxHQUFxQjBELEVBQVMxRCxHQUFPLE1BQU8sR0FHckQsSUFBQThELEVBR0U5RCxFQUhNLFNBQ1IrRCxFQUVFL0QsRUFGUyxZQUNYZ0UsRUFDRWhFLEVBRFMsWUFHYixnQkFDTThELEVBQVcsQ0FBRUEsU0FBUSxHQUFLLENBQUVBLFNBQVUsU0FDdENDLEdBQWUsQ0FBRUEsWUFBVyxJQUM1QkMsR0FBZSxDQUFFQSxZQUFXLEtBYXBDLGFBT0UsV0FBWXhHLEVBQXlCRixHQUNuQ2xDLEtBQUt1QyxTQUFXSCxFQUFRRyxTQUN4QnZDLEtBQUt5QyxJQUFNTCxFQUFRSyxJQUNuQnpDLEtBQUtzQyxJQUFNRixFQUFRRSxJQUNuQnRDLEtBQUs2SSxRQUFVekcsRUFBUXlHLFNBQVcsR0FDbEM3SSxLQUFLa0MsU0FBV0EsRUFvSXBCLE9BaklRLFlBQUFRLFFBQU4sU0FBY29HLEVBQWdCeEcsRUFBYUYsRyw0R0FzQnhCLE9BckJYMkcsRUFBUSxVQUFRL0ksS0FBS3VDLFNBQVEsSUFBSXZDLEtBQUt5QyxLQUN0Q29HLEVBQVUsRUFBSCxHQUNYRyxjQUFlLFNBQVNELEdBQ3JCL0ksS0FBSzZJLFNBQ0x6RyxhQUFPLEVBQVBBLEVBQVN5RyxTQUdQekcsa0JBQVN5RyxRQUVYQSxFQUFRLHdCQUVKQSxFQUFRLGdCQUdYSSxFQUFTLEVBQUgsR0FBUTdHLElBRWhCQSxhQUFPLEVBQVBBLEVBQVNnRCxRQUFTbkYsT0FBT2lKLG9CQUFvQjlHLGFBQU8sRUFBUEEsRUFBU2dELE9BQU8rRCxPQUFTLElBQ3hFRixFQUFPRyxhQUFlaEgsRUFBUWdELGFBQ3ZCNkQsRUFBTzdELE9BR0MsR0FBTSxVQUNyQixVQUFRcEYsS0FBS3NDLElBQUtBLEdBQUksR0FFcEJ3RyxPQUFRQSxFQUFPTyxvQkFDZlIsUUFBTyxFQUNQUyxpQkFBaUIsR0FDZEwsSyxjQUlGMUUsT0FWQ0EsRUFBVyxlQVVKLEVBQVJBLEVBQVVnRixJQUFYLE9BQ2NoRixhQUFRLEVBQVJBLEVBQVVDLE9BQVE4RCxFQUFTL0QsRUFBU0MsTUFDaEQsSUExRGNnRixFQTBET2pGLEVBQVNDLEtBekRoQ2lGLEVBQWMsR0FDYixJQUFJQyxTQUFRLFNBQUNDLEVBQVNDLEdBQzNCSixFQUFPSyxHQUFHLFFBQVEsU0FBQ0MsR0FBZSxPQUFBTCxFQUFPTSxLQUFQRCxNQUNsQ04sRUFBT0ssR0FBRyxRQUFTRCxHQUNuQkosRUFBT0ssR0FBRyxPQUFPLFdBQU0sT0FBQUYsRUFBUUssT0FBT0MsT0FBT1IsR0FBUVMsU0FBOUIsaUJBb0RMLE0sY0FDWixXLGFBQ0EsU0FBTTNGLGFBQVEsRUFBUkEsRUFBVTRGLFEsT0FBaEIsVyxpQkFFSixNQUpNOUQsRUFBVSxFQUlWLElBQUksVUFBUyxDQUNqQkYsT0FBUTVCLGFBQVEsRUFBUkEsRUFBVTRCLE9BQ2xCQyxXQUFZN0IsYUFBUSxFQUFSQSxFQUFVNkIsV0FDdEI1QixLQUFNLENBQUU2QixRQUFPLEssT0FLWCxPLEtBQUEsR0FBTTlCLGFBQVEsRUFBUkEsRUFBVTRGLFEsT0FEeEIsVUFDRSxFQUFBM0YsS0FBTSxTQUNOLEVBQUEyQixPQUFRNUIsYUFBUSxFQUFSQSxFQUFVNEIsT0FDbEIsSUF2RWlCLElBQUNxRCxFQUNoQkMsU0F5RU4sWUFBQXJFLE1BQUEsU0FBTTBELEVBQWdCeEcsRUFBYThDLEVBQVloRCxHQUM3QyxPQUFPcEMsS0FBSzBDLFFBQVFvRyxFQUFReEcsRUFBRyxHQUFJOEMsTUFBSyxHQUFLaEQsS0FHL0MsWUFBQWdJLFFBQUEsU0FBUXRCLEVBQWdCeEcsRUFBYWtCLEVBQVdwQixHQUM5QyxPQUFPcEMsS0FBSzBDLFFBQVFvRyxFQUFReEcsRUFBRyxHQUM3QnVHLFFBQVMsQ0FBRSxlQUFnQixxQ0FDM0JyRSxLQUFNaEIsR0FDSHBCLEtBSVAsWUFBQXpCLElBQUEsU0FBSTJCLEVBQWE4QyxFQUFhaEQsR0FDNUIsT0FBT3BDLEtBQUtvRixNQUFNLE1BQU85QyxFQUFLOEMsRUFBT2hELElBR3ZDLFlBQUFpSSxLQUFBLFNBQUsvSCxFQUFhOEMsRUFBWWhELEdBQzVCLE9BQU9wQyxLQUFLb0YsTUFBTSxPQUFROUMsRUFBSzhDLEVBQU9oRCxJQUd4QyxZQUFBQSxRQUFBLFNBQVFFLEVBQWE4QyxFQUFZaEQsR0FDL0IsT0FBT3BDLEtBQUtvRixNQUFNLFVBQVc5QyxFQUFLOEMsRUFBT2hELElBRzNDLFlBQUFrRCxLQUFBLFNBQUtoRCxFQUFha0IsRUFBV3BCLEdBQzNCLE9BQU9wQyxLQUFLb0ssUUFBUSxPQUFROUgsRUFBS2tCLEVBQU1wQixJQUd6QyxZQUFBMkYsVUFBQSxTQUFVekYsRUFBYWtCLEdBRXJCLElBQU10QixFQUFxQixJQUFJbEMsS0FBS2tDLFNBbUNwQyxPQTlCQWpDLE9BQU9xSyxLQUFLOUcsR0FDVCtHLFFBQU8sU0FBVTlILEdBQU8sT0FBT2UsRUFBS2YsTUFDcEMrSCxTQUFRLFNBQVUvSCxHQUNqQixHQUFZLGVBQVJBLEVBa0JBeUYsTUFBTUMsUUFBUTNFLEVBQUtmLElBQ3JCZSxFQUFLZixHQUFLK0gsU0FBUSxTQUFVNUYsR0FDMUIxQyxFQUFTdUksT0FBT2hJLEVBQUttQyxNQUd2QjFDLEVBQVN1SSxPQUFPaEksRUFBS2UsRUFBS2YsUUF2QjVCLENBQ0UsSUFBTWlJLEVBQU1sSCxFQUFLK0UsV0FFakIsR0FBSUwsTUFBTUMsUUFBUXVDLEdBQ2hCQSxFQUFJRixTQUFRLFNBQVU1RixHQUNwQixJQUFNcEIsRUFBT29CLEVBQUtwQixLQUFPb0IsRUFBS3BCLEtBQU9vQixFQUMvQnhDLEVBQVVxRyxFQUFxQjdELEdBQ3BDMUMsRUFBaUJ1SSxPQUFPaEksRUFBS2UsRUFBTXBCLFVBRWpDLENBQ0wsSUFBTSxFQUFPa0csRUFBU29DLEdBQU9BLEVBQU1BLEVBQUlsSCxLQUNqQ3BCLEVBQVVxRyxFQUFxQmlDLEdBQ3BDeEksRUFBaUJ1SSxPQUFPaEksRUFBSyxFQUFNTCxRQWVyQ3BDLEtBQUtvSyxRQUFRLE9BQVE5SCxFQUFLSixFQWxDYixDQUNsQjJHLFFBQVMsQ0FBRSxlQUFnQixTQW9DL0IsWUFBQWxELElBQUEsU0FBSXJELEVBQWFrQixFQUFXcEIsR0FDMUIsT0FBT3BDLEtBQUtvSyxRQUFRLE1BQU85SCxFQUFLa0IsRUFBTXBCLElBR3hDLFlBQUF3RixNQUFBLFNBQU10RixFQUFha0IsRUFBV3BCLEdBQzVCLE9BQU9wQyxLQUFLb0ssUUFBUSxRQUFTOUgsRUFBS2tCLEVBQU1wQixJQUcxQyxZQUFBb0QsT0FBQSxTQUFPbEQsRUFBYWtCLEVBQVlwQixHQUM5QixPQUFPcEMsS0FBS29LLFFBQVEsU0FBVTlILEVBQUtrQixFQUFNcEIsSUFFN0MsRUFoSkEsR0FrSkEsVUFBZXVJLEcsMEVDcExmLGlCQUdFLFdBQVlqSSxHQUNWMUMsS0FBSzBDLFFBQVVBLEVBMkJuQixPQXhCRSxZQUFBeUMsS0FBQSxTQUFLQyxHQUNILE9BQU9wRixLQUFLMEMsUUFBUS9CLElBQUksYUFBY3lFLEdBQ25DQyxNQUFLLFNBQUNkLEdBQWEsT0FBQUEsRUFBU0MsS0FBVCxVQUd4QixZQUFBN0QsSUFBQSxTQUFJb0csR0FDRixPQUFPL0csS0FBSzBDLFFBQVEvQixJQUFJLGNBQWNvRyxHQUNuQzFCLE1BQUssU0FBQ2QsR0FBYSxPQUFBQSxFQUFTQyxLQUFULFVBR3hCLFlBQUFoRCxPQUFBLFNBQU9nQyxHQUNMLE9BQU94RCxLQUFLMEMsUUFBUTRDLEtBQUssYUFBYzlCLEdBQ3BDNkIsTUFBSyxTQUFDZCxHQUFhLE9BQUFBLEVBQVNDLEtBQVQsVUFHeEIsWUFBQWtELE9BQUEsU0FBT1gsRUFBWXZELEdBQ2pCLE9BQU94RCxLQUFLMEMsUUFBUWlELElBQUksY0FBY29CLEVBQU12RCxHQUN6QzZCLE1BQUssU0FBQ2QsR0FBYSxPQUFBQSxFQUFBLFNBR3hCLFlBQUFnQixRQUFBLFNBQVF3QixHQUNOLE9BQU8vRyxLQUFLMEMsUUFBUThDLE9BQU8sY0FBY3VCLEdBQ3RDMUIsTUFBSyxTQUFDZCxHQUFhLE9BQUFBLEVBQUEsU0FFMUIsRUEvQkEsRyxtTENGQSxlQUlBLEVBTUUsU0FBWWYsR0FDVnhELEtBQUs0SyxNQUFRLElBQUlDLEtBQUtySCxFQUFLb0gsT0FDM0I1SyxLQUFLOEssSUFBTSxJQUFJRCxLQUFLckgsRUFBS3NILEtBQ3pCOUssS0FBSytLLFdBQWF2SCxFQUFLdUgsV0FDdkIvSyxLQUFLOEMsTUFBUVUsRUFBS1YsTUFBTTZCLEtBQUksU0FBVXFHLEdBRXBDLE9BREFBLEVBQUtDLEtBQU8sSUFBSUosS0FBS0csRUFBS0MsTUFDbkJELE1BS2IsYUFHRSxXQUFZdEksR0FDVjFDLEtBQUswQyxRQUFVQSxFQWdCbkIsT0FiRSxZQUFBd0ksWUFBQSxTQUFZM0csR0FDVixPQUFPLElBQUk0RyxFQUFNNUcsRUFBU0MsT0FHNUIsWUFBQTRHLFVBQUEsU0FBVXJHLEVBQWdCSyxHQUN4QixPQUFPcEYsS0FBSzBDLFFBQVEvQixJQUFJLFVBQVEsTUFBT29FLEVBQVEsZUFBZ0JLLEdBQzVEQyxLQUFLckYsS0FBS2tMLGNBR2YsWUFBQUcsV0FBQSxTQUFXakcsR0FDVCxPQUFPcEYsS0FBSzBDLFFBQVEvQixJQUFJLGtCQUFtQnlFLEdBQ3hDQyxLQUFLckYsS0FBS2tMLGNBRWpCLEVBcEJBLEcsbUxDckJBLGdCQUNBLFdBT01JLEVBQWdCLENBQ3BCekMsUUFBUyxDQUFFLGVBQWdCLHFCQUc3QixFQU9FLFNBQVlyRixHQUNWeEQsS0FBSzZCLEtBQU8sVUFDWjdCLEtBQUt1TCxRQUFVL0gsRUFBSytILFFBQ3BCdkwsS0FBS3dMLE1BQVFoSSxFQUFLZ0ksS0FDbEJ4TCxLQUFLdUcsTUFBUS9DLEVBQUsrQyxNQUNsQnZHLEtBQUtpRSxXQUFhLElBQUk0RyxLQUFLckgsRUFBS1MsYUFJcEMsRUFLRSxTQUFZVCxHQUNWeEQsS0FBSzZCLEtBQU8sYUFDWjdCLEtBQUt1TCxRQUFVL0gsRUFBSytILFFBQ3BCdkwsS0FBS2lFLFdBQWEsSUFBSTRHLEtBQUtySCxFQUFLUyxhQUlwQyxFQU1FLFNBQVlULEdBQ1Z4RCxLQUFLNkIsS0FBTyxlQUNaN0IsS0FBS3VMLFFBQVUvSCxFQUFLK0gsUUFDcEJ2TCxLQUFLeUwsS0FBT2pJLEVBQUtpSSxLQUNqQnpMLEtBQUtpRSxXQUFhLElBQUk0RyxLQUFLckgsRUFBS1MsYUFJcEMsYUFRRSxXQUFZdkIsR0FDVjFDLEtBQUswQyxRQUFVQSxFQUNmMUMsS0FBSzBMLE9BQVMsQ0FDWkMsUUFBU0MsRUFDVEMsV0FBWUMsRUFDWkMsYUFBY0MsR0F1RXBCLE9BbkVFLFlBQUFsRixXQUFBLFNBQVdDLEVBQVlrRixHQUNyQixJQUNRN0csRUFEVSxVQUFJN0IsTUFBTTBJLEdBQVMsR0FDeEIsTUFFYixNQUFPLENBQ0xsRixHQUFFLEVBQ0ZTLEtBQU1wQyxFQUFNb0MsS0FDWitELFFBQVNuRyxFQUFNbUcsUUFDZmpKLElBQUsySixJQUlULFlBQUFoRixnQkFBQSxTQUFnQjFDLEdBQWhCLFdBRUUsT0FEY3RFLE9BQU9pSCxRQUFRM0MsRUFBU0MsS0FBSzJDLFFBQzlCQyxRQUNYLFNBQUNDLEVBQVUsRyxJQUFDTixFQUFFLEtBQUV6RSxFQUFHLEtBRWpCLE9BREErRSxFQUFJTixHQUFNLEVBQUtELFdBQVdDLEVBQUl6RSxHQUN2QitFLElBQ04sS0FHUCxZQUFBNkUsV0FBQSxTQUFXM0gsRUFBaUQ0SCxHQUMxRCxJQUFNM0ksRUFBTyxHQU1iLE9BSkFBLEVBQUtrQixNQUFRSCxFQUFTQyxLQUFLRSxNQUFNQyxLQUFJLFNBQUN5SCxHQUFXLFdBQUlELEVBQUosTUFFakQzSSxFQUFLK0QsTUFBUXZILEtBQUtpSCxnQkFBZ0IxQyxHQUUzQmYsR0FHVCxZQUFBNkksV0FBQSxTQUFXOUgsRUFBeUI0SCxHQUNsQyxPQUFPLElBQUlBLEVBQU01SCxFQUFTQyxPQUc1QixZQUFBVyxLQUFBLFNBQUtKLEVBQWdCbEQsRUFBY3VELEdBQW5DLFdBQ1FrSCxFQUFTdE0sS0FBSzBMLE9BQWU3SixHQUVuQyxPQUFPN0IsS0FBSzBDLFFBQ1QvQixJQUFJLFVBQVEsS0FBTW9FLEVBQVFsRCxHQUFPdUQsR0FDakNDLE1BQUssU0FBQ2QsR0FBb0QsU0FBSzJILFdBQVczSCxFQUFoQixPQUcvRCxZQUFBNUQsSUFBQSxTQUFJb0UsRUFBZ0JsRCxFQUFjMEosR0FBbEMsV0FDUWUsRUFBU3RNLEtBQUswTCxPQUFlN0osR0FFbkMsT0FBTzdCLEtBQUswQyxRQUNUL0IsSUFBSSxVQUFRLEtBQU1vRSxFQUFRbEQsRUFBTTBLLG1CQUFtQmhCLEtBQ25EbEcsTUFBSyxTQUFDZCxHQUE0QixTQUFLOEgsV0FBVzlILEVBQWhCLE9BR3ZDLFlBQUEvQyxPQUFBLFNBQU91RCxFQUFnQmxELEVBQWMyQixHQU1uQyxPQUpLMEUsTUFBTUMsUUFBUTNFLEtBQ2pCQSxFQUFPLENBQUNBLElBR0h4RCxLQUFLMEMsUUFDWDRDLEtBQUssVUFBUSxLQUFNUCxFQUFRbEQsR0FBTzJCLEVBQU04SCxHQUN4Q2pHLE1BQUssU0FBQ2QsR0FBNEIsT0FBQUEsRUFBQSxTQUdyQyxZQUFBZ0IsUUFBQSxTQUFRUixFQUFnQmxELEVBQWMwSixHQUNwQyxPQUFPdkwsS0FBSzBDLFFBQ1g4QyxPQUFPLFVBQVEsS0FBTVQsRUFBUWxELEVBQU0wSyxtQkFBbUJoQixLQUN0RGxHLE1BQUssU0FBQ2QsR0FBNEIsT0FBQUEsRUFBQSxTQUV2QyxFQXBGQSxHLFlBc0ZBMUUsRUFBT0QsUUFBVTRNLEcsMEVDeklqQixpQkFHRSxXQUFZOUosR0FDVjFDLEtBQUswQyxRQUFVQSxFQU9uQixPQUpFLFlBQUEvQixJQUFBLFNBQUk0SyxHQUNGLE9BQU92TCxLQUFLMEMsUUFBUS9CLElBQUksdUJBQXdCLENBQUU0SyxRQUFPLElBQ3REbEcsTUFBSyxTQUFDZCxHQUFhLE9BQUFBLEVBQUEsU0FFMUIsRUFYQSxHLG1MQ0hBLGVBR0EsRUFJRSxTQUFZd0MsRUFBWXZELEdBQ3RCeEQsS0FBSytHLEdBQUtBLEVBQ1YvRyxLQUFLc0MsSUFBTWtCLEVBQUtsQixLQUlwQixhQUdFLFdBQVlJLEdBQ1YxQyxLQUFLMEMsUUFBVUEsRUE4Q25CLE9BM0NFLFlBQUErSixrQkFBQSxTQUFrQmxJLEdBQ2hCLE9BQU9BLEVBQVNDLEtBQUs1QixVQUd2QixZQUFBOEosb0JBQUEsU0FBb0IzRixHQUNsQixPQUFPLFNBQVV4QyxHQUNmLE9BQU8sSUFBSW9JLEVBQVE1RixFQUFJeEMsRUFBU0MsS0FBS29JLFdBSXpDLFlBQUFDLGtCQUFBLFNBQWtCdEksR0FDaEIsTUFBTyxDQUFFaUgsS0FBTWpILEVBQVNDLEtBQUtnSCxLQUFNbkYsUUFBUzlCLEVBQVNDLEtBQUs2QixVQUc1RCxZQUFBbEIsS0FBQSxTQUFLSixFQUFnQkssR0FDbkIsT0FBT3BGLEtBQUswQyxRQUFRL0IsSUFBSSxVQUFRLGNBQWVvRSxFQUFRLFlBQWFLLEdBQ2pFQyxLQUFLckYsS0FBS3lNLG9CQUdmLFlBQUE5TCxJQUFBLFNBQUlvRSxFQUFnQmdDLEdBQ2xCLE9BQU8vRyxLQUFLMEMsUUFBUS9CLElBQUksVUFBUSxjQUFlb0UsRUFBUSxXQUFZZ0MsSUFDaEUxQixLQUFLckYsS0FBSzBNLG9CQUFvQjNGLEtBR25DLFlBQUF2RixPQUFBLFNBQU91RCxFQUFnQmdDLEVBQVl6RSxFQUFhd0ssR0FDOUMsT0FBSUEsRUFDSzlNLEtBQUswQyxRQUFRaUQsSUFBSSxVQUFRLGNBQWVaLEVBQVEsV0FBWWdDLEVBQUksUUFBUyxDQUFFekUsSUFBRyxJQUNsRitDLEtBQUtyRixLQUFLNk0sbUJBR1I3TSxLQUFLMEMsUUFBUTRDLEtBQUssVUFBUSxjQUFlUCxFQUFRLFlBQWEsQ0FBRWdDLEdBQUUsRUFBRXpFLElBQUcsSUFDM0UrQyxLQUFLckYsS0FBSzBNLG9CQUFvQjNGLEtBR25DLFlBQUFXLE9BQUEsU0FBTzNDLEVBQWdCZ0MsRUFBWXpFLEdBQ2pDLE9BQU90QyxLQUFLMEMsUUFBUWlELElBQUksVUFBUSxjQUFlWixFQUFRLFdBQVlnQyxHQUFLLENBQUV6RSxJQUFHLElBQzFFK0MsS0FBS3JGLEtBQUswTSxvQkFBb0IzRixLQUduQyxZQUFBeEIsUUFBQSxTQUFRUixFQUFnQmdDLEdBQ3RCLE9BQU8vRyxLQUFLMEMsUUFBUThDLE9BQU8sVUFBUSxjQUFlVCxFQUFRLFdBQVlnQyxJQUNuRTFCLEtBQUtyRixLQUFLME0sb0JBQW9CM0YsS0FFckMsRUFsREEsRyxzQkNiQyxXQUNDLGFBY0FsSCxFQUFPRCxRQVpQLFNBQWNtTixHQVNaLE9BTklBLGFBQWUvQyxPQUNSK0MsRUFFQS9DLE9BQU9nRCxLQUFLRCxFQUFJN0MsV0FBWSxXQUd6QkEsU0FBUyxXQVozQixJLHFCQ29EQXJLLEVBQU9ELFFBNUNQLFNBQXlCcU4sR0FDckIsSUFBSyxVQUFVSCxLQUFLRyxHQUNoQixNQUFNLElBQUl6TSxVQUFVLG9FQUt4QixNQUFNME0sR0FGTkQsRUFBTUEsRUFBSUUsUUFBUSxTQUFVLEtBRUxDLFFBQVEsS0FDL0IsSUFBb0IsSUFBaEJGLEdBQXFCQSxHQUFjLEVBQ25DLE1BQU0sSUFBSTFNLFVBQVUsdUJBR3hCLE1BQU02TSxFQUFPSixFQUFJSyxVQUFVLEVBQUdKLEdBQVl0RyxNQUFNLEtBQ2hELElBQUkyRyxFQUFVLEdBQ1ZDLEdBQVMsRUFDYixNQUFNM0wsRUFBT3dMLEVBQUssSUFBTSxhQUN4QixJQUFJSSxFQUFXNUwsRUFDZixJQUFLLElBQUk2TCxFQUFJLEVBQUdBLEVBQUlMLEVBQUtsRSxPQUFRdUUsSUFDYixXQUFaTCxFQUFLSyxHQUNMRixHQUFTLEdBR1RDLEdBQVksSUFBSUosRUFBS0ssS0FDZSxJQUFoQ0wsRUFBS0ssR0FBR04sUUFBUSxjQUNoQkcsRUFBVUYsRUFBS0ssR0FBR0osVUFBVSxLQUtuQ0QsRUFBSyxJQUFPRSxFQUFRcEUsU0FDckJzRSxHQUFZLG9CQUNaRixFQUFVLFlBR2QsTUFBTUksRUFBV0gsRUFBUyxTQUFXLFFBQy9CaEssRUFBT29LLFNBQVNYLEVBQUlLLFVBQVVKLEVBQWEsSUFDM0NXLEVBQVM3RCxPQUFPZ0QsS0FBS3hKLEVBQU1tSyxHQU1qQyxPQUpBRSxFQUFPaE0sS0FBT0EsRUFDZGdNLEVBQU9KLFNBQVdBLEVBRWxCSSxFQUFPTixRQUFVQSxFQUNWTSxJLHlCQzNDWDVOLE9BQU9DLGVBQWVOLEVBQVMsYUFBL0IsQ0FBK0NPLE9BQU8sSUFxQnRELE1BQU0yTixFQUFjLElBQUloTixRQU9sQmlOLEVBQVcsSUFBSWpOLFFBUXJCLFNBQVNrTixFQUFHQyxHQUNSLE1BQU1DLEVBQU9KLEVBQVluTixJQUFJc04sR0FNN0IsT0FMQUUsUUFBUUMsT0FDSSxNQUFSRixFQUNBLDhDQUNBRCxHQUVHQyxFQU9YLFNBQVNHLEVBQWM3SyxHQUNTLE1BQXhCQSxFQUFLOEssZ0JBWUo5SyxFQUFLeUssTUFBTU0sYUFJaEIvSyxFQUFLZ0wsVUFBVyxFQUN5QixtQkFBOUJoTCxFQUFLeUssTUFBTVEsZ0JBQ2xCakwsRUFBS3lLLE1BQU1RLGtCQWhCWSxvQkFBWk4sU0FDa0IsbUJBQWxCQSxRQUFRNUgsT0FFZjRILFFBQVE1SCxNQUNKLHFFQUNBL0MsRUFBSzhLLGlCQXlCckIsU0FBU0ksRUFBTUMsRUFBYVYsR0FDeEJILEVBQVl4TSxJQUFJdEIsS0FBTSxDQUNsQjJPLGNBQ0FWLFFBQ0FXLFdBQVksRUFDWkMsY0FBZUYsRUFDZkgsVUFBVSxFQUNWTSxTQUFTLEVBQ1RDLGtCQUFrQixFQUNsQlQsZ0JBQWlCLEtBQ2pCVSxVQUFXZixFQUFNZSxXQUFhbkUsS0FBS29FLFFBSXZDaFAsT0FBT0MsZUFBZUYsS0FBTSxZQUFhLENBQUVHLE9BQU8sRUFBT2EsWUFBWSxJQUdyRSxNQUFNc0osRUFBT3JLLE9BQU9xSyxLQUFLMkQsR0FDekIsSUFBSyxJQUFJUCxFQUFJLEVBQUdBLEVBQUlwRCxFQUFLbkIsU0FBVXVFLEVBQUcsQ0FDbEMsTUFBTWpMLEVBQU02SCxFQUFLb0QsR0FDWGpMLEtBQU96QyxNQUNUQyxPQUFPQyxlQUFlRixLQUFNeUMsRUFBS3lNLEVBQXlCek0sS0F5T3RFLFNBQVN5TSxFQUF5QnpNLEdBQzlCLE1BQU8sQ0FDSCxNQUNJLE9BQU91TCxFQUFHaE8sTUFBTWlPLE1BQU14TCxJQUUxQixJQUFJdEMsR0FDQTZOLEVBQUdoTyxNQUFNaU8sTUFBTXhMLEdBQU90QyxHQUUxQmdCLGNBQWMsRUFDZEgsWUFBWSxHQVVwQixTQUFTbU8sRUFBcUIxTSxHQUMxQixNQUFPLENBQ0gsUUFDSSxNQUFNd0wsRUFBUUQsRUFBR2hPLE1BQU1pTyxNQUN2QixPQUFPQSxFQUFNeEwsR0FBSzJNLE1BQU1uQixFQUFPb0IsWUFFbkNsTyxjQUFjLEVBQ2RILFlBQVksR0FtRHBCLFNBQVNzTyxFQUFXQyxHQUNoQixHQUFhLE1BQVRBLEdBQWlCQSxJQUFVdFAsT0FBT1ksVUFDbEMsT0FBTzZOLEVBR1gsSUFBSWMsRUFBVXpCLEVBQVNwTixJQUFJNE8sR0FLM0IsT0FKZSxNQUFYQyxJQUNBQSxFQS9DUixTQUF1QkMsRUFBV0YsR0FDOUIsTUFBTWpGLEVBQU9ySyxPQUFPcUssS0FBS2lGLEdBQ3pCLEdBQW9CLElBQWhCakYsRUFBS25CLE9BQ0wsT0FBT3NHLEVBSVgsU0FBU0MsRUFBWWYsRUFBYVYsR0FDOUJ3QixFQUFVaE8sS0FBS3pCLEtBQU0yTyxFQUFhVixHQUd0Q3lCLEVBQVk3TyxVQUFZWixPQUFPdUIsT0FBT2lPLEVBQVU1TyxVQUFXLENBQ3ZEOE8sWUFBYSxDQUFFeFAsTUFBT3VQLEVBQWF2TyxjQUFjLEVBQU15TyxVQUFVLEtBSXJFLElBQUssSUFBSWxDLEVBQUksRUFBR0EsRUFBSXBELEVBQUtuQixTQUFVdUUsRUFBRyxDQUNsQyxNQUFNakwsRUFBTTZILEVBQUtvRCxHQUNqQixLQUFNakwsS0FBT2dOLEVBQVU1TyxXQUFZLENBQy9CLE1BQ01nUCxFQUFxQyxtQkFEeEI1UCxPQUFPNlAseUJBQXlCUCxFQUFPOU0sR0FDekJ0QyxNQUNqQ0YsT0FBT0MsZUFDSHdQLEVBQVk3TyxVQUNaNEIsRUFDQW9OLEVBQ01WLEVBQXFCMU0sR0FDckJ5TSxFQUF5QnpNLEtBSzNDLE9BQU9pTixFQWdCT0ssQ0FBY1QsRUFBV3JQLE9BQU8rUCxlQUFlVCxJQUFTQSxHQUNsRXhCLEVBQVN6TSxJQUFJaU8sRUFBT0MsSUFFakJBLEVBcUJYLFNBQVNTLEVBQVVoQyxHQUNmLE9BQU9ELEVBQUdDLEdBQU9jLGlCQWdDckIsU0FBU21CLEVBQW1CakMsRUFBT0ssR0FDL0JOLEVBQUdDLEdBQU9LLGdCQUFrQkEsRUFqWGhDSSxFQUFNN04sVUFBWSxDQUtkLFdBQ0ksT0FBT21OLEVBQUdoTyxNQUFNaU8sTUFBTXBNLE1BTzFCLGFBQ0ksT0FBT21NLEVBQUdoTyxNQUFNMk8sYUFPcEIsb0JBQ0ksT0FBT1gsRUFBR2hPLE1BQU02TyxlQU1wQixlQUNJLE1BQU1BLEVBQWdCYixFQUFHaE8sTUFBTTZPLGNBQy9CLE9BQXFCLE1BQWpCQSxFQUNPLEdBRUosQ0FBQ0EsSUFPWixXQUNJLE9BQU8sR0FPWCxzQkFDSSxPQUFPLEdBT1gsZ0JBQ0ksT0FBTyxHQU9YLHFCQUNJLE9BQU8sR0FPWCxpQkFDSSxPQUFPYixFQUFHaE8sTUFBTTRPLFlBT3BCLGtCQUNJLE1BQU1wTCxFQUFPd0ssRUFBR2hPLE1BRWhCd0QsRUFBS3NMLFNBQVUsRUFDMkIsbUJBQS9CdEwsRUFBS3lLLE1BQU1rQyxpQkFDbEIzTSxFQUFLeUssTUFBTWtDLG1CQVFuQiwyQkFDSSxNQUFNM00sRUFBT3dLLEVBQUdoTyxNQUVoQndELEVBQUtzTCxTQUFVLEVBQ2Z0TCxFQUFLdUwsa0JBQW1CLEVBQzJCLG1CQUF4Q3ZMLEVBQUt5SyxNQUFNbUMsMEJBQ2xCNU0sRUFBS3lLLE1BQU1tQyw0QkFRbkIsY0FDSSxPQUFPQyxRQUFRckMsRUFBR2hPLE1BQU1pTyxNQUFNcUMsVUFPbEMsaUJBQ0ksT0FBT0QsUUFBUXJDLEVBQUdoTyxNQUFNaU8sTUFBTU0sYUFPbEMsaUJBQ0lGLEVBQWNMLEVBQUdoTyxRQU9yQix1QkFDSSxPQUFPZ08sRUFBR2hPLE1BQU13TyxVQU9wQixlQUNJLE9BQU82QixRQUFRckMsRUFBR2hPLE1BQU1pTyxNQUFNc0MsV0FPbEMsZ0JBQ0ksT0FBT3ZDLEVBQUdoTyxNQUFNZ1AsV0FRcEIsaUJBQ0ksT0FBT2hCLEVBQUdoTyxNQUFNMk8sYUFRcEIsbUJBQ0ksT0FBT1gsRUFBR2hPLE1BQU04TyxTQUVwQixpQkFBaUIzTyxHQUNiLElBQUtBLEVBQ0QsT0FFSixNQUFNcUQsRUFBT3dLLEVBQUdoTyxNQUVoQndELEVBQUtzTCxTQUFVLEVBQ3dCLGtCQUE1QnRMLEVBQUt5SyxNQUFNdUMsZUFDbEJoTixFQUFLeUssTUFBTXVDLGNBQWUsSUFTbEMsa0JBQ0ksT0FBUXhDLEVBQUdoTyxNQUFNd08sVUFFckIsZ0JBQWdCck8sR0FDUEEsR0FDRGtPLEVBQWNMLEVBQUdoTyxRQVd6QixlQU1KQyxPQUFPQyxlQUFld08sRUFBTTdOLFVBQVcsY0FBZSxDQUNsRFYsTUFBT3VPLEVBQ1B2TixjQUFjLEVBQ2R5TyxVQUFVLElBSVEsb0JBQVhhLGFBQWtELElBQWpCQSxPQUFPL0IsUUFDL0N6TyxPQUFPeVEsZUFBZWhDLEVBQU03TixVQUFXNFAsT0FBTy9CLE1BQU03TixXQUdwRGtOLEVBQVN6TSxJQUFJbVAsT0FBTy9CLE1BQU03TixVQUFXNk4sSUF3S3pDLE1BQU1pQyxFQUFlLElBQUk3UCxRQVl6QixTQUFTOFAsRUFBU0MsR0FDZCxPQUFhLE9BQU5BLEdBQTJCLGlCQUFOQSxFQVNoQyxTQUFTQyxFQUFhbkMsR0FDbEIsTUFBTW9DLEVBQVlKLEVBQWFoUSxJQUFJZ08sR0FDbkMsR0FBaUIsTUFBYm9DLEVBQ0EsTUFBTSxJQUFJdlEsVUFDTixvRUFHUixPQUFPdVEsRUE0RVgsU0FBU25RLEVBQXFCb1EsRUFBc0JDLEdBQ2hEaFIsT0FBT0MsZUFDSDhRLEVBQ0EsS0FBS0MsSUF0RWIsU0FBd0NBLEdBQ3BDLE1BQU8sQ0FDSCxNQUVJLElBQUlDLEVBRGNKLEVBQWE5USxNQUNWVyxJQUFJc1EsR0FDekIsS0FBZSxNQUFSQyxHQUFjLENBQ2pCLEdBdkNFLElBdUNFQSxFQUFLQyxhQUNMLE9BQU9ELEVBQUtFLFNBRWhCRixFQUFPQSxFQUFLRyxLQUVoQixPQUFPLE1BR1gsSUFBSUQsR0FDd0IsbUJBQWJBLEdBQTRCUixFQUFTUSxLQUM1Q0EsRUFBVyxNQUVmLE1BQU1MLEVBQVlELEVBQWE5USxNQUcvQixJQUFJc1IsRUFBTyxLQUNQSixFQUFPSCxFQUFVcFEsSUFBSXNRLEdBQ3pCLEtBQWUsTUFBUkMsR0F4REQsSUF5REVBLEVBQUtDLGFBRVEsT0FBVEcsRUFDQUEsRUFBS0QsS0FBT0gsRUFBS0csS0FDSSxPQUFkSCxFQUFLRyxLQUNaTixFQUFVelAsSUFBSTJQLEVBQVdDLEVBQUtHLE1BRTlCTixFQUFVdkwsT0FBT3lMLEdBR3JCSyxFQUFPSixFQUdYQSxFQUFPQSxFQUFLRyxLQUloQixHQUFpQixPQUFiRCxFQUFtQixDQUNuQixNQUFNRyxFQUFVLENBQ1pILFdBQ0FELGFBN0VGLEVBOEVFSyxTQUFTLEVBQ1RDLE1BQU0sRUFDTkosS0FBTSxNQUVHLE9BQVRDLEVBQ0FQLEVBQVV6UCxJQUFJMlAsRUFBV00sR0FFekJELEVBQUtELEtBQU9FLElBSXhCcFEsY0FBYyxFQUNkSCxZQUFZLEdBY1owUSxDQUErQlQsSUFVdkMsU0FBU1UsRUFBd0JDLEdBRTdCLFNBQVNDLElBQ0x2UixFQUFZbUIsS0FBS3pCLE1BR3JCNlIsRUFBa0JoUixVQUFZWixPQUFPdUIsT0FBT2xCLEVBQVlPLFVBQVcsQ0FDL0Q4TyxZQUFhLENBQ1R4UCxNQUFPMFIsRUFDUDFRLGNBQWMsRUFDZHlPLFVBQVUsS0FJbEIsSUFBSyxJQUFJbEMsRUFBSSxFQUFHQSxFQUFJa0UsRUFBV3pJLFNBQVV1RSxFQUNyQzlNLEVBQXFCaVIsRUFBa0JoUixVQUFXK1EsRUFBV2xFLElBR2pFLE9BQU9tRSxFQWdCWCxTQUFTdlIsSUFFTCxLQUFJTixnQkFBZ0JNLEdBQXBCLENBSUEsR0FBeUIsSUFBckIrTyxVQUFVbEcsUUFBZ0JqQixNQUFNQyxRQUFRa0gsVUFBVSxJQUNsRCxPQUFPc0MsRUFBd0J0QyxVQUFVLElBRTdDLEdBQUlBLFVBQVVsRyxPQUFTLEVBQUcsQ0FDdEIsTUFBTTJJLEVBQVEsSUFBSTVKLE1BQU1tSCxVQUFVbEcsUUFDbEMsSUFBSyxJQUFJdUUsRUFBSSxFQUFHQSxFQUFJMkIsVUFBVWxHLFNBQVV1RSxFQUNwQ29FLEVBQU1wRSxHQUFLMkIsVUFBVTNCLEdBRXpCLE9BQU9pRSxFQUF3QkcsR0FFbkMsTUFBTSxJQUFJdFIsVUFBVSxxQ0FiaEJtUSxFQUFhclAsSUFBSXRCLEtBQU0sSUFBSStSLEtBa0JuQ3pSLEVBQVlPLFVBQVksQ0FRcEIsaUJBQWlCb1EsRUFBV0csRUFBVWhQLEdBQ2xDLEdBQWdCLE1BQVpnUCxFQUNBLE9BRUosR0FBd0IsbUJBQWJBLElBQTRCUixFQUFTUSxHQUM1QyxNQUFNLElBQUk1USxVQUFVLGlEQUd4QixNQUFNdVEsRUFBWUQsRUFBYTlRLE1BQ3pCZ1MsRUFBZXBCLEVBQVN4TyxHQUl4QitPLEdBSFVhLEVBQ1YzQixRQUFRak8sRUFBUTZQLFNBQ2hCNUIsUUFBUWpPLElBL0xOLEVBQ0QsRUFnTURtUCxFQUFVLENBQ1pILFdBQ0FELGVBQ0FLLFFBQVNRLEdBQWdCM0IsUUFBUWpPLEVBQVFvUCxTQUN6Q0MsS0FBTU8sR0FBZ0IzQixRQUFRak8sRUFBUXFQLE1BQ3RDSixLQUFNLE1BSVYsSUFBSUgsRUFBT0gsRUFBVXBRLElBQUlzUSxHQUN6QixRQUFhaUIsSUFBVGhCLEVBRUEsWUFEQUgsRUFBVXpQLElBQUkyUCxFQUFXTSxHQUs3QixJQUFJRCxFQUFPLEtBQ1gsS0FBZSxNQUFSSixHQUFjLENBQ2pCLEdBQ0lBLEVBQUtFLFdBQWFBLEdBQ2xCRixFQUFLQyxlQUFpQkEsRUFHdEIsT0FFSkcsRUFBT0osRUFDUEEsRUFBT0EsRUFBS0csS0FJaEJDLEVBQUtELEtBQU9FLEdBVWhCLG9CQUFvQk4sRUFBV0csRUFBVWhQLEdBQ3JDLEdBQWdCLE1BQVpnUCxFQUNBLE9BR0osTUFBTUwsRUFBWUQsRUFBYTlRLE1BSXpCbVIsR0FIVVAsRUFBU3hPLEdBQ25CaU8sUUFBUWpPLEVBQVE2UCxTQUNoQjVCLFFBQVFqTyxJQWpQTixFQUNELEVBbVBQLElBQUlrUCxFQUFPLEtBQ1BKLEVBQU9ILEVBQVVwUSxJQUFJc1EsR0FDekIsS0FBZSxNQUFSQyxHQUFjLENBQ2pCLEdBQ0lBLEVBQUtFLFdBQWFBLEdBQ2xCRixFQUFLQyxlQUFpQkEsRUFTdEIsWUFQYSxPQUFURyxFQUNBQSxFQUFLRCxLQUFPSCxFQUFLRyxLQUNJLE9BQWRILEVBQUtHLEtBQ1pOLEVBQVV6UCxJQUFJMlAsRUFBV0MsRUFBS0csTUFFOUJOLEVBQVV2TCxPQUFPeUwsSUFLekJLLEVBQU9KLEVBQ1BBLEVBQU9BLEVBQUtHLE9BU3BCLGNBQWNwRCxHQUNWLEdBQWEsTUFBVEEsR0FBdUMsaUJBQWZBLEVBQU1wTSxLQUM5QixNQUFNLElBQUlyQixVQUFVLG9DQUl4QixNQUFNdVEsRUFBWUQsRUFBYTlRLE1BQ3pCaVIsRUFBWWhELEVBQU1wTSxLQUN4QixJQUFJcVAsRUFBT0gsRUFBVXBRLElBQUlzUSxHQUN6QixHQUFZLE1BQVJDLEVBQ0EsT0FBTyxFQUlYLE1BQU1pQixFQTlWZCxTQUFtQnhELEVBQWFWLEdBRTVCLE9BQU8sSUFEU3FCLEVBQVdyUCxPQUFPK1AsZUFBZS9CLElBQzFDLENBQVlVLEVBQWFWLEdBNFZQbUUsQ0FBVXBTLEtBQU1pTyxHQUlyQyxJQUFJcUQsRUFBTyxLQUNYLEtBQWUsTUFBUkosR0FBYyxDQW1CakIsR0FqQklBLEVBQUtPLEtBQ1EsT0FBVEgsRUFDQUEsRUFBS0QsS0FBT0gsRUFBS0csS0FDSSxPQUFkSCxFQUFLRyxLQUNaTixFQUFVelAsSUFBSTJQLEVBQVdDLEVBQUtHLE1BRTlCTixFQUFVdkwsT0FBT3lMLEdBR3JCSyxFQUFPSixFQUlYaEIsRUFDSWlDLEVBQ0FqQixFQUFLTSxRQUFVTixFQUFLRSxTQUFXLE1BRU4sbUJBQWxCRixFQUFLRSxTQUNaLElBQ0lGLEVBQUtFLFNBQVMzUCxLQUFLekIsS0FBTW1TLEdBQzNCLE1BQU9FLEdBRWtCLG9CQUFabEUsU0FDa0IsbUJBQWxCQSxRQUFRNUgsT0FFZjRILFFBQVE1SCxNQUFNOEwsUUEzVHBCLElBK1RGbkIsRUFBS0MsY0FDZ0MsbUJBQTlCRCxFQUFLRSxTQUFTa0IsYUFFckJwQixFQUFLRSxTQUFTa0IsWUFBWUgsR0FJOUIsR0FBSWxDLEVBQVVrQyxHQUNWLE1BR0pqQixFQUFPQSxFQUFLRyxLQU1oQixPQUpBbkIsRUFBbUJpQyxFQUFjLE1Belh6QyxTQUF1QmxFLEVBQU9XLEdBQzFCWixFQUFHQyxHQUFPVyxXQXlYc0IsRUFBNUIyRCxDQUFjSixHQS9XdEIsU0FBMEJsRSxFQUFPWSxHQUM3QmIsRUFBR0MsR0FBT1ksY0ErV3lCLEtBQS9CMkQsQ0FBaUJMLElBRVRBLEVBQWFNLG1CQUs3QnhTLE9BQU9DLGVBQWVJLEVBQVlPLFVBQVcsY0FBZSxDQUN4RFYsTUFBT0csRUFDUGEsY0FBYyxFQUNkeU8sVUFBVSxJQUtRLG9CQUFYYSxhQUN1QixJQUF2QkEsT0FBT25RLGFBRWRMLE9BQU95USxlQUFlcFEsRUFBWU8sVUFBVzRQLE9BQU9uUSxZQUFZTyxXQUdwRWpCLEVBQVFnQixxQkFBdUJBLEVBQy9CaEIsRUFBUVUsWUFBY0EsRUFDdEJWLEVBQVFvQyxRQUFVMUIsRUFFbEJULEVBQU9ELFFBQVVVLEVBQ2pCVCxFQUFPRCxRQUFRVSxZQUFjVCxFQUFPRCxRQUFQLFFBQTRCVSxFQUN6RFQsRUFBT0QsUUFBUWdCLHFCQUF1QkEsRyxhQ3IyQnRDLE1BQU0sU0FBQzhSLEdBQVksRUFBUSxLQUtyQkMsRUFBSyxJQUFJN1IsUUFZZixNQUFNOFIsRUFTTCxZQUFZQyxFQUFZLEdBQUl6USxFQUFVLENBQUNQLEtBQU0sS0FDNUMsSUFBSWlSLEVBQU8sRUFFWCxNQUFNQyxFQUFRRixFQUFVbE8sS0FBSXFPLElBQzNCLElBQUluRixFQWNKLE9BWkNBLEVBREdtRixhQUFtQmhKLE9BQ2JnSixFQUNDQyxZQUFZQyxPQUFPRixHQUNwQmhKLE9BQU9nRCxLQUFLZ0csRUFBUW5GLE9BQVFtRixFQUFRRyxXQUFZSCxFQUFRSSxZQUN2REosYUFBbUJDLFlBQ3BCakosT0FBT2dELEtBQUtnRyxHQUNYQSxhQUFtQkosRUFDcEJJLEVBRUFoSixPQUFPZ0QsS0FBd0IsaUJBQVpnRyxFQUF1QkEsRUFBVUssT0FBT0wsSUFHckVGLEdBQVFqRixFQUFPMUUsUUFBVTBFLEVBQU9pRixNQUFRLEVBQ2pDakYsS0FHRmhNLE9BQXdCcVEsSUFBakI5UCxFQUFRUCxLQUFxQixHQUFLd1IsT0FBT2pSLEVBQVFQLE1BQU15UixjQUVwRVgsRUFBR3JSLElBQUl0QixLQUFNLENBQ1o2QixLQUFNLG1CQUFtQmlMLEtBQUtqTCxHQUFRLEdBQUtBLEVBQzNDaVIsT0FDQUMsVUFRRixXQUNDLE9BQU9KLEVBQUdoUyxJQUFJWCxNQUFNOFMsS0FNckIsV0FDQyxPQUFPSCxFQUFHaFMsSUFBSVgsTUFBTTZCLEtBVXJCLGFBQ0MsT0FBT21JLE9BQU9nRCxXQUFXaE4sS0FBS3VULGVBQWVySixXQVU5QyxvQkFDQyxNQUFNMUcsRUFBTyxJQUFJZ1EsV0FBV3hULEtBQUs4UyxNQUNqQyxJQUFJVyxFQUFTLEVBQ2IsVUFBVyxNQUFNM0osS0FBUzlKLEtBQUt3SixTQUM5QmhHLEVBQUtsQyxJQUFJd0ksRUFBTzJKLEdBQ2hCQSxHQUFVM0osRUFBTVgsT0FHakIsT0FBTzNGLEVBQUtxSyxPQVNiLFNBQ0MsT0FBTzZFLEVBQVMxRixLQXBHbEIwRyxnQkFBc0JYLEdBQ3JCLElBQUssTUFBTVksS0FBUVosRUFDZCxXQUFZWSxRQUNQQSxFQUFLbkssZUFFUG1LLEVBK0ZjQyxDQUFLakIsRUFBR2hTLElBQUlYLE1BQU0rUyxRQVl4QyxNQUFNbkksRUFBUSxFQUFHRSxFQUFNOUssS0FBSzhTLEtBQU1qUixFQUFPLElBQ3hDLE1BQU0sS0FBQ2lSLEdBQVE5UyxLQUVmLElBQUk2VCxFQUFnQmpKLEVBQVEsRUFBSWtKLEtBQUtDLElBQUlqQixFQUFPbEksRUFBTyxHQUFLa0osS0FBS0UsSUFBSXBKLEVBQU9rSSxHQUN4RW1CLEVBQWNuSixFQUFNLEVBQUlnSixLQUFLQyxJQUFJakIsRUFBT2hJLEVBQUssR0FBS2dKLEtBQUtFLElBQUlsSixFQUFLZ0ksR0FFcEUsTUFBTW9CLEVBQU9KLEtBQUtDLElBQUlFLEVBQWNKLEVBQWUsR0FDN0NkLEVBQVFKLEVBQUdoUyxJQUFJWCxNQUFNK1MsTUFBTW9CLFNBQzNCdEIsRUFBWSxHQUNsQixJQUFJdUIsRUFBUSxFQUVaLElBQUssTUFBTVQsS0FBUVosRUFBTyxDQUN6QixNQUFNRCxFQUFPRyxZQUFZQyxPQUFPUyxHQUFRQSxFQUFLUCxXQUFhTyxFQUFLYixLQUMvRCxHQUFJZSxHQUFpQmYsR0FBUWUsRUFHNUJBLEdBQWlCZixFQUNqQm1CLEdBQWVuQixNQUNULENBQ04sTUFBTWhKLEVBQVE2SixFQUFLVSxNQUFNUixFQUFlQyxLQUFLRSxJQUFJbEIsRUFBTW1CLElBTXZELEdBTEFwQixFQUFVOUksS0FBS0QsR0FDZnNLLEdBQVNuQixZQUFZQyxPQUFPcEosR0FBU0EsRUFBTXNKLFdBQWF0SixFQUFNZ0osS0FDOURlLEVBQWdCLEVBR1pPLEdBQVNGLEVBQ1osT0FLSCxNQUFNSSxFQUFPLElBQUkxQixFQUFLLEdBQUksQ0FBQy9RLFNBRzNCLE9BRkE1QixPQUFPc1UsT0FBTzVCLEVBQUdoUyxJQUFJMlQsR0FBTyxDQUFDeEIsS0FBTW9CLEVBQU1uQixNQUFPRixJQUV6Q3lCLEVBR1IzVCxJQUFLTSxPQUFPQyxlQUNYLE1BQU8sT0FHUixPQUFRRCxPQUFPdVQsYUFBYUMsR0FDM0IsTUFDbUIsaUJBQVhBLEdBQ2tCLG1CQUFsQkEsRUFBT2pMLFFBQ1csSUFBekJpTCxFQUFPakwsT0FBT0wsUUFDZ0IsbUJBQXZCc0wsRUFBTzlFLGFBQ2QsZ0JBQWdCN0MsS0FBSzJILEVBQU94VCxPQUFPQyxlQUt0Q2pCLE9BQU9jLGlCQUFpQjZSLEVBQUsvUixVQUFXLENBQ3ZDaVMsS0FBTSxDQUFDOVIsWUFBWSxHQUNuQmEsS0FBTSxDQUFDYixZQUFZLEdBQ25CcVQsTUFBTyxDQUFDclQsWUFBWSxLQUdyQm5CLEVBQU9ELFFBQVVnVCxHLDJCQ2hMakIsTUFBTThCLEVBQVEsRUFBUSxLQUNoQnRULEVBQWtCLEVBQVEsS0F3QmhDLEdBcEJLdVQsT0FBT0QsUUFDWEMsT0FBT0QsTUFBUSxDQUFDcFMsRUFBS0YsSUFBWXNTLEVBQU1wUyxFQUFLLENBQUNzUyxjQUh4QixPQUd5RHhTLEtBRzFFdVMsT0FBT0UsVUFDWEYsT0FBT0UsUUFBVUgsRUFBTUcsU0FHbkJGLE9BQU9oSyxVQUNYZ0ssT0FBT2hLLFFBQVUrSixFQUFNL0osU0FHbkJnSyxPQUFPRyxXQUNYSCxPQUFPRyxTQUFXSixFQUFNSSxVQUdwQkgsT0FBT3ZULGtCQUNYdVQsT0FBT3ZULGdCQUFrQkEsSUFHckJ1VCxPQUFPSSxlQUNYLElBQ0NKLE9BQU9JLGVBQWlCLEVBQVEsS0FDL0IsTUFBT0MsSUFHVm5WLEVBQU9ELFFBQVUsRUFBakIsTSxnQkNoQ0MsSUFBa0JELElBSVgsV0FBZSxhQUl0QixNQUFNc1YsRUFBVSxHQUVWQyxFQUFZQyxHQUVHLG9CQUFUQyxNQUF3QkEsTUFBUUQsS0FBWUMsS0FDL0NBLEtBSWMsb0JBQVgzRSxRQUEwQkEsUUFBVTBFLEtBQVkxRSxPQUNuREEsT0FHYyxvQkFBWGtFLFFBQTBCQSxRQUFVUSxLQUFZUixPQUNuREEsT0FJa0Isb0JBQWZVLFlBQThCQSxXQUNqQ0EsZ0JBRFIsRUFLS0MsRUFBbUIsQ0FDeEIsVUFDQSxVQUNBLFdBQ0EsaUJBQ0EsUUFDQSxrQkFDQSxZQUdELElBQUssTUFBTUgsS0FBWUcsRUFDdEJyVixPQUFPQyxlQUFlK1UsRUFBU0UsRUFBVSxDQUN4QyxNQUNDLE1BQU1JLEVBQWVMLEVBQVVDLEdBQ3pCaFYsRUFBUW9WLEdBQWdCQSxFQUFhSixHQUMzQyxNQUF3QixtQkFBVmhWLEVBQXVCQSxFQUFNcVYsS0FBS0QsR0FBZ0JwVixLQUtuRSxNQUFNeVEsRUFBV3pRLEdBQW1CLE9BQVZBLEdBQW1DLGlCQUFWQSxFQUM3Q3NWLEVBQTZELG1CQUE1QlIsRUFBUTdULGdCQUN6Q3NVLEVBQW9ELG1CQUEzQlQsRUFBUUYsZUFDakNZLEVBQStDLG1CQUFyQlYsRUFBUWhULFNBRWxDMlQsRUFBZSxDQUFDQyxFQUFTQyxLQUM5QixNQUFNQyxFQUFTLElBQUlkLEVBQVFKLFFBQVFnQixHQUFXLElBQ3hDRyxFQUFvQkYsYUFBbUJiLEVBQVFKLFFBQy9Db0IsRUFBUyxJQUFJaEIsRUFBUUosUUFBUWlCLEdBQVcsSUFFOUMsSUFBSyxNQUFPclQsRUFBS3RDLEtBQVU4VixFQUNyQkQsR0FBK0IsY0FBVjdWLFFBQW9DK1IsSUFBVi9SLEVBQ25ENFYsRUFBT3ZRLE9BQU8vQyxHQUVkc1QsRUFBT3pVLElBQUltQixFQUFLdEMsR0FJbEIsT0FBTzRWLEdBR0ZHLEVBQVksSUFBSUMsS0FDckIsSUFBSUMsRUFBYyxHQUNkdk4sRUFBVSxHQUVkLElBQUssTUFBTW9OLEtBQVVFLEVBQVMsQ0FDN0IsR0FBSWpPLE1BQU1DLFFBQVE4TixHQUNYL04sTUFBTUMsUUFBUWlPLEtBQ25CQSxFQUFjLElBR2ZBLEVBQWMsSUFBSUEsS0FBZ0JILFFBQzVCLEdBQUlyRixFQUFTcUYsR0FBUyxDQUM1QixJQUFLLElBQUt4VCxFQUFLdEMsS0FBVUYsT0FBT2lILFFBQVErTyxHQUNuQ3JGLEVBQVN6USxJQUFXc0MsS0FBTzJULElBQzlCalcsRUFBUStWLEVBQVVFLEVBQVkzVCxHQUFNdEMsSUFHckNpVyxFQUFjLElBQUlBLEVBQWEsQ0FBQzNULEdBQU10QyxHQUduQ3lRLEVBQVNxRixFQUFPcE4sV0FDbkJBLEVBQVUrTSxFQUFhL00sRUFBU29OLEVBQU9wTixVQUl6Q3VOLEVBQVl2TixRQUFVQSxFQUd2QixPQUFPdU4sR0FHRkMsRUFBaUIsQ0FDdEIsTUFDQSxPQUNBLE1BQ0EsUUFDQSxPQUNBLFVBR0tDLEVBQWdCLENBQ3JCbk0sS0FBTSxtQkFDTm9NLEtBQU0sU0FDTnJVLFNBQVUsc0JBQ1ZxUixZQUFhLE1BQ2JlLEtBQU0sT0FzQkRrQyxFQUF3QixDQUM3QixJQUNBLElBQ0EsS0FHS0MsRUFBT3hWLE9BQU8sUUFFcEIsTUFBTXlWLFVBQWtCbFUsTUFDdkIsWUFBWStCLEdBR1hoRSxNQUNDZ0UsRUFBUzZCLFlBQ1RpTixPQUNzQixJQUFwQjlPLEVBQVM0QixRQUFnQjVCLEVBQVM0QixPQUNsQzVCLEVBQVM0QixPQUFTLDJCQUdyQm5HLEtBQUsyRCxLQUFPLFlBQ1ozRCxLQUFLdUUsU0FBV0EsR0FJbEIsTUFBTW9TLFVBQXFCblUsTUFDMUIsWUFBWUUsR0FDWG5DLE1BQU0scUJBQ05QLEtBQUsyRCxLQUFPLGVBQ1ozRCxLQUFLMEMsUUFBVUEsR0FJakIsTUFBTWtVLEVBQVFDLEdBQU0sSUFBSW5OLFNBQVFDLEdBQVdtTixXQUFXbk4sRUFBU2tOLEtBdUJ6REUsRUFBeUJDLEdBQVNYLEVBQWVZLFNBQVNELEdBQVNBLEVBQU1FLGNBQWdCRixFQUV6RkcsRUFBc0IsQ0FDM0JDLE1BQU8sRUFDUEMsUUE5RW9CLENBQ3BCLE1BQ0EsTUFDQSxPQUNBLFNBQ0EsVUFDQSxTQXlFQUMsWUF0RXdCLENBQ3hCLElBQ0EsSUFDQSxJQUNBLElBQ0EsSUFDQSxJQUNBLEtBZ0VBQyxpQkFBa0JmLEdBR2JnQixFQUF3QixDQUFDQyxFQUFRLE1BQ3RDLEdBQXFCLGlCQUFWQSxFQUNWLE1BQU8sSUFDSE4sRUFDSEMsTUFBT0ssR0FJVCxHQUFJQSxFQUFNSixVQUFZblAsTUFBTUMsUUFBUXNQLEVBQU1KLFNBQ3pDLE1BQU0sSUFBSTdVLE1BQU0sa0NBR2pCLEdBQUlpVixFQUFNSCxjQUFnQnBQLE1BQU1DLFFBQVFzUCxFQUFNSCxhQUM3QyxNQUFNLElBQUk5VSxNQUFNLHNDQUdqQixNQUFPLElBQ0gyVSxLQUNBTSxFQUNIRixpQkFBa0JmLElBS2RrQixFQUFpQixXQUV2QixNQUFNQyxFQUNMLFlBQVlYLEVBQU81VSxFQUFVLElBcUI1QixHQXBCQXBDLEtBQUs0WCxZQUFjLEVBQ25CNVgsS0FBSzZYLE9BQVNiLEVBQ2RoWCxLQUFLOFgsU0FBVyxDQUVmQyxZQUFhL1gsS0FBSzZYLE9BQU9FLGFBQWUsaUJBQ3JDM1YsRUFDSHlHLFFBQVMrTSxFQUFhNVYsS0FBSzZYLE9BQU9oUCxRQUFTekcsRUFBUXlHLFNBQ25EbVAsTUFBTzlCLEVBQVUsQ0FDaEIrQixjQUFlLEdBQ2ZDLFlBQWEsR0FDYkMsY0FBZSxJQUNiL1YsRUFBUTRWLE9BQ1hsUCxPQUFRaU8sRUFBdUIzVSxFQUFRMEcsUUFBVTlJLEtBQUs2WCxPQUFPL08sUUFDN0RzUCxVQUFXL0UsT0FBT2pSLEVBQVFnVyxXQUFhLElBQ3ZDWCxNQUFPRCxFQUFzQnBWLEVBQVFxVixPQUNyQ25PLGlCQUE2QyxJQUE1QmxILEVBQVFrSCxnQkFDekIrTyxhQUFvQyxJQUFwQmpXLEVBQVFpVyxRQUEwQixJQUFRalcsRUFBUWlXLFFBQ2xFM0QsTUFBT3RTLEVBQVFzUyxPQUFTTyxFQUFRUCxPQUdOLGlCQUFoQjFVLEtBQUs2WCxVQUF5QjdYLEtBQUs2WCxrQkFBa0JTLEtBQU90WSxLQUFLNlgsa0JBQWtCNUMsRUFBUXRLLFNBQ3JHLE1BQU0sSUFBSW5LLFVBQVUsNkNBR3JCLEdBQUlSLEtBQUs4WCxTQUFTTSxXQUFvQyxpQkFBaEJwWSxLQUFLNlgsT0FBcUIsQ0FDL0QsR0FBSTdYLEtBQUs2WCxPQUFPVSxXQUFXLEtBQzFCLE1BQU0sSUFBSS9WLE1BQU0sOERBR1p4QyxLQUFLOFgsU0FBU00sVUFBVUksU0FBUyxPQUNyQ3hZLEtBQUs4WCxTQUFTTSxXQUFhLEtBRzVCcFksS0FBSzZYLE9BQVM3WCxLQUFLOFgsU0FBU00sVUFBWXBZLEtBQUs2WCxPQWdCOUMsR0FiSXBDLElBQ0h6VixLQUFLeVksZ0JBQWtCLElBQUl4RCxFQUFRN1QsZ0JBQy9CcEIsS0FBSzhYLFNBQVN2VyxRQUNqQnZCLEtBQUs4WCxTQUFTdlcsT0FBT21YLGlCQUFpQixTQUFTLEtBQzlDMVksS0FBS3lZLGdCQUFnQjFXLFdBSXZCL0IsS0FBSzhYLFNBQVN2VyxPQUFTdkIsS0FBS3lZLGdCQUFnQmxYLFFBRzdDdkIsS0FBSzBDLFFBQVUsSUFBSXVTLEVBQVF0SyxRQUFRM0ssS0FBSzZYLE9BQVE3WCxLQUFLOFgsVUFFakQ5WCxLQUFLOFgsU0FBUzFPLGFBQWMsQ0FDL0IsTUFBTUEsRUFBZSxJQUFNLElBQUl1UCxnQkFBZ0IzWSxLQUFLOFgsU0FBUzFPLGNBQWNjLFdBQ3JFNUgsRUFBTXRDLEtBQUswQyxRQUFRSixJQUFJNkssUUFBUSxvQkFBcUIvRCxLQUdwRHVNLEdBQW9CM1YsS0FBSzhYLFNBQVN0VCxnQkFBZ0J5USxFQUFRaFQsVUFBYWpDLEtBQUs4WCxTQUFTdFQsZ0JBQWdCbVUsa0JBQXNCM1ksS0FBSzhYLFNBQVNqUCxTQUFXN0ksS0FBSzhYLFNBQVNqUCxRQUFRLGlCQUMvSzdJLEtBQUswQyxRQUFRbUcsUUFBUXJELE9BQU8sZ0JBRzdCeEYsS0FBSzBDLFFBQVUsSUFBSXVTLEVBQVF0SyxRQUFRLElBQUlzSyxFQUFRdEssUUFBUXJJLEVBQUt0QyxLQUFLMEMsU0FBVTFDLEtBQUs4WCxlQUd0RDVGLElBQXZCbFMsS0FBSzhYLFNBQVMzTixPQUNqQm5LLEtBQUs4WCxTQUFTdFQsS0FBT29VLEtBQUtDLFVBQVU3WSxLQUFLOFgsU0FBUzNOLE1BQ2xEbkssS0FBSzBDLFFBQVFtRyxRQUFRdkgsSUFBSSxlQUFnQixvQkFDekN0QixLQUFLMEMsUUFBVSxJQUFJdVMsRUFBUXRLLFFBQVEzSyxLQUFLMEMsUUFBUyxDQUFDOEIsS0FBTXhFLEtBQUs4WCxTQUFTdFQsUUFHdkUsTUFBTXNVLEVBQUtwRixVQUNWLEdBQUkxVCxLQUFLOFgsU0FBU08sUUFBVVgsRUFDM0IsTUFBTSxJQUFJcUIsV0FBVyxnRUFHaEJuQyxFQUFNLEdBQ1osSUFBSXJTLFFBQWlCdkUsS0FBS2daLFNBRTFCLElBQUssTUFBTUMsS0FBUWpaLEtBQUs4WCxTQUFTRSxNQUFNRyxjQUFlLENBRXJELE1BQU1lLFFBQXlCRCxFQUM5QmpaLEtBQUswQyxRQUNMMUMsS0FBSzhYLFNBQ0w5WCxLQUFLbVosa0JBQWtCNVUsRUFBUzZVLFVBRzdCRixhQUE0QmpFLEVBQVFILFdBQ3ZDdlEsRUFBVzJVLEdBTWIsR0FGQWxaLEtBQUttWixrQkFBa0I1VSxJQUVsQkEsRUFBU2dGLElBQU12SixLQUFLOFgsU0FBU3hPLGdCQUNqQyxNQUFNLElBQUlvTixFQUFVblMsR0FLckIsR0FBSXZFLEtBQUs4WCxTQUFTdUIsbUJBQW9CLENBQ3JDLEdBQWdELG1CQUFyQ3JaLEtBQUs4WCxTQUFTdUIsbUJBQ3hCLE1BQU0sSUFBSTdZLFVBQVUsc0RBR3JCLElBQUtrVixFQUNKLE1BQU0sSUFBSWxULE1BQU0sK0VBR2pCLE9BQU94QyxLQUFLc1osUUFBUS9VLEVBQVM2VSxRQUFTcFosS0FBSzhYLFNBQVN1QixvQkFHckQsT0FBTzlVLEdBSUZ3UixFQURvQi9WLEtBQUs4WCxTQUFTTCxNQUFNSixRQUFRSixTQUFTalgsS0FBSzBDLFFBQVFvRyxPQUFPd0ssZUFDaER0VCxLQUFLdVosT0FBT1QsR0FBTUEsSUFFckQsSUFBSyxNQUFPalgsRUFBTTJYLEtBQWF2WixPQUFPaUgsUUFBUW9QLEdBQzdDUCxFQUFPbFUsR0FBUTZSLFVBQ2QxVCxLQUFLMEMsUUFBUW1HLFFBQVF2SCxJQUFJLFNBQVV0QixLQUFLMEMsUUFBUW1HLFFBQVFsSSxJQUFJLFdBQWE2WSxHQUV6RSxNQUFNalYsU0FBa0J3UixHQUFRcUQsUUFFaEMsR0FBYSxTQUFUdlgsRUFBaUIsQ0FDcEIsR0FBd0IsTUFBcEIwQyxFQUFTNEIsT0FDWixNQUFPLEdBR1IsR0FBSS9ELEVBQVFxWCxVQUNYLE9BQU9yWCxFQUFRcVgsZ0JBQWdCbFYsRUFBU2dTLFFBSTFDLE9BQU9oUyxFQUFTMUMsTUFJbEIsT0FBT2tVLEVBR1IscUJBQXFCeFAsR0FHcEIsR0FGQXZHLEtBQUs0WCxjQUVENVgsS0FBSzRYLFlBQWM1WCxLQUFLOFgsU0FBU0wsTUFBTUwsU0FBVzdRLGFBQWlCb1EsR0FBZSxDQUNyRixHQUFJcFEsYUFBaUJtUSxFQUFXLENBQy9CLElBQUsxVyxLQUFLOFgsU0FBU0wsTUFBTUgsWUFBWUwsU0FBUzFRLEVBQU1oQyxTQUFTNEIsUUFDNUQsT0FBTyxFQUdSLE1BQU11VCxFQUFhblQsRUFBTWhDLFNBQVNzRSxRQUFRbEksSUFBSSxlQUM5QyxHQUFJK1ksR0FBYzFaLEtBQUs4WCxTQUFTTCxNQUFNRixpQkFBaUJOLFNBQVMxUSxFQUFNaEMsU0FBUzRCLFFBQVMsQ0FDdkYsSUFBSXdULEVBQVFDLE9BQU9GLEdBT25CLE9BTklFLE9BQU9DLE1BQU1GLEdBQ2hCQSxFQUFROU8sS0FBS3RILE1BQU1tVyxHQUFjN08sS0FBS29FLE1BRXRDMEssR0FBUyxTQUd1QyxJQUF0QzNaLEtBQUs4WCxTQUFTTCxNQUFNcUMsZUFBaUNILEVBQVEzWixLQUFLOFgsU0FBU0wsTUFBTXFDLGNBQ3BGLEVBR0RILEVBR1IsR0FBOEIsTUFBMUJwVCxFQUFNaEMsU0FBUzRCLE9BQ2xCLE9BQU8sRUFLVCxNQUR1QixHQUNFLElBQU1uRyxLQUFLNFgsWUFBYyxHQUFNLElBR3pELE9BQU8sRUFHUixrQkFBa0JyVCxHQU9qQixPQU5JdkUsS0FBSzhYLFNBQVMyQixZQUNqQmxWLEVBQVM0RixLQUFPdUosU0FDUjFULEtBQUs4WCxTQUFTMkIsZ0JBQWdCbFYsRUFBU2dTLFNBSXpDaFMsRUFHUixhQUFhdVUsR0FDWixJQUNDLGFBQWFBLElBQ1osTUFBT3ZTLEdBQ1IsTUFBTXNRLEVBQUsvQyxLQUFLRSxJQUFJaFUsS0FBSytaLHFCQUFxQnhULEdBQVFtUixHQUN0RCxHQUFXLElBQVBiLEdBQVk3VyxLQUFLNFgsWUFBYyxFQUFHLE9BQy9CaEIsRUFBTUMsR0FFWixJQUFLLE1BQU1vQyxLQUFRalosS0FBSzhYLFNBQVNFLE1BQU1FLFlBVXRDLFNBUnlCZSxFQUFLLENBQzdCdlcsUUFBUzFDLEtBQUswQyxRQUNkTixRQUFTcEMsS0FBSzhYLFNBQ2R2UixRQUNBeVQsV0FBWWhhLEtBQUs0WCxnQkFJQ25CLEVBQ2xCLE9BSUYsT0FBT3pXLEtBQUt1WixPQUFPVCxHQUdwQixHQUFJOVksS0FBSzhYLFNBQVN4TyxnQkFDakIsTUFBTS9DLEdBS1QsZUFDQyxJQUFLLE1BQU0wUyxLQUFRalosS0FBSzhYLFNBQVNFLE1BQU1DLGNBQWUsQ0FFckQsTUFBTWxDLFFBQWVrRCxFQUFLalosS0FBSzBDLFFBQVMxQyxLQUFLOFgsVUFFN0MsR0FBSS9CLGFBQWtCcEwsUUFBUyxDQUM5QjNLLEtBQUswQyxRQUFVcVQsRUFDZixNQUdELEdBQUlBLGFBQWtCakIsU0FDckIsT0FBT2lCLEVBSVQsT0FBOEIsSUFBMUIvVixLQUFLOFgsU0FBU08sUUFDVnJZLEtBQUs4WCxTQUFTcEQsTUFBTTFVLEtBQUswQyxRQUFRMFcsVUFqUzFCMVcsRUFvU0ExQyxLQUFLMEMsUUFBUTBXLFFBcFNKWCxFQW9TYXpZLEtBQUt5WSxnQkFwU0RyVyxFQW9Ta0JwQyxLQUFLOFgsU0FuU2pFLElBQUlwTyxTQUFRLENBQUNDLEVBQVNDLEtBQ3JCLE1BQU1xUSxFQUFZbkQsWUFBVyxLQUN4QjJCLEdBQ0hBLEVBQWdCMVcsUUFHakI2SCxFQUFPLElBQUkrTSxFQUFhalUsTUFDdEJOLEVBQVFpVyxTQUdYalcsRUFBUXNTLE1BQU1oUyxHQUNaMkMsS0FBS3NFLEdBQ0x1USxNQUFNdFEsR0FDTnZFLE1BQUssS0FDTDhVLGFBQWFGLFVBZkQsSUFBQ3ZYLEVBQVMrVixFQUFpQnJXLEVBd1MxQyxRQUFRbUMsRUFBVThVLEdBQ2pCLE1BQU1lLEVBQWFSLE9BQU9yVixFQUFTc0UsUUFBUWxJLElBQUksb0JBQXNCLEVBQ3JFLElBQUkwWixFQUFtQixFQUV2QixPQUFPLElBQUlwRixFQUFRSCxTQUNsQixJQUFJRyxFQUFRRixlQUFlLENBQzFCLE1BQU1qVCxHQUNMLE1BQU13WSxFQUFTL1YsRUFBU0MsS0FBSytWLFlBRXpCbEIsR0FDSEEsRUFBbUIsQ0FBQ21CLFFBQVMsRUFBR0gsaUJBQWtCLEVBQUdELGNBQWEsSUFBSTVHLFlBR3ZFRSxlQUFlRSxJQUNkLE1BQU0sS0FBQzZHLEVBQUksTUFBRXRhLFNBQWVtYSxFQUFPMUcsT0FDL0I2RyxFQUNIM1ksRUFBVzRZLFNBSVJyQixJQUNIZ0IsR0FBb0JsYSxFQUFNaVQsV0FFMUJpRyxFQUFtQixDQUFDbUIsUUFEVyxJQUFmSixFQUFtQixFQUFJQyxFQUFtQkQsRUFDN0JDLG1CQUFrQkQsY0FBYWphLElBRzdEMkIsRUFBVzZZLFFBQVF4YSxHQUNuQnlULEtBR0RBLFFBT0wsTUFBTWdILEVBQW1CLElBQUl6RSxLQUM1QixJQUFLLE1BQU1GLEtBQVVFLEVBQ3BCLEtBQU12RixFQUFTcUYsSUFBVy9OLE1BQU1DLFFBQVE4TixVQUE4QixJQUFYQSxFQUMxRCxNQUFNLElBQUl6VixVQUFVLDRDQUl0QixPQUFPMFYsRUFBVSxNQUFPQyxJQUduQjBFLEVBQWlCQyxJQUN0QixNQUFNQyxFQUFLLENBQUMvRCxFQUFPNVUsSUFBWSxJQUFJdVYsRUFBR1gsRUFBTzRELEVBQWlCRSxFQUFVMVksSUFFeEUsSUFBSyxNQUFNMEcsS0FBVXVOLEVBQ3BCMEUsRUFBR2pTLEdBQVUsQ0FBQ2tPLEVBQU81VSxJQUFZLElBQUl1VixFQUFHWCxFQUFPNEQsRUFBaUJFLEVBQVUxWSxFQUFTLENBQUMwRyxZQVNyRixPQU5BaVMsRUFBR3JFLFVBQVlBLEVBQ2ZxRSxFQUFHcEUsYUFBZUEsRUFDbEJvRSxFQUFHdlosT0FBU3daLEdBQWVILEVBQWVELEVBQWlCSSxJQUMzREQsRUFBR0UsT0FBU0QsR0FBZUgsRUFBZUQsRUFBaUJFLEVBQVVFLElBQ3JFRCxFQUFHdEUsS0FBT0EsRUFFSHNFLEdBS1IsT0FGWUYsS0FwaEJtRGhiLEVBQU9ELFFBQVVELEssMkJDQ2pGQyxFQUFVQyxFQUFPRCxRQUFVOFUsRUFFM0IsTUFBTXdHLEVBQU8sRUFBUSxLQUNmQyxFQUFRLEVBQVEsS0FDaEJDLEVBQU8sRUFBUSxLQUNmQyxFQUFTLEVBQVEsS0FDakJDLEVBQWtCLEVBQVEsS0FDMUJDLEVBQU8sRUFBUSxLQUNmM0ksRUFBTyxFQUFRLElBQ2Y0SSxFQUFTLEVBQVEsS0FDakJsWixFQUFNLEVBQVEsS0FFcEIsTUFBTW1aLFVBQXVCalosTUFDNUIsWUFBWTZELEVBQVN4RSxHQUNwQnRCLE1BQU04RixHQUVON0QsTUFBTWtaLGtCQUFrQjFiLEtBQU1BLEtBQUsyUCxhQUVuQzNQLEtBQUs2QixLQUFPQSxFQUdiLFdBQ0MsT0FBTzdCLEtBQUsyUCxZQUFZaE0sS0FHekJoRCxJQUFLTSxPQUFPQyxlQUNYLE9BQU9sQixLQUFLMlAsWUFBWWhNLE1BVzFCLE1BQU1nWSxVQUFtQkYsRUFNeEIsWUFBWXBWLEVBQVN4RSxFQUFNK1osR0FDMUJyYixNQUFNOEYsRUFBU3hFLEdBRVgrWixJQUVINWIsS0FBS3dMLEtBQU94TCxLQUFLNmIsTUFBUUQsRUFBWXBRLEtBQ3JDeEwsS0FBSzhiLGVBQWlCRixFQUFZRyxVQVdyQyxNQUFNQyxFQUFPL2EsT0FBT0MsWUFTZCthLEVBQXdCeEgsR0FFVixpQkFBWEEsR0FDa0IsbUJBQWxCQSxFQUFPaEssUUFDVyxtQkFBbEJnSyxFQUFPalAsUUFDUSxtQkFBZmlQLEVBQU85VCxLQUNXLG1CQUFsQjhULEVBQU95SCxRQUNRLG1CQUFmekgsRUFBTzBILEtBQ1EsbUJBQWYxSCxFQUFPblQsS0FDUyxtQkFBaEJtVCxFQUFPMkgsTUFDRyxvQkFBakIzSCxFQUFPdUgsR0FVSEssRUFBUzVILEdBRUssaUJBQVhBLEdBQ3VCLG1CQUF2QkEsRUFBT2xCLGFBQ1MsaUJBQWhCa0IsRUFBTzVTLE1BQ1csbUJBQWxCNFMsRUFBT2pMLFFBQ2dCLG1CQUF2QmlMLEVBQU85RSxhQUNkLGdCQUFnQjdDLEtBQUsySCxFQUFPdUgsSUFVOUIsU0FBU00sRUFBVzdILEdBQ25CLE1BQ21CLGlCQUFYQSxHQUNrQixtQkFBbEJBLEVBQU9oSyxRQUNRLG1CQUFmZ0ssRUFBT25ULEtBQ1EsbUJBQWZtVCxFQUFPOVQsS0FDVyxtQkFBbEI4VCxFQUFPeUgsUUFDVyxtQkFBbEJ6SCxFQUFPalAsUUFDUyxtQkFBaEJpUCxFQUFPbkssTUFDVyxtQkFBbEJtSyxFQUFPTixRQUNZLG1CQUFuQk0sRUFBT3ZOLFNBQ2dCLG1CQUF2QnVOLEVBQU85RSxhQUNHLGFBQWpCOEUsRUFBT3VILEdBVVQsTUFPTU8sRUFBVyxPQUNYQyxFQUFTLElBQUlDLE9BQU8sR0FDcEJDLEVBQWlCMVMsT0FBT29KLFdBQVdtSixHQUtuQ0ksRUFBWUMsR0FBWSxHQUFHSixJQUFTSSxJQUFXSixJQUFTRCxFQUFTRSxPQUFPLEtBUzlFLFNBQVNJLEVBQVVELEVBQVVqWixFQUFNbVosR0FDbEMsSUFBSUMsRUFBUyxHQVViLE9BUkFBLEdBQVUsR0FBR1AsSUFBU0ksUUFDdEJHLEdBQVUseUNBQXlDcFosS0FFL0MwWSxFQUFPUyxLQUNWQyxHQUFVLGVBQWVELEVBQU1uWixZQUMvQm9aLEdBQVUsaUJBQWlCRCxFQUFNamIsTUFBUSw4QkFHbkMsR0FBR2tiLElBQVNSLEVBQVNFLE9BQU8sS0FvRHBDLE1BQU1PLEVBQVkvYixPQUFPLGtCQVd6QixNQUFNZ2MsRUFDTCxZQUFZelksR0FBTSxLQUNqQnNPLEVBQU8sR0FDSixJQUNILElBQUk4SixFQUFXLEtBRUYsT0FBVHBZLEVBRUhBLEVBQU8sS0FDR3lYLEVBQXNCelgsR0FFaENBLEVBQU93RixPQUFPZ0QsS0FBS3hJLEVBQUswRixZQUNkbVMsRUFBTzdYLElBQWtCd0YsT0FBT2tULFNBQVMxWSxLQUFrQitXLEVBQUt6SixNQUFNcUwsaUJBQWlCM1ksR0FFakdBLEVBQU93RixPQUFPZ0QsS0FBS3hJLEdBQ1R5TyxZQUFZQyxPQUFPMU8sR0FFN0JBLEVBQU93RixPQUFPZ0QsS0FBS3hJLEVBQUtxSixPQUFRckosRUFBSzJPLFdBQVkzTyxFQUFLNE8sWUFDNUM1TyxhQUFnQjZXLElBQW1CaUIsRUFBVzlYLElBRXhEb1ksRUFBVyw0QkE3RVlwQixFQUFPNEIsWUFBWSxHQUFHbFQsU0FBUyxTQThFdEQxRixFQUFPNlcsRUFBTzNJLFNBQVMxRixLQXhFMUIwRyxnQkFBa0MySixFQUFNVCxHQUN2QyxJQUFLLE1BQU9qWixFQUFNeEQsS0FBVWtkLFFBQ3JCUixFQUFVRCxFQUFValosRUFBTXhELEdBRTVCa2MsRUFBT2xjLFNBQ0ZBLEVBQU1xSixlQUVSckosUUFHRG9jLFFBR0RJLEVBQVVDLEdBMkRjVSxDQUFpQjlZLEVBQU1vWSxLQUluRHBZLEVBQU93RixPQUFPZ0QsS0FBS3FHLE9BQU83TyxNQUczQnhFLEtBQUtnZCxHQUFhLENBQ2pCeFksT0FDQW9ZLFdBQ0FXLFdBQVcsRUFDWGhYLE1BQU8sTUFFUnZHLEtBQUs4UyxLQUFPQSxFQUVSdE8sYUFBZ0I2VyxHQUNuQjdXLEVBQUtxRixHQUFHLFNBQVN3SSxJQUNoQixNQUFNOUwsRUFBUThMLGFBQWVvSixFQUM1QnBKLEVBQ0EsSUFBSXNKLEVBQVcsK0NBQStDM2IsS0FBS3NDLFFBQVErUCxFQUFJaE0sVUFBVyxTQUFVZ00sR0FDckdyUyxLQUFLZ2QsR0FBV3pXLE1BQVFBLEtBSzNCLFdBQ0MsT0FBT3ZHLEtBQUtnZCxHQUFXeFksS0FHeEIsZUFDQyxPQUFPeEUsS0FBS2dkLEdBQVdPLFVBUXhCLG9CQUNDLE1BQU0sT0FBQzFQLEVBQU0sV0FBRXNGLEVBQVUsV0FBRUMsU0FBb0JvSyxFQUFZeGQsTUFDM0QsT0FBTzZOLEVBQU93RyxNQUFNbEIsRUFBWUEsRUFBYUMsR0FROUMsYUFDQyxNQUFNcUssRUFBTXpkLEtBQUs2SSxTQUFXN0ksS0FBSzZJLFFBQVFsSSxJQUFJLGlCQUFxQlgsS0FBS2dkLEdBQVd4WSxNQUFReEUsS0FBS2dkLEdBQVd4WSxLQUFLM0MsTUFBUyxHQUNsSDZiLFFBQVkxZCxLQUFLNk4sU0FFdkIsT0FBTyxJQUFJK0UsRUFBSyxDQUFDOEssR0FBTSxDQUN0QjdiLEtBQU00YixJQVNSLGFBQ0MsTUFBTTVQLFFBQWUyUCxFQUFZeGQsTUFDakMsT0FBTzRZLEtBQUtyVixNQUFNc0ssRUFBTzNELFlBUTFCLGFBRUMsYUFEcUJzVCxFQUFZeGQsT0FDbkJrSyxXQVFmLFNBQ0MsT0FBT3NULEVBQVl4ZCxPQXFCckIwVCxlQUFlOEosRUFBWWhhLEdBQzFCLEdBQUlBLEVBQUt3WixHQUFXTyxVQUNuQixNQUFNLElBQUkvYyxVQUFVLDBCQUEwQmdELEVBQUtsQixPQUtwRCxHQUZBa0IsRUFBS3daLEdBQVdPLFdBQVksRUFFeEIvWixFQUFLd1osR0FBV3pXLE1BQ25CLE1BQU0vQyxFQUFLd1osR0FBV3pXLE1BR3ZCLElBQUksS0FBQy9CLEdBQVFoQixFQUdiLEdBQWEsT0FBVGdCLEVBQ0gsT0FBT3dGLE9BQU8yVCxNQUFNLEdBU3JCLEdBTEl0QixFQUFPN1gsS0FDVkEsRUFBT0EsRUFBS2dGLFVBSVRRLE9BQU9rVCxTQUFTMVksR0FDbkIsT0FBT0EsRUFJUixLQUFNQSxhQUFnQjZXLEdBQ3JCLE9BQU9yUixPQUFPMlQsTUFBTSxHQUtyQixNQUFNQyxFQUFRLEdBQ2QsSUFBSUMsRUFBYSxFQUVqQixJQUNDLFVBQVcsTUFBTS9ULEtBQVN0RixFQUFNLENBQy9CLEdBQUloQixFQUFLc1AsS0FBTyxHQUFLK0ssRUFBYS9ULEVBQU1YLE9BQVMzRixFQUFLc1AsS0FBTSxDQUMzRCxNQUFNVCxFQUFNLElBQUlzSixFQUFXLG1CQUFtQm5ZLEVBQUtsQixtQkFBbUJrQixFQUFLc1AsT0FBUSxZQUVuRixNQURBdE8sRUFBS2UsUUFBUThNLEdBQ1BBLEVBR1B3TCxHQUFjL1QsRUFBTVgsT0FDcEJ5VSxFQUFNN1QsS0FBS0QsSUFFWCxNQUFPdkQsR0FDUixNQUFJQSxhQUFpQmtWLEVBQ2RsVixFQUdBLElBQUlvVixFQUFXLCtDQUErQ25ZLEVBQUtsQixRQUFRaUUsRUFBTUYsVUFBVyxTQUFVRSxHQUk5RyxJQUEyQixJQUF2Qi9CLEVBQUtzWixnQkFBd0QsSUFBOUJ0WixFQUFLdVosZUFBZUMsTUFXdEQsTUFBTSxJQUFJckMsRUFBVyw0REFBNERuWSxFQUFLbEIsT0FWdEYsSUFDQyxPQUFJc2IsRUFBTUssT0FBTUMsR0FBa0IsaUJBQU5BLElBQ3BCbFUsT0FBT2dELEtBQUs0USxFQUFNeFYsS0FBSyxLQUd4QjRCLE9BQU9DLE9BQU8yVCxFQUFPQyxHQUMzQixNQUFPdFgsR0FDUixNQUFNLElBQUlvVixFQUFXLGtEQUFrRG5ZLEVBQUtsQixRQUFRaUUsRUFBTUYsVUFBVyxTQUFVRSxJQWxGbEh0RyxPQUFPYyxpQkFBaUJrYyxFQUFLcGMsVUFBVyxDQUN2QzJELEtBQU0sQ0FBQ3hELFlBQVksR0FDbkJtZCxTQUFVLENBQUNuZCxZQUFZLEdBQ3ZCdVMsWUFBYSxDQUFDdlMsWUFBWSxHQUMxQnNULEtBQU0sQ0FBQ3RULFlBQVksR0FDbkJtSixLQUFNLENBQUNuSixZQUFZLEdBQ25CdVYsS0FBTSxDQUFDdlYsWUFBWSxLQTBGcEIsTUFBTW9ZLEVBQVEsQ0FBQ2dGLEVBQVV4SixLQUN4QixJQUFJeUosRUFDQUMsR0FDQSxLQUFDOVosR0FBUTRaLEVBR2IsR0FBSUEsRUFBU0QsU0FDWixNQUFNLElBQUkzYixNQUFNLHNDQWdCakIsT0FYS2dDLGFBQWdCNlcsR0FBd0MsbUJBQXJCN1csRUFBSytaLGNBRTVDRixFQUFLLElBQUloRCxFQUFPbUQsWUFBWSxDQUFDNUosa0JBQzdCMEosRUFBSyxJQUFJakQsRUFBT21ELFlBQVksQ0FBQzVKLGtCQUM3QnBRLEVBQUtnRSxLQUFLNlYsR0FDVjdaLEVBQUtnRSxLQUFLOFYsR0FFVkYsRUFBU3BCLEdBQVd4WSxLQUFPNlosRUFDM0I3WixFQUFPOFosR0FHRDlaLEdBYUZpYSxFQUFxQixDQUFDamEsRUFBTTlCLElBRXBCLE9BQVQ4QixFQUNJLEtBSVksaUJBQVRBLEVBQ0gsMkJBSUp5WCxFQUFzQnpYLEdBQ2xCLGtEQUlKNlgsRUFBTzdYLEdBQ0hBLEVBQUszQyxNQUFRLEtBSWpCbUksT0FBT2tULFNBQVMxWSxJQUFTK1csRUFBS3pKLE1BQU1xTCxpQkFBaUIzWSxJQUFTeU8sWUFBWUMsT0FBTzFPLEdBQzdFLEtBSUpBLEdBQW9DLG1CQUFyQkEsRUFBSytaLFlBQ2hCLGdDQUFnQy9aLEVBQUsrWixnQkFHekNqQyxFQUFXOVgsR0FDUCxpQ0FBaUM5QixFQUFRc2EsR0FBV0osV0FJeERwWSxhQUFnQjZXLEVBQ1osS0FJRCwyQkEwRUZxRCxFQUF3RCxtQkFBNUJ4RCxFQUFLd0QsbUJBQ3RDeEQsRUFBS3dELG1CQUNML2EsSUFDQyxJQUFLLDBCQUEwQm1KLEtBQUtuSixHQUFPLENBQzFDLE1BQU0wTyxFQUFNLElBQUk3UixVQUFVLDJDQUEyQ21ELE1BRXJFLE1BREExRCxPQUFPQyxlQUFlbVMsRUFBSyxPQUFRLENBQUNsUyxNQUFPLDJCQUNyQ2tTLElBSUhzTSxFQUEwRCxtQkFBN0J6RCxFQUFLeUQsb0JBQ3ZDekQsRUFBS3lELG9CQUNMLENBQUNoYixFQUFNeEQsS0FDTixHQUFJLGtDQUFrQzJNLEtBQUszTSxHQUFRLENBQ2xELE1BQU1rUyxFQUFNLElBQUk3UixVQUFVLHlDQUF5Q21ELE9BRW5FLE1BREExRCxPQUFPQyxlQUFlbVMsRUFBSyxPQUFRLENBQUNsUyxNQUFPLHFCQUNyQ2tTLElBZ0JULE1BQU13QyxVQUFnQjhELGdCQU9yQixZQUFZaUcsR0FHWCxJQUFJN0ksRUFBUyxHQUNiLEdBQUk2SSxhQUFnQi9KLEVBQVMsQ0FDNUIsTUFBTWdLLEVBQU1ELEVBQUtDLE1BQ2pCLElBQUssTUFBT2xiLEVBQU13USxLQUFXbFUsT0FBT2lILFFBQVEyWCxHQUMzQzlJLEVBQU9oTSxRQUFRb0ssRUFBT3hQLEtBQUl4RSxHQUFTLENBQUN3RCxFQUFNeEQsV0FFckMsR0FBWSxNQUFSeWUsT0FBcUIsSUFBb0IsaUJBQVRBLEdBQXNCckQsRUFBS3pKLE1BQU1nTixpQkFBaUJGLEdBK0I1RixNQUFNLElBQUlwZSxVQUFVLHdJQS9CK0UsQ0FDbkcsTUFBTXNJLEVBQVM4VixFQUFLM2QsT0FBTzhkLFVBRTNCLEdBQWMsTUFBVmpXLEVBRUhpTixFQUFPaE0sUUFBUTlKLE9BQU9pSCxRQUFRMFgsUUFDeEIsQ0FDTixHQUFzQixtQkFBWDlWLEVBQ1YsTUFBTSxJQUFJdEksVUFBVSxpQ0FLckJ1VixFQUFTLElBQUk2SSxHQUNYamEsS0FBSXFhLElBQ0osR0FDaUIsaUJBQVRBLEdBQXFCekQsRUFBS3pKLE1BQU1nTixpQkFBaUJFLEdBRXhELE1BQU0sSUFBSXhlLFVBQVUsK0NBR3JCLE1BQU8sSUFBSXdlLE1BQ1RyYSxLQUFJcWEsSUFDTixHQUFvQixJQUFoQkEsRUFBSzdWLE9BQ1IsTUFBTSxJQUFJM0ksVUFBVSwrQ0FHckIsTUFBTyxJQUFJd2UsUUFxQmYsT0FiQWpKLEVBQ0NBLEVBQU81TSxPQUFTLEVBQ2Y0TSxFQUFPcFIsS0FBSSxFQUFFaEIsRUFBTXhELE1BQ2xCdWUsRUFBbUIvYSxHQUNuQmdiLEVBQW9CaGIsRUFBTTBQLE9BQU9sVCxJQUMxQixDQUFDa1QsT0FBTzFQLEdBQU0yUCxjQUFlRCxPQUFPbFQsWUFFNUMrUixFQUVGM1IsTUFBTXdWLEdBSUMsSUFBSWtKLE1BQU1qZixLQUFNLENBQ3RCLElBQUlrZixFQUFRQyxFQUFHQyxHQUNkLE9BQVFELEdBQ1AsSUFBSyxTQUNMLElBQUssTUFDSixNQUFPLENBQUN4YixFQUFNeEQsS0FDYnVlLEVBQW1CL2EsR0FDbkJnYixFQUFvQmhiLEVBQU0wUCxPQUFPbFQsSUFDMUJ3WSxnQkFBZ0I5WCxVQUFVc2UsR0FBRzFkLEtBQ25DMmQsRUFDQS9MLE9BQU8xUCxHQUFNMlAsY0FDYkQsT0FBT2xULEtBSVYsSUFBSyxTQUNMLElBQUssTUFDTCxJQUFLLFNBQ0osT0FBT3dELElBQ04rYSxFQUFtQi9hLEdBQ1pnVixnQkFBZ0I5WCxVQUFVc2UsR0FBRzFkLEtBQ25DMmQsRUFDQS9MLE9BQU8xUCxHQUFNMlAsZ0JBSWhCLElBQUssT0FDSixNQUFPLEtBQ040TCxFQUFPOUMsT0FDQSxJQUFJaUQsSUFBSTFHLGdCQUFnQjlYLFVBQVV5SixLQUFLN0ksS0FBS3lkLElBQVM1VSxRQUc5RCxRQUNDLE9BQU9nVixRQUFRM2UsSUFBSXVlLEVBQVFDLEVBQUdDLE9BT25DemUsSUFBS00sT0FBT0MsZUFDWCxPQUFPbEIsS0FBSzJQLFlBQVloTSxLQUd6QixXQUNDLE9BQU8xRCxPQUFPWSxVQUFVcUosU0FBU3pJLEtBQUt6QixNQUd2QyxJQUFJMkQsR0FDSCxNQUFNd1EsRUFBU25VLEtBQUtrYyxPQUFPdlksR0FDM0IsR0FBc0IsSUFBbEJ3USxFQUFPaEwsT0FDVixPQUFPLEtBR1IsSUFBSWhKLEVBQVFnVSxFQUFPL0wsS0FBSyxNQUt4QixNQUpJLHNCQUFzQjBFLEtBQUtuSixLQUM5QnhELEVBQVFBLEVBQU1tVCxlQUdSblQsRUFHUixRQUFRb2YsR0FDUCxJQUFLLE1BQU01YixLQUFRM0QsS0FBS3NLLE9BQ3ZCaVYsRUFBU3ZmLEtBQUtXLElBQUlnRCxHQUFPQSxHQUkzQixVQUNDLElBQUssTUFBTUEsS0FBUTNELEtBQUtzSyxhQUNqQnRLLEtBQUtXLElBQUlnRCxHQU9qQixXQUNDLElBQUssTUFBTUEsS0FBUTNELEtBQUtzSyxZQUNqQixDQUFDM0csRUFBTTNELEtBQUtXLElBQUlnRCxJQUl4QixDQUFDMUMsT0FBTzhkLFlBQ1AsT0FBTy9lLEtBQUtrSCxVQVFiLE1BQ0MsTUFBTyxJQUFJbEgsS0FBS3NLLFFBQVFsRCxRQUFPLENBQUMyTyxFQUFRdFQsS0FDdkNzVCxFQUFPdFQsR0FBT3pDLEtBQUtrYyxPQUFPelosR0FDbkJzVCxJQUNMLElBTUosQ0FBQzlVLE9BQU91ZSxJQUFJLGlDQUNYLE1BQU8sSUFBSXhmLEtBQUtzSyxRQUFRbEQsUUFBTyxDQUFDMk8sRUFBUXRULEtBQ3ZDLE1BQU0wUixFQUFTblUsS0FBS2tjLE9BQU96WixHQVMzQixPQUxDc1QsRUFBT3RULEdBREksU0FBUkEsRUFDVzBSLEVBQU8sR0FFUEEsRUFBT2hMLE9BQVMsRUFBSWdMLEVBQVNBLEVBQU8sR0FHNUM0QixJQUNMLEtBUUw5VixPQUFPYyxpQkFDTjhULEVBQVFoVSxVQUNSLENBQUMsTUFBTyxVQUFXLFVBQVcsVUFBVXVHLFFBQU8sQ0FBQzJPLEVBQVFaLEtBQ3ZEWSxFQUFPWixHQUFZLENBQUNuVSxZQUFZLEdBQ3pCK1UsSUFDTCxLQWdDSixNQUFNMEosRUFBaUIsSUFBSUosSUFBSSxDQUFDLElBQUssSUFBSyxJQUFLLElBQUssTUFROUNLLEVBQWFsVSxHQUNYaVUsRUFBZXRELElBQUkzUSxHQVNyQm1VLEVBQWMxZSxPQUFPLHNCQVMzQixNQUFNNlQsVUFBaUJtSSxFQUN0QixZQUFZelksRUFBTyxLQUFNcEMsRUFBVSxJQUNsQzdCLE1BQU1pRSxFQUFNcEMsR0FFWixNQUFNK0QsRUFBUy9ELEVBQVErRCxRQUFVLElBQzNCMEMsRUFBVSxJQUFJZ00sRUFBUXpTLEVBQVF5RyxTQUVwQyxHQUFhLE9BQVRyRSxJQUFrQnFFLEVBQVFzVCxJQUFJLGdCQUFpQixDQUNsRCxNQUFNeFQsRUFBYzhWLEVBQW1CamEsR0FDbkNtRSxHQUNIRSxFQUFRNEIsT0FBTyxlQUFnQjlCLEdBSWpDM0ksS0FBSzJmLEdBQWUsQ0FDbkJyZCxJQUFLRixFQUFRRSxJQUNiNkQsU0FDQUMsV0FBWWhFLEVBQVFnRSxZQUFjLEdBQ2xDeUMsVUFDQStXLFFBQVN4ZCxFQUFRd2QsUUFDakJoTCxjQUFleFMsRUFBUXdTLGVBSXpCLFVBQ0MsT0FBTzVVLEtBQUsyZixHQUFhcmQsS0FBTyxHQUdqQyxhQUNDLE9BQU90QyxLQUFLMmYsR0FBYXhaLE9BTTFCLFNBQ0MsT0FBT25HLEtBQUsyZixHQUFheFosUUFBVSxLQUFPbkcsS0FBSzJmLEdBQWF4WixPQUFTLElBR3RFLGlCQUNDLE9BQU9uRyxLQUFLMmYsR0FBYUMsUUFBVSxFQUdwQyxpQkFDQyxPQUFPNWYsS0FBSzJmLEdBQWF2WixXQUcxQixjQUNDLE9BQU9wRyxLQUFLMmYsR0FBYTlXLFFBRzFCLG9CQUNDLE9BQU83SSxLQUFLMmYsR0FBYS9LLGNBUTFCLFFBQ0MsT0FBTyxJQUFJRSxFQUFTc0UsRUFBTXBaLEtBQU1BLEtBQUs0VSxlQUFnQixDQUNwRHRTLElBQUt0QyxLQUFLc0MsSUFDVjZELE9BQVFuRyxLQUFLbUcsT0FDYkMsV0FBWXBHLEtBQUtvRyxXQUNqQnlDLFFBQVM3SSxLQUFLNkksUUFDZFUsR0FBSXZKLEtBQUt1SixHQUNUc1csV0FBWTdmLEtBQUs2ZixXQUNqQi9NLEtBQU05UyxLQUFLOFMsT0FTYixnQkFBZ0J4USxFQUFLNkQsRUFBUyxLQUM3QixJQUFLdVosRUFBV3ZaLEdBQ2YsTUFBTSxJQUFJNFMsV0FBVyxtRUFHdEIsT0FBTyxJQUFJakUsRUFBUyxLQUFNLENBQ3pCak0sUUFBUyxDQUNSaVgsU0FBVSxJQUFJeEgsSUFBSWhXLEdBQUs0SCxZQUV4Qi9ELFdBSUZ4RixJQUFLTSxPQUFPQyxlQUNYLE1BQU8sWUFJVGpCLE9BQU9jLGlCQUFpQitULEVBQVNqVSxVQUFXLENBQzNDeUIsSUFBSyxDQUFDdEIsWUFBWSxHQUNsQm1GLE9BQVEsQ0FBQ25GLFlBQVksR0FDckJ1SSxHQUFJLENBQUN2SSxZQUFZLEdBQ2pCNmUsV0FBWSxDQUFDN2UsWUFBWSxHQUN6Qm9GLFdBQVksQ0FBQ3BGLFlBQVksR0FDekI2SCxRQUFTLENBQUM3SCxZQUFZLEdBQ3RCb1ksTUFBTyxDQUFDcFksWUFBWSxLQUdyQixNQVVNK2UsRUFBYzllLE9BQU8scUJBUXJCK2UsRUFBWXZMLEdBRUUsaUJBQVhBLEdBQ3dCLGlCQUF4QkEsRUFBT3NMLEdBV2hCLE1BQU1wVixVQUFnQnNTLEVBQ3JCLFlBQVlqRyxFQUFPNEgsRUFBTyxJQUN6QixJQUFJcUIsRUFHQUQsRUFBVWhKLEdBQ2JpSixFQUFZLElBQUkzSCxJQUFJdEIsRUFBTTFVLE1BRTFCMmQsRUFBWSxJQUFJM0gsSUFBSXRCLEdBQ3BCQSxFQUFRLElBR1QsSUFBSWxPLEVBQVM4VixFQUFLOVYsUUFBVWtPLEVBQU1sTyxRQUFVLE1BSTVDLEdBSEFBLEVBQVNBLEVBQU9vTyxlQUdHLE1BQWIwSCxFQUFLcGEsTUFBZ0J3YixFQUFVaEosS0FBMEIsT0FBZkEsRUFBTXhTLE9BQ3pDLFFBQVhzRSxHQUErQixTQUFYQSxHQUNyQixNQUFNLElBQUl0SSxVQUFVLGlEQUdyQixNQUFNMGYsRUFBWXRCLEVBQUtwYSxLQUN0Qm9hLEVBQUtwYSxLQUNKd2IsRUFBVWhKLElBQXlCLE9BQWZBLEVBQU14UyxLQUMxQjRVLEVBQU1wQyxHQUNOLEtBRUZ6VyxNQUFNMmYsRUFBVyxDQUNoQnBOLEtBQU04TCxFQUFLOUwsTUFBUWtFLEVBQU1sRSxNQUFRLElBR2xDLE1BQU1qSyxFQUFVLElBQUlnTSxFQUFRK0osRUFBSy9WLFNBQVdtTyxFQUFNbk8sU0FBVyxJQUU3RCxHQUFrQixPQUFkcVgsSUFBdUJyWCxFQUFRc1QsSUFBSSxnQkFBaUIsQ0FDdkQsTUFBTXhULEVBQWM4VixFQUFtQnlCLEVBQVdsZ0IsTUFDOUMySSxHQUNIRSxFQUFRNEIsT0FBTyxlQUFnQjlCLEdBSWpDLElBQUlwSCxFQUFTeWUsRUFBVWhKLEdBQ3RCQSxFQUFNelYsT0FDTixLQUtELEdBSkksV0FBWXFkLElBQ2ZyZCxFQUFTcWQsRUFBS3JkLFFBR0EsT0FBWEEsSUFyNUJjLGlCQUZFa1QsRUF1NUJrQmxULElBcDVCckIsZ0JBQWpCa1QsRUFBT3VILElBcTVCTixNQUFNLElBQUl4YixVQUFVLG1EQXg1QkRpVSxNQTI1QnBCelUsS0FBSytmLEdBQWUsQ0FDbkJqWCxTQUNBcVgsU0FBVXZCLEVBQUt1QixVQUFZbkosRUFBTW1KLFVBQVksU0FDN0N0WCxVQUNBb1gsWUFDQTFlLFVBSUR2QixLQUFLb2dCLFlBQXlCbE8sSUFBaEIwTSxFQUFLd0IsWUFBeUNsTyxJQUFqQjhFLEVBQU1vSixPQUF1QixHQUFLcEosRUFBTW9KLE9BQVV4QixFQUFLd0IsT0FDbEdwZ0IsS0FBS3FnQixjQUE2Qm5PLElBQWxCME0sRUFBS3lCLGNBQTZDbk8sSUFBbkI4RSxFQUFNcUosVUFBZ0NySixFQUFNcUosU0FBWXpCLEVBQUt5QixTQUM1R3JnQixLQUFLNGYsUUFBVWhCLEVBQUtnQixTQUFXNUksRUFBTTRJLFNBQVcsRUFDaEQ1ZixLQUFLc2dCLE1BQVExQixFQUFLMEIsT0FBU3RKLEVBQU1zSixNQUNqQ3RnQixLQUFLNFUsY0FBZ0JnSyxFQUFLaEssZUFBaUJvQyxFQUFNcEMsZUFBaUIsTUFDbEU1VSxLQUFLdWdCLG1CQUFxQjNCLEVBQUsyQixvQkFBc0J2SixFQUFNdUoscUJBQXNCLEVBR2xGLGFBQ0MsT0FBT3ZnQixLQUFLK2YsR0FBYWpYLE9BRzFCLFVBQ0MsT0FBT3hHLEVBQUlrZSxPQUFPeGdCLEtBQUsrZixHQUFhRSxXQUdyQyxjQUNDLE9BQU9qZ0IsS0FBSytmLEdBQWFsWCxRQUcxQixlQUNDLE9BQU83SSxLQUFLK2YsR0FBYUksU0FHMUIsYUFDQyxPQUFPbmdCLEtBQUsrZixHQUFheGUsT0FRMUIsUUFDQyxPQUFPLElBQUlvSixFQUFRM0ssTUFHcEJXLElBQUtNLE9BQU9DLGVBQ1gsTUFBTyxXQUlUakIsT0FBT2MsaUJBQWlCNEosRUFBUTlKLFVBQVcsQ0FDMUNpSSxPQUFRLENBQUM5SCxZQUFZLEdBQ3JCc0IsSUFBSyxDQUFDdEIsWUFBWSxHQUNsQjZILFFBQVMsQ0FBQzdILFlBQVksR0FDdEJtZixTQUFVLENBQUNuZixZQUFZLEdBQ3ZCb1ksTUFBTyxDQUFDcFksWUFBWSxHQUNwQk8sT0FBUSxDQUFDUCxZQUFZLEtBbUZ0QixNQUFNeWYsVUFBbUJoRixFQUN4QixZQUFZcFYsRUFBU3hFLEVBQU8sV0FDM0J0QixNQUFNOEYsRUFBU3hFLElBWWpCLE1BQU02ZSxFQUFtQixJQUFJckIsSUFBSSxDQUFDLFFBQVMsUUFBUyxXQVNwRDNMLGVBQWVnQixFQUFNcFMsRUFBS3FlLEdBQ3pCLE9BQU8sSUFBSWpYLFNBQVEsQ0FBQ0MsRUFBU0MsS0FFNUIsTUFBTWxILEVBQVUsSUFBSWlJLEVBQVFySSxFQUFLcWUsR0FDM0J2ZSxFQXJHc0JNLEtBQzdCLE1BQU0sVUFBQ3VkLEdBQWF2ZCxFQUFRcWQsR0FDdEJsWCxFQUFVLElBQUlnTSxFQUFRblMsRUFBUXFkLEdBQWFsWCxTQUc1Q0EsRUFBUXNULElBQUksV0FDaEJ0VCxFQUFRdkgsSUFBSSxTQUFVLE9BSXZCLElBQUlzZixFQUFxQixLQUt6QixHQUpxQixPQUFqQmxlLEVBQVE4QixNQUFpQixnQkFBZ0JzSSxLQUFLcEssRUFBUW9HLFVBQ3pEOFgsRUFBcUIsS0FHRCxPQUFqQmxlLEVBQVE4QixLQUFlLENBQzFCLE1BQU00VixFQXRtQmMxWCxLQUNyQixNQUFNLEtBQUM4QixHQUFROUIsRUFHZixPQUFhLE9BQVQ4QixFQUNJLEVBSUo2WCxFQUFPN1gsR0FDSEEsRUFBS3NPLEtBSVQ5SSxPQUFPa1QsU0FBUzFZLEdBQ1pBLEVBQUsyRSxPQUlUM0UsR0FBc0MsbUJBQXZCQSxFQUFLcWMsY0FDaEJyYyxFQUFLc2MsZ0JBQWtCdGMsRUFBS3NjLGlCQUFtQnRjLEVBQUtxYyxnQkFBa0IsS0FJMUV2RSxFQUFXOVgsR0E3VmhCLFNBQTJCNlksRUFBTVQsR0FDaEMsSUFBSXpULEVBQVMsRUFFYixJQUFLLE1BQU94RixFQUFNeEQsS0FBVWtkLEVBQzNCbFUsR0FBVWEsT0FBT29KLFdBQVd5SixFQUFVRCxFQUFValosRUFBTXhELElBRWxEa2MsRUFBT2xjLEdBQ1ZnSixHQUFVaEosRUFBTTJTLEtBRWhCM0osR0FBVWEsT0FBT29KLFdBQVdDLE9BQU9sVCxJQUdwQ2dKLEdBQVV1VCxFQUtYLE9BRkF2VCxHQUFVYSxPQUFPb0osV0FBV3VKLEVBQVVDLElBRS9CelQsRUE2VUM0WCxDQUFrQnJlLEVBQVFzYSxHQUFXSixVQUl0QyxNQXlrQmFvRSxDQUFjdGUsR0FFUCxpQkFBZjBYLEdBQTRCUixPQUFPQyxNQUFNTyxLQUNuRHdHLEVBQXFCdk4sT0FBTytHLElBSTFCd0csR0FDSC9YLEVBQVF2SCxJQUFJLGlCQUFrQnNmLEdBSTFCL1gsRUFBUXNULElBQUksZUFDaEJ0VCxFQUFRdkgsSUFBSSxhQUFjLGNBSXZCb0IsRUFBUTJkLFdBQWF4WCxFQUFRc1QsSUFBSSxvQkFDcEN0VCxFQUFRdkgsSUFBSSxrQkFBbUIsbUJBR2hDLElBQUksTUFBQ2dmLEdBQVM1ZCxFQUNPLG1CQUFWNGQsSUFDVkEsRUFBUUEsRUFBTUwsSUFHVnBYLEVBQVFzVCxJQUFJLGVBQWtCbUUsR0FDbEN6WCxFQUFRdkgsSUFBSSxhQUFjLFNBTTNCLE1BQU0yZixFQXRNV2hCLEtBQ2pCLEdBQUlBLEVBQVVnQixPQUNiLE9BQU9oQixFQUFVZ0IsT0FHbEIsTUFBTUMsRUFBYWpCLEVBQVVrQixLQUFLaFksT0FBUyxFQUNyQ2lZLEVBQU9uQixFQUFVbUIsT0FBd0MsTUFBL0JuQixFQUFVa0IsS0FBS0QsR0FBc0IsSUFBTSxJQUMzRSxNQUFvRCxNQUE3Q2pCLEVBQVVrQixLQUFLRCxFQUFhRSxFQUFLalksUUFBa0IsSUFBTSxJQStMakRrWSxDQUFVcEIsR0FtQnpCLE1BaEJ1QixDQUN0QnFCLEtBQU1yQixFQUFVc0IsU0FBV04sRUFDM0JNLFNBQVV0QixFQUFVc0IsU0FDcEJDLFNBQVV2QixFQUFVdUIsU0FDcEJDLFNBQVV4QixFQUFVd0IsU0FDcEJDLEtBQU16QixFQUFVeUIsS0FDaEJOLEtBQU1uQixFQUFVbUIsS0FDaEJILE9BQVFoQixFQUFVZ0IsT0FDbEI3YixNQUFPNmEsRUFBVTdhLE1BQ2pCK2IsS0FBTWxCLEVBQVVrQixLQUNoQnJZLE9BQVFwRyxFQUFRb0csT0FDaEJELFFBQVNBLEVBQVE1SCxPQUFPdWUsSUFBSSxpQ0FDNUJlLG1CQUFvQjdkLEVBQVE2ZCxtQkFDNUJELFVBb0NnQnFCLENBQXNCamYsR0FDdEMsSUFBS2dlLEVBQWlCdkUsSUFBSS9aLEVBQVFxZixVQUNqQyxNQUFNLElBQUlqaEIsVUFBVSwwQkFBMEI4QixrQkFBb0JGLEVBQVFxZixTQUFTdFUsUUFBUSxLQUFNLDBCQUdsRyxHQUF5QixVQUFyQi9LLEVBQVFxZixTQUFzQixDQUNqQyxNQUFNamUsRUFBTzhYLEVBQWdCNVksRUFBUUosS0FDL0JpQyxFQUFXLElBQUl1USxFQUFTdFIsRUFBTSxDQUFDcUYsUUFBUyxDQUFDLGVBQWdCckYsRUFBS2lLLFlBRXBFLFlBREE5RCxFQUFRcEYsR0FLVCxNQUFNcWQsR0FBNkIsV0FBckJ4ZixFQUFRcWYsU0FBd0J0RyxFQUFRRCxHQUFNeFksU0FDdEQsT0FBQ25CLEdBQVVtQixFQUNqQixJQUFJNkIsRUFBVyxLQUVmLE1BQU14QyxFQUFRLEtBQ2IsTUFBTXdFLEVBQVEsSUFBSWthLEVBQVcsOEJBQzdCN1csRUFBT3JELEdBQ0g3RCxFQUFROEIsTUFBUTlCLEVBQVE4QixnQkFBZ0I2VyxFQUFPM0ksVUFDbERoUSxFQUFROEIsS0FBS2UsUUFBUWdCLEdBR2pCaEMsR0FBYUEsRUFBU0MsTUFJM0JELEVBQVNDLEtBQUtxZCxLQUFLLFFBQVN0YixJQUc3QixHQUFJaEYsR0FBVUEsRUFBT2QsUUFFcEIsWUFEQXNCLElBSUQsTUFBTStmLEVBQW1CLEtBQ3hCL2YsSUFDQWdnQixLQUlLQyxFQUFXSixFQUFLeGYsR0FFbEJiLEdBQ0hBLEVBQU9tWCxpQkFBaUIsUUFBU29KLEdBR2xDLE1BQU1DLEVBQVcsS0FDaEJDLEVBQVNqZ0IsUUFDTFIsR0FDSEEsRUFBTzBnQixvQkFBb0IsUUFBU0gsSUFJdENFLEVBQVNuWSxHQUFHLFNBQVN3SSxJQUNwQnpJLEVBQU8sSUFBSStSLEVBQVcsY0FBY2paLEVBQVFKLHVCQUF1QitQLEVBQUloTSxVQUFXLFNBQVVnTSxJQUM1RjBQLE9BR0RDLEVBQVNuWSxHQUFHLFlBQVlxWSxJQUN2QkYsRUFBU2xMLFdBQVcsR0FDcEIsTUFBTWpPLEVBcGRULFNBQXdCQSxFQUFVLElBQ2pDLE9BQU8sSUFBSWdNLEVBQ1ZoTSxFQUVFekIsUUFBTyxDQUFDMk8sRUFBUTVWLEVBQU9naUIsRUFBT0MsS0FDMUJELEVBQVEsR0FBTSxHQUNqQnBNLEVBQU9oTSxLQUFLcVksRUFBTS9OLE1BQU04TixFQUFPQSxFQUFRLElBR2pDcE0sSUFDTCxJQUNGeEwsUUFBTyxFQUFFNUcsRUFBTXhELE1BQ2YsSUFHQyxPQUZBdWUsRUFBbUIvYSxHQUNuQmdiLEVBQW9CaGIsRUFBTTBQLE9BQU9sVCxLQUMxQixFQUNOLE1BQ0QsT0FBTyxPQW1jT2tpQixDQUFlSCxFQUFVSSxZQUd6QyxHQUFJNUMsRUFBV3dDLEVBQVVLLFlBQWEsQ0FFckMsTUFBTXpDLEVBQVdqWCxFQUFRbEksSUFBSSxZQUd2QjZoQixFQUEyQixPQUFiMUMsRUFBb0IsS0FBTyxJQUFJeEgsSUFBSXdILEVBQVVwZCxFQUFRSixLQUd6RSxPQUFRSSxFQUFReWQsVUFDZixJQUFLLFFBR0osT0FGQXZXLEVBQU8sSUFBSStSLEVBQVcsMEVBQTBFalosRUFBUUosTUFBTyxxQkFDL0d5ZixJQUVELElBQUssU0FFSixHQUFvQixPQUFoQlMsRUFFSCxJQUNDM1osRUFBUXZILElBQUksV0FBWWtoQixHQUV2QixNQUFPamMsR0FDUnFELEVBQU9yRCxHQUlULE1BQ0QsSUFBSyxTQUFVLENBRWQsR0FBb0IsT0FBaEJpYyxFQUNILE1BSUQsR0FBSTlmLEVBQVFrZCxTQUFXbGQsRUFBUTBkLE9BRzlCLE9BRkF4VyxFQUFPLElBQUkrUixFQUFXLGdDQUFnQ2paLEVBQVFKLE1BQU8sc0JBQ3JFeWYsSUFNRCxNQUFNVSxFQUFpQixDQUN0QjVaLFFBQVMsSUFBSWdNLEVBQVFuUyxFQUFRbUcsU0FDN0J1WCxPQUFRMWQsRUFBUTBkLE9BQ2hCUixRQUFTbGQsRUFBUWtkLFFBQVUsRUFDM0JVLE1BQU81ZCxFQUFRNGQsTUFDZkQsU0FBVTNkLEVBQVEyZCxTQUNsQnZYLE9BQVFwRyxFQUFRb0csT0FDaEJ0RSxLQUFNOUIsRUFBUThCLEtBQ2RqRCxPQUFRbUIsRUFBUW5CLE9BQ2hCdVIsS0FBTXBRLEVBQVFvUSxNQUlmLE9BQTZCLE1BQXpCb1AsRUFBVUssWUFBc0I3ZixFQUFROEIsTUFBUW1jLEVBQVNuYyxnQkFBZ0I2VyxFQUFPM0ksVUFDbkY5SSxFQUFPLElBQUkrUixFQUFXLDJEQUE0RCw4QkFDbEZvRyxNQUs0QixNQUF6QkcsRUFBVUssYUFBaUQsTUFBekJMLEVBQVVLLFlBQStDLE1BQXpCTCxFQUFVSyxZQUEwQyxTQUFuQjdmLEVBQVFvRyxVQUM5RzJaLEVBQWUzWixPQUFTLE1BQ3hCMlosRUFBZWplLFVBQU8wTixFQUN0QnVRLEVBQWU1WixRQUFRckQsT0FBTyxtQkFJL0JtRSxFQUFRK0ssRUFBTSxJQUFJL0osRUFBUTZYLEVBQWFDLFVBQ3ZDVixPQVFIRyxFQUFVelEsS0FBSyxPQUFPLEtBQ2pCbFEsR0FDSEEsRUFBTzBnQixvQkFBb0IsUUFBU0gsTUFJdEMsSUFBSXRkLEVBQU82VyxFQUFPcUgsU0FBU1IsRUFBVyxJQUFJN0csRUFBT21ELGFBQWVqWSxJQUMvRHFELEVBQU9yRCxNQUdKb2MsUUFBUUMsUUFBVSxVQUNyQlYsRUFBVXJZLEdBQUcsVUFBV2lZLEdBR3pCLE1BQU1lLEVBQWtCLENBQ3ZCdmdCLElBQUtJLEVBQVFKLElBQ2I2RCxPQUFRK2IsRUFBVUssV0FDbEJuYyxXQUFZOGIsRUFBVVksY0FDdEJqYSxVQUNBaUssS0FBTXBRLEVBQVFvUSxLQUNkOE0sUUFBU2xkLEVBQVFrZCxRQUNqQmhMLGNBQWVsUyxFQUFRa1MsZUFJbEJtTyxFQUFVbGEsRUFBUWxJLElBQUksb0JBVTVCLElBQUsrQixFQUFRMmQsVUFBK0IsU0FBbkIzZCxFQUFRb0csUUFBaUMsT0FBWmlhLEdBQTZDLE1BQXpCYixFQUFVSyxZQUErQyxNQUF6QkwsRUFBVUssV0FHbkgsT0FGQWhlLEVBQVcsSUFBSXVRLEVBQVN0USxFQUFNcWUsUUFDOUJsWixFQUFRcEYsR0FTVCxNQUFNeWUsRUFBYyxDQUNuQkMsTUFBTzdILEVBQUs4SCxhQUNaQyxZQUFhL0gsRUFBSzhILGNBSW5CLEdBQWdCLFNBQVpILEdBQWtDLFdBQVpBLEVBTXpCLE9BTEF2ZSxFQUFPNlcsRUFBT3FILFNBQVNsZSxFQUFNNFcsRUFBS2dJLGFBQWFKLElBQWN6YyxJQUM1RHFELEVBQU9yRCxNQUVSaEMsRUFBVyxJQUFJdVEsRUFBU3RRLEVBQU1xZSxRQUM5QmxaLEVBQVFwRixHQUtULEdBQWdCLFlBQVp3ZSxHQUFxQyxjQUFaQSxFQUE3QixDQXlCQSxHQUFnQixPQUFaQSxFQU1ILE9BTEF2ZSxFQUFPNlcsRUFBT3FILFNBQVNsZSxFQUFNNFcsRUFBS2lJLDBCQUEwQjljLElBQzNEcUQsRUFBT3JELE1BRVJoQyxFQUFXLElBQUl1USxFQUFTdFEsRUFBTXFlLFFBQzlCbFosRUFBUXBGLEdBS1RBLEVBQVcsSUFBSXVRLEVBQVN0USxFQUFNcWUsR0FDOUJsWixFQUFRcEYsUUFqQ0s4VyxFQUFPcUgsU0FBU1IsRUFBVyxJQUFJN0csRUFBT21ELGFBQWVqWSxJQUNoRXFELEVBQU9yRCxNQUVKa0wsS0FBSyxRQUFRM0gsSUFHZnRGLEVBRHlCLElBQVYsR0FBWHNGLEVBQU0sSUFDSHVSLEVBQU9xSCxTQUFTbGUsRUFBTTRXLEVBQUtrSSxpQkFBaUIvYyxJQUNsRHFELEVBQU9yRCxNQUdEOFUsRUFBT3FILFNBQVNsZSxFQUFNNFcsRUFBS21JLG9CQUFvQmhkLElBQ3JEcUQsRUFBT3JELE1BSVRoQyxFQUFXLElBQUl1USxFQUFTdFEsRUFBTXFlLEdBQzlCbFosRUFBUXBGLFNBbjNCUyxFQUFDaWYsR0FBT2hmLFdBQ2hCLE9BQVRBLEVBRUhnZixFQUFLMVksTUFDS3VSLEVBQU83WCxHQUVqQkEsRUFBS2dGLFNBQVNoQixLQUFLZ2IsR0FDVHhaLE9BQU9rVCxTQUFTMVksSUFFMUJnZixFQUFLQyxNQUFNamYsR0FDWGdmLEVBQUsxWSxPQUdMdEcsRUFBS2dFLEtBQUtnYixJQTAzQlZFLENBQWMxQixFQUFVdGYsTUFJMUI5QyxFQUFRNmdCLFdBQWFBLEVBQ3JCN2dCLEVBQVErYixXQUFhQSxFQUNyQi9iLEVBQVFpVixRQUFVQSxFQUNsQmpWLEVBQVErSyxRQUFVQSxFQUNsQi9LLEVBQVFrVixTQUFXQSxFQUNuQmxWLEVBQVFvQyxRQUFVMFMsRUFDbEI5VSxFQUFROGYsV0FBYUEsRyxPQ2o4Q3JCLFNBQVNpRSxFQUFXNVcsR0FDbEIsT0FBT0EsRUFDRUksUUFBUSxTQUFVLEtBQ2xCQSxRQUFRLFFBQVMsS0FDakJBLFFBQVEsUUFBUyxLQUNqQkEsUUFBUSxRQUFTLE9BRzVCdE4sRUFBT0QsUUFBVSxXQUNmLElBQUlna0IsRUFBUyxHQUFHdlAsTUFBTTVTLEtBQUs0TixVQUFXLEdBQUdqSCxLQUFLLEtBQzlDLE9BQU91YixFQUFVQyxLLDJjQ05uQixNQUFNQyxFQUFtQyxtQkFBWDVpQixRQUFvRCxpQkFBcEJBLE9BQU84ZCxTQUNqRTlkLE9BQ0E2aUIsR0FBZSxVQUFVQSxLQUc3QixTQUFTQyxLQWVULE1BQU05TyxFQVhrQixvQkFBVEcsS0FDQUEsS0FFZ0Isb0JBQVgzRSxPQUNMQSxPQUVnQixvQkFBWGtFLE9BQ0xBLFlBRE4sRUFPVCxTQUFTcVAsRUFBYW5ULEdBQ2xCLE1BQXFCLGlCQUFOQSxHQUF3QixPQUFOQSxHQUE0QixtQkFBTkEsRUFFM0QsTUFBTW9ULEVBQWtDRixFQUVsQ0csRUFBa0J4YSxRQUNsQnlhLEVBQXNCemEsUUFBUTdJLFVBQVV3RSxLQUN4QytlLEVBQXlCMWEsUUFBUUMsUUFBUTZMLEtBQUswTyxHQUM5Q0csRUFBd0IzYSxRQUFRRSxPQUFPNEwsS0FBSzBPLEdBQ2xELFNBQVNJLEVBQVdDLEdBQ2hCLE9BQU8sSUFBSUwsRUFBZ0JLLEdBRS9CLFNBQVNDLEVBQW9CcmtCLEdBQ3pCLE9BQU9pa0IsRUFBdUJqa0IsR0FFbEMsU0FBU3NrQixFQUFvQkMsR0FDekIsT0FBT0wsRUFBc0JLLEdBRWpDLFNBQVNDLEVBQW1CQyxFQUFTQyxFQUFhQyxHQUc5QyxPQUFPWCxFQUFvQjFpQixLQUFLbWpCLEVBQVNDLEVBQWFDLEdBRTFELFNBQVNDLEVBQVlILEVBQVNDLEVBQWFDLEdBQ3ZDSCxFQUFtQkEsRUFBbUJDLEVBQVNDLEVBQWFDLFFBQWE1UyxFQUFXK1IsR0FFeEYsU0FBU2UsRUFBZ0JKLEVBQVNDLEdBQzlCRSxFQUFZSCxFQUFTQyxHQUV6QixTQUFTSSxFQUFjTCxFQUFTRSxHQUM1QkMsRUFBWUgsT0FBUzFTLEVBQVc0UyxHQUVwQyxTQUFTSSxFQUFxQk4sRUFBU08sRUFBb0JDLEdBQ3ZELE9BQU9ULEVBQW1CQyxFQUFTTyxFQUFvQkMsR0FFM0QsU0FBU0MsRUFBMEJULEdBQy9CRCxFQUFtQkMsT0FBUzFTLEVBQVcrUixHQUUzQyxNQUFNcUIsRUFBaUIsTUFDbkIsTUFBTUMsRUFBdUJ0USxHQUFXQSxFQUFRcVEsZUFDaEQsR0FBb0MsbUJBQXpCQyxFQUNQLE9BQU9BLEVBRVgsTUFBTUMsRUFBa0JoQixPQUFvQnRTLEdBQzVDLE9BQVE0RyxHQUFPNkwsRUFBbUJhLEVBQWlCMU0sSUFOaEMsR0FRdkIsU0FBUzJNLEVBQVlDLEVBQUdDLEVBQUdDLEdBQ3ZCLEdBQWlCLG1CQUFORixFQUNQLE1BQU0sSUFBSWxsQixVQUFVLDhCQUV4QixPQUFPcWxCLFNBQVNobEIsVUFBVXVPLE1BQU0zTixLQUFLaWtCLEVBQUdDLEVBQUdDLEdBRS9DLFNBQVNFLEVBQVlKLEVBQUdDLEVBQUdDLEdBQ3ZCLElBQ0ksT0FBT3BCLEVBQW9CaUIsRUFBWUMsRUFBR0MsRUFBR0MsSUFFakQsTUFBT3psQixHQUNILE9BQU9za0IsRUFBb0J0a0IsSUFhbkMsTUFBTTRsQixFQUNGLGNBQ0kvbEIsS0FBS2dtQixRQUFVLEVBQ2ZobUIsS0FBS2ltQixNQUFRLEVBRWJqbUIsS0FBS2ttQixPQUFTLENBQ1ZDLFVBQVcsR0FDWEMsV0FBT2xVLEdBRVhsUyxLQUFLcW1CLE1BQVFybUIsS0FBS2ttQixPQUlsQmxtQixLQUFLZ21CLFFBQVUsRUFFZmhtQixLQUFLaW1CLE1BQVEsRUFFakIsYUFDSSxPQUFPam1CLEtBQUtpbUIsTUFNaEIsS0FBS2pULEdBQ0QsTUFBTXNULEVBQVV0bUIsS0FBS3FtQixNQUNyQixJQUFJRSxFQUFVRCxFQUNtQkUsUUFBN0JGLEVBQVFILFVBQVVoZCxTQUNsQm9kLEVBQVUsQ0FDTkosVUFBVyxHQUNYQyxXQUFPbFUsSUFLZm9VLEVBQVFILFVBQVVwYyxLQUFLaUosR0FDbkJ1VCxJQUFZRCxJQUNadG1CLEtBQUtxbUIsTUFBUUUsRUFDYkQsRUFBUUYsTUFBUUcsS0FFbEJ2bUIsS0FBS2ltQixNQUlYLFFBQ0ksTUFBTVEsRUFBV3ptQixLQUFLa21CLE9BQ3RCLElBQUlRLEVBQVdELEVBQ2YsTUFBTUUsRUFBWTNtQixLQUFLZ21CLFFBQ3ZCLElBQUlZLEVBQVlELEVBQVksRUFDNUIsTUFBTUUsRUFBV0osRUFBU04sVUFDcEJuVCxFQUFVNlQsRUFBU0YsR0FhekIsT0F0RXFCLFFBMERqQkMsSUFDQUYsRUFBV0QsRUFBU0wsTUFDcEJRLEVBQVksS0FHZDVtQixLQUFLaW1CLE1BQ1BqbUIsS0FBS2dtQixRQUFVWSxFQUNYSCxJQUFhQyxJQUNiMW1CLEtBQUtrbUIsT0FBU1EsR0FHbEJHLEVBQVNGLFFBQWF6VSxFQUNmYyxFQVVYLFFBQVF1TSxHQUNKLElBQUk3UixFQUFJMU4sS0FBS2dtQixRQUNUOVUsRUFBT2xSLEtBQUtrbUIsT0FDWlcsRUFBVzNWLEVBQUtpVixVQUNwQixPQUFPelksSUFBTW1aLEVBQVMxZCxhQUF5QitJLElBQWZoQixFQUFLa1YsT0FDN0IxWSxJQUFNbVosRUFBUzFkLFNBQ2YrSCxFQUFPQSxFQUFLa1YsTUFDWlMsRUFBVzNWLEVBQUtpVixVQUNoQnpZLEVBQUksRUFDb0IsSUFBcEJtWixFQUFTMWQsVUFJakJvVyxFQUFTc0gsRUFBU25aLE1BQ2hCQSxFQUtWLE9BQ0ksTUFBTW9aLEVBQVE5bUIsS0FBS2ttQixPQUNiYSxFQUFTL21CLEtBQUtnbUIsUUFDcEIsT0FBT2MsRUFBTVgsVUFBVVksSUFJL0IsU0FBU0MsRUFBc0MxTSxFQUFROVEsR0FDbkQ4USxFQUFPMk0scUJBQXVCemQsRUFDOUJBLEVBQU8wZCxRQUFVNU0sRUFDSyxhQUFsQjlRLEVBQU8yZCxPQUNQQyxFQUFxQzlNLEdBRWQsV0FBbEI5USxFQUFPMmQsT0FzQ3BCLFNBQXdEN00sR0FDcEQ4TSxFQUFxQzlNLEdBQ3JDK00sRUFBa0MvTSxHQXZDOUJnTixDQUErQ2hOLEdBRy9DaU4sRUFBK0NqTixFQUFROVEsRUFBT2dlLGNBS3RFLFNBQVNDLEVBQWtDbk4sRUFBUW9LLEdBRS9DLE9BQU9nRCxHQURRcE4sRUFBTzJNLHFCQUNjdkMsR0FFeEMsU0FBU2lELEVBQW1Dck4sR0FDRyxhQUF2Q0EsRUFBTzJNLHFCQUFxQkUsT0FDNUJTLEVBQWlDdE4sRUFBUSxJQUFJOVosVUFBVSxxRkFvQy9ELFNBQW1EOFosRUFBUW9LLEdBQ3ZENkMsRUFBK0NqTixFQWxDTyxJQUFJOVosVUFBVSxxRkFBaEVxbkIsQ0FBMEN2TixHQUU5Q0EsRUFBTzJNLHFCQUFxQkMsYUFBVWhWLEVBQ3RDb0ksRUFBTzJNLDBCQUF1Qi9VLEVBR2xDLFNBQVM0VixFQUFvQm5rQixHQUN6QixPQUFPLElBQUluRCxVQUFVLFVBQVltRCxFQUFPLHFDQUc1QyxTQUFTeWpCLEVBQXFDOU0sR0FDMUNBLEVBQU95TixlQUFpQnpELEdBQVcsQ0FBQzNhLEVBQVNDLEtBQ3pDMFEsRUFBTzBOLHVCQUF5QnJlLEVBQ2hDMlEsRUFBTzJOLHNCQUF3QnJlLEtBR3ZDLFNBQVMyZCxFQUErQ2pOLEVBQVFvSyxHQUM1RDBDLEVBQXFDOU0sR0FDckNzTixFQUFpQ3ROLEVBQVFvSyxHQU03QyxTQUFTa0QsRUFBaUN0TixFQUFRb0ssUUFDVHhTLElBQWpDb0ksRUFBTzJOLHdCQUdYNUMsRUFBMEIvSyxFQUFPeU4sZ0JBQ2pDek4sRUFBTzJOLHNCQUFzQnZELEdBQzdCcEssRUFBTzBOLDRCQUF5QjlWLEVBQ2hDb0ksRUFBTzJOLDJCQUF3Qi9WLEdBS25DLFNBQVNtVixFQUFrQy9NLFFBQ0RwSSxJQUFsQ29JLEVBQU8wTix5QkFHWDFOLEVBQU8wTiw0QkFBdUI5VixHQUM5Qm9JLEVBQU8wTiw0QkFBeUI5VixFQUNoQ29JLEVBQU8yTiwyQkFBd0IvVixHQUduQyxNQUFNZ1csRUFBYXJFLEVBQWUsa0JBQzVCc0UsRUFBYXRFLEVBQWUsa0JBQzVCdUUsRUFBY3ZFLEVBQWUsbUJBQzdCd0UsRUFBWXhFLEVBQWUsaUJBSTNCeUUsRUFBaUIxTyxPQUFPMk8sVUFBWSxTQUFVMVgsR0FDaEQsTUFBb0IsaUJBQU5BLEdBQWtCMFgsU0FBUzFYLElBS3ZDMlgsRUFBWTFVLEtBQUsyVSxPQUFTLFNBQVVDLEdBQ3RDLE9BQU9BLEVBQUksRUFBSTVVLEtBQUs2VSxLQUFLRCxHQUFLNVUsS0FBSzhVLE1BQU1GLElBTzdDLFNBQVNHLEVBQWlCbmUsRUFBS29lLEdBQzNCLFFBQVk1VyxJQUFSeEgsR0FIZ0IsaUJBREZtRyxFQUlxQm5HLElBSE0sbUJBQU5tRyxFQUluQyxNQUFNLElBQUlyUSxVQUFVLEdBQUdzb0IsdUJBTC9CLElBQXNCalksRUFTdEIsU0FBU2tZLEVBQWVsWSxFQUFHaVksR0FDdkIsR0FBaUIsbUJBQU5qWSxFQUNQLE1BQU0sSUFBSXJRLFVBQVUsR0FBR3NvQix3QkFPL0IsU0FBU0UsRUFBYW5ZLEVBQUdpWSxHQUNyQixJQUpKLFNBQWtCalksR0FDZCxNQUFxQixpQkFBTkEsR0FBd0IsT0FBTkEsR0FBNEIsbUJBQU5BLEVBR2xERCxDQUFTQyxHQUNWLE1BQU0sSUFBSXJRLFVBQVUsR0FBR3NvQix1QkFHL0IsU0FBU0csRUFBdUJwWSxFQUFHcVksRUFBVUosR0FDekMsUUFBVTVXLElBQU5yQixFQUNBLE1BQU0sSUFBSXJRLFVBQVUsYUFBYTBvQixxQkFBNEJKLE9BR3JFLFNBQVNLLEVBQW9CdFksRUFBR2lNLEVBQU9nTSxHQUNuQyxRQUFVNVcsSUFBTnJCLEVBQ0EsTUFBTSxJQUFJclEsVUFBVSxHQUFHc2MscUJBQXlCZ00sT0FJeEQsU0FBU00sRUFBMEJqcEIsR0FDL0IsT0FBT3laLE9BQU96WixHQUVsQixTQUFTa3BCLEVBQW1CeFksR0FDeEIsT0FBYSxJQUFOQSxFQUFVLEVBQUlBLEVBTXpCLFNBQVN5WSxFQUF3Q25wQixFQUFPMm9CLEdBQ3BELE1BQ01TLEVBQWEzUCxPQUFPNFAsaUJBQzFCLElBQUkzWSxFQUFJK0ksT0FBT3paLEdBRWYsR0FEQTBRLEVBQUl3WSxFQUFtQnhZLElBQ2xCeVgsRUFBZXpYLEdBQ2hCLE1BQU0sSUFBSXJRLFVBQVUsR0FBR3NvQiw0QkFHM0IsR0FEQWpZLEVBWkosU0FBcUJBLEdBQ2pCLE9BQU93WSxFQUFtQmIsRUFBVTNYLElBV2hDNFksQ0FBWTVZLEdBQ1pBLEVBUmUsR0FRR0EsRUFBSTBZLEVBQ3RCLE1BQU0sSUFBSS9vQixVQUFVLEdBQUdzb0IsMkNBQTZEUyxnQkFFeEYsT0FBS2pCLEVBQWV6WCxJQUFZLElBQU5BLEVBT25CQSxFQU5JLEVBU2YsU0FBUzZZLEVBQXFCN1ksRUFBR2lZLEdBQzdCLElBQUthLEdBQWlCOVksR0FDbEIsTUFBTSxJQUFJclEsVUFBVSxHQUFHc29CLDhCQUsvQixTQUFTYyxFQUFtQ3BnQixHQUN4QyxPQUFPLElBQUlxZ0IsRUFBNEJyZ0IsR0FHM0MsU0FBU3NnQixFQUE2QnRnQixFQUFRdWdCLEdBQzFDdmdCLEVBQU8wZCxRQUFROEMsY0FBY2pnQixLQUFLZ2dCLEdBRXRDLFNBQVNFLEVBQWlDemdCLEVBQVFNLEVBQU8yUSxHQUNyRCxNQUNNc1AsRUFEU3ZnQixFQUFPMGQsUUFDSzhDLGNBQWNFLFFBQ3JDelAsRUFDQXNQLEVBQVlJLGNBR1pKLEVBQVlLLFlBQVl0Z0IsR0FHaEMsU0FBU3VnQixFQUFpQzdnQixHQUN0QyxPQUFPQSxFQUFPMGQsUUFBUThDLGNBQWM3Z0IsT0FFeEMsU0FBU21oQixFQUErQjlnQixHQUNwQyxNQUFNOFEsRUFBUzlRLEVBQU8wZCxRQUN0QixZQUFlaFYsSUFBWG9JLEtBR0NpUSxHQUE4QmpRLEdBVXZDLE1BQU11UCxFQUNGLFlBQVlyZ0IsR0FHUixHQUZBeWYsRUFBdUJ6ZixFQUFRLEVBQUcsK0JBQ2xDa2dCLEVBQXFCbGdCLEVBQVEsbUJBQ3pCZ2hCLEdBQXVCaGhCLEdBQ3ZCLE1BQU0sSUFBSWhKLFVBQVUsK0VBRXhCd21CLEVBQXNDaG5CLEtBQU13SixHQUM1Q3hKLEtBQUtncUIsY0FBZ0IsSUFBSWpFLEVBTTdCLGFBQ0ksT0FBS3dFLEdBQThCdnFCLE1BRzVCQSxLQUFLK25CLGVBRkR0RCxFQUFvQmdHLEdBQWlDLFdBT3BFLE9BQU8vRixHQUNILE9BQUs2RixHQUE4QnZxQixXQUdEa1MsSUFBOUJsUyxLQUFLaW5CLHFCQUNFeEMsRUFBb0JxRCxFQUFvQixXQUU1Q0wsRUFBa0N6bkIsS0FBTTBrQixHQUxwQ0QsRUFBb0JnRyxHQUFpQyxXQVlwRSxPQUNJLElBQUtGLEdBQThCdnFCLE1BQy9CLE9BQU95a0IsRUFBb0JnRyxHQUFpQyxTQUVoRSxRQUFrQ3ZZLElBQTlCbFMsS0FBS2luQixxQkFDTCxPQUFPeEMsRUFBb0JxRCxFQUFvQixjQUVuRCxJQUFJNEMsRUFDQUMsRUFDSixNQUFNL0YsRUFBVU4sR0FBVyxDQUFDM2EsRUFBU0MsS0FDakM4Z0IsRUFBaUIvZ0IsRUFDakJnaEIsRUFBZ0IvZ0IsS0FRcEIsT0FEQWdoQixHQUFnQzVxQixLQUxaLENBQ2hCb3FCLFlBQWF0Z0IsR0FBUzRnQixFQUFlLENBQUV2cUIsTUFBTzJKLEVBQU8yUSxNQUFNLElBQzNEMFAsWUFBYSxJQUFNTyxFQUFlLENBQUV2cUIsV0FBTytSLEVBQVd1SSxNQUFNLElBQzVEb1EsWUFBYUMsR0FBS0gsRUFBY0csS0FHN0JsRyxFQVdYLGNBQ0ksSUFBSzJGLEdBQThCdnFCLE1BQy9CLE1BQU15cUIsR0FBaUMsZUFFM0MsUUFBa0N2WSxJQUE5QmxTLEtBQUtpbkIscUJBQVQsQ0FHQSxHQUFJam5CLEtBQUtncUIsY0FBYzdnQixPQUFTLEVBQzVCLE1BQU0sSUFBSTNJLFVBQVUsdUZBRXhCbW5CLEVBQW1DM25CLFFBZ0IzQyxTQUFTdXFCLEdBQThCMVosR0FDbkMsUUFBS21ULEVBQWFuVCxNQUdiNVEsT0FBT1ksVUFBVWtxQixlQUFldHBCLEtBQUtvUCxFQUFHLGlCQUtqRCxTQUFTK1osR0FBZ0N0USxFQUFReVAsR0FDN0MsTUFBTXZnQixFQUFTOFEsRUFBTzJNLHFCQUN0QnpkLEVBQU93aEIsWUFBYSxFQUNFLFdBQWxCeGhCLEVBQU8yZCxPQUNQNEMsRUFBWUksY0FFVyxZQUFsQjNnQixFQUFPMmQsT0FDWjRDLEVBQVljLFlBQVlyaEIsRUFBT2dlLGNBRy9CaGUsRUFBT3loQiwwQkFBMEI1QyxHQUFXMEIsR0FJcEQsU0FBU1UsR0FBaUM5bUIsR0FDdEMsT0FBTyxJQUFJbkQsVUFBVSx5Q0FBeUNtRCx1REFyQ2xFMUQsT0FBT2MsaUJBQWlCOG9CLEVBQTRCaHBCLFVBQVcsQ0FDM0RxcUIsT0FBUSxDQUFFbHFCLFlBQVksR0FDdEI0UyxLQUFNLENBQUU1UyxZQUFZLEdBQ3BCbXFCLFlBQWEsQ0FBRW5xQixZQUFZLEdBQzNCb3FCLE9BQVEsQ0FBRXBxQixZQUFZLEtBRWdCLGlCQUEvQjZpQixFQUFlM2lCLGFBQ3RCakIsT0FBT0MsZUFBZTJwQixFQUE0QmhwQixVQUFXZ2pCLEVBQWUzaUIsWUFBYSxDQUNyRmYsTUFBTyw4QkFDUGdCLGNBQWMsSUFpQ3RCLE1BQU1rcUIsR0FBeUJwckIsT0FBTytQLGVBQWUvUCxPQUFPK1AsZ0JBQWUwRCxzQkFBd0I3UyxXQUduRyxNQUFNeXFCLEdBQ0YsWUFBWWhSLEVBQVFpUixHQUNoQnZyQixLQUFLd3JCLHFCQUFrQnRaLEVBQ3ZCbFMsS0FBS3lyQixhQUFjLEVBQ25CenJCLEtBQUtrbkIsUUFBVTVNLEVBQ2Z0YSxLQUFLMHJCLGVBQWlCSCxFQUUxQixPQUNJLE1BQU1JLEVBQVksSUFBTTNyQixLQUFLNHJCLGFBSTdCLE9BSEE1ckIsS0FBS3dyQixnQkFBa0J4ckIsS0FBS3dyQixnQkFDeEJ0RyxFQUFxQmxsQixLQUFLd3JCLGdCQUFpQkcsRUFBV0EsR0FDdERBLElBQ0czckIsS0FBS3dyQixnQkFFaEIsT0FBT3JyQixHQUNILE1BQU0wckIsRUFBYyxJQUFNN3JCLEtBQUs4ckIsYUFBYTNyQixHQUM1QyxPQUFPSCxLQUFLd3JCLGdCQUNSdEcsRUFBcUJsbEIsS0FBS3dyQixnQkFBaUJLLEVBQWFBLEdBQ3hEQSxJQUVSLGFBQ0ksR0FBSTdyQixLQUFLeXJCLFlBQ0wsT0FBTy9oQixRQUFRQyxRQUFRLENBQUV4SixXQUFPK1IsRUFBV3VJLE1BQU0sSUFFckQsTUFBTUgsRUFBU3RhLEtBQUtrbkIsUUFDcEIsUUFBb0NoVixJQUFoQ29JLEVBQU8yTSxxQkFDUCxPQUFPeEMsRUFBb0JxRCxFQUFvQixZQUVuRCxJQUFJNEMsRUFDQUMsRUFDSixNQUFNL0YsRUFBVU4sR0FBVyxDQUFDM2EsRUFBU0MsS0FDakM4Z0IsRUFBaUIvZ0IsRUFDakJnaEIsRUFBZ0IvZ0IsS0F1QnBCLE9BREFnaEIsR0FBZ0N0USxFQXBCWixDQUNoQjhQLFlBQWF0Z0IsSUFDVDlKLEtBQUt3ckIscUJBQWtCdFosRUFHdkJvVCxHQUFlLElBQU1vRixFQUFlLENBQUV2cUIsTUFBTzJKLEVBQU8yUSxNQUFNLE9BRTlEMFAsWUFBYSxLQUNUbnFCLEtBQUt3ckIscUJBQWtCdFosRUFDdkJsUyxLQUFLeXJCLGFBQWMsRUFDbkI5RCxFQUFtQ3JOLEdBQ25Db1EsRUFBZSxDQUFFdnFCLFdBQU8rUixFQUFXdUksTUFBTSxLQUU3Q29RLFlBQWFuRyxJQUNUMWtCLEtBQUt3ckIscUJBQWtCdFosRUFDdkJsUyxLQUFLeXJCLGFBQWMsRUFDbkI5RCxFQUFtQ3JOLEdBQ25DcVEsRUFBY2pHLE1BSWZFLEVBRVgsYUFBYXprQixHQUNULEdBQUlILEtBQUt5ckIsWUFDTCxPQUFPL2hCLFFBQVFDLFFBQVEsQ0FBRXhKLFFBQU9zYSxNQUFNLElBRTFDemEsS0FBS3lyQixhQUFjLEVBQ25CLE1BQU1uUixFQUFTdGEsS0FBS2tuQixRQUNwQixRQUFvQ2hWLElBQWhDb0ksRUFBTzJNLHFCQUNQLE9BQU94QyxFQUFvQnFELEVBQW9CLHFCQUVuRCxJQUFLOW5CLEtBQUswckIsZUFBZ0IsQ0FDdEIsTUFBTTNWLEVBQVMwUixFQUFrQ25OLEVBQVFuYSxHQUV6RCxPQURBd25CLEVBQW1Dck4sR0FDNUI0SyxFQUFxQm5QLEdBQVEsS0FBTSxDQUFHNVYsUUFBT3NhLE1BQU0sTUFHOUQsT0FEQWtOLEVBQW1Dck4sR0FDNUJrSyxFQUFvQixDQUFFcmtCLFFBQU9zYSxNQUFNLEtBR2xELE1BQU1zUixHQUF1QyxDQUN6QyxPQUNJLE9BQUtDLEdBQThCaHNCLE1BRzVCQSxLQUFLaXNCLG1CQUFtQjVhLE9BRnBCb1QsRUFBb0J5SCxHQUF1QyxVQUkxRSxPQUFPL3JCLEdBQ0gsT0FBSzZyQixHQUE4QmhzQixNQUc1QkEsS0FBS2lzQixtQkFBbUJFLE9BQU9oc0IsR0FGM0Jza0IsRUFBb0J5SCxHQUF1QyxhQWdCOUUsU0FBU0YsR0FBOEJuYixHQUNuQyxRQUFLbVQsRUFBYW5ULE1BR2I1USxPQUFPWSxVQUFVa3FCLGVBQWV0cEIsS0FBS29QLEVBQUcsc0JBTWpELFNBQVNxYixHQUF1Q3ZvQixHQUM1QyxPQUFPLElBQUluRCxVQUFVLCtCQUErQm1ELDJEQXRCekJ1TyxJQUEzQm1aLElBQ0FwckIsT0FBT3lRLGVBQWVxYixHQUFzQ1YsSUEwQmhFLE1BQU1lLEdBQWN4UyxPQUFPQyxPQUFTLFNBQVVoSixHQUUxQyxPQUFPQSxHQUFNQSxHQUdqQixTQUFTd2IsR0FBMEIzRCxHQUMvQixRQVFKLFNBQTZCQSxHQUN6QixNQUFpQixpQkFBTkEsS0FHUDBELEdBQVkxRCxNQUdaQSxFQUFJLElBZkg0RCxDQUFvQjVELElBR3JCQSxJQUFNNkQsSUFrQmQsU0FBU0MsR0FBYUMsR0FDbEIsTUFBTXpOLEVBQU95TixFQUFVQyxPQUFPeEMsUUFLOUIsT0FKQXVDLEVBQVVFLGlCQUFtQjNOLEVBQUtsTSxLQUM5QjJaLEVBQVVFLGdCQUFrQixJQUM1QkYsRUFBVUUsZ0JBQWtCLEdBRXpCM04sRUFBSzdlLE1BRWhCLFNBQVN5c0IsR0FBcUJILEVBQVd0c0IsRUFBTzJTLEdBRTVDLElBQUt1WixHQURMdlosRUFBTzhHLE9BQU85RyxJQUVWLE1BQU0sSUFBSWlHLFdBQVcsd0RBRXpCMFQsRUFBVUMsT0FBTzNpQixLQUFLLENBQUU1SixRQUFPMlMsU0FDL0IyWixFQUFVRSxpQkFBbUI3WixFQU1qQyxTQUFTK1osR0FBV0osR0FDaEJBLEVBQVVDLE9BQVMsSUFBSTNHLEVBQ3ZCMEcsRUFBVUUsZ0JBQWtCLEVBR2hDLFNBQVNHLEdBQW9CakcsR0FHekIsT0FBT0EsRUFBU3hTLFFBbUJwQixNQUFNMFksR0FDRixjQUNJLE1BQU0sSUFBSXZzQixVQUFVLHVCQUt4QixXQUNJLElBQUt3c0IsR0FBNEJodEIsTUFDN0IsTUFBTWl0QixHQUErQixRQUV6QyxPQUFPanRCLEtBQUtrdEIsTUFFaEIsUUFBUUMsR0FDSixJQUFLSCxHQUE0Qmh0QixNQUM3QixNQUFNaXRCLEdBQStCLFdBSXpDLEdBRkFoRSxFQUF1QmtFLEVBQWMsRUFBRyxXQUN4Q0EsRUFBZTdELEVBQXdDNkQsRUFBYyx3QkFDaEJqYixJQUFqRGxTLEtBQUtvdEIsd0NBQ0wsTUFBTSxJQUFJNXNCLFVBQVUsMENBRUhSLEtBQUtrdEIsTUFBTXJmLE9BdWZ4QyxTQUE2Qy9MLEVBQVlxckIsR0FFckQsSUFBS2QsR0FETGMsRUFBZXZULE9BQU91VCxJQUVsQixNQUFNLElBQUlwVSxXQUFXLGlDQUV6QnNVLEdBQTRDdnJCLEVBQVlxckIsR0EzZnBERyxDQUFvQ3R0QixLQUFLb3RCLHdDQUF5Q0QsR0FFdEYsbUJBQW1CSSxHQUNmLElBQUtQLEdBQTRCaHRCLE1BQzdCLE1BQU1pdEIsR0FBK0Isc0JBR3pDLEdBREFoRSxFQUF1QnNFLEVBQU0sRUFBRyx1QkFDM0J0YSxZQUFZQyxPQUFPcWEsR0FDcEIsTUFBTSxJQUFJL3NCLFVBQVUsZ0RBRXhCLEdBQXdCLElBQXBCK3NCLEVBQUtuYSxXQUNMLE1BQU0sSUFBSTVTLFVBQVUsdUNBRXhCLEdBQStCLElBQTNCK3NCLEVBQUsxZixPQUFPdUYsV0FDWixNQUFNLElBQUk1UyxVQUFVLGdEQUV4QixRQUFxRDBSLElBQWpEbFMsS0FBS290Qix3Q0FDTCxNQUFNLElBQUk1c0IsVUFBVSwyQ0E0ZWhDLFNBQXdEc0IsRUFBWXlyQixHQUNoRSxNQUFNQyxFQUFrQjFyQixFQUFXMnJCLGtCQUFrQkMsT0FDckQsR0FBSUYsRUFBZ0JyYSxXQUFhcWEsRUFBZ0JHLGNBQWdCSixFQUFLcGEsV0FDbEUsTUFBTSxJQUFJNEYsV0FBVywyREFFekIsR0FBSXlVLEVBQWdCcGEsYUFBZW1hLEVBQUtuYSxXQUNwQyxNQUFNLElBQUkyRixXQUFXLDhEQUV6QnlVLEVBQWdCM2YsT0FBUzBmLEVBQUsxZixPQUM5QndmLEdBQTRDdnJCLEVBQVl5ckIsRUFBS25hLFlBbmZ6RHdhLENBQStDNXRCLEtBQUtvdEIsd0NBQXlDRyxJQUdyR3R0QixPQUFPYyxpQkFBaUJnc0IsR0FBMEJsc0IsVUFBVyxDQUN6RGd0QixRQUFTLENBQUU3c0IsWUFBWSxHQUN2QjhzQixtQkFBb0IsQ0FBRTlzQixZQUFZLEdBQ2xDdXNCLEtBQU0sQ0FBRXZzQixZQUFZLEtBRWtCLGlCQUEvQjZpQixFQUFlM2lCLGFBQ3RCakIsT0FBT0MsZUFBZTZzQixHQUEwQmxzQixVQUFXZ2pCLEVBQWUzaUIsWUFBYSxDQUNuRmYsTUFBTyw0QkFDUGdCLGNBQWMsSUFRdEIsTUFBTTRzQixHQUNGLGNBQ0ksTUFBTSxJQUFJdnRCLFVBQVUsdUJBS3hCLGtCQUNJLElBQUt3dEIsR0FBK0JodUIsTUFDaEMsTUFBTWl1QixHQUF3QyxlQUVsRCxHQUEwQixPQUF0Qmp1QixLQUFLa3VCLGNBQXlCbHVCLEtBQUt5dEIsa0JBQWtCdGtCLE9BQVMsRUFBRyxDQUNqRSxNQUFNcWtCLEVBQWtCeHRCLEtBQUt5dEIsa0JBQWtCQyxPQUN6Q0gsRUFBTyxJQUFJL1osV0FBV2dhLEVBQWdCM2YsT0FBUTJmLEVBQWdCcmEsV0FBYXFhLEVBQWdCRyxZQUFhSCxFQUFnQnBhLFdBQWFvYSxFQUFnQkcsYUFDckpRLEVBQWNsdUIsT0FBT3VCLE9BQU91ckIsR0FBMEJsc0IsWUE2ZnhFLFNBQXdDNkIsRUFBU1osRUFBWXlyQixHQUN6RDdxQixFQUFRMHFCLHdDQUEwQ3RyQixFQUNsRFksRUFBUXdxQixNQUFRSyxFQTlmUmEsQ0FBK0JELEVBQWFudUIsS0FBTXV0QixHQUNsRHZ0QixLQUFLa3VCLGFBQWVDLEVBRXhCLE9BQU9udUIsS0FBS2t1QixhQU1oQixrQkFDSSxJQUFLRixHQUErQmh1QixNQUNoQyxNQUFNaXVCLEdBQXdDLGVBRWxELE9BQU9JLEdBQTJDcnVCLE1BTXRELFFBQ0ksSUFBS2d1QixHQUErQmh1QixNQUNoQyxNQUFNaXVCLEdBQXdDLFNBRWxELEdBQUlqdUIsS0FBS3N1QixnQkFDTCxNQUFNLElBQUk5dEIsVUFBVSw4REFFeEIsTUFBTXNELEVBQVE5RCxLQUFLdXVCLDhCQUE4QnBILE9BQ2pELEdBQWMsYUFBVnJqQixFQUNBLE1BQU0sSUFBSXRELFVBQVUsa0JBQWtCc0QsK0RBaVdsRCxTQUEyQ2hDLEdBQ3ZDLE1BQU0wSCxFQUFTMUgsRUFBV3lzQiw4QkFDMUIsSUFBSXpzQixFQUFXd3NCLGlCQUFxQyxhQUFsQjlrQixFQUFPMmQsT0FHekMsR0FBSXJsQixFQUFXNnFCLGdCQUFrQixFQUM3QjdxQixFQUFXd3NCLGlCQUFrQixNQURqQyxDQUlBLEdBQUl4c0IsRUFBVzJyQixrQkFBa0J0a0IsT0FBUyxHQUNUckgsRUFBVzJyQixrQkFBa0JDLE9BQ2pDQyxZQUFjLEVBQUcsQ0FDdEMsTUFBTTdDLEVBQUksSUFBSXRxQixVQUFVLDJEQUV4QixNQURBZ3VCLEdBQWtDMXNCLEVBQVlncEIsR0FDeENBLEVBR2QyRCxHQUE0QzNzQixHQUM1QzRzQixHQUFvQmxsQixJQWpYaEJtbEIsQ0FBa0MzdUIsTUFFdEMsUUFBUThKLEdBQ0osSUFBS2trQixHQUErQmh1QixNQUNoQyxNQUFNaXVCLEdBQXdDLFdBR2xELEdBREFoRixFQUF1Qm5mLEVBQU8sRUFBRyxZQUM1Qm1KLFlBQVlDLE9BQU9wSixHQUNwQixNQUFNLElBQUl0SixVQUFVLHNDQUV4QixHQUF5QixJQUFyQnNKLEVBQU1zSixXQUNOLE1BQU0sSUFBSTVTLFVBQVUsdUNBRXhCLEdBQWdDLElBQTVCc0osRUFBTStELE9BQU91RixXQUNiLE1BQU0sSUFBSTVTLFVBQVUsZ0RBRXhCLEdBQUlSLEtBQUtzdUIsZ0JBQ0wsTUFBTSxJQUFJOXRCLFVBQVUsZ0NBRXhCLE1BQU1zRCxFQUFROUQsS0FBS3V1Qiw4QkFBOEJwSCxPQUNqRCxHQUFjLGFBQVZyakIsRUFDQSxNQUFNLElBQUl0RCxVQUFVLGtCQUFrQnNELG9FQThWbEQsU0FBNkNoQyxFQUFZZ0ksR0FDckQsTUFBTU4sRUFBUzFILEVBQVd5c0IsOEJBQzFCLEdBQUl6c0IsRUFBV3dzQixpQkFBcUMsYUFBbEI5a0IsRUFBTzJkLE9BQ3JDLE9BRUosTUFBTXRaLEVBQVMvRCxFQUFNK0QsT0FDZnNGLEVBQWFySixFQUFNcUosV0FDbkJDLEVBQWF0SixFQUFNc0osV0FDbkJ3YixFQUF3Qy9nQixFQUMxQ3ljLEVBQStCOWdCLEdBQ2tCLElBQTdDNmdCLEVBQWlDN2dCLEdBQ2pDcWxCLEdBQWdEL3NCLEVBQVk4c0IsRUFBbUJ6YixFQUFZQyxHQUkzRjZXLEVBQWlDemdCLEVBRFQsSUFBSWdLLFdBQVdvYixFQUFtQnpiLEVBQVlDLElBQ1osR0FHekQwYixHQUE0QnRsQixJQUVqQ3FsQixHQUFnRC9zQixFQUFZOHNCLEVBQW1CemIsRUFBWUMsR0FDM0YyYixHQUFpRWp0QixJQUdqRStzQixHQUFnRC9zQixFQUFZOHNCLEVBQW1CemIsRUFBWUMsR0FFL0Y0YixHQUE2Q2x0QixHQXRYekNtdEIsQ0FBb0NqdkIsS0FBTThKLEdBSzlDLE1BQU1naEIsR0FDRixJQUFLa0QsR0FBK0JodUIsTUFDaEMsTUFBTWl1QixHQUF3QyxTQUVsRE8sR0FBa0N4dUIsS0FBTThxQixHQUc1QyxDQUFDMUMsR0FBYTFELEdBQ04xa0IsS0FBS3l0QixrQkFBa0J0a0IsT0FBUyxJQUNSbkosS0FBS3l0QixrQkFBa0JDLE9BQy9CQyxZQUFjLEdBRWxDZCxHQUFXN3NCLE1BQ1gsTUFBTStWLEVBQVMvVixLQUFLa3ZCLGlCQUFpQnhLLEdBRXJDLE9BREErSixHQUE0Q3p1QixNQUNyQytWLEVBR1gsQ0FBQ3NTLEdBQVcwQixHQUNSLE1BQU12Z0IsRUFBU3hKLEtBQUt1dUIsOEJBQ3BCLEdBQUl2dUIsS0FBSzJzQixnQkFBa0IsRUFBRyxDQUMxQixNQUFNd0MsRUFBUW52QixLQUFLMHNCLE9BQU94QyxRQUMxQmxxQixLQUFLMnNCLGlCQUFtQndDLEVBQU0vYixXQUM5QmdjLEdBQTZDcHZCLE1BQzdDLE1BQU11dEIsRUFBTyxJQUFJL1osV0FBVzJiLEVBQU10aEIsT0FBUXNoQixFQUFNaGMsV0FBWWdjLEVBQU0vYixZQUVsRSxZQURBMlcsRUFBWUssWUFBWW1ELEdBRzVCLE1BQU04QixFQUF3QnJ2QixLQUFLc3ZCLHVCQUNuQyxRQUE4QnBkLElBQTFCbWQsRUFBcUMsQ0FDckMsSUFBSXhoQixFQUNKLElBQ0lBLEVBQVMsSUFBSW9GLFlBQVlvYyxHQUU3QixNQUFPRSxHQUVILFlBREF4RixFQUFZYyxZQUFZMEUsR0FHNUIsTUFBTUMsRUFBcUIsQ0FDdkIzaEIsU0FDQXNGLFdBQVksRUFDWkMsV0FBWWljLEVBQ1oxQixZQUFhLEVBQ2I4QixZQUFhLEVBQ2JDLGdCQUFpQmxjLFdBQ2pCbWMsV0FBWSxXQUVoQjN2QixLQUFLeXRCLGtCQUFrQjFqQixLQUFLeWxCLEdBRWhDMUYsRUFBNkJ0Z0IsRUFBUXVnQixHQUNyQ2lGLEdBQTZDaHZCLE9BaUJyRCxTQUFTZ3VCLEdBQStCbmQsR0FDcEMsUUFBS21ULEVBQWFuVCxNQUdiNVEsT0FBT1ksVUFBVWtxQixlQUFldHBCLEtBQUtvUCxFQUFHLGlDQUtqRCxTQUFTbWMsR0FBNEJuYyxHQUNqQyxRQUFLbVQsRUFBYW5ULE1BR2I1USxPQUFPWSxVQUFVa3FCLGVBQWV0cEIsS0FBS29QLEVBQUcsMkNBS2pELFNBQVNtZSxHQUE2Q2x0QixJQWtOdEQsU0FBb0RBLEdBQ2hELE1BQU0wSCxFQUFTMUgsRUFBV3lzQiw4QkFDMUIsTUFBc0IsYUFBbEIva0IsRUFBTzJkLFVBR1BybEIsRUFBV3dzQixvQkFHVnhzQixFQUFXOHRCLGNBR1p0RixFQUErQjlnQixJQUFXNmdCLEVBQWlDN2dCLEdBQVUsUUFHckZzbEIsR0FBNEJ0bEIsSUFBV3FtQixHQUFxQ3JtQixHQUFVLElBR3RFNmtCLEdBQTJDdnNCLEdBQzdDLE9Bbk9DZ3VCLENBQTJDaHVCLEtBSTFEQSxFQUFXaXVCLFNBQ1hqdUIsRUFBV2t1QixZQUFhLEdBRzVCbHVCLEVBQVdpdUIsVUFBVyxFQUd0QmhMLEVBRG9CampCLEVBQVdtdUIsa0JBQ04sS0FDckJudUIsRUFBV2l1QixVQUFXLEVBQ2xCanVCLEVBQVdrdUIsYUFDWGx1QixFQUFXa3VCLFlBQWEsRUFDeEJoQixHQUE2Q2x0QixPQUVsRGdwQixJQUNDMEQsR0FBa0Mxc0IsRUFBWWdwQixRQU90RCxTQUFTb0YsR0FBcUQxbUIsRUFBUWdtQixHQUNsRSxJQUFJL1UsR0FBTyxFQUNXLFdBQWxCalIsRUFBTzJkLFNBQ1AxTSxHQUFPLEdBRVgsTUFBTTBWLEVBQWFDLEdBQXNEWixHQUNuQyxZQUFsQ0EsRUFBbUJHLFdBQ25CMUYsRUFBaUN6Z0IsRUFBUTJtQixFQUFZMVYsR0FpVzdELFNBQThDalIsRUFBUU0sRUFBTzJRLEdBQ3pELE1BQ000VixFQURTN21CLEVBQU8wZCxRQUNTb0osa0JBQWtCcEcsUUFDN0N6UCxFQUNBNFYsRUFBZ0JsRyxZQUFZcmdCLEdBRzVCdW1CLEVBQWdCakcsWUFBWXRnQixHQXJXNUJ5bUIsQ0FBcUMvbUIsRUFBUTJtQixFQUFZMVYsR0FHakUsU0FBUzJWLEdBQXNEWixHQUMzRCxNQUFNN0IsRUFBYzZCLEVBQW1CN0IsWUFDakM4QixFQUFjRCxFQUFtQkMsWUFDdkMsT0FBTyxJQUFJRCxFQUFtQkUsZ0JBQWdCRixFQUFtQjNoQixPQUFRMmhCLEVBQW1CcmMsV0FBWXdhLEVBQWM4QixHQUUxSCxTQUFTWixHQUFnRC9zQixFQUFZK0wsRUFBUXNGLEVBQVlDLEdBQ3JGdFIsRUFBVzRxQixPQUFPM2lCLEtBQUssQ0FBRThELFNBQVFzRixhQUFZQyxlQUM3Q3RSLEVBQVc2cUIsaUJBQW1CdlosRUFFbEMsU0FBU29kLEdBQTREMXVCLEVBQVkwdEIsR0FDN0UsTUFBTUMsRUFBY0QsRUFBbUJDLFlBQ2pDZ0IsRUFBc0JqQixFQUFtQjdCLFlBQWM2QixFQUFtQjdCLFlBQWM4QixFQUN4RmlCLEVBQWlCNWMsS0FBS0UsSUFBSWxTLEVBQVc2cUIsZ0JBQWlCNkMsRUFBbUJwYyxXQUFhb2MsRUFBbUI3QixhQUN6R2dELEVBQWlCbkIsRUFBbUI3QixZQUFjK0MsRUFDbERFLEVBQWtCRCxFQUFpQkEsRUFBaUJsQixFQUMxRCxJQUFJb0IsRUFBNEJILEVBQzVCSSxHQUFRLEVBQ1JGLEVBQWtCSCxJQUNsQkksRUFBNEJELEVBQWtCcEIsRUFBbUI3QixZQUNqRW1ELEdBQVEsR0FFWixNQUFNQyxFQUFRanZCLEVBQVc0cUIsT0FDekIsS0FBT21FLEVBQTRCLEdBQUcsQ0FDbEMsTUFBTUcsRUFBY0QsRUFBTXJELE9BQ3BCdUQsRUFBY25kLEtBQUtFLElBQUk2YyxFQUEyQkcsRUFBWTVkLFlBQzlEOGQsRUFBWTFCLEVBQW1CcmMsV0FBYXFjLEVBQW1CN0IsWUE1U2pEbkssRUE2U0RnTSxFQUFtQjNoQixPQTdTWnNqQixFQTZTb0JELEVBN1NSRSxFQTZTbUJKLEVBQVluakIsT0E3UzFCd2pCLEVBNlNrQ0wsRUFBWTdkLFdBN1NuQ21lLEVBNlMrQ0wsRUE1U3pHLElBQUl6ZCxXQUFXZ1EsR0FBTWxpQixJQUFJLElBQUlrUyxXQUFXNGQsRUFBS0MsRUFBV0MsR0FBSUgsR0E2U3BESCxFQUFZNWQsYUFBZTZkLEVBQzNCRixFQUFNN0csU0FHTjhHLEVBQVk3ZCxZQUFjOGQsRUFDMUJELEVBQVk1ZCxZQUFjNmQsR0FFOUJudkIsRUFBVzZxQixpQkFBbUJzRSxFQUM5Qk0sR0FBdUR6dkIsRUFBWW12QixFQUFhekIsR0FDaEZxQixHQUE2QkksRUF2VHJDLElBQTRCek4sRUFBTTJOLEVBQVlDLEVBQUtDLEVBQVdDLEVBeVQxRCxPQUFPUixFQUVYLFNBQVNTLEdBQXVEenZCLEVBQVlnUixFQUFNMGMsR0FDOUVnQyxHQUFrRDF2QixHQUNsRDB0QixFQUFtQjdCLGFBQWU3YSxFQUV0QyxTQUFTc2MsR0FBNkN0dEIsR0FDZixJQUEvQkEsRUFBVzZxQixpQkFBeUI3cUIsRUFBV3dzQixpQkFDL0NHLEdBQTRDM3NCLEdBQzVDNHNCLEdBQW9CNXNCLEVBQVd5c0IsZ0NBRy9CUyxHQUE2Q2x0QixHQUdyRCxTQUFTMHZCLEdBQWtEMXZCLEdBQ3ZCLE9BQTVCQSxFQUFXb3NCLGVBR2Zwc0IsRUFBV29zQixhQUFhZCw2Q0FBMENsYixFQUNsRXBRLEVBQVdvc0IsYUFBYWhCLE1BQVEsS0FDaENwckIsRUFBV29zQixhQUFlLE1BRTlCLFNBQVNhLEdBQWlFanRCLEdBQ3RFLEtBQU9BLEVBQVcyckIsa0JBQWtCdGtCLE9BQVMsR0FBRyxDQUM1QyxHQUFtQyxJQUEvQnJILEVBQVc2cUIsZ0JBQ1gsT0FFSixNQUFNNkMsRUFBcUIxdEIsRUFBVzJyQixrQkFBa0JDLE9BQ3BEOEMsR0FBNEQxdUIsRUFBWTB0QixLQUN4RWlDLEdBQWlEM3ZCLEdBQ2pEb3VCLEdBQXFEcHVCLEVBQVd5c0IsOEJBQStCaUIsS0FtRjNHLFNBQVNuQyxHQUE0Q3ZyQixFQUFZcXJCLEdBQzdELE1BQU1LLEVBQWtCMXJCLEVBQVcyckIsa0JBQWtCQyxPQUVyRCxHQUFjLFdBREE1ckIsRUFBV3lzQiw4QkFBOEJwSCxPQUMvQixDQUNwQixHQUFxQixJQUFqQmdHLEVBQ0EsTUFBTSxJQUFJM3NCLFVBQVUscUVBcENoQyxTQUEwRHNCLEVBQVkwckIsR0FDbEVBLEVBQWdCM2YsT0FBNkIyZixFQUFnQjNmLE9BQzdELE1BQU1yRSxFQUFTMUgsRUFBV3lzQiw4QkFDMUIsR0FBSU8sR0FBNEJ0bEIsR0FDNUIsS0FBT3FtQixHQUFxQ3JtQixHQUFVLEdBRWxEMG1CLEdBQXFEMW1CLEVBRDFCaW9CLEdBQWlEM3ZCLElBaUNoRjR2QixDQUFpRDV2QixFQUFZMHJCLFFBNUJyRSxTQUE0RDFyQixFQUFZcXJCLEVBQWNxQyxHQUNsRixHQUFJQSxFQUFtQjdCLFlBQWNSLEVBQWVxQyxFQUFtQnBjLFdBQ25FLE1BQU0sSUFBSTJGLFdBQVcsNkJBR3pCLEdBREF3WSxHQUF1RHp2QixFQUFZcXJCLEVBQWNxQyxHQUM3RUEsRUFBbUI3QixZQUFjNkIsRUFBbUJDLFlBRXBELE9BRUpnQyxHQUFpRDN2QixHQUNqRCxNQUFNNnZCLEVBQWdCbkMsRUFBbUI3QixZQUFjNkIsRUFBbUJDLFlBQzFFLEdBQUlrQyxFQUFnQixFQUFHLENBQ25CLE1BQU03bUIsRUFBTTBrQixFQUFtQnJjLFdBQWFxYyxFQUFtQjdCLFlBQ3pEaUUsRUFBWXBDLEVBQW1CM2hCLE9BQU93RyxNQUFNdkosRUFBTTZtQixFQUFlN21CLEdBQ3ZFK2pCLEdBQWdEL3NCLEVBQVk4dkIsRUFBVyxFQUFHQSxFQUFVeGUsWUFFeEZvYyxFQUFtQjNoQixPQUE2QjJoQixFQUFtQjNoQixPQUNuRTJoQixFQUFtQjdCLGFBQWVnRSxFQUNsQ3pCLEdBQXFEcHVCLEVBQVd5c0IsOEJBQStCaUIsR0FDL0ZULEdBQWlFanRCLEdBWTdEK3ZCLENBQW1EL3ZCLEVBQVlxckIsRUFBY0ssR0FFakZ3QixHQUE2Q2x0QixHQUVqRCxTQUFTMnZCLEdBQWlEM3ZCLEdBQ3RELE1BQU1nd0IsRUFBYWh3QixFQUFXMnJCLGtCQUFrQnZELFFBRWhELE9BREFzSCxHQUFrRDF2QixHQUMzQ2d3QixFQXlCWCxTQUFTckQsR0FBNEMzc0IsR0FDakRBLEVBQVdtdUIsb0JBQWlCL2QsRUFDNUJwUSxFQUFXb3RCLHNCQUFtQmhkLEVBbURsQyxTQUFTc2MsR0FBa0Mxc0IsRUFBWWdwQixHQUNuRCxNQUFNdGhCLEVBQVMxSCxFQUFXeXNCLDhCQUNKLGFBQWxCL2tCLEVBQU8yZCxTQTFRZixTQUEyRHJsQixHQUN2RDB2QixHQUFrRDF2QixHQUNsREEsRUFBVzJyQixrQkFBb0IsSUFBSTFILEVBMlFuQ2dNLENBQWtEandCLEdBQ2xEK3FCLEdBQVcvcUIsR0FDWDJzQixHQUE0QzNzQixHQUM1Q2t3QixHQUFvQnhvQixFQUFRc2hCLElBRWhDLFNBQVN1RCxHQUEyQ3ZzQixHQUNoRCxNQUFNZ0MsRUFBUWhDLEVBQVd5c0IsOEJBQThCcEgsT0FDdkQsTUFBYyxZQUFWcmpCLEVBQ08sS0FFRyxXQUFWQSxFQUNPLEVBRUpoQyxFQUFXbXdCLGFBQWVud0IsRUFBVzZxQixnQkFrRWhELFNBQVNNLEdBQStCdHBCLEdBQ3BDLE9BQU8sSUFBSW5ELFVBQVUsdUNBQXVDbUQscURBR2hFLFNBQVNzcUIsR0FBd0N0cUIsR0FDN0MsT0FBTyxJQUFJbkQsVUFBVSwwQ0FBMENtRCx3REFRbkUsU0FBU3V1QixHQUFpQzFvQixFQUFRNm1CLEdBQzlDN21CLEVBQU8wZCxRQUFRb0osa0JBQWtCdm1CLEtBQUtzbUIsR0FZMUMsU0FBU1IsR0FBcUNybUIsR0FDMUMsT0FBT0EsRUFBTzBkLFFBQVFvSixrQkFBa0JubkIsT0FFNUMsU0FBUzJsQixHQUE0QnRsQixHQUNqQyxNQUFNOFEsRUFBUzlRLEVBQU8wZCxRQUN0QixZQUFlaFYsSUFBWG9JLEtBR0M2WCxHQUEyQjdYLEdBcGJwQ3JhLE9BQU9jLGlCQUFpQmd0QixHQUE2Qmx0QixVQUFXLENBQzVENlosTUFBTyxDQUFFMVosWUFBWSxHQUNyQjJaLFFBQVMsQ0FBRTNaLFlBQVksR0FDdkJ1RixNQUFPLENBQUV2RixZQUFZLEdBQ3JCbXRCLFlBQWEsQ0FBRW50QixZQUFZLEdBQzNCb3hCLFlBQWEsQ0FBRXB4QixZQUFZLEtBRVcsaUJBQS9CNmlCLEVBQWUzaUIsYUFDdEJqQixPQUFPQyxlQUFlNnRCLEdBQTZCbHRCLFVBQVdnakIsRUFBZTNpQixZQUFhLENBQ3RGZixNQUFPLCtCQUNQZ0IsY0FBYyxJQW9idEIsTUFBTWt4QixHQUNGLFlBQVk3b0IsR0FHUixHQUZBeWYsRUFBdUJ6ZixFQUFRLEVBQUcsNEJBQ2xDa2dCLEVBQXFCbGdCLEVBQVEsbUJBQ3pCZ2hCLEdBQXVCaGhCLEdBQ3ZCLE1BQU0sSUFBSWhKLFVBQVUsK0VBRXhCLElBQUt3dEIsR0FBK0J4a0IsRUFBT3loQiwyQkFDdkMsTUFBTSxJQUFJenFCLFVBQVUsK0ZBR3hCd21CLEVBQXNDaG5CLEtBQU13SixHQUM1Q3hKLEtBQUtzd0Isa0JBQW9CLElBQUl2SyxFQU1qQyxhQUNJLE9BQUtvTSxHQUEyQm55QixNQUd6QkEsS0FBSytuQixlQUZEdEQsRUFBb0I2TixHQUE4QixXQU9qRSxPQUFPNU4sR0FDSCxPQUFLeU4sR0FBMkJueUIsV0FHRWtTLElBQTlCbFMsS0FBS2luQixxQkFDRXhDLEVBQW9CcUQsRUFBb0IsV0FFNUNMLEVBQWtDem5CLEtBQU0wa0IsR0FMcENELEVBQW9CNk4sR0FBOEIsV0FZakUsS0FBSy9FLEdBQ0QsSUFBSzRFLEdBQTJCbnlCLE1BQzVCLE9BQU95a0IsRUFBb0I2TixHQUE4QixTQUU3RCxJQUFLcmYsWUFBWUMsT0FBT3FhLEdBQ3BCLE9BQU85SSxFQUFvQixJQUFJamtCLFVBQVUsc0NBRTdDLEdBQXdCLElBQXBCK3NCLEVBQUtuYSxXQUNMLE9BQU9xUixFQUFvQixJQUFJamtCLFVBQVUsdUNBRTdDLEdBQStCLElBQTNCK3NCLEVBQUsxZixPQUFPdUYsV0FDWixPQUFPcVIsRUFBb0IsSUFBSWprQixVQUFVLGdEQUU3QyxRQUFrQzBSLElBQTlCbFMsS0FBS2luQixxQkFDTCxPQUFPeEMsRUFBb0JxRCxFQUFvQixjQUVuRCxJQUFJNEMsRUFDQUMsRUFDSixNQUFNL0YsRUFBVU4sR0FBVyxDQUFDM2EsRUFBU0MsS0FDakM4Z0IsRUFBaUIvZ0IsRUFDakJnaEIsRUFBZ0IvZ0IsS0FRcEIsT0E4Q1IsU0FBc0MwUSxFQUFRaVQsRUFBTThDLEdBQ2hELE1BQU03bUIsRUFBUzhRLEVBQU8yTSxxQkFDdEJ6ZCxFQUFPd2hCLFlBQWEsRUFDRSxZQUFsQnhoQixFQUFPMmQsT0FDUGtKLEVBQWdCeEYsWUFBWXJoQixFQUFPZ2UsY0FyYTNDLFNBQThDMWxCLEVBQVl5ckIsRUFBTThDLEdBQzVELE1BQU03bUIsRUFBUzFILEVBQVd5c0IsOEJBQzFCLElBQUlrQixFQUFjLEVBQ2RsQyxFQUFLNWQsY0FBZ0I0aUIsV0FDckI5QyxFQUFjbEMsRUFBSzVkLFlBQVk2aUIsbUJBRW5DLE1BQU1DLEVBQU9sRixFQUFLNWQsWUFFWjZmLEVBQXFCLENBQ3ZCM2hCLE9BRitCMGYsRUFBSzFmLE9BR3BDc0YsV0FBWW9hLEVBQUtwYSxXQUNqQkMsV0FBWW1hLEVBQUtuYSxXQUNqQnVhLFlBQWEsRUFDYjhCLGNBQ0FDLGdCQUFpQitDLEVBQ2pCOUMsV0FBWSxRQUVoQixHQUFJN3RCLEVBQVcyckIsa0JBQWtCdGtCLE9BQVMsRUFNdEMsT0FMQXJILEVBQVcyckIsa0JBQWtCMWpCLEtBQUt5bEIsUUFJbEMwQyxHQUFpQzFvQixFQUFRNm1CLEdBRzdDLEdBQXNCLFdBQWxCN21CLEVBQU8yZCxPQUFYLENBS0EsR0FBSXJsQixFQUFXNnFCLGdCQUFrQixFQUFHLENBQ2hDLEdBQUk2RCxHQUE0RDF1QixFQUFZMHRCLEdBQXFCLENBQzdGLE1BQU1XLEVBQWFDLEdBQXNEWixHQUd6RSxPQUZBSixHQUE2Q3R0QixRQUM3Q3V1QixFQUFnQmpHLFlBQVkrRixHQUdoQyxHQUFJcnVCLEVBQVd3c0IsZ0JBQWlCLENBQzVCLE1BQU14RCxFQUFJLElBQUl0cUIsVUFBVSwyREFHeEIsT0FGQWd1QixHQUFrQzFzQixFQUFZZ3BCLFFBQzlDdUYsRUFBZ0J4RixZQUFZQyxJQUlwQ2hwQixFQUFXMnJCLGtCQUFrQjFqQixLQUFLeWxCLEdBQ2xDMEMsR0FBaUMxb0IsRUFBUTZtQixHQUN6Q3JCLEdBQTZDbHRCLE9BckI3QyxDQUNJLE1BQU00d0IsRUFBWSxJQUFJRCxFQUFLakQsRUFBbUIzaEIsT0FBUTJoQixFQUFtQnJjLFdBQVksR0FDckZrZCxFQUFnQmxHLFlBQVl1SSxJQTZZNUJDLENBQXFDbnBCLEVBQU95aEIsMEJBQTJCc0MsRUFBTThDLEdBdEQ3RXVDLENBQTZCNXlCLEtBQU11dEIsRUFMWCxDQUNwQm5ELFlBQWF0Z0IsR0FBUzRnQixFQUFlLENBQUV2cUIsTUFBTzJKLEVBQU8yUSxNQUFNLElBQzNEMFAsWUFBYXJnQixHQUFTNGdCLEVBQWUsQ0FBRXZxQixNQUFPMkosRUFBTzJRLE1BQU0sSUFDM0RvUSxZQUFhQyxHQUFLSCxFQUFjRyxLQUc3QmxHLEVBV1gsY0FDSSxJQUFLdU4sR0FBMkJueUIsTUFDNUIsTUFBTXN5QixHQUE4QixlQUV4QyxRQUFrQ3BnQixJQUE5QmxTLEtBQUtpbkIscUJBQVQsQ0FHQSxHQUFJam5CLEtBQUtzd0Isa0JBQWtCbm5CLE9BQVMsRUFDaEMsTUFBTSxJQUFJM0ksVUFBVSx1RkFFeEJtbkIsRUFBbUMzbkIsUUFnQjNDLFNBQVNteUIsR0FBMkJ0aEIsR0FDaEMsUUFBS21ULEVBQWFuVCxNQUdiNVEsT0FBT1ksVUFBVWtxQixlQUFldHBCLEtBQUtvUCxFQUFHLHFCQWdCakQsU0FBU3loQixHQUE4QjN1QixHQUNuQyxPQUFPLElBQUluRCxVQUFVLHNDQUFzQ21ELG9EQUcvRCxTQUFTa3ZCLEdBQXFCQyxFQUFVQyxHQUNwQyxNQUFNLGNBQUVuZSxHQUFrQmtlLEVBQzFCLFFBQXNCNWdCLElBQWxCMEMsRUFDQSxPQUFPbWUsRUFFWCxHQUFJM0csR0FBWXhYLElBQWtCQSxFQUFnQixFQUM5QyxNQUFNLElBQUltRSxXQUFXLHlCQUV6QixPQUFPbkUsRUFFWCxTQUFTb2UsR0FBcUJGLEdBQzFCLE1BQU0sS0FBRWhnQixHQUFTZ2dCLEVBQ2pCLE9BQUtoZ0IsR0FDTSxLQUFNLEdBS3JCLFNBQVNtZ0IsR0FBdUJyVSxFQUFNa0ssR0FDbENELEVBQWlCakssRUFBTWtLLEdBQ3ZCLE1BQU1sVSxFQUFnQmdLLGFBQW1DLEVBQVNBLEVBQUtoSyxjQUNqRTlCLEVBQU84TCxhQUFtQyxFQUFTQSxFQUFLOUwsS0FDOUQsTUFBTyxDQUNIOEIsbUJBQWlDMUMsSUFBbEIwQyxPQUE4QjFDLEVBQVlrWCxFQUEwQnhVLEdBQ25GOUIsVUFBZVosSUFBVFksT0FBcUJaLEVBQVlnaEIsR0FBMkJwZ0IsRUFBTSxHQUFHZ1csNkJBR25GLFNBQVNvSyxHQUEyQnBhLEVBQUlnUSxHQUVwQyxPQURBQyxFQUFlalEsRUFBSWdRLEdBQ1poZixHQUFTc2YsRUFBMEJ0USxFQUFHaFAsSUEwQmpELFNBQVNxcEIsR0FBbUNyYSxFQUFJc2EsRUFBVXRLLEdBRXRELE9BREFDLEVBQWVqUSxFQUFJZ1EsR0FDWHBFLEdBQVdvQixFQUFZaE4sRUFBSXNhLEVBQVUsQ0FBQzFPLElBRWxELFNBQVMyTyxHQUFtQ3ZhLEVBQUlzYSxFQUFVdEssR0FFdEQsT0FEQUMsRUFBZWpRLEVBQUlnUSxHQUNaLElBQU1oRCxFQUFZaE4sRUFBSXNhLEVBQVUsSUFFM0MsU0FBU0UsR0FBbUN4YSxFQUFJc2EsRUFBVXRLLEdBRXRELE9BREFDLEVBQWVqUSxFQUFJZ1EsR0FDWGhuQixHQUFlMmpCLEVBQVkzTSxFQUFJc2EsRUFBVSxDQUFDdHhCLElBRXRELFNBQVN5eEIsR0FBbUN6YSxFQUFJc2EsRUFBVXRLLEdBRXRELE9BREFDLEVBQWVqUSxFQUFJZ1EsR0FDWixDQUFDaGYsRUFBT2hJLElBQWVna0IsRUFBWWhOLEVBQUlzYSxFQUFVLENBQUN0cEIsRUFBT2hJLElBR3BFLFNBQVMweEIsR0FBcUIzaUIsRUFBR2lZLEdBQzdCLElBQUsySyxHQUFpQjVpQixHQUNsQixNQUFNLElBQUlyUSxVQUFVLEdBQUdzb0IsOEJBL0cvQjdvQixPQUFPYyxpQkFBaUJzeEIsR0FBeUJ4eEIsVUFBVyxDQUN4RHFxQixPQUFRLENBQUVscUIsWUFBWSxHQUN0QjRTLEtBQU0sQ0FBRTVTLFlBQVksR0FDcEJtcUIsWUFBYSxDQUFFbnFCLFlBQVksR0FDM0JvcUIsT0FBUSxDQUFFcHFCLFlBQVksS0FFZ0IsaUJBQS9CNmlCLEVBQWUzaUIsYUFDdEJqQixPQUFPQyxlQUFlbXlCLEdBQXlCeHhCLFVBQVdnakIsRUFBZTNpQixZQUFhLENBQ2xGZixNQUFPLDJCQUNQZ0IsY0FBYyxJQStHdEIsTUFBTXV5QixHQUNGLFlBQVlDLEVBQW9CLEdBQUlDLEVBQWMsU0FDcEIxaEIsSUFBdEJ5aEIsRUFDQUEsRUFBb0IsS0FHcEIzSyxFQUFhMkssRUFBbUIsbUJBRXBDLE1BQU1iLEVBQVdHLEdBQXVCVyxFQUFhLG9CQUMvQ0MsRUE1RGQsU0FBK0JULEVBQVV0SyxHQUNyQ0QsRUFBaUJ1SyxFQUFVdEssR0FDM0IsTUFBTS9tQixFQUFRcXhCLGFBQTJDLEVBQVNBLEVBQVNyeEIsTUFDckUyWSxFQUFRMFksYUFBMkMsRUFBU0EsRUFBUzFZLE1BQ3JFOVAsRUFBUXdvQixhQUEyQyxFQUFTQSxFQUFTeG9CLE1BQ3JFL0ksRUFBT3V4QixhQUEyQyxFQUFTQSxFQUFTdnhCLEtBQ3BFNGhCLEVBQVEyUCxhQUEyQyxFQUFTQSxFQUFTM1AsTUFDM0UsTUFBTyxDQUNIMWhCLFdBQWlCbVEsSUFBVm5RLE9BQ0htUSxFQUNBaWhCLEdBQW1DcHhCLEVBQU9xeEIsRUFBVSxHQUFHdEssNkJBQzNEcE8sV0FBaUJ4SSxJQUFWd0ksT0FDSHhJLEVBQ0FtaEIsR0FBbUMzWSxFQUFPMFksRUFBVSxHQUFHdEssNkJBQzNEbGUsV0FBaUJzSCxJQUFWdEgsT0FDSHNILEVBQ0FvaEIsR0FBbUMxb0IsRUFBT3dvQixFQUFVLEdBQUd0Syw2QkFDM0RyRixXQUFpQnZSLElBQVZ1UixPQUNIdlIsRUFDQXFoQixHQUFtQzlQLEVBQU8yUCxFQUFVLEdBQUd0Syw2QkFDM0RqbkIsUUF3Q3VCaXlCLENBQXNCSCxFQUFtQixtQkFHaEUsR0FGQUksR0FBeUIvekIsV0FFWmtTLElBREEyaEIsRUFBZWh5QixLQUV4QixNQUFNLElBQUlrWCxXQUFXLDZCQUV6QixNQUFNaWIsRUFBZ0JoQixHQUFxQkYsSUFpb0JuRCxTQUFnRXRwQixFQUFRcXFCLEVBQWdCamYsRUFBZW9mLEdBQ25HLE1BQU1seUIsRUFBYTdCLE9BQU91QixPQUFPeXlCLEdBQWdDcHpCLFdBQ2pFLElBQUlxekIsRUFBaUIsT0FDakJDLEVBQWlCLElBQU0zUCxPQUFvQnRTLEdBQzNDa2lCLEVBQWlCLElBQU01UCxPQUFvQnRTLEdBQzNDbWlCLEVBQWlCLElBQU03UCxPQUFvQnRTLFFBQ2xCQSxJQUF6QjJoQixFQUFlanBCLFFBQ2ZzcEIsRUFBaUIsSUFBTUwsRUFBZWpwQixNQUFNOUksU0FFbkJvUSxJQUF6QjJoQixFQUFlcFEsUUFDZjBRLEVBQWlCcnFCLEdBQVMrcEIsRUFBZXBRLE1BQU0zWixFQUFPaEksU0FFN0JvUSxJQUF6QjJoQixFQUFlblosUUFDZjBaLEVBQWlCLElBQU1QLEVBQWVuWixjQUVieEksSUFBekIyaEIsRUFBZTl4QixRQUNmc3lCLEVBQWlCM1AsR0FBVW1QLEVBQWU5eEIsTUFBTTJpQixJQUVwRDRQLEdBQXFDOXFCLEVBQVExSCxFQUFZb3lCLEVBQWdCQyxFQUFnQkMsRUFBZ0JDLEVBQWdCemYsRUFBZW9mLEdBanBCcElPLENBQXVEdjBCLEtBQU02ekIsRUFEdkNoQixHQUFxQkMsRUFBVSxHQUN1Q2tCLEdBS2hHLGFBQ0ksSUFBS1AsR0FBaUJ6ekIsTUFDbEIsTUFBTXcwQixHQUEwQixVQUVwQyxPQUFPQyxHQUF1QnowQixNQVdsQyxNQUFNMGtCLEdBQ0YsT0FBSytPLEdBQWlCenpCLE1BR2xCeTBCLEdBQXVCejBCLE1BQ2hCeWtCLEVBQW9CLElBQUlqa0IsVUFBVSxvREFFdENrMEIsR0FBb0IxMEIsS0FBTTBrQixHQUx0QkQsRUFBb0IrUCxHQUEwQixVQWU3RCxRQUNJLE9BQUtmLEdBQWlCenpCLE1BR2xCeTBCLEdBQXVCejBCLE1BQ2hCeWtCLEVBQW9CLElBQUlqa0IsVUFBVSxvREFFekNtMEIsR0FBb0MzMEIsTUFDN0J5a0IsRUFBb0IsSUFBSWprQixVQUFVLDJDQUV0Q28wQixHQUFvQjUwQixNQVJoQnlrQixFQUFvQitQLEdBQTBCLFVBa0I3RCxZQUNJLElBQUtmLEdBQWlCenpCLE1BQ2xCLE1BQU13MEIsR0FBMEIsYUFFcEMsT0FBT0ssR0FBbUM3MEIsT0FnQmxELFNBQVM2MEIsR0FBbUNyckIsR0FDeEMsT0FBTyxJQUFJc3JCLEdBQTRCdHJCLEdBVTNDLFNBQVN1cUIsR0FBeUJ2cUIsR0FDOUJBLEVBQU8yZCxPQUFTLFdBR2hCM2QsRUFBT2dlLGtCQUFldFYsRUFDdEIxSSxFQUFPdXJCLGFBQVU3aUIsRUFHakIxSSxFQUFPd3JCLCtCQUE0QjlpQixFQUduQzFJLEVBQU95ckIsZUFBaUIsSUFBSWxQLEVBRzVCdmMsRUFBTzByQiwyQkFBd0JoakIsRUFHL0IxSSxFQUFPMnJCLG1CQUFnQmpqQixFQUd2QjFJLEVBQU80ckIsMkJBQXdCbGpCLEVBRS9CMUksRUFBTzZyQiwwQkFBdUJuakIsRUFFOUIxSSxFQUFPOHJCLGVBQWdCLEVBRTNCLFNBQVM3QixHQUFpQjVpQixHQUN0QixRQUFLbVQsRUFBYW5ULE1BR2I1USxPQUFPWSxVQUFVa3FCLGVBQWV0cEIsS0FBS29QLEVBQUcsNkJBS2pELFNBQVM0akIsR0FBdUJqckIsR0FDNUIsWUFBdUIwSSxJQUFuQjFJLEVBQU91ckIsUUFLZixTQUFTTCxHQUFvQmxyQixFQUFRa2IsR0FDakMsTUFBTTVnQixFQUFRMEYsRUFBTzJkLE9BQ3JCLEdBQWMsV0FBVnJqQixHQUFnQyxZQUFWQSxFQUN0QixPQUFPMGdCLE9BQW9CdFMsR0FFL0IsUUFBb0NBLElBQWhDMUksRUFBTzZyQixxQkFDUCxPQUFPN3JCLEVBQU82ckIscUJBQXFCRSxTQUV2QyxJQUFJQyxHQUFxQixFQUNYLGFBQVYxeEIsSUFDQTB4QixHQUFxQixFQUVyQjlRLE9BQVN4UyxHQUViLE1BQU0wUyxFQUFVTixHQUFXLENBQUMzYSxFQUFTQyxLQUNqQ0osRUFBTzZyQixxQkFBdUIsQ0FDMUJFLGNBQVVyakIsRUFDVnVqQixTQUFVOXJCLEVBQ1YrckIsUUFBUzlyQixFQUNUK3JCLFFBQVNqUixFQUNUa1Isb0JBQXFCSixNQU83QixPQUpBaHNCLEVBQU82ckIscUJBQXFCRSxTQUFXM1EsRUFDbEM0USxHQUNESyxHQUE0QnJzQixFQUFRa2IsR0FFakNFLEVBRVgsU0FBU2dRLEdBQW9CcHJCLEdBQ3pCLE1BQU0xRixFQUFRMEYsRUFBTzJkLE9BQ3JCLEdBQWMsV0FBVnJqQixHQUFnQyxZQUFWQSxFQUN0QixPQUFPMmdCLEVBQW9CLElBQUlqa0IsVUFBVSxrQkFBa0JzRCwrREFFL0QsTUFBTThnQixFQUFVTixHQUFXLENBQUMzYSxFQUFTQyxLQUNqQyxNQUFNa3NCLEVBQWUsQ0FDakJMLFNBQVU5ckIsRUFDVityQixRQUFTOXJCLEdBRWJKLEVBQU8yckIsY0FBZ0JXLEtBRXJCQyxFQUFTdnNCLEVBQU91ckIsUUFnZjFCLElBQThDanpCLEVBM2UxQyxZQUplb1EsSUFBWDZqQixHQUF3QnZzQixFQUFPOHJCLGVBQTJCLGFBQVZ4eEIsR0FDaERreUIsR0FBaUNELEdBK2VyQ25KLEdBRDBDOXFCLEVBNWVMMEgsRUFBT3dyQiwwQkE2ZVhpQixHQUFlLEdBQ2hEQyxHQUFvRHAwQixHQTdlN0M4aUIsRUFhWCxTQUFTdVIsR0FBZ0Mzc0IsRUFBUWpELEdBRS9CLGFBREFpRCxFQUFPMmQsT0FLckJpUCxHQUE2QjVzQixHQUh6QnFzQixHQUE0QnJzQixFQUFRakQsR0FLNUMsU0FBU3N2QixHQUE0QnJzQixFQUFRa2IsR0FDekMsTUFBTTVpQixFQUFhMEgsRUFBT3dyQiwwQkFDMUJ4ckIsRUFBTzJkLE9BQVMsV0FDaEIzZCxFQUFPZ2UsYUFBZTlDLEVBQ3RCLE1BQU1xUixFQUFTdnNCLEVBQU91ckIsYUFDUDdpQixJQUFYNmpCLEdBQ0FNLEdBQXNETixFQUFRclIsSUE4RXRFLFNBQWtEbGIsR0FDOUMsWUFBcUMwSSxJQUFqQzFJLEVBQU8wckIsNEJBQXdFaGpCLElBQWpDMUksRUFBTzRyQixzQkE3RXBEa0IsQ0FBeUM5c0IsSUFBVzFILEVBQVc4dEIsVUFDaEV3RyxHQUE2QjVzQixHQUdyQyxTQUFTNHNCLEdBQTZCNXNCLEdBQ2xDQSxFQUFPMmQsT0FBUyxVQUNoQjNkLEVBQU93ckIsMEJBQTBCN00sS0FDakMsTUFBTW9PLEVBQWMvc0IsRUFBT2dlLGFBSzNCLEdBSkFoZSxFQUFPeXJCLGVBQWV6cUIsU0FBUWdzQixJQUMxQkEsRUFBYWQsUUFBUWEsTUFFekIvc0IsRUFBT3lyQixlQUFpQixJQUFJbFAsT0FDUTdULElBQWhDMUksRUFBTzZyQixxQkFFUCxZQURBb0IsR0FBa0RqdEIsR0FHdEQsTUFBTWt0QixFQUFlbHRCLEVBQU82ckIscUJBRTVCLEdBREE3ckIsRUFBTzZyQiwwQkFBdUJuakIsRUFDMUJ3a0IsRUFBYWQsb0JBR2IsT0FGQWMsRUFBYWhCLFFBQVFhLFFBQ3JCRSxHQUFrRGp0QixHQUl0RHViLEVBRGdCdmIsRUFBT3dyQiwwQkFBMEI5TSxHQUFZd08sRUFBYWYsVUFDckQsS0FDakJlLEVBQWFqQixXQUNiZ0IsR0FBa0RqdEIsTUFDbERrYixJQUNBZ1MsRUFBYWhCLFFBQVFoUixHQUNyQitSLEdBQWtEanRCLE1BeUMxRCxTQUFTbXJCLEdBQW9DbnJCLEdBQ3pDLFlBQTZCMEksSUFBekIxSSxFQUFPMnJCLG9CQUFnRWpqQixJQUFqQzFJLEVBQU80ckIsc0JBa0JyRCxTQUFTcUIsR0FBa0RqdEIsUUFDMUIwSSxJQUF6QjFJLEVBQU8yckIsZ0JBQ1AzckIsRUFBTzJyQixjQUFjTyxRQUFRbHNCLEVBQU9nZSxjQUNwQ2hlLEVBQU8yckIsbUJBQWdCampCLEdBRTNCLE1BQU02akIsRUFBU3ZzQixFQUFPdXJCLGFBQ1A3aUIsSUFBWDZqQixHQUNBWSxHQUFpQ1osRUFBUXZzQixFQUFPZ2UsY0FHeEQsU0FBU29QLEdBQWlDcHRCLEVBQVFxdEIsR0FDOUMsTUFBTWQsRUFBU3ZzQixFQUFPdXJCLGFBQ1A3aUIsSUFBWDZqQixHQUF3QmMsSUFBaUJydEIsRUFBTzhyQixnQkFDNUN1QixFQXdoQlosU0FBd0NkLEdBQ3BDZSxHQUFvQ2YsR0F4aEI1QmdCLENBQStCaEIsR0FHL0JDLEdBQWlDRCxJQUd6Q3ZzQixFQUFPOHJCLGNBQWdCdUIsRUF6UDNCNTJCLE9BQU9jLGlCQUFpQjJ5QixHQUFlN3lCLFVBQVcsQ0FDOUNrQixNQUFPLENBQUVmLFlBQVksR0FDckIwWixNQUFPLENBQUUxWixZQUFZLEdBQ3JCZzJCLFVBQVcsQ0FBRWgyQixZQUFZLEdBQ3pCaTJCLE9BQVEsQ0FBRWoyQixZQUFZLEtBRWdCLGlCQUEvQjZpQixFQUFlM2lCLGFBQ3RCakIsT0FBT0MsZUFBZXd6QixHQUFlN3lCLFVBQVdnakIsRUFBZTNpQixZQUFhLENBQ3hFZixNQUFPLGlCQUNQZ0IsY0FBYyxJQXVQdEIsTUFBTTJ6QixHQUNGLFlBQVl0ckIsR0FHUixHQUZBeWYsRUFBdUJ6ZixFQUFRLEVBQUcsK0JBQ2xDZ3FCLEdBQXFCaHFCLEVBQVEsbUJBQ3pCaXJCLEdBQXVCanJCLEdBQ3ZCLE1BQU0sSUFBSWhKLFVBQVUsK0VBRXhCUixLQUFLazNCLHFCQUF1QjF0QixFQUM1QkEsRUFBT3VyQixRQUFVLzBCLEtBQ2pCLE1BQU04RCxFQUFRMEYsRUFBTzJkLE9BQ3JCLEdBQWMsYUFBVnJqQixHQUNLNndCLEdBQW9DbnJCLElBQVdBLEVBQU84ckIsY0FDdkR3QixHQUFvQzkyQixNQUdwQ20zQixHQUE4Q24zQixNQUVsRG8zQixHQUFxQ3AzQixXQUVwQyxHQUFjLGFBQVY4RCxFQUNMdXpCLEdBQThDcjNCLEtBQU13SixFQUFPZ2UsY0FDM0Q0UCxHQUFxQ3AzQixXQUVwQyxHQUFjLFdBQVY4RCxFQUNMcXpCLEdBQThDbjNCLE1BZ2N0RG8zQixHQS9idURwM0IsTUFnY3ZEczNCLEdBaGN1RHQzQixVQUU5QyxDQUNELE1BQU11MkIsRUFBYy9zQixFQUFPZ2UsYUFDM0I2UCxHQUE4Q3IzQixLQUFNdTJCLEdBQ3BEZ0IsR0FBK0N2M0IsS0FBTXUyQixJQU83RCxhQUNJLE9BQUtpQixHQUE4QngzQixNQUc1QkEsS0FBSytuQixlQUZEdEQsRUFBb0JnVCxHQUFpQyxXQVlwRSxrQkFDSSxJQUFLRCxHQUE4QngzQixNQUMvQixNQUFNeTNCLEdBQWlDLGVBRTNDLFFBQWtDdmxCLElBQTlCbFMsS0FBS2szQixxQkFDTCxNQUFNUSxHQUEyQixlQUVyQyxPQXVJUixTQUFtRDNCLEdBQy9DLE1BQU12c0IsRUFBU3VzQixFQUFPbUIscUJBQ2hCcHpCLEVBQVEwRixFQUFPMmQsT0FDckIsTUFBYyxZQUFWcmpCLEdBQWlDLGFBQVZBLEVBQ2hCLEtBRUcsV0FBVkEsRUFDTyxFQUVKNnpCLEdBQThDbnVCLEVBQU93ckIsMkJBaEpqRDRDLENBQTBDNTNCLE1BVXJELFlBQ0ksT0FBS3czQixHQUE4QngzQixNQUc1QkEsS0FBSzYzQixjQUZEcFQsRUFBb0JnVCxHQUFpQyxVQU9wRSxNQUFNL1MsR0FDRixPQUFLOFMsR0FBOEJ4M0IsV0FHRGtTLElBQTlCbFMsS0FBS2szQixxQkFDRXpTLEVBQW9CaVQsR0FBMkIsVUE0RWxFLFNBQTBDM0IsRUFBUXJSLEdBRTlDLE9BQU9nUSxHQURRcUIsRUFBT21CLHFCQUNheFMsR0E1RXhCb1QsQ0FBaUM5M0IsS0FBTTBrQixHQUxuQ0QsRUFBb0JnVCxHQUFpQyxVQVVwRSxRQUNJLElBQUtELEdBQThCeDNCLE1BQy9CLE9BQU95a0IsRUFBb0JnVCxHQUFpQyxVQUVoRSxNQUFNanVCLEVBQVN4SixLQUFLazNCLHFCQUNwQixZQUFlaGxCLElBQVgxSSxFQUNPaWIsRUFBb0JpVCxHQUEyQixVQUV0RC9DLEdBQW9DbnJCLEdBQzdCaWIsRUFBb0IsSUFBSWprQixVQUFVLDJDQUV0Q3UzQixHQUFpQy8zQixNQVk1QyxjQUNJLElBQUt3M0IsR0FBOEJ4M0IsTUFDL0IsTUFBTXkzQixHQUFpQyxvQkFHNUJ2bEIsSUFEQWxTLEtBQUtrM0Isc0JBSXBCYyxHQUFtQ2g0QixNQUV2QyxNQUFNOEosR0FDRixPQUFLMHRCLEdBQThCeDNCLFdBR0RrUyxJQUE5QmxTLEtBQUtrM0IscUJBQ0V6UyxFQUFvQmlULEdBQTJCLGFBRW5ETyxHQUFpQ2o0QixLQUFNOEosR0FMbkMyYSxFQUFvQmdULEdBQWlDLFdBd0J4RSxTQUFTRCxHQUE4QjNtQixHQUNuQyxRQUFLbVQsRUFBYW5ULE1BR2I1USxPQUFPWSxVQUFVa3FCLGVBQWV0cEIsS0FBS29QLEVBQUcsd0JBVWpELFNBQVNrbkIsR0FBaUNoQyxHQUV0QyxPQUFPbkIsR0FEUW1CLEVBQU9tQixzQkFzQjFCLFNBQVNiLEdBQXNETixFQUFReHZCLEdBQ2pDLFlBQTlCd3ZCLEVBQU9tQyxtQkFDUEMsR0FBZ0NwQyxFQUFReHZCLEdBa1ZoRCxTQUFrRHd2QixFQUFRclIsR0FDdEQyUyxHQUE4Q3RCLEVBQVFyUixHQWhWbEQwVCxDQUF5Q3JDLEVBQVF4dkIsR0FjekQsU0FBU3l4QixHQUFtQ2pDLEdBQ3hDLE1BQU12c0IsRUFBU3VzQixFQUFPbUIscUJBQ2hCbUIsRUFBZ0IsSUFBSTczQixVQUFVLG9GQUNwQzYxQixHQUFzRE4sRUFBUXNDLEdBOUJsRSxTQUFnRXRDLEVBQVF4dkIsR0FDakMsWUFBL0J3dkIsRUFBT3VDLG9CQUNQM0IsR0FBaUNaLEVBQVF4dkIsR0FrVGpELFNBQW1Ed3ZCLEVBQVFyUixHQUN2RDZTLEdBQStDeEIsRUFBUXJSLEdBaFRuRDZULENBQTBDeEMsRUFBUXh2QixHQTRCdERpeUIsQ0FBdUR6QyxFQUFRc0MsR0FDL0Q3dUIsRUFBT3VyQixhQUFVN2lCLEVBQ2pCNmpCLEVBQU9tQiwwQkFBdUJobEIsRUFFbEMsU0FBUytsQixHQUFpQ2xDLEVBQVFqc0IsR0FDOUMsTUFBTU4sRUFBU3VzQixFQUFPbUIscUJBQ2hCcDFCLEVBQWEwSCxFQUFPd3JCLDBCQUNwQnlELEVBcUlWLFNBQXFEMzJCLEVBQVlnSSxHQUM3RCxJQUNJLE9BQU9oSSxFQUFXNDJCLHVCQUF1QjV1QixHQUU3QyxNQUFPNnVCLEdBRUgsT0FEQUMsR0FBNkM5MkIsRUFBWTYyQixHQUNsRCxHQTNJT0UsQ0FBNEMvMkIsRUFBWWdJLEdBQzFFLEdBQUlOLElBQVd1c0IsRUFBT21CLHFCQUNsQixPQUFPelMsRUFBb0JpVCxHQUEyQixhQUUxRCxNQUFNNXpCLEVBQVEwRixFQUFPMmQsT0FDckIsR0FBYyxZQUFWcmpCLEVBQ0EsT0FBTzJnQixFQUFvQmpiLEVBQU9nZSxjQUV0QyxHQUFJbU4sR0FBb0NuckIsSUFBcUIsV0FBVjFGLEVBQy9DLE9BQU8yZ0IsRUFBb0IsSUFBSWprQixVQUFVLDZEQUU3QyxHQUFjLGFBQVZzRCxFQUNBLE9BQU8yZ0IsRUFBb0JqYixFQUFPZ2UsY0FFdEMsTUFBTTVDLEVBclhWLFNBQXVDcGIsR0FRbkMsT0FQZ0I4YSxHQUFXLENBQUMzYSxFQUFTQyxLQUNqQyxNQUFNNHNCLEVBQWUsQ0FDakJmLFNBQVU5ckIsRUFDVityQixRQUFTOXJCLEdBRWJKLEVBQU95ckIsZUFBZWxyQixLQUFLeXNCLE1BK1dmc0MsQ0FBOEJ0dkIsR0FFOUMsT0FpSUosU0FBOEMxSCxFQUFZZ0ksRUFBTzJ1QixHQUM3RCxJQUNJN0wsR0FBcUI5cUIsRUFBWWdJLEVBQU8ydUIsR0FFNUMsTUFBT00sR0FFSCxZQURBSCxHQUE2QzkyQixFQUFZaTNCLEdBRzdELE1BQU12dkIsRUFBUzFILEVBQVdrM0IsMEJBQ3JCckUsR0FBb0NuckIsSUFBNkIsYUFBbEJBLEVBQU8yZCxRQUV2RHlQLEdBQWlDcHRCLEVBRFp5dkIsR0FBK0NuM0IsSUFHeEVvMEIsR0FBb0RwMEIsR0EvSXBEbzNCLENBQXFDcDNCLEVBQVlnSSxFQUFPMnVCLEdBQ2pEN1QsRUFyR1gza0IsT0FBT2MsaUJBQWlCK3pCLEdBQTRCajBCLFVBQVcsQ0FDM0RrQixNQUFPLENBQUVmLFlBQVksR0FDckIwWixNQUFPLENBQUUxWixZQUFZLEdBQ3JCbXFCLFlBQWEsQ0FBRW5xQixZQUFZLEdBQzNCeWlCLE1BQU8sQ0FBRXppQixZQUFZLEdBQ3JCb3FCLE9BQVEsQ0FBRXBxQixZQUFZLEdBQ3RCb3hCLFlBQWEsQ0FBRXB4QixZQUFZLEdBQzNCOHZCLE1BQU8sQ0FBRTl2QixZQUFZLEtBRWlCLGlCQUEvQjZpQixFQUFlM2lCLGFBQ3RCakIsT0FBT0MsZUFBZTQwQixHQUE0QmowQixVQUFXZ2pCLEVBQWUzaUIsWUFBYSxDQUNyRmYsTUFBTyw4QkFDUGdCLGNBQWMsSUEyRnRCLE1BQU04MEIsR0FBZ0IsR0FNdEIsTUFBTWhDLEdBQ0YsY0FDSSxNQUFNLElBQUl6ekIsVUFBVSx1QkFTeEIsTUFBTXNxQixHQUNGLElBaUNDOUcsRUFEa0NuVCxFQWhDSTdRLFFBb0N0Q0MsT0FBT1ksVUFBVWtxQixlQUFldHBCLEtBQUtvUCxFQUFHLDZCQW5DckMsTUFBTSxJQUFJclEsVUFBVSx5R0ErQmhDLElBQTJDcVEsRUE1QnJCLGFBREE3USxLQUFLZzVCLDBCQUEwQjdSLFFBTTdDZ1MsR0FBcUNuNUIsS0FBTThxQixHQUcvQyxDQUFDNUMsR0FBWXhELEdBQ1QsTUFBTTNPLEVBQVMvVixLQUFLbzVCLGdCQUFnQjFVLEdBRXBDLE9BREEyVSxHQUErQ3I1QixNQUN4QytWLEVBR1gsQ0FBQ29TLEtBQ0cwRSxHQUFXN3NCLE9Bc0JuQixTQUFTczBCLEdBQXFDOXFCLEVBQVExSCxFQUFZb3lCLEVBQWdCQyxFQUFnQkMsRUFBZ0JDLEVBQWdCemYsRUFBZW9mLEdBQzdJbHlCLEVBQVdrM0IsMEJBQTRCeHZCLEVBQ3ZDQSxFQUFPd3JCLDBCQUE0Qmx6QixFQUVuQ0EsRUFBVzRxQixZQUFTeGEsRUFDcEJwUSxFQUFXNnFCLHFCQUFrQnphLEVBQzdCMmEsR0FBVy9xQixHQUNYQSxFQUFXOHRCLFVBQVcsRUFDdEI5dEIsRUFBVzQyQix1QkFBeUIxRSxFQUNwQ2x5QixFQUFXbXdCLGFBQWVyZCxFQUMxQjlTLEVBQVd3M0IsZ0JBQWtCbkYsRUFDN0JyeUIsRUFBV3kzQixnQkFBa0JuRixFQUM3QnR5QixFQUFXczNCLGdCQUFrQi9FLEVBQzdCLE1BQU13QyxFQUFlb0MsR0FBK0NuM0IsR0FDcEU4MEIsR0FBaUNwdEIsRUFBUXF0QixHQUd6QzlSLEVBRHFCUCxFQUREMFAsTUFFTSxLQUN0QnB5QixFQUFXOHRCLFVBQVcsRUFDdEJzRyxHQUFvRHAwQixNQUNyRDAzQixJQUNDMTNCLEVBQVc4dEIsVUFBVyxFQUN0QnVHLEdBQWdDM3NCLEVBQVFnd0IsTUF3QmhELFNBQVNILEdBQStDdjNCLEdBQ3BEQSxFQUFXdzNCLHFCQUFrQnBuQixFQUM3QnBRLEVBQVd5M0IscUJBQWtCcm5CLEVBQzdCcFEsRUFBV3MzQixxQkFBa0JsbkIsRUFDN0JwUSxFQUFXNDJCLDRCQUF5QnhtQixFQWV4QyxTQUFTeWxCLEdBQThDNzFCLEdBQ25ELE9BQU9BLEVBQVdtd0IsYUFBZW53QixFQUFXNnFCLGdCQWtCaEQsU0FBU3VKLEdBQW9EcDBCLEdBQ3pELE1BQU0wSCxFQUFTMUgsRUFBV2szQiwwQkFDMUIsSUFBS2wzQixFQUFXOHRCLFNBQ1osT0FFSixRQUFxQzFkLElBQWpDMUksRUFBTzByQixzQkFDUCxPQUdKLEdBQWMsYUFEQTFyQixFQUFPMmQsT0FHakIsWUFEQWlQLEdBQTZCNXNCLEdBR2pDLEdBQWlDLElBQTdCMUgsRUFBVzRxQixPQUFPdmpCLE9BQ2xCLE9BRUosTUFBTWhKLEVBQXVCMkIsRUF2a0RONHFCLE9BQU9nQixPQUNsQnZ0QixNQXVrRFJBLElBQVU4MUIsR0FZbEIsU0FBcURuMEIsR0FDakQsTUFBTTBILEVBQVMxSCxFQUFXazNCLDJCQTFiOUIsU0FBZ0R4dkIsR0FDNUNBLEVBQU80ckIsc0JBQXdCNXJCLEVBQU8yckIsY0FDdEMzckIsRUFBTzJyQixtQkFBZ0JqakIsR0F5YnZCdW5CLENBQXVDandCLEdBQ3ZDZ2pCLEdBQWExcUIsR0FDYixNQUFNNDNCLEVBQW1CNTNCLEVBQVd5M0Isa0JBQ3BDRixHQUErQ3YzQixHQUMvQ2lqQixFQUFZMlUsR0FBa0IsTUF4ZWxDLFNBQTJDbHdCLEdBQ3ZDQSxFQUFPNHJCLHNCQUFzQkssY0FBU3ZqQixHQUN0QzFJLEVBQU80ckIsMkJBQXdCbGpCLEVBRWpCLGFBREExSSxFQUFPMmQsU0FHakIzZCxFQUFPZ2Usa0JBQWV0VixPQUNjQSxJQUFoQzFJLEVBQU82ckIsdUJBQ1A3ckIsRUFBTzZyQixxQkFBcUJJLFdBQzVCanNCLEVBQU82ckIsMEJBQXVCbmpCLElBR3RDMUksRUFBTzJkLE9BQVMsU0FDaEIsTUFBTTRPLEVBQVN2c0IsRUFBT3VyQixhQUNQN2lCLElBQVg2akIsR0FDQXVCLEdBQWtDdkIsR0EwZGxDNEQsQ0FBa0Nud0IsTUFDbkNrYixLQXhkUCxTQUFvRGxiLEVBQVFqRCxHQUN4RGlELEVBQU80ckIsc0JBQXNCTSxRQUFRbnZCLEdBQ3JDaUQsRUFBTzRyQiwyQkFBd0JsakIsT0FFS0EsSUFBaEMxSSxFQUFPNnJCLHVCQUNQN3JCLEVBQU82ckIscUJBQXFCSyxRQUFRbnZCLEdBQ3BDaUQsRUFBTzZyQiwwQkFBdUJuakIsR0FFbENpa0IsR0FBZ0Mzc0IsRUFBUWpELEdBaWRwQ3F6QixDQUEyQ3B3QixFQUFRa2IsTUFwQm5EbVYsQ0FBNEMvM0IsR0F1QnBELFNBQXFEQSxFQUFZZ0ksR0FDN0QsTUFBTU4sRUFBUzFILEVBQVdrM0IsMkJBbGM5QixTQUFxRHh2QixHQUNqREEsRUFBTzByQixzQkFBd0IxckIsRUFBT3lyQixlQUFlL0ssUUFrY3JENFAsQ0FBNEN0d0IsR0FFNUN1YixFQUR5QmpqQixFQUFXdzNCLGdCQUFnQnh2QixJQUN0QixNQTNmbEMsU0FBMkNOLEdBQ3ZDQSxFQUFPMHJCLHNCQUFzQk8sY0FBU3ZqQixHQUN0QzFJLEVBQU8wckIsMkJBQXdCaGpCLEVBMGYzQjZuQixDQUFrQ3Z3QixHQUNsQyxNQUFNMUYsRUFBUTBGLEVBQU8yZCxPQUVyQixHQURBcUYsR0FBYTFxQixJQUNSNnlCLEdBQW9DbnJCLElBQXFCLGFBQVYxRixFQUFzQixDQUN0RSxNQUFNK3lCLEVBQWVvQyxHQUErQ24zQixHQUNwRTgwQixHQUFpQ3B0QixFQUFRcXRCLEdBRTdDWCxHQUFvRHAwQixNQUNyRDRpQixJQUN1QixhQUFsQmxiLEVBQU8yZCxRQUNQa1MsR0FBK0N2M0IsR0FsZ0IzRCxTQUFvRDBILEVBQVFqRCxHQUN4RGlELEVBQU8wckIsc0JBQXNCUSxRQUFRbnZCLEdBQ3JDaUQsRUFBTzByQiwyQkFBd0JoakIsRUFDL0Jpa0IsR0FBZ0Mzc0IsRUFBUWpELEdBaWdCcEN5ekIsQ0FBMkN4d0IsRUFBUWtiLE1BckNuRHVWLENBQTRDbjRCLEVBQVkzQixHQUdoRSxTQUFTeTRCLEdBQTZDOTJCLEVBQVl5RSxHQUNWLGFBQWhEekUsRUFBV2szQiwwQkFBMEI3UixRQUNyQ2dTLEdBQXFDcjNCLEVBQVl5RSxHQW1DekQsU0FBUzB5QixHQUErQ24zQixHQUVwRCxPQURvQjYxQixHQUE4QzcxQixJQUM1QyxFQUcxQixTQUFTcTNCLEdBQXFDcjNCLEVBQVl5RSxHQUN0RCxNQUFNaUQsRUFBUzFILEVBQVdrM0IsMEJBQzFCSyxHQUErQ3YzQixHQUMvQyt6QixHQUE0QnJzQixFQUFRakQsR0FHeEMsU0FBU2l1QixHQUEwQjd3QixHQUMvQixPQUFPLElBQUluRCxVQUFVLDRCQUE0Qm1ELDBDQUdyRCxTQUFTOHpCLEdBQWlDOXpCLEdBQ3RDLE9BQU8sSUFBSW5ELFVBQVUseUNBQXlDbUQsdURBRWxFLFNBQVMrekIsR0FBMkIvekIsR0FDaEMsT0FBTyxJQUFJbkQsVUFBVSxVQUFZbUQsRUFBTyxxQ0FFNUMsU0FBU3l6QixHQUFxQ3JCLEdBQzFDQSxFQUFPaE8sZUFBaUJ6RCxHQUFXLENBQUMzYSxFQUFTQyxLQUN6Q21zQixFQUFPL04sdUJBQXlCcmUsRUFDaENvc0IsRUFBTzlOLHNCQUF3QnJlLEVBQy9CbXNCLEVBQU91QyxvQkFBc0IsYUFHckMsU0FBU2YsR0FBK0N4QixFQUFRclIsR0FDNUQwUyxHQUFxQ3JCLEdBQ3JDWSxHQUFpQ1osRUFBUXJSLEdBTTdDLFNBQVNpUyxHQUFpQ1osRUFBUXJSLFFBQ1R4UyxJQUFqQzZqQixFQUFPOU4sd0JBR1g1QyxFQUEwQjBRLEVBQU9oTyxnQkFDakNnTyxFQUFPOU4sc0JBQXNCdkQsR0FDN0JxUixFQUFPL04sNEJBQXlCOVYsRUFDaEM2akIsRUFBTzlOLDJCQUF3Qi9WLEVBQy9CNmpCLEVBQU91QyxvQkFBc0IsWUFLakMsU0FBU2hCLEdBQWtDdkIsUUFDRDdqQixJQUFsQzZqQixFQUFPL04seUJBR1grTixFQUFPL04sNEJBQXVCOVYsR0FDOUI2akIsRUFBTy9OLDRCQUF5QjlWLEVBQ2hDNmpCLEVBQU85TiwyQkFBd0IvVixFQUMvQjZqQixFQUFPdUMsb0JBQXNCLFlBRWpDLFNBQVN4QixHQUFvQ2YsR0FDekNBLEVBQU84QixjQUFnQnZULEdBQVcsQ0FBQzNhLEVBQVNDLEtBQ3hDbXNCLEVBQU9tRSxzQkFBd0J2d0IsRUFDL0Jvc0IsRUFBT29FLHFCQUF1QnZ3QixLQUVsQ21zQixFQUFPbUMsbUJBQXFCLFVBRWhDLFNBQVNiLEdBQThDdEIsRUFBUXJSLEdBQzNEb1MsR0FBb0NmLEdBQ3BDb0MsR0FBZ0NwQyxFQUFRclIsR0FFNUMsU0FBU3lTLEdBQThDcEIsR0FDbkRlLEdBQW9DZixHQUNwQ0MsR0FBaUNELEdBRXJDLFNBQVNvQyxHQUFnQ3BDLEVBQVFyUixRQUNUeFMsSUFBaEM2akIsRUFBT29FLHVCQUdYOVUsRUFBMEIwUSxFQUFPOEIsZUFDakM5QixFQUFPb0UscUJBQXFCelYsR0FDNUJxUixFQUFPbUUsMkJBQXdCaG9CLEVBQy9CNmpCLEVBQU9vRSwwQkFBdUJqb0IsRUFDOUI2akIsRUFBT21DLG1CQUFxQixZQVFoQyxTQUFTbEMsR0FBaUNELFFBQ0Q3akIsSUFBakM2akIsRUFBT21FLHdCQUdYbkUsRUFBT21FLDJCQUFzQmhvQixHQUM3QjZqQixFQUFPbUUsMkJBQXdCaG9CLEVBQy9CNmpCLEVBQU9vRSwwQkFBdUJqb0IsRUFDOUI2akIsRUFBT21DLG1CQUFxQixhQXBRaENqNEIsT0FBT2MsaUJBQWlCa3pCLEdBQWdDcHpCLFVBQVcsQ0FDL0QwRixNQUFPLENBQUV2RixZQUFZLEtBRWlCLGlCQUEvQjZpQixFQUFlM2lCLGFBQ3RCakIsT0FBT0MsZUFBZSt6QixHQUFnQ3B6QixVQUFXZ2pCLEVBQWUzaUIsWUFBYSxDQUN6RmYsTUFBTyxrQ0FDUGdCLGNBQWMsSUErUXRCLE1BQU1pNUIsR0FBNkMsb0JBQWpCQyxhQUErQkEsa0JBQWVub0IsRUEyQjFFb29CLEdBeEJOLFNBQW1DN0gsR0FDL0IsR0FBc0IsbUJBQVRBLEdBQXVDLGlCQUFUQSxFQUN2QyxPQUFPLEVBRVgsSUFFSSxPQURBLElBQUlBLEdBQ0csRUFFWCxNQUFPOEgsR0FDSCxPQUFPLEdBZVFDLENBQTBCSixJQUFzQkEsR0FadkUsV0FDSSxNQUFNM0gsRUFBTyxTQUFzQnBzQixFQUFTMUMsR0FDeEMzRCxLQUFLcUcsUUFBVUEsR0FBVyxHQUMxQnJHLEtBQUsyRCxLQUFPQSxHQUFRLFFBQ2hCbkIsTUFBTWtaLG1CQUNObFosTUFBTWtaLGtCQUFrQjFiLEtBQU1BLEtBQUsyUCxjQUszQyxPQUZBOGlCLEVBQUs1eEIsVUFBWVosT0FBT3VCLE9BQU9nQixNQUFNM0IsV0FDckNaLE9BQU9DLGVBQWV1eUIsRUFBSzV4QixVQUFXLGNBQWUsQ0FBRVYsTUFBT3N5QixFQUFNN2lCLFVBQVUsRUFBTXpPLGNBQWMsSUFDM0ZzeEIsRUFFaUZnSSxHQUU1RixTQUFTQyxHQUFxQnprQixFQUFRdU4sRUFBTW1YLEVBQWNDLEVBQWNyUCxFQUFlaHFCLEdBQ25GLE1BQU0rWSxFQUFTc1AsRUFBbUMzVCxHQUM1QzhmLEVBQVNsQixHQUFtQ3JSLEdBQ2xEdk4sRUFBTytVLFlBQWEsRUFDcEIsSUFBSTZQLEdBQWUsRUFFZkMsRUFBZXRXLE9BQW9CdFMsR0FDdkMsT0FBT29TLEdBQVcsQ0FBQzNhLEVBQVNDLEtBQ3hCLElBQUl5cUIsRUFDSixRQUFlbmlCLElBQVgzUSxFQUFzQixDQXNCdEIsR0FyQkE4eUIsRUFBaUIsS0FDYixNQUFNOXRCLEVBQVEsSUFBSSt6QixHQUFlLFVBQVcsY0FDdENTLEVBQVUsR0FDWEgsR0FDREcsRUFBUWh4QixNQUFLLElBQ1csYUFBaEJ5WixFQUFLMkQsT0FDRXVOLEdBQW9CbFIsRUFBTWpkLEdBRTlCaWUsT0FBb0J0UyxLQUc5QnFaLEdBQ0R3UCxFQUFRaHhCLE1BQUssSUFDYSxhQUFsQmtNLEVBQU9rUixPQUNBTyxHQUFxQnpSLEVBQVExUCxHQUVqQ2llLE9BQW9CdFMsS0FHbkM4b0IsR0FBbUIsSUFBTXR4QixRQUFRdXhCLElBQUlGLEVBQVFwMkIsS0FBSXUyQixHQUFVQSxTQUFZLEVBQU0zMEIsSUFFN0VoRixFQUFPZCxRQUVQLFlBREE0ekIsSUFHSjl5QixFQUFPbVgsaUJBQWlCLFFBQVMyYixHQXlGckMsSUFBMkI3cUIsRUFBUW9iLEVBQVNzVyxFQXhCNUMsR0EzQkFDLEVBQW1CbGxCLEVBQVFxRSxFQUFPeU4sZ0JBQWdCd08sSUFDekNxRSxFQUlEUSxHQUFTLEVBQU03RSxHQUhmeUUsR0FBbUIsSUFBTXRHLEdBQW9CbFIsRUFBTStTLEtBQWMsRUFBTUEsTUFPL0U0RSxFQUFtQjNYLEVBQU11UyxFQUFPaE8sZ0JBQWdCd08sSUFDdkNoTCxFQUlENlAsR0FBUyxFQUFNN0UsR0FIZnlFLEdBQW1CLElBQU10VCxHQUFxQnpSLEVBQVFzZ0IsS0FBYyxFQUFNQSxNQXdDdkQvc0IsRUFqQ1R5TSxFQWlDaUIyTyxFQWpDVHRLLEVBQU95TixlQWlDV21ULEVBakNLLEtBQ3hDUCxFQUlEUyxJQUhBSixHQUFtQixJQTVmbkMsU0FBOERqRixHQUMxRCxNQUFNdnNCLEVBQVN1c0IsRUFBT21CLHFCQUNoQnB6QixFQUFRMEYsRUFBTzJkLE9BQ3JCLE9BQUl3TixHQUFvQ25yQixJQUFxQixXQUFWMUYsRUFDeEMwZ0IsT0FBb0J0UyxHQUVqQixZQUFWcE8sRUFDTzJnQixFQUFvQmpiLEVBQU9nZSxjQUUvQnVRLEdBQWlDaEMsR0FtZkhzRixDQUFxRHRGLE1BZ0M1RCxXQUFsQnZzQixFQUFPMmQsT0FDUCtULElBR0FsVyxFQUFnQkosRUFBU3NXLEdBN0I3QnZHLEdBQW9DblIsSUFBeUIsV0FBaEJBLEVBQUsyRCxPQUFxQixDQUN2RSxNQUFNbVUsRUFBYSxJQUFJOTZCLFVBQVUsK0VBQzVCK3FCLEVBSUQ2UCxHQUFTLEVBQU1FLEdBSGZOLEdBQW1CLElBQU10VCxHQUFxQnpSLEVBQVFxbEIsS0FBYSxFQUFNQSxHQU9qRixTQUFTQyxJQUdMLE1BQU1DLEVBQWtCVixFQUN4QixPQUFPblcsRUFBbUJtVyxHQUFjLElBQU1VLElBQW9CVixFQUFlUyxTQUEwQnJwQixJQUUvRyxTQUFTaXBCLEVBQW1CM3hCLEVBQVFvYixFQUFTc1csR0FDbkIsWUFBbEIxeEIsRUFBTzJkLE9BQ1ArVCxFQUFPMXhCLEVBQU9nZSxjQUdkdkMsRUFBY0wsRUFBU3NXLEdBVy9CLFNBQVNGLEVBQW1CRSxFQUFRTyxFQUFpQkMsR0FXakQsU0FBU0MsSUFDTDVXLEVBQVltVyxLQUFVLElBQU1uWixFQUFTMFosRUFBaUJDLEtBQWdCRSxHQUFZN1osR0FBUyxFQUFNNlosS0FYakdmLElBR0pBLEdBQWUsRUFDSyxhQUFoQnJYLEVBQUsyRCxRQUEwQndOLEdBQW9DblIsR0FJbkVtWSxJQUhBM1csRUFBZ0J1VyxJQUF5QkksSUFTakQsU0FBU1AsRUFBU1MsRUFBU3QxQixHQUNuQnMwQixJQUdKQSxHQUFlLEVBQ0ssYUFBaEJyWCxFQUFLMkQsUUFBMEJ3TixHQUFvQ25SLEdBSW5FekIsRUFBUzhaLEVBQVN0MUIsR0FIbEJ5ZSxFQUFnQnVXLEtBQXlCLElBQU14WixFQUFTOFosRUFBU3QxQixNQU16RSxTQUFTd2IsRUFBUzhaLEVBQVN0MUIsR0FDdkJ5eEIsR0FBbUNqQyxHQUNuQ3BPLEVBQW1Dck4sUUFDcEJwSSxJQUFYM1EsR0FDQUEsRUFBTzBnQixvQkFBb0IsUUFBU29TLEdBRXBDd0gsRUFDQWp5QixFQUFPckQsR0FHUG9ELE9BQVF1SSxHQTVEaEJtVCxFQXBFV2YsR0FBVyxDQUFDd1gsRUFBYUMsTUFDNUIsU0FBUzFxQixFQUFLb0osR0FDTkEsRUFDQXFoQixJQUtBblgsRUFPUmtXLEVBQ09yVyxHQUFvQixHQUV4QkcsRUFBbUJvUixFQUFPOEIsZUFBZSxJQUNyQ3ZULEdBQVcsQ0FBQzBYLEVBQWFDLEtBQzVCclIsR0FBZ0N0USxFQUFRLENBQ3BDOFAsWUFBYXRnQixJQUNUZ3hCLEVBQWVuVyxFQUFtQnNULEdBQWlDbEMsRUFBUWpzQixRQUFRb0ksRUFBVzZSLEdBQzlGaVksR0FBWSxJQUVoQjdSLFlBQWEsSUFBTTZSLEdBQVksR0FDL0JuUixZQUFhb1IsU0FsQmtCNXFCLEVBQU0wcUIsR0FHN0MxcUIsRUFBSyxVQWdJckIsTUFBTTZxQixHQUNGLGNBQ0ksTUFBTSxJQUFJMTdCLFVBQVUsdUJBTXhCLGtCQUNJLElBQUsyN0IsR0FBa0NuOEIsTUFDbkMsTUFBTW84QixHQUFxQyxlQUUvQyxPQUFPQyxHQUE4Q3I4QixNQU16RCxRQUNJLElBQUttOEIsR0FBa0NuOEIsTUFDbkMsTUFBTW84QixHQUFxQyxTQUUvQyxJQUFLRSxHQUFpRHQ4QixNQUNsRCxNQUFNLElBQUlRLFVBQVUsbURBRXhCKzdCLEdBQXFDdjhCLE1BRXpDLFFBQVE4SixHQUNKLElBQUtxeUIsR0FBa0NuOEIsTUFDbkMsTUFBTW84QixHQUFxQyxXQUUvQyxJQUFLRSxHQUFpRHQ4QixNQUNsRCxNQUFNLElBQUlRLFVBQVUscURBRXhCLE9BQU9nOEIsR0FBdUN4OEIsS0FBTThKLEdBS3hELE1BQU1naEIsR0FDRixJQUFLcVIsR0FBa0NuOEIsTUFDbkMsTUFBTW84QixHQUFxQyxTQUUvQ0ssR0FBcUN6OEIsS0FBTThxQixHQUcvQyxDQUFDMUMsR0FBYTFELEdBQ1ZtSSxHQUFXN3NCLE1BQ1gsTUFBTStWLEVBQVMvVixLQUFLa3ZCLGlCQUFpQnhLLEdBRXJDLE9BREFnWSxHQUErQzE4QixNQUN4QytWLEVBR1gsQ0FBQ3NTLEdBQVcwQixHQUNSLE1BQU12Z0IsRUFBU3hKLEtBQUsyOEIsMEJBQ3BCLEdBQUkzOEIsS0FBSzBzQixPQUFPdmpCLE9BQVMsRUFBRyxDQUN4QixNQUFNVyxFQUFRMGlCLEdBQWF4c0IsTUFDdkJBLEtBQUtzdUIsaUJBQTBDLElBQXZCdHVCLEtBQUswc0IsT0FBT3ZqQixRQUNwQ3V6QixHQUErQzE4QixNQUMvQzB1QixHQUFvQmxsQixJQUdwQm96QixHQUFnRDU4QixNQUVwRCtwQixFQUFZSyxZQUFZdGdCLFFBR3hCZ2dCLEVBQTZCdGdCLEVBQVF1Z0IsR0FDckM2UyxHQUFnRDU4QixPQWlCNUQsU0FBU204QixHQUFrQ3RyQixHQUN2QyxRQUFLbVQsRUFBYW5ULE1BR2I1USxPQUFPWSxVQUFVa3FCLGVBQWV0cEIsS0FBS29QLEVBQUcsNkJBS2pELFNBQVMrckIsR0FBZ0Q5NkIsR0FDbEMrNkIsR0FBOEMvNkIsS0FJN0RBLEVBQVdpdUIsU0FDWGp1QixFQUFXa3VCLFlBQWEsR0FHNUJsdUIsRUFBV2l1QixVQUFXLEVBRXRCaEwsRUFEb0JqakIsRUFBV211QixrQkFDTixLQUNyQm51QixFQUFXaXVCLFVBQVcsRUFDbEJqdUIsRUFBV2t1QixhQUNYbHVCLEVBQVdrdUIsWUFBYSxFQUN4QjRNLEdBQWdEOTZCLE9BRXJEZ3BCLElBQ0MyUixHQUFxQzM2QixFQUFZZ3BCLFFBR3pELFNBQVMrUixHQUE4Qy82QixHQUNuRCxNQUFNMEgsRUFBUzFILEVBQVc2NkIsMEJBQzFCLFFBQUtMLEdBQWlEeDZCLE9BR2pEQSxFQUFXOHRCLGNBR1pwRixHQUF1QmhoQixJQUFXNmdCLEVBQWlDN2dCLEdBQVUsSUFHN0Q2eUIsR0FBOEN2NkIsR0FDaEQsSUFLdEIsU0FBUzQ2QixHQUErQzU2QixHQUNwREEsRUFBV211QixvQkFBaUIvZCxFQUM1QnBRLEVBQVdvdEIsc0JBQW1CaGQsRUFDOUJwUSxFQUFXNDJCLDRCQUF5QnhtQixFQUd4QyxTQUFTcXFCLEdBQXFDejZCLEdBQzFDLElBQUt3NkIsR0FBaUR4NkIsR0FDbEQsT0FFSixNQUFNMEgsRUFBUzFILEVBQVc2NkIsMEJBQzFCNzZCLEVBQVd3c0IsaUJBQWtCLEVBQ0ksSUFBN0J4c0IsRUFBVzRxQixPQUFPdmpCLFNBQ2xCdXpCLEdBQStDNTZCLEdBQy9DNHNCLEdBQW9CbGxCLElBRzVCLFNBQVNnekIsR0FBdUMxNkIsRUFBWWdJLEdBQ3hELElBQUt3eUIsR0FBaUR4NkIsR0FDbEQsT0FFSixNQUFNMEgsRUFBUzFILEVBQVc2NkIsMEJBQzFCLEdBQUluUyxHQUF1QmhoQixJQUFXNmdCLEVBQWlDN2dCLEdBQVUsRUFDN0V5Z0IsRUFBaUN6Z0IsRUFBUU0sR0FBTyxPQUUvQyxDQUNELElBQUkydUIsRUFDSixJQUNJQSxFQUFZMzJCLEVBQVc0MkIsdUJBQXVCNXVCLEdBRWxELE1BQU82dUIsR0FFSCxNQURBOEQsR0FBcUMzNkIsRUFBWTYyQixHQUMzQ0EsRUFFVixJQUNJL0wsR0FBcUI5cUIsRUFBWWdJLEVBQU8ydUIsR0FFNUMsTUFBT00sR0FFSCxNQURBMEQsR0FBcUMzNkIsRUFBWWkzQixHQUMzQ0EsR0FHZDZELEdBQWdEOTZCLEdBRXBELFNBQVMyNkIsR0FBcUMzNkIsRUFBWWdwQixHQUN0RCxNQUFNdGhCLEVBQVMxSCxFQUFXNjZCLDBCQUNKLGFBQWxCbnpCLEVBQU8yZCxTQUdYMEYsR0FBVy9xQixHQUNYNDZCLEdBQStDNTZCLEdBQy9Da3dCLEdBQW9CeG9CLEVBQVFzaEIsSUFFaEMsU0FBU3VSLEdBQThDdjZCLEdBQ25ELE1BQU1nQyxFQUFRaEMsRUFBVzY2QiwwQkFBMEJ4VixPQUNuRCxNQUFjLFlBQVZyakIsRUFDTyxLQUVHLFdBQVZBLEVBQ08sRUFFSmhDLEVBQVdtd0IsYUFBZW53QixFQUFXNnFCLGdCQVNoRCxTQUFTMlAsR0FBaUR4NkIsR0FDdEQsTUFBTWdDLEVBQVFoQyxFQUFXNjZCLDBCQUEwQnhWLE9BQ25ELE9BQUtybEIsRUFBV3dzQixpQkFBNkIsYUFBVnhxQixFQUt2QyxTQUFTZzVCLEdBQXFDdHpCLEVBQVExSCxFQUFZb3lCLEVBQWdCNkksRUFBZUMsRUFBaUJwb0IsRUFBZW9mLEdBQzdIbHlCLEVBQVc2NkIsMEJBQTRCbnpCLEVBQ3ZDMUgsRUFBVzRxQixZQUFTeGEsRUFDcEJwUSxFQUFXNnFCLHFCQUFrQnphLEVBQzdCMmEsR0FBVy9xQixHQUNYQSxFQUFXOHRCLFVBQVcsRUFDdEI5dEIsRUFBV3dzQixpQkFBa0IsRUFDN0J4c0IsRUFBV2t1QixZQUFhLEVBQ3hCbHVCLEVBQVdpdUIsVUFBVyxFQUN0Qmp1QixFQUFXNDJCLHVCQUF5QjFFLEVBQ3BDbHlCLEVBQVdtd0IsYUFBZXJkLEVBQzFCOVMsRUFBV211QixlQUFpQjhNLEVBQzVCajdCLEVBQVdvdEIsaUJBQW1COE4sRUFDOUJ4ekIsRUFBT3loQiwwQkFBNEJucEIsRUFFbkNpakIsRUFBWVAsRUFEUTBQLE1BQzBCLEtBQzFDcHlCLEVBQVc4dEIsVUFBVyxFQUN0QmdOLEdBQWdEOTZCLE1BQ2pEMDNCLElBQ0NpRCxHQUFxQzM2QixFQUFZMDNCLE1Bb0J6RCxTQUFTNEMsR0FBcUN6NEIsR0FDMUMsT0FBTyxJQUFJbkQsVUFBVSw2Q0FBNkNtRCwyREFxSHRFLFNBQVNzNUIsR0FBc0Nua0IsRUFBSXNhLEVBQVV0SyxHQUV6RCxPQURBQyxFQUFlalEsRUFBSWdRLEdBQ1hwRSxHQUFXb0IsRUFBWWhOLEVBQUlzYSxFQUFVLENBQUMxTyxJQUVsRCxTQUFTd1ksR0FBb0Nwa0IsRUFBSXNhLEVBQVV0SyxHQUV2RCxPQURBQyxFQUFlalEsRUFBSWdRLEdBQ1hobkIsR0FBZWdrQixFQUFZaE4sRUFBSXNhLEVBQVUsQ0FBQ3R4QixJQUV0RCxTQUFTcTdCLEdBQXFDcmtCLEVBQUlzYSxFQUFVdEssR0FFeEQsT0FEQUMsRUFBZWpRLEVBQUlnUSxHQUNYaG5CLEdBQWUyakIsRUFBWTNNLEVBQUlzYSxFQUFVLENBQUN0eEIsSUFFdEQsU0FBU3M3QixHQUEwQnY3QixFQUFNaW5CLEdBRXJDLEdBQWEsVUFEYmpuQixFQUFPLEdBQUdBLEtBRU4sTUFBTSxJQUFJckIsVUFBVSxHQUFHc29CLE1BQVlqbkIsOERBRXZDLE9BQU9BLEVBVVgsU0FBU3c3QixHQUFnQ0MsRUFBTXhVLEdBRTNDLEdBQWEsU0FEYndVLEVBQU8sR0FBR0EsS0FFTixNQUFNLElBQUk5OEIsVUFBVSxHQUFHc29CLE1BQVl3VSxvRUFFdkMsT0FBT0EsRUFTWCxTQUFTQyxHQUFtQm43QixFQUFTMG1CLEdBQ2pDRCxFQUFpQnptQixFQUFTMG1CLEdBQzFCLE1BQU04UixFQUFleDRCLGFBQXlDLEVBQVNBLEVBQVF3NEIsYUFDekVyUCxFQUFnQm5wQixhQUF5QyxFQUFTQSxFQUFRbXBCLGNBQzFFb1AsRUFBZXY0QixhQUF5QyxFQUFTQSxFQUFRdTRCLGFBQ3pFcDVCLEVBQVNhLGFBQXlDLEVBQVNBLEVBQVFiLE9BSXpFLFlBSGUyUSxJQUFYM1EsR0FVUixTQUEyQkEsRUFBUXVuQixHQUMvQixJQXZvQkosU0FBdUIzb0IsR0FDbkIsR0FBcUIsaUJBQVZBLEdBQWdDLE9BQVZBLEVBQzdCLE9BQU8sRUFFWCxJQUNJLE1BQWdDLGtCQUFsQkEsRUFBTU0sUUFFeEIsTUFBTzg1QixHQUVILE9BQU8sR0E4bkJOaUQsQ0FBY2o4QixHQUNmLE1BQU0sSUFBSWYsVUFBVSxHQUFHc29CLDRCQVh2QjJVLENBQWtCbDhCLEVBQVEsR0FBR3VuQiw4QkFFMUIsQ0FDSDhSLGFBQWN2cUIsUUFBUXVxQixHQUN0QnJQLGNBQWVsYixRQUFRa2IsR0FDdkJvUCxhQUFjdHFCLFFBQVFzcUIsR0FDdEJwNUIsVUE1VlJ0QixPQUFPYyxpQkFBaUJtN0IsR0FBZ0NyN0IsVUFBVyxDQUMvRDZaLE1BQU8sQ0FBRTFaLFlBQVksR0FDckIyWixRQUFTLENBQUUzWixZQUFZLEdBQ3ZCdUYsTUFBTyxDQUFFdkYsWUFBWSxHQUNyQm94QixZQUFhLENBQUVweEIsWUFBWSxLQUVXLGlCQUEvQjZpQixFQUFlM2lCLGFBQ3RCakIsT0FBT0MsZUFBZWc4QixHQUFnQ3I3QixVQUFXZ2pCLEVBQWUzaUIsWUFBYSxDQUN6RmYsTUFBTyxrQ0FDUGdCLGNBQWMsSUE0V3RCLE1BQU00VCxHQUNGLFlBQVkyb0IsRUFBc0IsR0FBSTlKLEVBQWMsU0FDcEIxaEIsSUFBeEJ3ckIsRUFDQUEsRUFBc0IsS0FHdEIxVSxFQUFhMFUsRUFBcUIsbUJBRXRDLE1BQU01SyxFQUFXRyxHQUF1QlcsRUFBYSxvQkFDL0MrSixFQWhIZCxTQUE4QzFuQixFQUFRNlMsR0FDbERELEVBQWlCNVMsRUFBUTZTLEdBQ3pCLE1BQU1zSyxFQUFXbmQsRUFDWG9aLEVBQXdCK0QsYUFBMkMsRUFBU0EsRUFBUy9ELHNCQUNyRm5FLEVBQVNrSSxhQUEyQyxFQUFTQSxFQUFTbEksT0FDdEUwUyxFQUFPeEssYUFBMkMsRUFBU0EsRUFBU3dLLEtBQ3BFaHpCLEVBQVF3b0IsYUFBMkMsRUFBU0EsRUFBU3hvQixNQUNyRS9JLEVBQU91eEIsYUFBMkMsRUFBU0EsRUFBU3Z4QixLQUMxRSxNQUFPLENBQ0h3dEIsMkJBQWlEbmQsSUFBMUJtZCxPQUNuQm5kLEVBQ0FvWCxFQUF3QytGLEVBQXVCLEdBQUd2Ryw2Q0FDdEVvQyxZQUFtQmhaLElBQVhnWixPQUNKaFosRUFDQStxQixHQUFzQy9SLEVBQVFrSSxFQUFVLEdBQUd0Syw4QkFDL0Q4VSxVQUFlMXJCLElBQVQwckIsT0FDRjFyQixFQUNBZ3JCLEdBQW9DVSxFQUFNeEssRUFBVSxHQUFHdEssNEJBQzNEbGUsV0FBaUJzSCxJQUFWdEgsT0FDSHNILEVBQ0FpckIsR0FBcUN2eUIsRUFBT3dvQixFQUFVLEdBQUd0Syw2QkFDN0RqbkIsVUFBZXFRLElBQVRyUSxPQUFxQnFRLEVBQVlrckIsR0FBMEJ2N0IsRUFBTSxHQUFHaW5CLDZCQTJGakQrVSxDQUFxQ0gsRUFBcUIsbUJBRW5GLEdBREFJLEdBQXlCOTlCLE1BQ0ssVUFBMUIyOUIsRUFBaUI5N0IsS0FBa0IsQ0FDbkMsUUFBc0JxUSxJQUFsQjRnQixFQUFTaGdCLEtBQ1QsTUFBTSxJQUFJaUcsV0FBVywrREEzeURyQyxTQUErRHZQLEVBQVF1MEIsRUFBc0JucEIsR0FDekYsTUFBTTlTLEVBQWE3QixPQUFPdUIsT0FBT3VzQixHQUE2Qmx0QixXQUM5RCxJQUFJcXpCLEVBQWlCLE9BQ2pCNkksRUFBZ0IsSUFBTXZZLE9BQW9CdFMsR0FDMUM4cUIsRUFBa0IsSUFBTXhZLE9BQW9CdFMsUUFDYkEsSUFBL0I2ckIsRUFBcUJuekIsUUFDckJzcEIsRUFBaUIsSUFBTTZKLEVBQXFCbnpCLE1BQU05SSxTQUVwQm9RLElBQTlCNnJCLEVBQXFCSCxPQUNyQmIsRUFBZ0IsSUFBTWdCLEVBQXFCSCxLQUFLOTdCLFNBRWhCb1EsSUFBaEM2ckIsRUFBcUI3UyxTQUNyQjhSLEVBQWtCdFksR0FBVXFaLEVBQXFCN1MsT0FBT3hHLElBRTVELE1BQU0ySyxFQUF3QjBPLEVBQXFCMU8sdUJBdEN2RCxTQUEyQzdsQixFQUFRMUgsRUFBWW95QixFQUFnQjZJLEVBQWVDLEVBQWlCcG9CLEVBQWV5YSxHQUMxSHZ0QixFQUFXeXNCLDhCQUFnQy9rQixFQUMzQzFILEVBQVdrdUIsWUFBYSxFQUN4Qmx1QixFQUFXaXVCLFVBQVcsRUFDdEJqdUIsRUFBV29zQixhQUFlLEtBRTFCcHNCLEVBQVc0cUIsT0FBUzVxQixFQUFXNnFCLHFCQUFrQnphLEVBQ2pEMmEsR0FBVy9xQixHQUNYQSxFQUFXd3NCLGlCQUFrQixFQUM3QnhzQixFQUFXOHRCLFVBQVcsRUFDdEI5dEIsRUFBV213QixhQUFlcmQsRUFDMUI5UyxFQUFXbXVCLGVBQWlCOE0sRUFDNUJqN0IsRUFBV290QixpQkFBbUI4TixFQUM5Qmw3QixFQUFXd3RCLHVCQUF5QkQsRUFDcEN2dEIsRUFBVzJyQixrQkFBb0IsSUFBSTFILEVBQ25DdmMsRUFBT3loQiwwQkFBNEJucEIsRUFFbkNpakIsRUFBWVAsRUFEUTBQLE1BQzBCLEtBQzFDcHlCLEVBQVc4dEIsVUFBVyxFQUN0QlosR0FBNkNsdEIsTUFDOUMwM0IsSUFDQ2hMLEdBQWtDMXNCLEVBQVkwM0IsTUFrQmxEd0UsQ0FBa0N4MEIsRUFBUTFILEVBQVlveUIsRUFBZ0I2SSxFQUFlQyxFQUFpQnBvQixFQUFleWEsR0EreEQ3RzRPLENBQXNEaitCLEtBQU0yOUIsRUFEdEM5SyxHQUFxQkMsRUFBVSxRQUdwRCxDQUNELE1BQU1rQixFQUFnQmhCLEdBQXFCRixJQXpPdkQsU0FBa0V0cEIsRUFBUW0wQixFQUFrQi9vQixFQUFlb2YsR0FDdkcsTUFBTWx5QixFQUFhN0IsT0FBT3VCLE9BQU8wNkIsR0FBZ0NyN0IsV0FDakUsSUFBSXF6QixFQUFpQixPQUNqQjZJLEVBQWdCLElBQU12WSxPQUFvQnRTLEdBQzFDOHFCLEVBQWtCLElBQU14WSxPQUFvQnRTLFFBQ2pCQSxJQUEzQnlyQixFQUFpQi95QixRQUNqQnNwQixFQUFpQixJQUFNeUosRUFBaUIveUIsTUFBTTlJLFNBRXBCb1EsSUFBMUJ5ckIsRUFBaUJDLE9BQ2pCYixFQUFnQixJQUFNWSxFQUFpQkMsS0FBSzk3QixTQUVoQm9RLElBQTVCeXJCLEVBQWlCelMsU0FDakI4UixFQUFrQnRZLEdBQVVpWixFQUFpQnpTLE9BQU94RyxJQUV4RG9ZLEdBQXFDdHpCLEVBQVExSCxFQUFZb3lCLEVBQWdCNkksRUFBZUMsRUFBaUJwb0IsRUFBZW9mLEdBNk5oSGtLLENBQXlEbCtCLEtBQU0yOUIsRUFEekM5SyxHQUFxQkMsRUFBVSxHQUMyQ2tCLElBTXhHLGFBQ0ksSUFBS3JLLEdBQWlCM3BCLE1BQ2xCLE1BQU1tK0IsR0FBNEIsVUFFdEMsT0FBTzNULEdBQXVCeHFCLE1BUWxDLE9BQU8wa0IsR0FDSCxPQUFLaUYsR0FBaUIzcEIsTUFHbEJ3cUIsR0FBdUJ4cUIsTUFDaEJ5a0IsRUFBb0IsSUFBSWprQixVQUFVLHFEQUV0Q2tuQixHQUFxQjFuQixLQUFNMGtCLEdBTHZCRCxFQUFvQjBaLEdBQTRCLFdBTy9ELFVBQVVDLEdBQ04sSUFBS3pVLEdBQWlCM3BCLE1BQ2xCLE1BQU1tK0IsR0FBNEIsYUFHdEMsWUFBcUJqc0IsSUFoSDdCLFNBQThCOVAsRUFBUzBtQixHQUNuQ0QsRUFBaUJ6bUIsRUFBUzBtQixHQUMxQixNQUFNd1UsRUFBT2w3QixhQUF5QyxFQUFTQSxFQUFRazdCLEtBQ3ZFLE1BQU8sQ0FDSEEsVUFBZXByQixJQUFUb3JCLE9BQXFCcHJCLEVBQVltckIsR0FBZ0NDLEVBQU0sR0FBR3hVLDZCQTJHaEV1VixDQUFxQkQsRUFBWSxtQkFDckNkLEtBQ0QxVCxFQUFtQzVwQixNQXB6RDNDLElBQUlxeUIsR0FzekRnQ3J5QixNQUUzQyxZQUFZcytCLEVBQWNGLEVBQWEsSUFDbkMsSUFBS3pVLEdBQWlCM3BCLE1BQ2xCLE1BQU1tK0IsR0FBNEIsZUFFdENsVixFQUF1QnFWLEVBQWMsRUFBRyxlQUN4QyxNQUFNQyxFQS9FZCxTQUFxQ3ZmLEVBQU04SixHQUN2Q0QsRUFBaUI3SixFQUFNOEosR0FDdkIsTUFBTTBWLEVBQVd4ZixhQUFtQyxFQUFTQSxFQUFLd2YsU0FDbEVyVixFQUFvQnFWLEVBQVUsV0FBWSx3QkFDMUM5VSxFQUFxQjhVLEVBQVUsR0FBRzFWLGdDQUNsQyxNQUFNbFosRUFBV29QLGFBQW1DLEVBQVNBLEVBQUtwUCxTQUdsRSxPQUZBdVosRUFBb0J2WixFQUFVLFdBQVksd0JBQzFDNGpCLEdBQXFCNWpCLEVBQVUsR0FBR2taLGdDQUMzQixDQUFFMFYsV0FBVTV1QixZQXVFRzZ1QixDQUE0QkgsRUFBYyxtQkFDdERsOEIsRUFBVW03QixHQUFtQmEsRUFBWSxvQkFDL0MsR0FBSTVULEdBQXVCeHFCLE1BQ3ZCLE1BQU0sSUFBSVEsVUFBVSxrRkFFeEIsR0FBSWkwQixHQUF1QjhKLEVBQVUzdUIsVUFDakMsTUFBTSxJQUFJcFAsVUFBVSxrRkFJeEIsT0FEQTZrQixFQURnQnFWLEdBQXFCMTZCLEtBQU11K0IsRUFBVTN1QixTQUFVeE4sRUFBUXU0QixhQUFjdjRCLEVBQVF3NEIsYUFBY3g0QixFQUFRbXBCLGNBQWVucEIsRUFBUWIsU0FFbklnOUIsRUFBVUMsU0FFckIsT0FBT0UsRUFBYU4sRUFBYSxJQUM3QixJQUFLelUsR0FBaUIzcEIsTUFDbEIsT0FBT3lrQixFQUFvQjBaLEdBQTRCLFdBRTNELFFBQW9CanNCLElBQWhCd3NCLEVBQ0EsT0FBT2phLEVBQW9CLHdDQUUvQixJQUFLZ1AsR0FBaUJpTCxHQUNsQixPQUFPamEsRUFBb0IsSUFBSWprQixVQUFVLDhFQUU3QyxJQUFJNEIsRUFDSixJQUNJQSxFQUFVbTdCLEdBQW1CYSxFQUFZLG9CQUU3QyxNQUFPdFQsR0FDSCxPQUFPckcsRUFBb0JxRyxHQUUvQixPQUFJTixHQUF1QnhxQixNQUNoQnlrQixFQUFvQixJQUFJamtCLFVBQVUsOEVBRXpDaTBCLEdBQXVCaUssR0FDaEJqYSxFQUFvQixJQUFJamtCLFVBQVUsOEVBRXRDazZCLEdBQXFCMTZCLEtBQU0wK0IsRUFBYXQ4QixFQUFRdTRCLGFBQWN2NEIsRUFBUXc0QixhQUFjeDRCLEVBQVFtcEIsY0FBZW5wQixFQUFRYixRQWE5SCxNQUNJLElBQUtvb0IsR0FBaUIzcEIsTUFDbEIsTUFBTW0rQixHQUE0QixPQUV0QyxNQUFNUSxFQXBUZCxTQUEyQm4xQixFQUFRbzFCLEdBQy9CLE1BQU10a0IsRUFBU3NQLEVBQW1DcGdCLEdBQ2xELElBR0lxMUIsRUFDQUMsRUFDQUMsRUFDQUMsRUFDQUMsRUFQQUMsR0FBVSxFQUNWQyxHQUFZLEVBQ1pDLEdBQVksRUFNaEIsTUFBTUMsRUFBZ0IvYSxHQUFXM2EsSUFDN0JzMUIsRUFBdUJ0MUIsS0FFM0IsU0FBU296QixJQUNMLE9BQUltQyxJQUdKQSxHQUFVLEVBcUNWdFUsR0FBZ0N0USxFQXBDWixDQUNoQjhQLFlBQWFqcUIsSUFJVG1sQixHQUFlLEtBQ1g0WixHQUFVLEVBQ1YsTUFBTUksRUFBU24vQixFQUNUby9CLEVBQVNwL0IsRUFNVmcvQixHQUNEM0MsR0FBdUN1QyxFQUFROVQsMEJBQTJCcVUsR0FFekVGLEdBQ0Q1QyxHQUF1Q3dDLEVBQVEvVCwwQkFBMkJzVSxHQUU5RU4sT0FBcUIvc0IsT0FHN0JpWSxZQUFhLEtBQ1QrVSxHQUFVLEVBQ0xDLEdBQ0Q1QyxHQUFxQ3dDLEVBQVE5VCwyQkFFNUNtVSxHQUNEN0MsR0FBcUN5QyxFQUFRL1QsNEJBR3JESixZQUFhLEtBQ1RxVSxHQUFVLE1BcENQMWEsT0FBb0J0UyxHQThEbkMsU0FBU2dpQixLQVVULE9BUEE2SyxFQUFVUyxHQUFxQnRMLEVBQWdCNkksR0F2Qi9DLFNBQTBCclksR0FHdEIsR0FGQXlhLEdBQVksRUFDWk4sRUFBVW5hLEVBQ04wYSxFQUFXLENBQ1gsTUFBTUssRUFBa0IzUyxHQUFvQixDQUFDK1IsRUFBU0MsSUFDaERZLEVBQWVoWSxHQUFxQmxlLEVBQVFpMkIsR0FDbERSLEVBQXFCUyxHQUV6QixPQUFPTCxLQWdCWEwsRUFBVVEsR0FBcUJ0TCxFQUFnQjZJLEdBZC9DLFNBQTBCclksR0FHdEIsR0FGQTBhLEdBQVksRUFDWk4sRUFBVXBhLEVBQ055YSxFQUFXLENBQ1gsTUFBTU0sRUFBa0IzUyxHQUFvQixDQUFDK1IsRUFBU0MsSUFDaERZLEVBQWVoWSxHQUFxQmxlLEVBQVFpMkIsR0FDbERSLEVBQXFCUyxHQUV6QixPQUFPTCxLQU9YcGEsRUFBYzNLLEVBQU95TixnQkFBaUJ5UixJQUNsQ2lELEdBQXFDc0MsRUFBUTlULDBCQUEyQnVPLEdBQ3hFaUQsR0FBcUN1QyxFQUFRL1QsMEJBQTJCdU8sR0FDeEV5RixPQUFxQi9zQixNQUVsQixDQUFDNnNCLEVBQVNDLEdBNk5JVyxDQUFrQjMvQixNQUNuQyxPQUFPOHNCLEdBQW9CNlIsR0FFL0IsT0FBT1AsR0FDSCxJQUFLelUsR0FBaUIzcEIsTUFDbEIsTUFBTW0rQixHQUE0QixVQUd0QyxPQWpqRlIsU0FBNEMzMEIsRUFBUStoQixHQUNoRCxNQUFNalIsRUFBU3NQLEVBQW1DcGdCLEdBQzVDbzJCLEVBQU8sSUFBSXRVLEdBQWdDaFIsRUFBUWlSLEdBQ25EeE0sRUFBVzllLE9BQU91QixPQUFPdXFCLElBRS9CLE9BREFoTixFQUFTa04sbUJBQXFCMlQsRUFDdkI3Z0IsRUE0aUZJOGdCLENBQW1DNy9CLEtBdktsRCxTQUFnQ29DLEVBQVMwbUIsR0FDckNELEVBQWlCem1CLEVBcUtzQyxtQkFwS3ZELE1BQU1tcEIsRUFBZ0JucEIsYUFBeUMsRUFBU0EsRUFBUW1wQixjQUNoRixNQUFPLENBQUVBLGNBQWVsYixRQUFRa2IsSUFtS1p1VSxDQUF1QjFCLEdBQ2lCN1MsZ0JBMkJoRSxTQUFTaVUsR0FBcUJ0TCxFQUFnQjZJLEVBQWVDLEVBQWlCcG9CLEVBQWdCLEVBQUdvZixFQUFnQixLQUFNLElBQ25ILE1BQU14cUIsRUFBU3ZKLE9BQU91QixPQUFPdVQsR0FBZWxVLFdBSTVDLE9BSEFpOUIsR0FBeUJ0MEIsR0FFekJzekIsR0FBcUN0ekIsRUFEbEJ2SixPQUFPdUIsT0FBTzA2QixHQUFnQ3I3QixXQUNScXpCLEVBQWdCNkksRUFBZUMsRUFBaUJwb0IsRUFBZW9mLEdBQ2pIeHFCLEVBRVgsU0FBU3MwQixHQUF5QnQwQixHQUM5QkEsRUFBTzJkLE9BQVMsV0FDaEIzZCxFQUFPMGQsYUFBVWhWLEVBQ2pCMUksRUFBT2dlLGtCQUFldFYsRUFDdEIxSSxFQUFPd2hCLFlBQWEsRUFFeEIsU0FBU3JCLEdBQWlCOVksR0FDdEIsUUFBS21ULEVBQWFuVCxNQUdiNVEsT0FBT1ksVUFBVWtxQixlQUFldHBCLEtBQUtvUCxFQUFHLDZCQUtqRCxTQUFTMlosR0FBdUJoaEIsR0FDNUIsWUFBdUIwSSxJQUFuQjFJLEVBQU8wZCxRQU1mLFNBQVNRLEdBQXFCbGUsRUFBUWtiLEdBRWxDLE9BREFsYixFQUFPd2hCLFlBQWEsRUFDRSxXQUFsQnhoQixFQUFPMmQsT0FDQTNDLE9BQW9CdFMsR0FFVCxZQUFsQjFJLEVBQU8yZCxPQUNBMUMsRUFBb0JqYixFQUFPZ2UsZUFFdENrSCxHQUFvQmxsQixHQUViMGIsRUFEcUIxYixFQUFPeWhCLDBCQUEwQjdDLEdBQWExRCxHQUN6QlgsSUFFckQsU0FBUzJLLEdBQW9CbGxCLEdBQ3pCQSxFQUFPMmQsT0FBUyxTQUNoQixNQUFNN00sRUFBUzlRLEVBQU8wZCxhQUNQaFYsSUFBWG9JLElBR0FpUSxHQUE4QmpRLEtBQzlCQSxFQUFPMFAsY0FBY3hmLFNBQVF1ZixJQUN6QkEsRUFBWUksaUJBRWhCN1AsRUFBTzBQLGNBQWdCLElBQUlqRSxHQUUvQnNCLEVBQWtDL00sSUFFdEMsU0FBUzBYLEdBQW9CeG9CLEVBQVFzaEIsR0FDakN0aEIsRUFBTzJkLE9BQVMsVUFDaEIzZCxFQUFPZ2UsYUFBZXNELEVBQ3RCLE1BQU14USxFQUFTOVEsRUFBTzBkLGFBQ1BoVixJQUFYb0ksSUFHQWlRLEdBQThCalEsSUFDOUJBLEVBQU8wUCxjQUFjeGYsU0FBUXVmLElBQ3pCQSxFQUFZYyxZQUFZQyxNQUU1QnhRLEVBQU8wUCxjQUFnQixJQUFJakUsSUFHM0J6TCxFQUFPZ1csa0JBQWtCOWxCLFNBQVE2bEIsSUFDN0JBLEVBQWdCeEYsWUFBWUMsTUFFaEN4USxFQUFPZ1csa0JBQW9CLElBQUl2SyxHQUVuQzZCLEVBQWlDdE4sRUFBUXdRLElBRzdDLFNBQVNxVCxHQUE0Qng2QixHQUNqQyxPQUFPLElBQUluRCxVQUFVLDRCQUE0Qm1ELDBDQUdyRCxTQUFTbzhCLEdBQTJCbmhCLEVBQU1rSyxHQUN0Q0QsRUFBaUJqSyxFQUFNa0ssR0FDdkIsTUFBTWxVLEVBQWdCZ0ssYUFBbUMsRUFBU0EsRUFBS2hLLGNBRXZFLE9BREF1VSxFQUFvQnZVLEVBQWUsZ0JBQWlCLHVCQUM3QyxDQUNIQSxjQUFld1UsRUFBMEJ4VSxJQTlHakQzVSxPQUFPYyxpQkFBaUJnVSxHQUFlbFUsVUFBVyxDQUM5Q3FxQixPQUFRLENBQUVscUIsWUFBWSxHQUN0QnVaLFVBQVcsQ0FBRXZaLFlBQVksR0FDekJnL0IsWUFBYSxDQUFFaC9CLFlBQVksR0FDM0JpL0IsT0FBUSxDQUFFai9CLFlBQVksR0FDdEJrL0IsSUFBSyxDQUFFbC9CLFlBQVksR0FDbkJtVCxPQUFRLENBQUVuVCxZQUFZLEdBQ3RCaTJCLE9BQVEsQ0FBRWoyQixZQUFZLEtBRWdCLGlCQUEvQjZpQixFQUFlM2lCLGFBQ3RCakIsT0FBT0MsZUFBZTZVLEdBQWVsVSxVQUFXZ2pCLEVBQWUzaUIsWUFBYSxDQUN4RWYsTUFBTyxpQkFDUGdCLGNBQWMsSUFHc0IsaUJBQWpDMGlCLEVBQWVzYyxlQUN0QmxnQyxPQUFPQyxlQUFlNlUsR0FBZWxVLFVBQVdnakIsRUFBZXNjLGNBQWUsQ0FDMUVoZ0MsTUFBTzRVLEdBQWVsVSxVQUFVc1QsT0FDaEN2RSxVQUFVLEVBQ1Z6TyxjQUFjLElBK0Z0QixNQUFNaS9CLEdBQXlCLFNBQWN0MkIsR0FDekMsT0FBT0EsRUFBTXNKLFlBT2pCLE1BQU1pdEIsR0FDRixZQUFZaitCLEdBQ1I2bUIsRUFBdUI3bUIsRUFBUyxFQUFHLDZCQUNuQ0EsRUFBVTI5QixHQUEyQjM5QixFQUFTLG1CQUM5Q3BDLEtBQUtzZ0Msd0NBQTBDbCtCLEVBQVF3UyxjQUszRCxvQkFDSSxJQUFLMnJCLEdBQTRCdmdDLE1BQzdCLE1BQU13Z0MsR0FBOEIsaUJBRXhDLE9BQU94Z0MsS0FBS3NnQyx3Q0FLaEIsV0FDSSxJQUFLQyxHQUE0QnZnQyxNQUM3QixNQUFNd2dDLEdBQThCLFFBRXhDLE9BQU9KLElBY2YsU0FBU0ksR0FBOEI3OEIsR0FDbkMsT0FBTyxJQUFJbkQsVUFBVSx1Q0FBdUNtRCxxREFFaEUsU0FBUzQ4QixHQUE0QjF2QixHQUNqQyxRQUFLbVQsRUFBYW5ULE1BR2I1USxPQUFPWSxVQUFVa3FCLGVBQWV0cEIsS0FBS29QLEVBQUcsMkNBbEJqRDVRLE9BQU9jLGlCQUFpQnMvQixHQUEwQngvQixVQUFXLENBQ3pEK1QsY0FBZSxDQUFFNVQsWUFBWSxHQUM3QjhSLEtBQU0sQ0FBRTlSLFlBQVksS0FFa0IsaUJBQS9CNmlCLEVBQWUzaUIsYUFDdEJqQixPQUFPQyxlQUFlbWdDLEdBQTBCeC9CLFVBQVdnakIsRUFBZTNpQixZQUFhLENBQ25GZixNQUFPLDRCQUNQZ0IsY0FBYyxJQWlCdEIsTUFBTXMvQixHQUFvQixXQUN0QixPQUFPLEdBT1gsTUFBTUMsR0FDRixZQUFZdCtCLEdBQ1I2bUIsRUFBdUI3bUIsRUFBUyxFQUFHLHdCQUNuQ0EsRUFBVTI5QixHQUEyQjM5QixFQUFTLG1CQUM5Q3BDLEtBQUsyZ0MsbUNBQXFDditCLEVBQVF3UyxjQUt0RCxvQkFDSSxJQUFLZ3NCLEdBQXVCNWdDLE1BQ3hCLE1BQU02Z0MsR0FBeUIsaUJBRW5DLE9BQU83Z0MsS0FBSzJnQyxtQ0FNaEIsV0FDSSxJQUFLQyxHQUF1QjVnQyxNQUN4QixNQUFNNmdDLEdBQXlCLFFBRW5DLE9BQU9KLElBY2YsU0FBU0ksR0FBeUJsOUIsR0FDOUIsT0FBTyxJQUFJbkQsVUFBVSxrQ0FBa0NtRCxnREFFM0QsU0FBU2k5QixHQUF1Qi92QixHQUM1QixRQUFLbVQsRUFBYW5ULE1BR2I1USxPQUFPWSxVQUFVa3FCLGVBQWV0cEIsS0FBS29QLEVBQUcsc0NBMkJqRCxTQUFTaXdCLEdBQWdDaG9CLEVBQUlzYSxFQUFVdEssR0FFbkQsT0FEQUMsRUFBZWpRLEVBQUlnUSxHQUNYaG5CLEdBQWVna0IsRUFBWWhOLEVBQUlzYSxFQUFVLENBQUN0eEIsSUFFdEQsU0FBU2kvQixHQUFnQ2pvQixFQUFJc2EsRUFBVXRLLEdBRW5ELE9BREFDLEVBQWVqUSxFQUFJZ1EsR0FDWGhuQixHQUFlMmpCLEVBQVkzTSxFQUFJc2EsRUFBVSxDQUFDdHhCLElBRXRELFNBQVNrL0IsR0FBb0Nsb0IsRUFBSXNhLEVBQVV0SyxHQUV2RCxPQURBQyxFQUFlalEsRUFBSWdRLEdBQ1osQ0FBQ2hmLEVBQU9oSSxJQUFlZ2tCLEVBQVloTixFQUFJc2EsRUFBVSxDQUFDdHBCLEVBQU9oSSxJQXZEcEU3QixPQUFPYyxpQkFBaUIyL0IsR0FBcUI3L0IsVUFBVyxDQUNwRCtULGNBQWUsQ0FBRTVULFlBQVksR0FDN0I4UixLQUFNLENBQUU5UixZQUFZLEtBRWtCLGlCQUEvQjZpQixFQUFlM2lCLGFBQ3RCakIsT0FBT0MsZUFBZXdnQyxHQUFxQjcvQixVQUFXZ2pCLEVBQWUzaUIsWUFBYSxDQUM5RWYsTUFBTyx1QkFDUGdCLGNBQWMsSUE0RHRCLE1BQU04L0IsR0FDRixZQUFZQyxFQUFpQixHQUFJQyxFQUFzQixHQUFJQyxFQUFzQixTQUN0RGx2QixJQUFuQmd2QixJQUNBQSxFQUFpQixNQUVyQixNQUFNRyxFQUFtQnBPLEdBQXVCa08sRUFBcUIsb0JBQy9ERyxFQUFtQnJPLEdBQXVCbU8sRUFBcUIsbUJBQy9ERyxFQWxEZCxTQUE0Qm5PLEVBQVV0SyxHQUNsQ0QsRUFBaUJ1SyxFQUFVdEssR0FDM0IsTUFBTTdGLEVBQVFtUSxhQUEyQyxFQUFTQSxFQUFTblEsTUFDckV1ZSxFQUFlcE8sYUFBMkMsRUFBU0EsRUFBU29PLGFBQzVFNTJCLEVBQVF3b0IsYUFBMkMsRUFBU0EsRUFBU3hvQixNQUNyRTJ6QixFQUFZbkwsYUFBMkMsRUFBU0EsRUFBU21MLFVBQ3pFa0QsRUFBZXJPLGFBQTJDLEVBQVNBLEVBQVNxTyxhQUNsRixNQUFPLENBQ0h4ZSxXQUFpQi9RLElBQVYrUSxPQUNIL1EsRUFDQTR1QixHQUFnQzdkLEVBQU9tUSxFQUFVLEdBQUd0Syw2QkFDeEQwWSxlQUNBNTJCLFdBQWlCc0gsSUFBVnRILE9BQ0hzSCxFQUNBNnVCLEdBQWdDbjJCLEVBQU93b0IsRUFBVSxHQUFHdEssNkJBQ3hEeVYsZUFBeUJyc0IsSUFBZHFzQixPQUNQcnNCLEVBQ0E4dUIsR0FBb0N6QyxFQUFXbkwsRUFBVSxHQUFHdEssaUNBQ2hFMlksZ0JBZ0NvQkMsQ0FBbUJSLEVBQWdCLG1CQUN2RCxRQUFpQ2h2QixJQUE3QnF2QixFQUFZQyxhQUNaLE1BQU0sSUFBSXpvQixXQUFXLGtDQUV6QixRQUFpQzdHLElBQTdCcXZCLEVBQVlFLGFBQ1osTUFBTSxJQUFJMW9CLFdBQVcsa0NBRXpCLE1BQU00b0IsRUFBd0I5TyxHQUFxQnlPLEVBQWtCLEdBQy9ETSxFQUF3QjVPLEdBQXFCc08sR0FDN0NPLEVBQXdCaFAsR0FBcUJ3TyxFQUFrQixHQUMvRFMsRUFBd0I5TyxHQUFxQnFPLEdBQ25ELElBQUlVLEdBMENaLFNBQW1DdjRCLEVBQVF3NEIsRUFBY0gsRUFBdUJDLEVBQXVCSCxFQUF1QkMsR0FDMUgsU0FBUzFOLElBQ0wsT0FBTzhOLEVBV1h4NEIsRUFBT3k0QixVQTUzRFgsU0FBOEIvTixFQUFnQkMsRUFBZ0JDLEVBQWdCQyxFQUFnQnpmLEVBQWdCLEVBQUdvZixFQUFnQixLQUFNLElBQ25JLE1BQU14cUIsRUFBU3ZKLE9BQU91QixPQUFPa3lCLEdBQWU3eUIsV0FJNUMsT0FIQWt6QixHQUF5QnZxQixHQUV6QjhxQixHQUFxQzlxQixFQURsQnZKLE9BQU91QixPQUFPeXlCLEdBQWdDcHpCLFdBQ1JxekIsRUFBZ0JDLEVBQWdCQyxFQUFnQkMsRUFBZ0J6ZixFQUFlb2YsR0FDakl4cUIsRUF1M0RZMDRCLENBQXFCaE8sR0FUeEMsU0FBd0JwcUIsR0FDcEIsT0FvTVIsU0FBa0ROLEVBQVFNLEdBQ3RELE1BQU1oSSxFQUFhMEgsRUFBTzI0QiwyQkFDMUIsT0FBSTM0QixFQUFPOHJCLGNBRUFwUSxFQUQyQjFiLEVBQU80NEIsNEJBQ2MsS0FDbkQsTUFBTXh5QixFQUFXcEcsRUFBT3k0QixVQUV4QixHQUFjLGFBREFyeUIsRUFBU3VYLE9BRW5CLE1BQU12WCxFQUFTNFgsYUFFbkIsT0FBTzZhLEdBQWlEdmdDLEVBQVlnSSxNQUdyRXU0QixHQUFpRHZnQyxFQUFZZ0ksR0FqTnpEdzRCLENBQXlDOTRCLEVBQVFNLE1BSzVELFdBQ0ksT0FtTlIsU0FBa0ROLEdBRTlDLE1BQU1nMUIsRUFBV2gxQixFQUFPKzRCLFVBQ2xCemdDLEVBQWEwSCxFQUFPMjRCLDJCQUNwQkssRUFBZTFnQyxFQUFXMmdDLGtCQUdoQyxPQUZBQyxHQUFnRDVnQyxHQUV6Q29qQixFQUFxQnNkLEdBQWMsS0FDdEMsR0FBd0IsWUFBcEJoRSxFQUFTclgsT0FDVCxNQUFNcVgsRUFBU2hYLGFBRW5CK1UsR0FBcUNpQyxFQUFTdlQsOEJBQy9DdU8sSUFFQyxNQURBbUosR0FBcUJuNUIsRUFBUWd3QixHQUN2QmdGLEVBQVNoWCxnQkFqT1JvYixDQUF5Q3A1QixNQUpwRCxTQUF3QmtiLEdBQ3BCLE9BZ05SLFNBQWtEbGIsRUFBUWtiLEdBSXRELE9BREFpZSxHQUFxQm41QixFQUFRa2IsR0FDdEJGLE9BQW9CdFMsR0FwTmhCMndCLENBQXlDcjVCLEVBQVFrYixLQUs0Q21kLEVBQXVCQyxHQVEvSHQ0QixFQUFPKzRCLFVBQVkvQyxHQUFxQnRMLEdBUHhDLFdBQ0ksT0FpT1IsU0FBbUQxcUIsR0FJL0MsT0FGQXM1QixHQUErQnQ1QixHQUFRLEdBRWhDQSxFQUFPNDRCLDJCQXJPSFcsQ0FBMEN2NUIsTUFFckQsU0FBeUJrYixHQUVyQixPQURBc2UsR0FBNEN4NUIsRUFBUWtiLEdBQzdDRixPQUFvQnRTLEtBRXlEeXZCLEVBQXVCQyxHQUUvR3A0QixFQUFPOHJCLG1CQUFnQnBqQixFQUN2QjFJLEVBQU80NEIsZ0NBQTZCbHdCLEVBQ3BDMUksRUFBT3k1Qix3Q0FBcUMvd0IsRUFDNUM0d0IsR0FBK0J0NUIsR0FBUSxHQUN2Q0EsRUFBTzI0QixnQ0FBNkJqd0IsRUFqRWhDZ3hCLENBQTBCbGpDLEtBSExza0IsR0FBVzNhLElBQzVCbzRCLEVBQXVCcDRCLEtBRW1CazRCLEVBQXVCQyxFQUF1QkgsRUFBdUJDLEdBZ0wzSCxTQUE4RHA0QixFQUFRKzNCLEdBQ2xFLE1BQU16L0IsRUFBYTdCLE9BQU91QixPQUFPMmhDLEdBQWlDdGlDLFdBQ2xFLElBQUl1aUMsRUFBc0J0NUIsSUFDdEIsSUFFSSxPQURBdTVCLEdBQXdDdmhDLEVBQVlnSSxHQUM3QzBhLE9BQW9CdFMsR0FFL0IsTUFBT294QixHQUNILE9BQU83ZSxFQUFvQjZlLEtBRy9CQyxFQUFpQixJQUFNL2UsT0FBb0J0UyxRQUNqQkEsSUFBMUJxdkIsRUFBWWhELFlBQ1o2RSxFQUFxQnQ1QixHQUFTeTNCLEVBQVloRCxVQUFVejBCLEVBQU9oSSxTQUVyQ29RLElBQXRCcXZCLEVBQVl0ZSxRQUNac2dCLEVBQWlCLElBQU1oQyxFQUFZdGUsTUFBTW5oQixJQXRCakQsU0FBK0MwSCxFQUFRMUgsRUFBWXNoQyxFQUFvQkcsR0FDbkZ6aEMsRUFBVzBoQywyQkFBNkJoNkIsRUFDeENBLEVBQU8yNEIsMkJBQTZCcmdDLEVBQ3BDQSxFQUFXMmhDLG9CQUFzQkwsRUFDakN0aEMsRUFBVzJnQyxnQkFBa0JjLEVBb0I3QkcsQ0FBc0NsNkIsRUFBUTFILEVBQVlzaEMsRUFBb0JHLEdBak0xRUksQ0FBcUQzakMsS0FBTXVoQyxRQUNqQ3J2QixJQUF0QnF2QixFQUFZMzJCLE1BQ1ptM0IsRUFBcUJSLEVBQVkzMkIsTUFBTTVLLEtBQUttaUMsNkJBRzVDSixPQUFxQjd2QixHQU03QixlQUNJLElBQUsweEIsR0FBa0I1akMsTUFDbkIsTUFBTTZqQyxHQUE0QixZQUV0QyxPQUFPN2pDLEtBQUt1aUMsVUFLaEIsZUFDSSxJQUFLcUIsR0FBa0I1akMsTUFDbkIsTUFBTTZqQyxHQUE0QixZQUV0QyxPQUFPN2pDLEtBQUtpaUMsV0EwQ3BCLFNBQVMyQixHQUFrQi95QixHQUN2QixRQUFLbVQsRUFBYW5ULE1BR2I1USxPQUFPWSxVQUFVa3FCLGVBQWV0cEIsS0FBS29QLEVBQUcsOEJBTWpELFNBQVM4eEIsR0FBcUJuNUIsRUFBUXNoQixHQUNsQzJSLEdBQXFDanpCLEVBQU8rNEIsVUFBVXRYLDBCQUEyQkgsR0FDakZrWSxHQUE0Q3g1QixFQUFRc2hCLEdBRXhELFNBQVNrWSxHQUE0Q3g1QixFQUFRc2hCLEdBQ3pENFgsR0FBZ0RsNUIsRUFBTzI0Qiw0QkFDdkR2SixHQUE2Q3B2QixFQUFPeTRCLFVBQVVqTiwwQkFBMkJsSyxHQUNyRnRoQixFQUFPOHJCLGVBSVB3TixHQUErQnQ1QixHQUFRLEdBRy9DLFNBQVNzNUIsR0FBK0J0NUIsRUFBUXF0QixRQUVGM2tCLElBQXRDMUksRUFBTzQ0Qiw0QkFDUDU0QixFQUFPeTVCLHFDQUVYejVCLEVBQU80NEIsMkJBQTZCOWQsR0FBVzNhLElBQzNDSCxFQUFPeTVCLG1DQUFxQ3Q1QixLQUVoREgsRUFBTzhyQixjQUFnQnVCLEVBdkUzQjUyQixPQUFPYyxpQkFBaUJrZ0MsR0FBZ0JwZ0MsVUFBVyxDQUMvQzI5QixTQUFVLENBQUV4OUIsWUFBWSxHQUN4QjRPLFNBQVUsQ0FBRTVPLFlBQVksS0FFYyxpQkFBL0I2aUIsRUFBZTNpQixhQUN0QmpCLE9BQU9DLGVBQWUrZ0MsR0FBZ0JwZ0MsVUFBV2dqQixFQUFlM2lCLFlBQWEsQ0FDekVmLE1BQU8sa0JBQ1BnQixjQUFjLElBd0V0QixNQUFNZ2lDLEdBQ0YsY0FDSSxNQUFNLElBQUkzaUMsVUFBVSx1QkFLeEIsa0JBQ0ksSUFBS3NqQyxHQUFtQzlqQyxNQUNwQyxNQUFNK2pDLEdBQXVDLGVBR2pELE9BQU8xSCxHQURvQnI4QixLQUFLd2pDLDJCQUEyQmpCLFVBQVV0WCwyQkFHekUsUUFBUW5oQixHQUNKLElBQUtnNkIsR0FBbUM5akMsTUFDcEMsTUFBTStqQyxHQUF1QyxXQUVqRFYsR0FBd0NyakMsS0FBTThKLEdBTWxELE1BQU00YSxHQUNGLElBQUtvZixHQUFtQzlqQyxNQUNwQyxNQUFNK2pDLEdBQXVDLFNBd0Z6RCxJQUEyRGpaLElBdEZQcEcsRUF1RmhEaWUsR0F2RjBDM2lDLEtBdUZWd2pDLDJCQUE0QjFZLEdBakY1RCxZQUNJLElBQUtnWixHQUFtQzlqQyxNQUNwQyxNQUFNK2pDLEdBQXVDLGNBd0Z6RCxTQUFtRGppQyxHQUMvQyxNQUFNMEgsRUFBUzFILEVBQVcwaEMsMkJBRTFCakgsR0FEMkIveUIsRUFBTys0QixVQUFVdFgsMkJBRzVDK1gsR0FBNEN4NUIsRUFEOUIsSUFBSWhKLFVBQVUsK0JBMUZ4QndqQyxDQUEwQ2hrQyxPQWdCbEQsU0FBUzhqQyxHQUFtQ2p6QixHQUN4QyxRQUFLbVQsRUFBYW5ULE1BR2I1USxPQUFPWSxVQUFVa3FCLGVBQWV0cEIsS0FBS29QLEVBQUcsOEJBK0JqRCxTQUFTNnhCLEdBQWdENWdDLEdBQ3JEQSxFQUFXMmhDLHlCQUFzQnZ4QixFQUNqQ3BRLEVBQVcyZ0MscUJBQWtCdndCLEVBRWpDLFNBQVNteEIsR0FBd0N2aEMsRUFBWWdJLEdBQ3pELE1BQU1OLEVBQVMxSCxFQUFXMGhDLDJCQUNwQlMsRUFBcUJ6NkIsRUFBTys0QixVQUFVdFgsMEJBQzVDLElBQUtxUixHQUFpRDJILEdBQ2xELE1BQU0sSUFBSXpqQyxVQUFVLHdEQUl4QixJQUNJZzhCLEdBQXVDeUgsRUFBb0JuNkIsR0FFL0QsTUFBT2doQixHQUdILE1BREFrWSxHQUE0Q3g1QixFQUFRc2hCLEdBQzlDdGhCLEVBQU8rNEIsVUFBVS9hLGNBbjNCL0IsU0FBd0QxbEIsR0FDcEQsT0FBSSs2QixHQUE4Qy82QixJQW8zQjdCb2lDLENBQStDRCxLQUMvQ3o2QixFQUFPOHJCLGVBQ3hCd04sR0FBK0J0NUIsR0FBUSxHQU0vQyxTQUFTNjRCLEdBQWlEdmdDLEVBQVlnSSxHQUVsRSxPQUFPb2IsRUFEa0JwakIsRUFBVzJoQyxvQkFBb0IzNUIsUUFDVm9JLEdBQVdzbkIsSUFFckQsTUFEQW1KLEdBQXFCN2dDLEVBQVcwaEMsMkJBQTRCaEssR0FDdERBLEtBeURkLFNBQVN1SyxHQUF1Q3BnQyxHQUM1QyxPQUFPLElBQUluRCxVQUFVLDhDQUE4Q21ELDREQUd2RSxTQUFTa2dDLEdBQTRCbGdDLEdBQ2pDLE9BQU8sSUFBSW5ELFVBQVUsNkJBQTZCbUQsMkNBOUl0RDFELE9BQU9jLGlCQUFpQm9pQyxHQUFpQ3RpQyxVQUFXLENBQ2hFOFosUUFBUyxDQUFFM1osWUFBWSxHQUN2QnVGLE1BQU8sQ0FBRXZGLFlBQVksR0FDckJtakMsVUFBVyxDQUFFbmpDLFlBQVksR0FDekJveEIsWUFBYSxDQUFFcHhCLFlBQVksS0FFVyxpQkFBL0I2aUIsRUFBZTNpQixhQUN0QmpCLE9BQU9DLGVBQWVpakMsR0FBaUN0aUMsVUFBV2dqQixFQUFlM2lCLFlBQWEsQ0FDMUZmLE1BQU8sbUNBQ1BnQixjQUFjLEsscUJDdmxIdEJ0QixFQUFPRCxRQUFVd2tDLFFBQVEsVyxxQkNBekJ2a0MsRUFBT0QsUUFBVXdrQyxRQUFRLFMscUJDQXpCdmtDLEVBQU9ELFFBQVV3a0MsUUFBUSxVLHFCQ0F6QnZrQyxFQUFPRCxRQUFVd2tDLFFBQVEsVyxxQkNBekJ2a0MsRUFBT0QsUUFBVXdrQyxRQUFRLFEscUJDQXpCdmtDLEVBQU9ELFFBQVV3a0MsUUFBUSxTLHFCQ0F6QnZrQyxFQUFPRCxRQUFVd2tDLFFBQVEsVUNDckJDLEVBQTJCLEdBRy9CLFNBQVNDLEVBQW9CQyxHQUU1QixHQUFHRixFQUF5QkUsR0FDM0IsT0FBT0YsRUFBeUJFLEdBQVUza0MsUUFHM0MsSUFBSUMsRUFBU3drQyxFQUF5QkUsR0FBWSxDQUdqRDNrQyxRQUFTLElBT1YsT0FIQTRrQyxFQUFvQkQsR0FBVTlpQyxLQUFLNUIsRUFBT0QsUUFBU0MsRUFBUUEsRUFBT0QsUUFBUzBrQyxHQUdwRXprQyxFQUFPRCxRQ2pCZixPQ0ZBMGtDLEVBQW9CbDRCLEVBQUksQ0FBQ3hNLEVBQVM2a0MsS0FDakMsSUFBSSxJQUFJaGlDLEtBQU9naUMsRUFDWEgsRUFBb0JJLEVBQUVELEVBQVloaUMsS0FBUzZoQyxFQUFvQkksRUFBRTlrQyxFQUFTNkMsSUFDNUV4QyxPQUFPQyxlQUFlTixFQUFTNkMsRUFBSyxDQUFFekIsWUFBWSxFQUFNTCxJQUFLOGpDLEVBQVdoaUMsTUNKM0U2aEMsRUFBb0JJLEVBQUksQ0FBQ2g2QixFQUFLaTZCLElBQVMxa0MsT0FBT1ksVUFBVWtxQixlQUFldHBCLEtBQUtpSixFQUFLaTZCLEdDQ2pGTCxFQUFvQjlLLEVBQUs1NUIsSUFDSCxvQkFBWHFCLFFBQTBCQSxPQUFPQyxhQUMxQ2pCLE9BQU9DLGVBQWVOLEVBQVNxQixPQUFPQyxZQUFhLENBQUVmLE1BQU8sV0FFN0RGLE9BQU9DLGVBQWVOLEVBQVMsYUFBYyxDQUFFTyxPQUFPLEtIRmhEbWtDLEVBQW9CLE0iLCJmaWxlIjoibWFpbGd1bi5qcyIsInNvdXJjZXNDb250ZW50IjpbIihmdW5jdGlvbiB3ZWJwYWNrVW5pdmVyc2FsTW9kdWxlRGVmaW5pdGlvbihyb290LCBmYWN0b3J5KSB7XG5cdGlmKHR5cGVvZiBleHBvcnRzID09PSAnb2JqZWN0JyAmJiB0eXBlb2YgbW9kdWxlID09PSAnb2JqZWN0Jylcblx0XHRtb2R1bGUuZXhwb3J0cyA9IGZhY3RvcnkoKTtcblx0ZWxzZSBpZih0eXBlb2YgZGVmaW5lID09PSAnZnVuY3Rpb24nICYmIGRlZmluZS5hbWQpXG5cdFx0ZGVmaW5lKFtdLCBmYWN0b3J5KTtcblx0ZWxzZSBpZih0eXBlb2YgZXhwb3J0cyA9PT0gJ29iamVjdCcpXG5cdFx0ZXhwb3J0c1tcIm1haWxndW5cIl0gPSBmYWN0b3J5KCk7XG5cdGVsc2Vcblx0XHRyb290W1wibWFpbGd1blwiXSA9IGZhY3RvcnkoKTtcbn0pKHRoaXMsIGZ1bmN0aW9uKCkge1xucmV0dXJuICIsIi8qKlxuICogQGF1dGhvciBUb3J1IE5hZ2FzaGltYSA8aHR0cHM6Ly9naXRodWIuY29tL215c3RpY2F0ZWE+XG4gKiBTZWUgTElDRU5TRSBmaWxlIGluIHJvb3QgZGlyZWN0b3J5IGZvciBmdWxsIGxpY2Vuc2UuXG4gKi9cbid1c2Ugc3RyaWN0JztcblxuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywgeyB2YWx1ZTogdHJ1ZSB9KTtcblxudmFyIGV2ZW50VGFyZ2V0U2hpbSA9IHJlcXVpcmUoJ2V2ZW50LXRhcmdldC1zaGltJyk7XG5cbi8qKlxuICogVGhlIHNpZ25hbCBjbGFzcy5cbiAqIEBzZWUgaHR0cHM6Ly9kb20uc3BlYy53aGF0d2cub3JnLyNhYm9ydHNpZ25hbFxuICovXG5jbGFzcyBBYm9ydFNpZ25hbCBleHRlbmRzIGV2ZW50VGFyZ2V0U2hpbS5FdmVudFRhcmdldCB7XG4gICAgLyoqXG4gICAgICogQWJvcnRTaWduYWwgY2Fubm90IGJlIGNvbnN0cnVjdGVkIGRpcmVjdGx5LlxuICAgICAqL1xuICAgIGNvbnN0cnVjdG9yKCkge1xuICAgICAgICBzdXBlcigpO1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKFwiQWJvcnRTaWduYWwgY2Fubm90IGJlIGNvbnN0cnVjdGVkIGRpcmVjdGx5XCIpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIGB0cnVlYCBpZiB0aGlzIGBBYm9ydFNpZ25hbGAncyBgQWJvcnRDb250cm9sbGVyYCBoYXMgc2lnbmFsZWQgdG8gYWJvcnQsIGFuZCBgZmFsc2VgIG90aGVyd2lzZS5cbiAgICAgKi9cbiAgICBnZXQgYWJvcnRlZCgpIHtcbiAgICAgICAgY29uc3QgYWJvcnRlZCA9IGFib3J0ZWRGbGFncy5nZXQodGhpcyk7XG4gICAgICAgIGlmICh0eXBlb2YgYWJvcnRlZCAhPT0gXCJib29sZWFuXCIpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoYEV4cGVjdGVkICd0aGlzJyB0byBiZSBhbiAnQWJvcnRTaWduYWwnIG9iamVjdCwgYnV0IGdvdCAke3RoaXMgPT09IG51bGwgPyBcIm51bGxcIiA6IHR5cGVvZiB0aGlzfWApO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBhYm9ydGVkO1xuICAgIH1cbn1cbmV2ZW50VGFyZ2V0U2hpbS5kZWZpbmVFdmVudEF0dHJpYnV0ZShBYm9ydFNpZ25hbC5wcm90b3R5cGUsIFwiYWJvcnRcIik7XG4vKipcbiAqIENyZWF0ZSBhbiBBYm9ydFNpZ25hbCBvYmplY3QuXG4gKi9cbmZ1bmN0aW9uIGNyZWF0ZUFib3J0U2lnbmFsKCkge1xuICAgIGNvbnN0IHNpZ25hbCA9IE9iamVjdC5jcmVhdGUoQWJvcnRTaWduYWwucHJvdG90eXBlKTtcbiAgICBldmVudFRhcmdldFNoaW0uRXZlbnRUYXJnZXQuY2FsbChzaWduYWwpO1xuICAgIGFib3J0ZWRGbGFncy5zZXQoc2lnbmFsLCBmYWxzZSk7XG4gICAgcmV0dXJuIHNpZ25hbDtcbn1cbi8qKlxuICogQWJvcnQgYSBnaXZlbiBzaWduYWwuXG4gKi9cbmZ1bmN0aW9uIGFib3J0U2lnbmFsKHNpZ25hbCkge1xuICAgIGlmIChhYm9ydGVkRmxhZ3MuZ2V0KHNpZ25hbCkgIT09IGZhbHNlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgYWJvcnRlZEZsYWdzLnNldChzaWduYWwsIHRydWUpO1xuICAgIHNpZ25hbC5kaXNwYXRjaEV2ZW50KHsgdHlwZTogXCJhYm9ydFwiIH0pO1xufVxuLyoqXG4gKiBBYm9ydGVkIGZsYWcgZm9yIGVhY2ggaW5zdGFuY2VzLlxuICovXG5jb25zdCBhYm9ydGVkRmxhZ3MgPSBuZXcgV2Vha01hcCgpO1xuLy8gUHJvcGVydGllcyBzaG91bGQgYmUgZW51bWVyYWJsZS5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKEFib3J0U2lnbmFsLnByb3RvdHlwZSwge1xuICAgIGFib3J0ZWQ6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxufSk7XG4vLyBgdG9TdHJpbmcoKWAgc2hvdWxkIHJldHVybiBgXCJbb2JqZWN0IEFib3J0U2lnbmFsXVwiYFxuaWYgKHR5cGVvZiBTeW1ib2wgPT09IFwiZnVuY3Rpb25cIiAmJiB0eXBlb2YgU3ltYm9sLnRvU3RyaW5nVGFnID09PSBcInN5bWJvbFwiKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KEFib3J0U2lnbmFsLnByb3RvdHlwZSwgU3ltYm9sLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZSxcbiAgICAgICAgdmFsdWU6IFwiQWJvcnRTaWduYWxcIixcbiAgICB9KTtcbn1cblxuLyoqXG4gKiBUaGUgQWJvcnRDb250cm9sbGVyLlxuICogQHNlZSBodHRwczovL2RvbS5zcGVjLndoYXR3Zy5vcmcvI2Fib3J0Y29udHJvbGxlclxuICovXG5jbGFzcyBBYm9ydENvbnRyb2xsZXIge1xuICAgIC8qKlxuICAgICAqIEluaXRpYWxpemUgdGhpcyBjb250cm9sbGVyLlxuICAgICAqL1xuICAgIGNvbnN0cnVjdG9yKCkge1xuICAgICAgICBzaWduYWxzLnNldCh0aGlzLCBjcmVhdGVBYm9ydFNpZ25hbCgpKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB0aGUgYEFib3J0U2lnbmFsYCBvYmplY3QgYXNzb2NpYXRlZCB3aXRoIHRoaXMgb2JqZWN0LlxuICAgICAqL1xuICAgIGdldCBzaWduYWwoKSB7XG4gICAgICAgIHJldHVybiBnZXRTaWduYWwodGhpcyk7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIEFib3J0IGFuZCBzaWduYWwgdG8gYW55IG9ic2VydmVycyB0aGF0IHRoZSBhc3NvY2lhdGVkIGFjdGl2aXR5IGlzIHRvIGJlIGFib3J0ZWQuXG4gICAgICovXG4gICAgYWJvcnQoKSB7XG4gICAgICAgIGFib3J0U2lnbmFsKGdldFNpZ25hbCh0aGlzKSk7XG4gICAgfVxufVxuLyoqXG4gKiBBc3NvY2lhdGVkIHNpZ25hbHMuXG4gKi9cbmNvbnN0IHNpZ25hbHMgPSBuZXcgV2Vha01hcCgpO1xuLyoqXG4gKiBHZXQgdGhlIGFzc29jaWF0ZWQgc2lnbmFsIG9mIGEgZ2l2ZW4gY29udHJvbGxlci5cbiAqL1xuZnVuY3Rpb24gZ2V0U2lnbmFsKGNvbnRyb2xsZXIpIHtcbiAgICBjb25zdCBzaWduYWwgPSBzaWduYWxzLmdldChjb250cm9sbGVyKTtcbiAgICBpZiAoc2lnbmFsID09IG51bGwpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgRXhwZWN0ZWQgJ3RoaXMnIHRvIGJlIGFuICdBYm9ydENvbnRyb2xsZXInIG9iamVjdCwgYnV0IGdvdCAke2NvbnRyb2xsZXIgPT09IG51bGwgPyBcIm51bGxcIiA6IHR5cGVvZiBjb250cm9sbGVyfWApO1xuICAgIH1cbiAgICByZXR1cm4gc2lnbmFsO1xufVxuLy8gUHJvcGVydGllcyBzaG91bGQgYmUgZW51bWVyYWJsZS5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKEFib3J0Q29udHJvbGxlci5wcm90b3R5cGUsIHtcbiAgICBzaWduYWw6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGFib3J0OiB7IGVudW1lcmFibGU6IHRydWUgfSxcbn0pO1xuaWYgKHR5cGVvZiBTeW1ib2wgPT09IFwiZnVuY3Rpb25cIiAmJiB0eXBlb2YgU3ltYm9sLnRvU3RyaW5nVGFnID09PSBcInN5bWJvbFwiKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KEFib3J0Q29udHJvbGxlci5wcm90b3R5cGUsIFN5bWJvbC50b1N0cmluZ1RhZywge1xuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIHZhbHVlOiBcIkFib3J0Q29udHJvbGxlclwiLFxuICAgIH0pO1xufVxuXG5leHBvcnRzLkFib3J0Q29udHJvbGxlciA9IEFib3J0Q29udHJvbGxlcjtcbmV4cG9ydHMuQWJvcnRTaWduYWwgPSBBYm9ydFNpZ25hbDtcbmV4cG9ydHMuZGVmYXVsdCA9IEFib3J0Q29udHJvbGxlcjtcblxubW9kdWxlLmV4cG9ydHMgPSBBYm9ydENvbnRyb2xsZXJcbm1vZHVsZS5leHBvcnRzLkFib3J0Q29udHJvbGxlciA9IG1vZHVsZS5leHBvcnRzW1wiZGVmYXVsdFwiXSA9IEFib3J0Q29udHJvbGxlclxubW9kdWxlLmV4cG9ydHMuQWJvcnRTaWduYWwgPSBBYm9ydFNpZ25hbFxuLy8jIHNvdXJjZU1hcHBpbmdVUkw9YWJvcnQtY29udHJvbGxlci5qcy5tYXBcbiIsImltcG9ydCBDbGllbnQgZnJvbSAnLi9saWIvY2xpZW50J1xuaW1wb3J0IE9wdGlvbnMgZnJvbSAnLi9saWIvaW50ZXJmYWNlcy9PcHRpb25zJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgTWFpbGd1biB7XG4gIHByaXZhdGUgZm9ybURhdGE6IG5ldyAoKSA9PiBGb3JtRGF0YVxuXG4gIGNvbnN0cnVjdG9yKEZvcm1EYXRhOiBuZXcgKCkgPT4gRm9ybURhdGEpIHtcbiAgICB0aGlzLmZvcm1EYXRhID0gRm9ybURhdGE7XG4gIH1cblxuICBjbGllbnQob3B0aW9uczogT3B0aW9ucykge1xuICAgIHJldHVybiBuZXcgQ2xpZW50KG9wdGlvbnMsIHRoaXMuZm9ybURhdGEpXG4gIH1cbn07IiwiaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi9yZXF1ZXN0JztcbmltcG9ydCBPcHRpb25zIGZyb20gJy4vaW50ZXJmYWNlcy9PcHRpb25zJztcbmltcG9ydCBSZXF1ZXN0T3B0aW9ucyBmcm9tICcuL2ludGVyZmFjZXMvUmVxdWVzdE9wdGlvbnMnO1xuXG5pbXBvcnQgRG9tYWluQ2xpZW50IGZyb20gJy4vZG9tYWlucyc7XG5pbXBvcnQgRXZlbnRDbGllbnQgZnJvbSAnLi9ldmVudHMnO1xuaW1wb3J0IFN0YXRzQ2xpZW50IGZyb20gJy4vc3RhdHMnO1xuaW1wb3J0IFN1cHByZXNzaW9uQ2xpZW50IGZyb20gJy4vc3VwcHJlc3Npb25zJztcbmltcG9ydCBXZWJob29rQ2xpZW50IGZyb20gJy4vd2ViaG9va3MnO1xuaW1wb3J0IE1lc3NhZ2VzQ2xpZW50IGZyb20gJy4vbWVzc2FnZXMnO1xuaW1wb3J0IFJvdXRlc0NsaWVudCBmcm9tICcuL3JvdXRlcyc7XG5pbXBvcnQgVmFsaWRhdGVDbGllbnQgZnJvbSAnLi92YWxpZGF0ZSc7XG5pbXBvcnQgUGFyc2VDbGllbnQgZnJvbSAnLi9wYXJzZSc7XG5pbXBvcnQgSXBzQ2xpZW50IGZyb20gJy4vaXBzJztcbmltcG9ydCBJcFBvb2xzQ2xpZW50IGZyb20gJy4vaXAtcG9vbHMnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBDbGllbnQge1xuICBwcml2YXRlIHJlcXVlc3Q7XG5cbiAgcHVibGljIGRvbWFpbnM7XG4gIHB1YmxpYyB3ZWJob29rcztcbiAgcHVibGljIGV2ZW50cztcbiAgcHVibGljIHN0YXRzO1xuICBwdWJsaWMgc3VwcHJlc3Npb25zO1xuICBwdWJsaWMgbWVzc2FnZXM7XG4gIHB1YmxpYyByb3V0ZXM7XG4gIHB1YmxpYyBwdWJsaWNfcmVxdWVzdDtcbiAgcHVibGljIHZhbGlkYXRlO1xuICBwdWJsaWMgcGFyc2U7XG4gIHB1YmxpYyBpcHM7XG4gIHB1YmxpYyBpcF9wb29scztcblxuICBjb25zdHJ1Y3RvcihvcHRpb25zOiBPcHRpb25zLCBmb3JtRGF0YTogbmV3ICgpID0+IEZvcm1EYXRhKSB7XG4gICAgY29uc3QgY29uZmlnOiBSZXF1ZXN0T3B0aW9ucyA9IHsgLi4ub3B0aW9ucyB9IGFzIFJlcXVlc3RPcHRpb25zO1xuXG4gICAgaWYgKCFjb25maWcudXJsKSB7XG4gICAgICBjb25maWcudXJsID0gJ2h0dHBzOi8vYXBpLm1haWxndW4ubmV0J1xuICAgIH1cblxuICAgIGlmICghY29uZmlnLnVzZXJuYW1lKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoJ1BhcmFtZXRlciBcInVzZXJuYW1lXCIgaXMgcmVxdWlyZWQnKTtcbiAgICB9XG5cbiAgICBpZiAoIWNvbmZpZy5rZXkpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignUGFyYW1ldGVyIFwia2V5XCIgaXMgcmVxdWlyZWQnKTtcbiAgICB9XG5cbiAgICAvKiogQGludGVybmFsICovXG4gICAgdGhpcy5yZXF1ZXN0ID0gbmV3IFJlcXVlc3QoY29uZmlnLCBmb3JtRGF0YSk7XG5cbiAgICB0aGlzLmRvbWFpbnMgPSBuZXcgRG9tYWluQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy53ZWJob29rcyA9IG5ldyBXZWJob29rQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5ldmVudHMgPSBuZXcgRXZlbnRDbGllbnQodGhpcy5yZXF1ZXN0KTtcbiAgICB0aGlzLnN0YXRzID0gbmV3IFN0YXRzQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5zdXBwcmVzc2lvbnMgPSBuZXcgU3VwcHJlc3Npb25DbGllbnQodGhpcy5yZXF1ZXN0KTtcbiAgICB0aGlzLm1lc3NhZ2VzID0gbmV3IE1lc3NhZ2VzQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5yb3V0ZXMgPSBuZXcgUm91dGVzQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5pcHMgPSBuZXcgSXBzQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5pcF9wb29scyA9IG5ldyBJcFBvb2xzQ2xpZW50KHRoaXMucmVxdWVzdCk7XG5cbiAgICBpZiAoY29uZmlnLnB1YmxpY19rZXkpIHtcbiAgICAgIGNvbmZpZy5rZXkgPSBjb25maWcucHVibGljX2tleTtcblxuICAgICAgdGhpcy5wdWJsaWNfcmVxdWVzdCA9IG5ldyBSZXF1ZXN0KGNvbmZpZywgZm9ybURhdGEpO1xuICAgICAgdGhpcy52YWxpZGF0ZSA9IG5ldyBWYWxpZGF0ZUNsaWVudCh0aGlzLnB1YmxpY19yZXF1ZXN0KTtcbiAgICAgIHRoaXMucGFyc2UgPSBuZXcgUGFyc2VDbGllbnQodGhpcy5wdWJsaWNfcmVxdWVzdCk7XG4gICAgfVxuICB9XG59XG4iLCJpbXBvcnQgdXJsam9pbiBmcm9tICd1cmwtam9pbic7XG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuL3JlcXVlc3QnO1xuXG5pbnRlcmZhY2UgRG9tYWluRGF0YSB7XG4gIG5hbWU6IHN0cmluZztcbiAgcmVxdWlyZV90bHM6IGFueTtcbiAgc2tpcF92ZXJpZmljYXRpb246IGFueTtcbiAgc3RhdGU6IGFueTtcbiAgd2lsZGNhcmQ6IGFueTtcbiAgc3BhbV9hY3Rpb246IGFueTtcbiAgY3JlYXRlZF9hdDogc3RyaW5nIHwgRGF0ZTtcbiAgc210cF9wYXNzd29yZDogc3RyaW5nO1xuICBzbXRwX2xvZ2luOiBzdHJpbmc7XG4gIHR5cGU6IHN0cmluZztcbn1cblxuY2xhc3MgRG9tYWluIHtcbiAgbmFtZTogYW55O1xuICByZXF1aXJlX3RsczogYW55O1xuICBza2lwX3ZlcmlmaWNhdGlvbjogYW55O1xuICBzdGF0ZTogYW55O1xuICB3aWxkY2FyZDogYW55O1xuICBzcGFtX2FjdGlvbjogYW55O1xuICBjcmVhdGVkX2F0OiBhbnk7XG4gIHNtdHBfcGFzc3dvcmQ6IGFueTtcbiAgc210cF9sb2dpbjogYW55O1xuICB0eXBlOiBhbnk7XG4gIHJlY2VpdmluZ19kbnNfcmVjb3JkczogYW55O1xuICBzZW5kaW5nX2Ruc19yZWNvcmRzOiBhbnk7XG5cbiAgY29uc3RydWN0b3IoZGF0YTogRG9tYWluRGF0YSwgcmVjZWl2aW5nPzogYW55LCBzZW5kaW5nPzogYW55KSB7XG4gICAgdGhpcy5uYW1lID0gZGF0YS5uYW1lO1xuICAgIHRoaXMucmVxdWlyZV90bHMgPSBkYXRhLnJlcXVpcmVfdGxzO1xuICAgIHRoaXMuc2tpcF92ZXJpZmljYXRpb24gPSBkYXRhLnNraXBfdmVyaWZpY2F0aW9uO1xuICAgIHRoaXMuc3RhdGUgPSBkYXRhLnN0YXRlO1xuICAgIHRoaXMud2lsZGNhcmQgPSBkYXRhLndpbGRjYXJkO1xuICAgIHRoaXMuc3BhbV9hY3Rpb24gPSBkYXRhLnNwYW1fYWN0aW9uO1xuICAgIHRoaXMuY3JlYXRlZF9hdCA9IGRhdGEuY3JlYXRlZF9hdDtcbiAgICB0aGlzLnNtdHBfcGFzc3dvcmQgPSBkYXRhLnNtdHBfcGFzc3dvcmQ7XG4gICAgdGhpcy5zbXRwX2xvZ2luID0gZGF0YS5zbXRwX2xvZ2luO1xuICAgIHRoaXMudHlwZSA9IGRhdGEudHlwZTtcblxuICAgIHRoaXMucmVjZWl2aW5nX2Ruc19yZWNvcmRzID0gcmVjZWl2aW5nIHx8IG51bGw7XG4gICAgdGhpcy5zZW5kaW5nX2Ruc19yZWNvcmRzID0gc2VuZGluZyB8fCBudWxsO1xuICB9XG59XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIERvbWFpbkNsaWVudCB7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBfcGFyc2VNZXNzYWdlKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSB7XG4gICAgcmV0dXJuIHJlc3BvbnNlLmJvZHk7XG4gIH1cblxuICBfcGFyc2VEb21haW5MaXN0KHJlc3BvbnNlOiB7IGJvZHk6IHsgaXRlbXM6IERvbWFpbkRhdGFbXSB9IH0pIHtcbiAgICByZXR1cm4gcmVzcG9uc2UuYm9keS5pdGVtcy5tYXAoZnVuY3Rpb24gKGl0ZW0pIHtcbiAgICAgIHJldHVybiBuZXcgRG9tYWluKGl0ZW0pO1xuICAgIH0pO1xuICB9XG5cbiAgX3BhcnNlRG9tYWluKHJlc3BvbnNlOiB7XG4gICAgYm9keToge1xuICAgICAgZG9tYWluOiBhbnksXG4gICAgICByZWNlaXZpbmdfZG5zX3JlY29yZHM6IGFueSxcbiAgICAgIHNlbmRpbmdfZG5zX3JlY29yZHM6IGFueVxuICAgIH1cbiAgfSkge1xuICAgIHJldHVybiBuZXcgRG9tYWluKFxuICAgICAgcmVzcG9uc2UuYm9keS5kb21haW4sXG4gICAgICByZXNwb25zZS5ib2R5LnJlY2VpdmluZ19kbnNfcmVjb3JkcyxcbiAgICAgIHJlc3BvbnNlLmJvZHkuc2VuZGluZ19kbnNfcmVjb3Jkc1xuICAgICk7XG4gIH1cblxuICBfcGFyc2VUcmFja2luZ1NldHRpbmdzKHJlc3BvbnNlOiB7IGJvZHk6IHsgdHJhY2tpbmc6IGFueSB9IH0pIHtcbiAgICByZXR1cm4gcmVzcG9uc2UuYm9keS50cmFja2luZztcbiAgfVxuXG4gIF9wYXJzZVRyYWNraW5nVXBkYXRlKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSB7XG4gICAgcmV0dXJuIHJlc3BvbnNlLmJvZHk7XG4gIH1cblxuICBsaXN0KHF1ZXJ5OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCgnL3YyL2RvbWFpbnMnLCBxdWVyeSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlRG9tYWluTGlzdCk7XG4gIH1cblxuICBnZXQoZG9tYWluOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldChgL3YyL2RvbWFpbnMvJHtkb21haW59YClcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlRG9tYWluKTtcbiAgfVxuXG4gIGNyZWF0ZShkYXRhOiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnBvc3QoJy92Mi9kb21haW5zJywgZGF0YSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlRG9tYWluKTtcbiAgfVxuXG4gIGRlc3Ryb3koZG9tYWluOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZShgL3YyL2RvbWFpbnMvJHtkb21haW59YClcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlTWVzc2FnZSk7XG4gIH1cblxuICAvLyBUcmFja2luZ1xuXG4gIGdldFRyYWNraW5nKGRvbWFpbjogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsam9pbignL3YyL2RvbWFpbnMnLCBkb21haW4sICd0cmFja2luZycpKVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VUcmFja2luZ1NldHRpbmdzKTtcbiAgfVxuXG4gIHVwZGF0ZVRyYWNraW5nKGRvbWFpbjogc3RyaW5nLCB0eXBlOiBzdHJpbmcsIGRhdGE6IGFueSkge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucHV0KHVybGpvaW4oJy92Mi9kb21haW5zJywgZG9tYWluLCAndHJhY2tpbmcnLCB0eXBlKSwgZGF0YSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlVHJhY2tpbmdVcGRhdGUpO1xuICB9XG5cbiAgLy8gSVBzXG5cbiAgZ2V0SXBzKGRvbWFpbjogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsam9pbignL3YyL2RvbWFpbnMnLCBkb21haW4sICdpcHMnKSlcbiAgICAgIC50aGVuKChyZXNwb25zZTogeyBib2R5OiB7IGl0ZW1zOiBzdHJpbmdbXSB9IH0pID0+IHJlc3BvbnNlPy5ib2R5Py5pdGVtcyk7XG4gIH1cblxuICBhc3NpZ25JcChkb21haW46IHN0cmluZywgaXA6IHN0cmluZykge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucG9zdCh1cmxqb2luKCcvdjIvZG9tYWlucycsIGRvbWFpbiwgJ2lwcycpLCB7IGlwIH0pO1xuICB9XG5cbiAgZGVsZXRlSXAoZG9tYWluOiBzdHJpbmcsIGlwOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZSh1cmxqb2luKCcvdjIvZG9tYWlucycsIGRvbWFpbiwgJ2lwcycsIGlwKSk7XG4gIH1cblxuICBsaW5rSXBQb29sKGRvbWFpbjogc3RyaW5nLCBwb29sX2lkOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnBvc3QodXJsam9pbignL3YyL2RvbWFpbnMnLCBkb21haW4sICdpcHMnKSwgeyBwb29sX2lkIH0pO1xuICB9XG5cbiAgdW5saW5rSXBQb2xsKGRvbWFpbjogc3RyaW5nLCBwb29sX2lkOiBzdHJpbmcsIGlwOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZSh1cmxqb2luKCcvdjIvZG9tYWlucycsIGRvbWFpbiwgJ2lwcycsICdpcF9wb29sJyksIHsgcG9vbF9pZCwgaXAgfSk7XG4gIH1cbn1cbiIsImltcG9ydCBBUElFcnJvck9wdGlvbnMgZnJvbSAnLi9pbnRlcmZhY2VzL0FQSUVycm9yT3B0aW9ucyc7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIEFQSUVycm9yIGV4dGVuZHMgRXJyb3Ige1xuICBzdGF0dXM6IG51bWJlciB8IHN0cmluZztcbiAgc3RhY2s6IHN0cmluZztcbiAgZGV0YWlsczogc3RyaW5nO1xuXG4gIGNvbnN0cnVjdG9yKHtcbiAgICBzdGF0dXMsXG4gICAgc3RhdHVzVGV4dCxcbiAgICBtZXNzYWdlLFxuICAgIGJvZHkgPSB7fVxuICB9OiBBUElFcnJvck9wdGlvbnMpIHtcbiAgICBjb25zdCB7IG1lc3NhZ2U6IGJvZHlNZXNzYWdlLCBlcnJvciB9ID0gYm9keTtcbiAgICBzdXBlcigpO1xuXG4gICAgdGhpcy5zdGFjayA9IG51bGw7XG4gICAgdGhpcy5zdGF0dXMgPSBzdGF0dXM7XG4gICAgdGhpcy5tZXNzYWdlID0gbWVzc2FnZSB8fCBlcnJvciB8fCBzdGF0dXNUZXh0O1xuICAgIHRoaXMuZGV0YWlscyA9IGJvZHlNZXNzYWdlO1xuICB9XG59XG4iLCJjb25zdCB1cmxqb2luID0gcmVxdWlyZSgndXJsLWpvaW4nKTtcblxuY29uc3QgTWdSZXF1ZXN0ID0gcmVxdWlyZSgnLi9yZXF1ZXN0Jyk7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIEV2ZW50Q2xpZW50IHtcbiAgcmVxdWVzdDogdHlwZW9mIE1nUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiB0eXBlb2YgTWdSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIF9wYXJzZVBhZ2VOdW1iZXIodXJsOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdXJsLnNwbGl0KCcvJykucG9wKCk7XG4gIH1cblxuICBfcGFyc2VQYWdlKGlkOiBzdHJpbmcsIHVybDogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHsgaWQsIG51bWJlcjogdGhpcy5fcGFyc2VQYWdlTnVtYmVyKHVybCksIHVybCB9O1xuICB9XG5cbiAgX3BhcnNlUGFnZUxpbmtzKHJlc3BvbnNlOiB7IGJvZHk6IHsgcGFnaW5nOiBhbnkgfSB9KSB7XG4gICAgY29uc3QgcGFnZXMgPSBPYmplY3QuZW50cmllcyhyZXNwb25zZS5ib2R5LnBhZ2luZyk7XG4gICAgcmV0dXJuIHBhZ2VzLnJlZHVjZShcbiAgICAgIChhY2M6IGFueSwgW2lkLCB1cmxdOiBbdXJsOiBzdHJpbmcsIGlkOiBzdHJpbmddKSA9PiB7XG4gICAgICAgIGFjY1tpZF0gPSB0aGlzLl9wYXJzZVBhZ2UoaWQsIHVybClcbiAgICAgICAgcmV0dXJuIGFjY1xuICAgICAgfSwge30pO1xuICB9XG5cbiAgX3BhcnNlRXZlbnRMaXN0KHJlc3BvbnNlOiB7IGJvZHk6IHsgaXRlbXM6IGFueSwgcGFnaW5nOiBhbnkgfSAgfSkge1xuICAgIHJldHVybiB7XG4gICAgICBpdGVtczogcmVzcG9uc2UuYm9keS5pdGVtcyxcbiAgICAgIHBhZ2VzOiB0aGlzLl9wYXJzZVBhZ2VMaW5rcyhyZXNwb25zZSlcbiAgICB9O1xuICB9XG5cbiAgZ2V0KGRvbWFpbjogc3RyaW5nLCBxdWVyeTogeyBwYWdlOiBhbnkgfSkge1xuICAgIGxldCB1cmw7XG5cbiAgICBpZiAocXVlcnkgJiYgcXVlcnkucGFnZSkge1xuICAgICAgdXJsID0gdXJsam9pbignL3YyJywgZG9tYWluLCAnZXZlbnRzJywgcXVlcnkucGFnZSk7XG4gICAgICBkZWxldGUgcXVlcnkucGFnZTtcbiAgICB9IGVsc2Uge1xuICAgICAgdXJsID0gdXJsam9pbignL3YyJywgZG9tYWluLCAnZXZlbnRzJyk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsLCBxdWVyeSlcbiAgICAgIC50aGVuKChyZXNwb25zZTogeyBib2R5OiB7IGl0ZW1zOiBhbnksIHBhZ2luZzogYW55IH0gfSkgPT4gdGhpcy5fcGFyc2VFdmVudExpc3QocmVzcG9uc2UpKTtcbiAgfVxufVxuIiwiY29uc3QgTWdSZXF1ZXN0ID0gcmVxdWlyZSgnLi9yZXF1ZXN0Jyk7XG5cbmltcG9ydCB7IElwUG9vbCB9IGZyb20gXCIuL2ludGVyZmFjZXMvSXBQb29sc1wiO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBJcFBvb2xzQ2xpZW50IHtcbiAgcmVxdWVzdDogdHlwZW9mIE1nUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiB0eXBlb2YgTWdSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIGxpc3QocXVlcnk6IGFueSk6IElwUG9vbFtdIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCgnL3YxL2lwX3Bvb2xzJywgcXVlcnkpXG4gICAgICAudGhlbigocmVzcG9uc2U6IHsgYm9keTogeyBpcF9wb29sczogSXBQb29sLCBtZXNzYWdlOiBzdHJpbmcgfSB9KSA9PiB0aGlzLnBhcnNlSXBQb29sc1Jlc3BvbnNlKHJlc3BvbnNlKSk7XG4gIH1cblxuICBjcmVhdGUoZGF0YTogeyBuYW1lOiBzdHJpbmcsIGRlc2NyaXB0aW9uPzogc3RyaW5nLCBpcHM/OiBzdHJpbmdbXSB9KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0KCcvdjEvaXBfcG9vbHMnLCBkYXRhKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IHsgbWVzc2FnZTogc3RyaW5nLCBwb29sX2lkOiBzdHJpbmcgfSB9KSA9PiByZXNwb25zZT8uYm9keSk7XG4gIH1cblxuICB1cGRhdGUocG9vbElkOiBzdHJpbmcsIGRhdGE6IHsgbmFtZTogc3RyaW5nLCBkZXNjcmlwdGlvbjogc3RyaW5nLCBhZGRfaXA6IHN0cmluZywgcmVtb3ZlX2lwOiBzdHJpbmcgfSkge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucGF0Y2goYC92MS9pcF9wb29scy8ke3Bvb2xJZH1gLCBkYXRhKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSA9PiByZXNwb25zZT8uYm9keSk7XG4gIH1cblxuICBkZWxldGUocG9vbElkOiBzdHJpbmcsIGRhdGE6IHsgaWQ6IHN0cmluZywgcG9vbF9pZDogc3RyaW5nIH0pIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZShgL3YxL2lwX3Bvb2xzLyR7cG9vbElkfWAsIGRhdGEpXG4gICAgICAudGhlbigocmVzcG9uc2U6IHsgYm9keTogYW55IH0pID0+IHJlc3BvbnNlPy5ib2R5KTtcbiAgfVxuXG4gIHByaXZhdGUgcGFyc2VJcFBvb2xzUmVzcG9uc2UocmVzcG9uc2U6IHsgYm9keTogYW55IHwgYW55IH0pIHtcbiAgICByZXR1cm4gcmVzcG9uc2UuYm9keS5pcF9wb29scztcbiAgfVxufVxuIiwiY29uc3QgTWdSZXF1ZXN0ID0gcmVxdWlyZSgnLi9yZXF1ZXN0Jyk7XG5pbXBvcnQgeyBJcERhdGEsIElwc0xpc3RSZXNwb25zZUJvZHkgfSBmcm9tICcuL2ludGVyZmFjZXMvSXBzJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgSXBzQ2xpZW50IHtcbiAgcmVxdWVzdDogdHlwZW9mIE1nUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiB0eXBlb2YgTWdSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIGxpc3QocXVlcnk6IGFueSkge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KCcvdjMvaXBzJywgcXVlcnkpXG4gICAgICAudGhlbigocmVzcG9uc2U6IHsgYm9keTogSXBzTGlzdFJlc3BvbnNlQm9keSB9KSA9PiB0aGlzLnBhcnNlSXBzUmVzcG9uc2UocmVzcG9uc2UpKTtcbiAgfVxuXG4gIGdldChpcDogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoYC92My9pcHMvJHtpcH1gKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IElwRGF0YSB9KSA9PiB0aGlzLnBhcnNlSXBzUmVzcG9uc2UocmVzcG9uc2UpKTtcbiAgfVxuXG4gIHByaXZhdGUgcGFyc2VJcHNSZXNwb25zZShyZXNwb25zZTogeyBib2R5OiBJcHNMaXN0UmVzcG9uc2VCb2R5IHwgSXBEYXRhIH0pIHtcbiAgICByZXR1cm4gcmVzcG9uc2UuYm9keTtcbiAgfVxufVxuIiwiaW1wb3J0IFJlcXVlc3QgZnJvbSBcIi4vcmVxdWVzdFwiO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBNZXNzYWdlc0NsaWVudCB7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBfcGFyc2VSZXNwb25zZShyZXNwb25zZTogeyBib2R5OiBhbnkgfSkge1xuICAgIGlmIChyZXNwb25zZS5ib2R5KSB7XG4gICAgICByZXR1cm4gcmVzcG9uc2UuYm9keTtcbiAgICB9XG5cbiAgICByZXR1cm4gcmVzcG9uc2U7XG4gIH1cblxuICBjcmVhdGUoZG9tYWluOiBzdHJpbmcsIGRhdGE6IGFueSkge1xuICAgIGlmIChkYXRhLm1lc3NhZ2UpIHtcbiAgICAgIHJldHVybiB0aGlzLnJlcXVlc3QucG9zdE11bHRpKGAvdjMvJHtkb21haW59L21lc3NhZ2VzLm1pbWVgLCBkYXRhKVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VSZXNwb25zZSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0TXVsdGkoYC92My8ke2RvbWFpbn0vbWVzc2FnZXNgLCBkYXRhKVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VSZXNwb25zZSk7XG4gIH1cbn1cbiIsImltcG9ydCBSZXF1ZXN0IGZyb20gJy4vcmVxdWVzdCc7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFBhcnNlQ2xpZW50IHtcbiAgcmVxdWVzdDogUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIGdldChhZGRyZXNzZXM6IHN0cmluZ1tdIHwgc3RyaW5nLCBlbmFibGVEbnNFc3BDaGVja3M6IGJvb2xlYW4pIHtcbiAgICBjb25zdCBxdWVyeSA9IHt9IGFzIHsgYWRkcmVzc2VzOiBzdHJpbmcsIHN5bnRheF9vbmx5OiBib29sZWFuIH07XG5cbiAgICBpZiAoQXJyYXkuaXNBcnJheShhZGRyZXNzZXMpKSB7XG4gICAgICBhZGRyZXNzZXMgPSBhZGRyZXNzZXMuam9pbignLCcpO1xuICAgIH1cblxuICAgIHF1ZXJ5LmFkZHJlc3NlcyA9IGFkZHJlc3NlcztcblxuICAgIGlmIChlbmFibGVEbnNFc3BDaGVja3MpIHtcbiAgICAgIHF1ZXJ5LnN5bnRheF9vbmx5ID0gZmFsc2U7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoJy92My9hZGRyZXNzL3BhcnNlJywgcXVlcnkpXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkpO1xuICB9XG59XG4iLCJcbmltcG9ydCBCdG9hIGZyb20gJ2J0b2EnO1xuaW1wb3J0IHVybGpvaW4gZnJvbSAndXJsLWpvaW4nO1xuaW1wb3J0IGt5IGZyb20gJ2t5LXVuaXZlcnNhbCc7XG5cbmltcG9ydCBBUElFcnJvciBmcm9tICcuL2Vycm9yJztcbmltcG9ydCBSZXF1ZXN0T3B0aW9ucyBmcm9tICcuL2ludGVyZmFjZXMvUmVxdWVzdE9wdGlvbnMnO1xuaW1wb3J0IEFQSUVycm9yT3B0aW9ucyBmcm9tICcuL2ludGVyZmFjZXMvQVBJRXJyb3JPcHRpb25zJztcblxuY29uc3QgaXNTdHJlYW0gPSAoYXR0YWNobWVudDogYW55KSA9PiB0eXBlb2YgYXR0YWNobWVudCA9PT0gJ29iamVjdCcgJiYgdHlwZW9mIGF0dGFjaG1lbnQucGlwZSA9PT0gJ2Z1bmN0aW9uJztcblxuY29uc3QgZ2V0QXR0YWNobWVudE9wdGlvbnMgPSAoaXRlbTogYW55KTogeyBmaWxlbmFtZT86IHN0cmluZywgY29udGVudFR5cGU/OiBzdHJpbmcsIGtub3duTGVuZ3RoPzogbnVtYmVyIH0gPT4ge1xuICBpZiAodHlwZW9mIGl0ZW0gIT09ICdvYmplY3QnIHx8IGlzU3RyZWFtKGl0ZW0pKSByZXR1cm4ge307XG5cbiAgY29uc3Qge1xuICAgIGZpbGVuYW1lLFxuICAgIGNvbnRlbnRUeXBlLFxuICAgIGtub3duTGVuZ3RoXG4gIH0gPSBpdGVtO1xuXG4gIHJldHVybiB7XG4gICAgLi4uKGZpbGVuYW1lID8geyBmaWxlbmFtZSB9IDogeyBmaWxlbmFtZTogJ2ZpbGUnIH0pLFxuICAgIC4uLihjb250ZW50VHlwZSAmJiB7IGNvbnRlbnRUeXBlIH0pLFxuICAgIC4uLihrbm93bkxlbmd0aCAmJiB7IGtub3duTGVuZ3RoIH0pXG4gIH07XG59XG5cbmNvbnN0IHN0cmVhbVRvU3RyaW5nID0gKHN0cmVhbTogYW55KSA9PiB7XG4gIGNvbnN0IGNodW5rczogYW55ID0gW11cbiAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICBzdHJlYW0ub24oJ2RhdGEnLCAoY2h1bms6IGFueSkgPT4gY2h1bmtzLnB1c2goY2h1bmspKVxuICAgIHN0cmVhbS5vbignZXJyb3InLCByZWplY3QpXG4gICAgc3RyZWFtLm9uKCdlbmQnLCAoKSA9PiByZXNvbHZlKEJ1ZmZlci5jb25jYXQoY2h1bmtzKS50b1N0cmluZygndXRmOCcpKSlcbiAgfSlcbn1cblxuY2xhc3MgUmVxdWVzdCB7XG4gIHByaXZhdGUgdXNlcm5hbWU7XG4gIHByaXZhdGUga2V5O1xuICBwcml2YXRlIHVybDtcbiAgcHJpdmF0ZSBoZWFkZXJzOiBhbnk7XG4gIHByaXZhdGUgZm9ybURhdGE6IG5ldyAoKSA9PiBGb3JtRGF0YTtcblxuICBjb25zdHJ1Y3RvcihvcHRpb25zOiBSZXF1ZXN0T3B0aW9ucywgZm9ybURhdGE6IG5ldyAoKSA9PiBGb3JtRGF0YSkge1xuICAgIHRoaXMudXNlcm5hbWUgPSBvcHRpb25zLnVzZXJuYW1lO1xuICAgIHRoaXMua2V5ID0gb3B0aW9ucy5rZXk7XG4gICAgdGhpcy51cmwgPSBvcHRpb25zLnVybDtcbiAgICB0aGlzLmhlYWRlcnMgPSBvcHRpb25zLmhlYWRlcnMgfHwge307XG4gICAgdGhpcy5mb3JtRGF0YSA9IGZvcm1EYXRhO1xuICB9XG5cbiAgYXN5bmMgcmVxdWVzdChtZXRob2Q6IHN0cmluZywgdXJsOiBzdHJpbmcsIG9wdGlvbnM/OiBhbnkpIHtcbiAgICBjb25zdCBiYXNpYyA9IEJ0b2EoYCR7dGhpcy51c2VybmFtZX06JHt0aGlzLmtleX1gKTtcbiAgICBjb25zdCBoZWFkZXJzID0ge1xuICAgICAgQXV0aG9yaXphdGlvbjogYEJhc2ljICR7YmFzaWN9YCxcbiAgICAgIC4uLnRoaXMuaGVhZGVycyxcbiAgICAgIC4uLm9wdGlvbnM/LmhlYWRlcnNcbiAgICB9O1xuXG4gICAgZGVsZXRlIG9wdGlvbnM/LmhlYWRlcnM7XG5cbiAgICBpZiAoIWhlYWRlcnNbJ0NvbnRlbnQtVHlwZSddKSB7XG4gICAgICAvLyBmb3IgZm9ybS1kYXRhIGl0IHdpbGwgYmUgTnVsbCBzbyB3ZSBuZWVkIHRvIHJlbW92ZSBpdFxuICAgICAgZGVsZXRlIGhlYWRlcnNbJ0NvbnRlbnQtVHlwZSddO1xuICAgIH1cblxuICAgIGNvbnN0IHBhcmFtcyA9IHsgLi4ub3B0aW9ucyB9O1xuXG4gICAgaWYgKG9wdGlvbnM/LnF1ZXJ5ICYmIE9iamVjdC5nZXRPd25Qcm9wZXJ0eU5hbWVzKG9wdGlvbnM/LnF1ZXJ5KS5sZW5ndGggPiAwKSB7XG4gICAgICBwYXJhbXMuc2VhcmNoUGFyYW1zID0gb3B0aW9ucy5xdWVyeTtcbiAgICAgIGRlbGV0ZSBwYXJhbXMucXVlcnlcbiAgICB9XG5cbiAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGt5KFxuICAgICAgdXJsam9pbih0aGlzLnVybCwgdXJsKSxcbiAgICAgIHtcbiAgICAgICAgbWV0aG9kOiBtZXRob2QudG9Mb2NhbGVVcHBlckNhc2UoKSxcbiAgICAgICAgaGVhZGVycyxcbiAgICAgICAgdGhyb3dIdHRwRXJyb3JzOiBmYWxzZSxcbiAgICAgICAgLi4ucGFyYW1zXG4gICAgICB9XG4gICAgKTtcblxuICAgIGlmICghcmVzcG9uc2U/Lm9rKSB7XG4gICAgICBjb25zdCBtZXNzYWdlID0gcmVzcG9uc2U/LmJvZHkgJiYgaXNTdHJlYW0ocmVzcG9uc2UuYm9keSlcbiAgICAgICAgPyBhd2FpdCBzdHJlYW1Ub1N0cmluZyhyZXNwb25zZS5ib2R5KVxuICAgICAgICA6IGF3YWl0IHJlc3BvbnNlPy5qc29uKCk7XG5cbiAgICAgIHRocm93IG5ldyBBUElFcnJvcih7XG4gICAgICAgIHN0YXR1czogcmVzcG9uc2U/LnN0YXR1cyxcbiAgICAgICAgc3RhdHVzVGV4dDogcmVzcG9uc2U/LnN0YXR1c1RleHQsXG4gICAgICAgIGJvZHk6IHsgbWVzc2FnZSB9XG4gICAgICB9IGFzIEFQSUVycm9yT3B0aW9ucyk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHtcbiAgICAgIGJvZHk6IGF3YWl0IHJlc3BvbnNlPy5qc29uKCksXG4gICAgICBzdGF0dXM6IHJlc3BvbnNlPy5zdGF0dXNcbiAgICB9O1xuICB9XG5cbiAgcXVlcnkobWV0aG9kOiBzdHJpbmcsIHVybDogc3RyaW5nLCBxdWVyeTogYW55LCBvcHRpb25zPzogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdChtZXRob2QsIHVybCwgeyBxdWVyeSwgLi4ub3B0aW9ucyB9KTtcbiAgfVxuXG4gIGNvbW1hbmQobWV0aG9kOiBzdHJpbmcsIHVybDogc3RyaW5nLCBkYXRhOiBhbnksIG9wdGlvbnM/OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KG1ldGhvZCwgdXJsLCB7XG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJyB9LFxuICAgICAgYm9keTogZGF0YSxcbiAgICAgIC4uLm9wdGlvbnNcbiAgICB9KTtcbiAgfVxuXG4gIGdldCh1cmw6IHN0cmluZywgcXVlcnk/OiBhbnksIG9wdGlvbnM/OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5xdWVyeSgnZ2V0JywgdXJsLCBxdWVyeSwgb3B0aW9ucyk7XG4gIH1cblxuICBoZWFkKHVybDogc3RyaW5nLCBxdWVyeTogYW55LCBvcHRpb25zOiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5xdWVyeSgnaGVhZCcsIHVybCwgcXVlcnksIG9wdGlvbnMpO1xuICB9XG5cbiAgb3B0aW9ucyh1cmw6IHN0cmluZywgcXVlcnk6IGFueSwgb3B0aW9uczogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucXVlcnkoJ29wdGlvbnMnLCB1cmwsIHF1ZXJ5LCBvcHRpb25zKTtcbiAgfVxuXG4gIHBvc3QodXJsOiBzdHJpbmcsIGRhdGE6IGFueSwgb3B0aW9ucz86IGFueSkge1xuICAgIHJldHVybiB0aGlzLmNvbW1hbmQoJ3Bvc3QnLCB1cmwsIGRhdGEsIG9wdGlvbnMpO1xuICB9XG5cbiAgcG9zdE11bHRpKHVybDogc3RyaW5nLCBkYXRhOiBhbnkpIHtcblxuICAgIGNvbnN0IGZvcm1EYXRhOiBGb3JtRGF0YSA9IG5ldyB0aGlzLmZvcm1EYXRhKCk7XG4gICAgY29uc3QgcGFyYW1zOiBhbnkgPSB7XG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiBudWxsIH1cbiAgICB9O1xuXG4gICAgT2JqZWN0LmtleXMoZGF0YSlcbiAgICAgIC5maWx0ZXIoZnVuY3Rpb24gKGtleSkgeyByZXR1cm4gZGF0YVtrZXldOyB9KVxuICAgICAgLmZvckVhY2goZnVuY3Rpb24gKGtleSkge1xuICAgICAgICBpZiAoa2V5ID09PSAnYXR0YWNobWVudCcpIHtcbiAgICAgICAgICBjb25zdCBvYmogPSBkYXRhLmF0dGFjaG1lbnQ7XG5cbiAgICAgICAgICBpZiAoQXJyYXkuaXNBcnJheShvYmopKSB7XG4gICAgICAgICAgICBvYmouZm9yRWFjaChmdW5jdGlvbiAoaXRlbSkge1xuICAgICAgICAgICAgICBjb25zdCBkYXRhID0gaXRlbS5kYXRhID8gaXRlbS5kYXRhIDogaXRlbTtcbiAgICAgICAgICAgICAgY29uc3Qgb3B0aW9ucyA9IGdldEF0dGFjaG1lbnRPcHRpb25zKGl0ZW0pO1xuICAgICAgICAgICAgICAoZm9ybURhdGEgYXMgYW55KS5hcHBlbmQoa2V5LCBkYXRhLCBvcHRpb25zKTtcbiAgICAgICAgICAgIH0pO1xuICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICBjb25zdCBkYXRhID0gaXNTdHJlYW0ob2JqKSA/IG9iaiA6IG9iai5kYXRhO1xuICAgICAgICAgICAgY29uc3Qgb3B0aW9ucyA9IGdldEF0dGFjaG1lbnRPcHRpb25zKG9iaik7XG4gICAgICAgICAgICAoZm9ybURhdGEgYXMgYW55KS5hcHBlbmQoa2V5LCBkYXRhLCBvcHRpb25zKTtcbiAgICAgICAgICB9XG5cbiAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cblxuICAgICAgICBpZiAoQXJyYXkuaXNBcnJheShkYXRhW2tleV0pKSB7XG4gICAgICAgICAgZGF0YVtrZXldLmZvckVhY2goZnVuY3Rpb24gKGl0ZW06IGFueSkge1xuICAgICAgICAgICAgZm9ybURhdGEuYXBwZW5kKGtleSwgaXRlbSk7XG4gICAgICAgICAgfSk7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgZm9ybURhdGEuYXBwZW5kKGtleSwgZGF0YVtrZXldKTtcbiAgICAgICAgfVxuICAgICAgfSk7XG5cbiAgICByZXR1cm4gdGhpcy5jb21tYW5kKCdwb3N0JywgdXJsLCBmb3JtRGF0YSwgcGFyYW1zKTtcbiAgfVxuXG4gIHB1dCh1cmw6IHN0cmluZywgZGF0YTogYW55LCBvcHRpb25zPzogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMuY29tbWFuZCgncHV0JywgdXJsLCBkYXRhLCBvcHRpb25zKTtcbiAgfVxuXG4gIHBhdGNoKHVybDogc3RyaW5nLCBkYXRhOiBhbnksIG9wdGlvbnM/OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5jb21tYW5kKCdwYXRjaCcsIHVybCwgZGF0YSwgb3B0aW9ucyk7XG4gIH1cblxuICBkZWxldGUodXJsOiBzdHJpbmcsIGRhdGE/OiBhbnksIG9wdGlvbnM/OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5jb21tYW5kKCdkZWxldGUnLCB1cmwsIGRhdGEsIG9wdGlvbnMpO1xuICB9XG59XG5cbmV4cG9ydCBkZWZhdWx0IFJlcXVlc3QiLCJpbXBvcnQgUmVxdWVzdCBmcm9tICcuL3JlcXVlc3QnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBSb3V0ZXNDbGllbnQge1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuXG4gIGNvbnN0cnVjdG9yKHJlcXVlc3Q6IFJlcXVlc3QpIHtcbiAgICB0aGlzLnJlcXVlc3QgPSByZXF1ZXN0O1xuICB9XG5cbiAgbGlzdChxdWVyeTogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoJy92My9yb3V0ZXMnLCBxdWVyeSlcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keS5pdGVtcyk7XG4gIH1cblxuICBnZXQoaWQ6IHN0cmluZykge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KGAvdjMvcm91dGVzLyR7aWR9YClcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keS5yb3V0ZSk7XG4gIH1cblxuICBjcmVhdGUoZGF0YTogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0KCcvdjMvcm91dGVzJywgZGF0YSlcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keS5yb3V0ZSk7XG4gIH1cblxuICB1cGRhdGUoaWQ6IHN0cmluZywgZGF0YTogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXQoYC92My9yb3V0ZXMvJHtpZH1gLCBkYXRhKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlKSA9PiByZXNwb25zZS5ib2R5KTtcbiAgfVxuXG4gIGRlc3Ryb3koaWQ6IHN0cmluZykge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZGVsZXRlKGAvdjMvcm91dGVzLyR7aWR9YClcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keSk7XG4gIH1cbn1cbiIsImltcG9ydCB1cmxqb2luIGZyb20gJ3VybC1qb2luJztcbmltcG9ydCBSZXF1ZXN0IGZyb20gJy4vcmVxdWVzdCc7XG5pbXBvcnQgU3RhdHNPcHRpb25zIGZyb20gJy4vaW50ZXJmYWNlcy9TdGF0c09wdGlvbnMnO1xuXG5jbGFzcyBTdGF0cyB7XG4gIHN0YXJ0OiBEYXRlO1xuICBlbmQ6IERhdGU7XG4gIHJlc29sdXRpb246IHN0cmluZztcbiAgc3RhdHM6IGFueTtcblxuICBjb25zdHJ1Y3RvcihkYXRhOiBTdGF0c09wdGlvbnMpIHtcbiAgICB0aGlzLnN0YXJ0ID0gbmV3IERhdGUoZGF0YS5zdGFydCk7XG4gICAgdGhpcy5lbmQgPSBuZXcgRGF0ZShkYXRhLmVuZCk7XG4gICAgdGhpcy5yZXNvbHV0aW9uID0gZGF0YS5yZXNvbHV0aW9uO1xuICAgIHRoaXMuc3RhdHMgPSBkYXRhLnN0YXRzLm1hcChmdW5jdGlvbiAoc3RhdDogeyB0aW1lOiBzdHJpbmcgfCBEYXRlIH0pIHtcbiAgICAgIHN0YXQudGltZSA9IG5ldyBEYXRlKHN0YXQudGltZSk7XG4gICAgICByZXR1cm4gc3RhdDtcbiAgICB9KTtcbiAgfVxufVxuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBTdGF0c0NsaWVudCB7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBfcGFyc2VTdGF0cyhyZXNwb25zZTogeyBib2R5OiBTdGF0c09wdGlvbnMgfSkge1xuICAgIHJldHVybiBuZXcgU3RhdHMocmVzcG9uc2UuYm9keSk7XG4gIH1cblxuICBnZXREb21haW4oZG9tYWluOiBzdHJpbmcsIHF1ZXJ5OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCh1cmxqb2luKCcvdjMnLCBkb21haW4sICdzdGF0cy90b3RhbCcpLCBxdWVyeSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlU3RhdHMpO1xuICB9XG5cbiAgZ2V0QWNjb3VudChxdWVyeTogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoJy92My9zdGF0cy90b3RhbCcsIHF1ZXJ5KVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VTdGF0cyk7XG4gIH1cbn1cbiIsImltcG9ydCB1cmwgZnJvbSAndXJsJztcbmltcG9ydCB1cmxqb2luIGZyb20gJ3VybC1qb2luJztcblxuaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi9yZXF1ZXN0JztcbmltcG9ydCB7IEJvdW5jZURhdGEsIENvbXBsYWludERhdGEsIFVuc3Vic2NyaWJlRGF0YSB9IGZyb20gJy4vaW50ZXJmYWNlcy9TdXByZXNzaW9ucyc7XG5cbnR5cGUgVE1vZGVsID0gdHlwZW9mIEJvdW5jZSB8IHR5cGVvZiBDb21wbGFpbnQgfCB0eXBlb2YgVW5zdWJzY3JpYmU7XG5cbmNvbnN0IGNyZWF0ZU9wdGlvbnMgPSB7XG4gIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9XG59O1xuXG5jbGFzcyBCb3VuY2Uge1xuICB0eXBlOiBzdHJpbmc7XG4gIGFkZHJlc3M6IHN0cmluZztcbiAgY29kZTogbnVtYmVyO1xuICBlcnJvcjogc3RyaW5nO1xuICBjcmVhdGVkX2F0OiBEYXRlO1xuXG4gIGNvbnN0cnVjdG9yKGRhdGE6IEJvdW5jZURhdGEpIHtcbiAgICB0aGlzLnR5cGUgPSAnYm91bmNlcyc7XG4gICAgdGhpcy5hZGRyZXNzID0gZGF0YS5hZGRyZXNzO1xuICAgIHRoaXMuY29kZSA9ICtkYXRhLmNvZGU7XG4gICAgdGhpcy5lcnJvciA9IGRhdGEuZXJyb3I7XG4gICAgdGhpcy5jcmVhdGVkX2F0ID0gbmV3IERhdGUoZGF0YS5jcmVhdGVkX2F0KTtcbiAgfVxufVxuXG5jbGFzcyBDb21wbGFpbnQge1xuICB0eXBlOiBzdHJpbmc7XG4gIGFkZHJlc3M6IGFueTtcbiAgY3JlYXRlZF9hdDogRGF0ZTtcblxuICBjb25zdHJ1Y3RvcihkYXRhOiBDb21wbGFpbnREYXRhKSB7XG4gICAgdGhpcy50eXBlID0gJ2NvbXBsYWludHMnO1xuICAgIHRoaXMuYWRkcmVzcyA9IGRhdGEuYWRkcmVzcztcbiAgICB0aGlzLmNyZWF0ZWRfYXQgPSBuZXcgRGF0ZShkYXRhLmNyZWF0ZWRfYXQpO1xuICB9XG59XG5cbmNsYXNzIFVuc3Vic2NyaWJlIHtcbiAgdHlwZTogc3RyaW5nO1xuICBhZGRyZXNzOiBzdHJpbmc7XG4gIHRhZ3M6IGFueTtcbiAgY3JlYXRlZF9hdDogRGF0ZTtcblxuICBjb25zdHJ1Y3RvcihkYXRhOiBVbnN1YnNjcmliZURhdGEpIHtcbiAgICB0aGlzLnR5cGUgPSAndW5zdWJzY3JpYmVzJztcbiAgICB0aGlzLmFkZHJlc3MgPSBkYXRhLmFkZHJlc3M7XG4gICAgdGhpcy50YWdzID0gZGF0YS50YWdzO1xuICAgIHRoaXMuY3JlYXRlZF9hdCA9IG5ldyBEYXRlKGRhdGEuY3JlYXRlZF9hdCk7XG4gIH1cbn1cblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgU3VwcHJlc3Npb25DbGllbnQge1xuICByZXF1ZXN0OiBhbnk7XG4gIG1vZGVsczoge1xuICAgIGJvdW5jZXM6IHR5cGVvZiBCb3VuY2U7XG4gICAgY29tcGxhaW50czogdHlwZW9mIENvbXBsYWludDtcbiAgICB1bnN1YnNjcmliZXM6IHR5cGVvZiBVbnN1YnNjcmliZTtcbiAgfTtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgICB0aGlzLm1vZGVscyA9IHtcbiAgICAgIGJvdW5jZXM6IEJvdW5jZSxcbiAgICAgIGNvbXBsYWludHM6IENvbXBsYWludCxcbiAgICAgIHVuc3Vic2NyaWJlczogVW5zdWJzY3JpYmVcbiAgICB9O1xuICB9XG5cbiAgX3BhcnNlUGFnZShpZDogc3RyaW5nLCBwYWdlVXJsOiBzdHJpbmcpIHtcbiAgICBjb25zdCBwYXJzZWRVcmwgPSB1cmwucGFyc2UocGFnZVVybCwgdHJ1ZSk7XG4gICAgY29uc3QgeyBxdWVyeSB9ID0gcGFyc2VkVXJsO1xuXG4gICAgcmV0dXJuIHtcbiAgICAgIGlkLFxuICAgICAgcGFnZTogcXVlcnkucGFnZSxcbiAgICAgIGFkZHJlc3M6IHF1ZXJ5LmFkZHJlc3MsXG4gICAgICB1cmw6IHBhZ2VVcmxcbiAgICB9O1xuICB9XG5cbiAgX3BhcnNlUGFnZUxpbmtzKHJlc3BvbnNlOiB7IGJvZHk6IHsgcGFnaW5nOiBhbnkgfSB9KSB7XG4gICAgY29uc3QgcGFnZXMgPSBPYmplY3QuZW50cmllcyhyZXNwb25zZS5ib2R5LnBhZ2luZyk7XG4gICAgcmV0dXJuIHBhZ2VzLnJlZHVjZShcbiAgICAgIChhY2M6IGFueSwgW2lkLCB1cmxdOiBbdXJsOiBzdHJpbmcsIGlkOiBzdHJpbmddKSA9PiB7XG4gICAgICAgIGFjY1tpZF0gPSB0aGlzLl9wYXJzZVBhZ2UoaWQsIHVybClcbiAgICAgICAgcmV0dXJuIGFjY1xuICAgICAgfSwge30pO1xuICB9XG5cbiAgX3BhcnNlTGlzdChyZXNwb25zZTogeyBib2R5OiB7IGl0ZW1zOiBhbnksIHBhZ2luZzogYW55IH0gfSwgTW9kZWw6IFRNb2RlbCkge1xuICAgIGNvbnN0IGRhdGEgPSB7fSBhcyBhbnk7XG5cbiAgICBkYXRhLml0ZW1zID0gcmVzcG9uc2UuYm9keS5pdGVtcy5tYXAoKGQ6IGFueSkgPT4gbmV3IE1vZGVsKGQpKTtcblxuICAgIGRhdGEucGFnZXMgPSB0aGlzLl9wYXJzZVBhZ2VMaW5rcyhyZXNwb25zZSk7XG5cbiAgICByZXR1cm4gZGF0YTtcbiAgfVxuXG4gIF9wYXJzZUl0ZW0ocmVzcG9uc2U6IHsgYm9keTogYW55IH0sIE1vZGVsOiBUTW9kZWwpIHtcbiAgICByZXR1cm4gbmV3IE1vZGVsKHJlc3BvbnNlLmJvZHkpO1xuICB9XG5cbiAgbGlzdChkb21haW46IHN0cmluZywgdHlwZTogc3RyaW5nLCBxdWVyeTogYW55KSB7XG4gICAgY29uc3QgbW9kZWwgPSAodGhpcy5tb2RlbHMgYXMgYW55KVt0eXBlXTtcblxuICAgIHJldHVybiB0aGlzLnJlcXVlc3RcbiAgICAgIC5nZXQodXJsam9pbigndjMnLCBkb21haW4sIHR5cGUpLCBxdWVyeSlcbiAgICAgIC50aGVuKChyZXNwb25zZTogeyBib2R5OiB7IGl0ZW1zOiBhbnksIHBhZ2luZzogYW55IH0gfSkgPT4gdGhpcy5fcGFyc2VMaXN0KHJlc3BvbnNlLCBtb2RlbCkpO1xuICB9XG5cbiAgZ2V0KGRvbWFpbjogc3RyaW5nLCB0eXBlOiBzdHJpbmcsIGFkZHJlc3M6IHN0cmluZykge1xuICAgIGNvbnN0IG1vZGVsID0gKHRoaXMubW9kZWxzIGFzIGFueSlbdHlwZV07XG5cbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0XG4gICAgICAuZ2V0KHVybGpvaW4oJ3YzJywgZG9tYWluLCB0eXBlLCBlbmNvZGVVUklDb21wb25lbnQoYWRkcmVzcykpKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSA9PiB0aGlzLl9wYXJzZUl0ZW0ocmVzcG9uc2UsIG1vZGVsKSk7XG4gIH1cblxuICBjcmVhdGUoZG9tYWluOiBzdHJpbmcsIHR5cGU6IHN0cmluZywgZGF0YTogYW55KSB7XG4gICAgLy8gc3VwcG9ydHMgYWRkaW5nIG11bHRpcGxlIHN1cHByZXNzaW9ucyBieSBkZWZhdWx0XG4gICAgaWYgKCFBcnJheS5pc0FycmF5KGRhdGEpKSB7XG4gICAgICBkYXRhID0gW2RhdGFdO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzLnJlcXVlc3RcbiAgICAucG9zdCh1cmxqb2luKCd2MycsIGRvbWFpbiwgdHlwZSksIGRhdGEsIGNyZWF0ZU9wdGlvbnMpXG4gICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSA9PiByZXNwb25zZS5ib2R5KTtcbiAgfVxuXG4gIGRlc3Ryb3koZG9tYWluOiBzdHJpbmcsIHR5cGU6IHN0cmluZywgYWRkcmVzczogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdFxuICAgIC5kZWxldGUodXJsam9pbigndjMnLCBkb21haW4sIHR5cGUsIGVuY29kZVVSSUNvbXBvbmVudChhZGRyZXNzKSkpXG4gICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSA9PiByZXNwb25zZS5ib2R5KTtcbiAgfVxufVxuXG5tb2R1bGUuZXhwb3J0cyA9IFN1cHByZXNzaW9uQ2xpZW50O1xuIiwiXG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuL3JlcXVlc3QnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBWYWxpZGF0ZUNsaWVudCB7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBnZXQoYWRkcmVzczogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoJy92My9hZGRyZXNzL3ZhbGlkYXRlJywgeyBhZGRyZXNzIH0pXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkpO1xuICB9XG59XG4iLCJpbXBvcnQgdXJsam9pbiBmcm9tICd1cmwtam9pbic7XG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuL3JlcXVlc3QnO1xuXG5jbGFzcyBXZWJob29rIHtcbiAgaWQ6IHN0cmluZztcbiAgdXJsOiBzdHJpbmc7XG5cbiAgY29uc3RydWN0b3IoaWQ6IHN0cmluZywgZGF0YTogeyB1cmw6IHN0cmluZyB9KSB7XG4gICAgdGhpcy5pZCA9IGlkO1xuICAgIHRoaXMudXJsID0gZGF0YS51cmw7XG4gIH1cbn1cblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgV2ViaG9va0NsaWVudCB7XG4gIHJlcXVlc3Q6IGFueTtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIF9wYXJzZVdlYmhvb2tMaXN0KHJlc3BvbnNlOiB7IGJvZHk6IHsgd2ViaG9va3M6IGFueSB9IH0pIHtcbiAgICByZXR1cm4gcmVzcG9uc2UuYm9keS53ZWJob29rcztcbiAgfVxuXG4gIF9wYXJzZVdlYmhvb2tXaXRoSUQoaWQ6IHN0cmluZykge1xuICAgIHJldHVybiBmdW5jdGlvbiAocmVzcG9uc2U6IHsgYm9keTogeyB3ZWJob29rOiBhbnkgfSB9KSB7XG4gICAgICByZXR1cm4gbmV3IFdlYmhvb2soaWQsIHJlc3BvbnNlLmJvZHkud2ViaG9vayk7XG4gICAgfTtcbiAgfVxuXG4gIF9wYXJzZVdlYmhvb2tUZXN0KHJlc3BvbnNlOiB7IGJvZHk6IHsgY29kZTogbnVtYmVyLCBtZXNzYWdlOiBzdHJpbmcgfSB9KSB7XG4gICAgcmV0dXJuIHsgY29kZTogcmVzcG9uc2UuYm9keS5jb2RlLCBtZXNzYWdlOiByZXNwb25zZS5ib2R5Lm1lc3NhZ2UgfTtcbiAgfVxuXG4gIGxpc3QoZG9tYWluOiBzdHJpbmcsIHF1ZXJ5OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCh1cmxqb2luKCcvdjIvZG9tYWlucycsIGRvbWFpbiwgJ3dlYmhvb2tzJyksIHF1ZXJ5KVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VXZWJob29rTGlzdCk7XG4gIH1cblxuICBnZXQoZG9tYWluOiBzdHJpbmcsIGlkOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCh1cmxqb2luKCcvdjIvZG9tYWlucycsIGRvbWFpbiwgJ3dlYmhvb2tzJywgaWQpKVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VXZWJob29rV2l0aElEKGlkKSk7XG4gIH1cblxuICBjcmVhdGUoZG9tYWluOiBzdHJpbmcsIGlkOiBzdHJpbmcsIHVybDogc3RyaW5nLCB0ZXN0OiBib29sZWFuKSB7XG4gICAgaWYgKHRlc3QpIHtcbiAgICAgIHJldHVybiB0aGlzLnJlcXVlc3QucHV0KHVybGpvaW4oJy92Mi9kb21haW5zJywgZG9tYWluLCAnd2ViaG9va3MnLCBpZCwgJ3Rlc3QnKSwgeyB1cmwgfSlcbiAgICAgICAgLnRoZW4odGhpcy5fcGFyc2VXZWJob29rVGVzdCk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0KHVybGpvaW4oJy92Mi9kb21haW5zJywgZG9tYWluLCAnd2ViaG9va3MnKSwgeyBpZCwgdXJsIH0pXG4gICAgICAudGhlbih0aGlzLl9wYXJzZVdlYmhvb2tXaXRoSUQoaWQpKTtcbiAgfVxuXG4gIHVwZGF0ZShkb21haW46IHN0cmluZywgaWQ6IHN0cmluZywgdXJsOiBzdHJpbmcsKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXQodXJsam9pbignL3YyL2RvbWFpbnMnLCBkb21haW4sICd3ZWJob29rcycsIGlkKSwgeyB1cmwgfSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlV2ViaG9va1dpdGhJRChpZCkpO1xuICB9XG5cbiAgZGVzdHJveShkb21haW46IHN0cmluZywgaWQ6IHN0cmluZykge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZGVsZXRlKHVybGpvaW4oJy92Mi9kb21haW5zJywgZG9tYWluLCAnd2ViaG9va3MnLCBpZCkpXG4gICAgICAudGhlbih0aGlzLl9wYXJzZVdlYmhvb2tXaXRoSUQoaWQpKTtcbiAgfVxufVxuIiwiKGZ1bmN0aW9uICgpIHtcbiAgXCJ1c2Ugc3RyaWN0XCI7XG5cbiAgZnVuY3Rpb24gYnRvYShzdHIpIHtcbiAgICB2YXIgYnVmZmVyO1xuXG4gICAgaWYgKHN0ciBpbnN0YW5jZW9mIEJ1ZmZlcikge1xuICAgICAgYnVmZmVyID0gc3RyO1xuICAgIH0gZWxzZSB7XG4gICAgICBidWZmZXIgPSBCdWZmZXIuZnJvbShzdHIudG9TdHJpbmcoKSwgJ2JpbmFyeScpO1xuICAgIH1cblxuICAgIHJldHVybiBidWZmZXIudG9TdHJpbmcoJ2Jhc2U2NCcpO1xuICB9XG5cbiAgbW9kdWxlLmV4cG9ydHMgPSBidG9hO1xufSgpKTtcbiIsIlwidXNlIHN0cmljdFwiO1xuLyoqXG4gKiBSZXR1cm5zIGEgYEJ1ZmZlcmAgaW5zdGFuY2UgZnJvbSB0aGUgZ2l2ZW4gZGF0YSBVUkkgYHVyaWAuXG4gKlxuICogQHBhcmFtIHtTdHJpbmd9IHVyaSBEYXRhIFVSSSB0byB0dXJuIGludG8gYSBCdWZmZXIgaW5zdGFuY2VcbiAqIEByZXR1cm4ge0J1ZmZlcn0gQnVmZmVyIGluc3RhbmNlIGZyb20gRGF0YSBVUklcbiAqIEBhcGkgcHVibGljXG4gKi9cbmZ1bmN0aW9uIGRhdGFVcmlUb0J1ZmZlcih1cmkpIHtcbiAgICBpZiAoIS9eZGF0YTovaS50ZXN0KHVyaSkpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignYHVyaWAgZG9lcyBub3QgYXBwZWFyIHRvIGJlIGEgRGF0YSBVUkkgKG11c3QgYmVnaW4gd2l0aCBcImRhdGE6XCIpJyk7XG4gICAgfVxuICAgIC8vIHN0cmlwIG5ld2xpbmVzXG4gICAgdXJpID0gdXJpLnJlcGxhY2UoL1xccj9cXG4vZywgJycpO1xuICAgIC8vIHNwbGl0IHRoZSBVUkkgdXAgaW50byB0aGUgXCJtZXRhZGF0YVwiIGFuZCB0aGUgXCJkYXRhXCIgcG9ydGlvbnNcbiAgICBjb25zdCBmaXJzdENvbW1hID0gdXJpLmluZGV4T2YoJywnKTtcbiAgICBpZiAoZmlyc3RDb21tYSA9PT0gLTEgfHwgZmlyc3RDb21tYSA8PSA0KSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ21hbGZvcm1lZCBkYXRhOiBVUkknKTtcbiAgICB9XG4gICAgLy8gcmVtb3ZlIHRoZSBcImRhdGE6XCIgc2NoZW1lIGFuZCBwYXJzZSB0aGUgbWV0YWRhdGFcbiAgICBjb25zdCBtZXRhID0gdXJpLnN1YnN0cmluZyg1LCBmaXJzdENvbW1hKS5zcGxpdCgnOycpO1xuICAgIGxldCBjaGFyc2V0ID0gJyc7XG4gICAgbGV0IGJhc2U2NCA9IGZhbHNlO1xuICAgIGNvbnN0IHR5cGUgPSBtZXRhWzBdIHx8ICd0ZXh0L3BsYWluJztcbiAgICBsZXQgdHlwZUZ1bGwgPSB0eXBlO1xuICAgIGZvciAobGV0IGkgPSAxOyBpIDwgbWV0YS5sZW5ndGg7IGkrKykge1xuICAgICAgICBpZiAobWV0YVtpXSA9PT0gJ2Jhc2U2NCcpIHtcbiAgICAgICAgICAgIGJhc2U2NCA9IHRydWU7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICB0eXBlRnVsbCArPSBgOyR7bWV0YVtpXX1gO1xuICAgICAgICAgICAgaWYgKG1ldGFbaV0uaW5kZXhPZignY2hhcnNldD0nKSA9PT0gMCkge1xuICAgICAgICAgICAgICAgIGNoYXJzZXQgPSBtZXRhW2ldLnN1YnN0cmluZyg4KTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbiAgICAvLyBkZWZhdWx0cyB0byBVUy1BU0NJSSBvbmx5IGlmIHR5cGUgaXMgbm90IHByb3ZpZGVkXG4gICAgaWYgKCFtZXRhWzBdICYmICFjaGFyc2V0Lmxlbmd0aCkge1xuICAgICAgICB0eXBlRnVsbCArPSAnO2NoYXJzZXQ9VVMtQVNDSUknO1xuICAgICAgICBjaGFyc2V0ID0gJ1VTLUFTQ0lJJztcbiAgICB9XG4gICAgLy8gZ2V0IHRoZSBlbmNvZGVkIGRhdGEgcG9ydGlvbiBhbmQgZGVjb2RlIFVSSS1lbmNvZGVkIGNoYXJzXG4gICAgY29uc3QgZW5jb2RpbmcgPSBiYXNlNjQgPyAnYmFzZTY0JyA6ICdhc2NpaSc7XG4gICAgY29uc3QgZGF0YSA9IHVuZXNjYXBlKHVyaS5zdWJzdHJpbmcoZmlyc3RDb21tYSArIDEpKTtcbiAgICBjb25zdCBidWZmZXIgPSBCdWZmZXIuZnJvbShkYXRhLCBlbmNvZGluZyk7XG4gICAgLy8gc2V0IGAudHlwZWAgYW5kIGAudHlwZUZ1bGxgIHByb3BlcnRpZXMgdG8gTUlNRSB0eXBlXG4gICAgYnVmZmVyLnR5cGUgPSB0eXBlO1xuICAgIGJ1ZmZlci50eXBlRnVsbCA9IHR5cGVGdWxsO1xuICAgIC8vIHNldCB0aGUgYC5jaGFyc2V0YCBwcm9wZXJ0eVxuICAgIGJ1ZmZlci5jaGFyc2V0ID0gY2hhcnNldDtcbiAgICByZXR1cm4gYnVmZmVyO1xufVxubW9kdWxlLmV4cG9ydHMgPSBkYXRhVXJpVG9CdWZmZXI7XG4vLyMgc291cmNlTWFwcGluZ1VSTD1pbmRleC5qcy5tYXAiLCIvKipcbiAqIEBhdXRob3IgVG9ydSBOYWdhc2hpbWEgPGh0dHBzOi8vZ2l0aHViLmNvbS9teXN0aWNhdGVhPlxuICogQGNvcHlyaWdodCAyMDE1IFRvcnUgTmFnYXNoaW1hLiBBbGwgcmlnaHRzIHJlc2VydmVkLlxuICogU2VlIExJQ0VOU0UgZmlsZSBpbiByb290IGRpcmVjdG9yeSBmb3IgZnVsbCBsaWNlbnNlLlxuICovXG4ndXNlIHN0cmljdCc7XG5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCAnX19lc01vZHVsZScsIHsgdmFsdWU6IHRydWUgfSk7XG5cbi8qKlxuICogQHR5cGVkZWYge29iamVjdH0gUHJpdmF0ZURhdGFcbiAqIEBwcm9wZXJ0eSB7RXZlbnRUYXJnZXR9IGV2ZW50VGFyZ2V0IFRoZSBldmVudCB0YXJnZXQuXG4gKiBAcHJvcGVydHkge3t0eXBlOnN0cmluZ319IGV2ZW50IFRoZSBvcmlnaW5hbCBldmVudCBvYmplY3QuXG4gKiBAcHJvcGVydHkge251bWJlcn0gZXZlbnRQaGFzZSBUaGUgY3VycmVudCBldmVudCBwaGFzZS5cbiAqIEBwcm9wZXJ0eSB7RXZlbnRUYXJnZXR8bnVsbH0gY3VycmVudFRhcmdldCBUaGUgY3VycmVudCBldmVudCB0YXJnZXQuXG4gKiBAcHJvcGVydHkge2Jvb2xlYW59IGNhbmNlbGVkIFRoZSBmbGFnIHRvIHByZXZlbnQgZGVmYXVsdC5cbiAqIEBwcm9wZXJ0eSB7Ym9vbGVhbn0gc3RvcHBlZCBUaGUgZmxhZyB0byBzdG9wIHByb3BhZ2F0aW9uLlxuICogQHByb3BlcnR5IHtib29sZWFufSBpbW1lZGlhdGVTdG9wcGVkIFRoZSBmbGFnIHRvIHN0b3AgcHJvcGFnYXRpb24gaW1tZWRpYXRlbHkuXG4gKiBAcHJvcGVydHkge0Z1bmN0aW9ufG51bGx9IHBhc3NpdmVMaXN0ZW5lciBUaGUgbGlzdGVuZXIgaWYgdGhlIGN1cnJlbnQgbGlzdGVuZXIgaXMgcGFzc2l2ZS4gT3RoZXJ3aXNlIHRoaXMgaXMgbnVsbC5cbiAqIEBwcm9wZXJ0eSB7bnVtYmVyfSB0aW1lU3RhbXAgVGhlIHVuaXggdGltZS5cbiAqIEBwcml2YXRlXG4gKi9cblxuLyoqXG4gKiBQcml2YXRlIGRhdGEgZm9yIGV2ZW50IHdyYXBwZXJzLlxuICogQHR5cGUge1dlYWtNYXA8RXZlbnQsIFByaXZhdGVEYXRhPn1cbiAqIEBwcml2YXRlXG4gKi9cbmNvbnN0IHByaXZhdGVEYXRhID0gbmV3IFdlYWtNYXAoKTtcblxuLyoqXG4gKiBDYWNoZSBmb3Igd3JhcHBlciBjbGFzc2VzLlxuICogQHR5cGUge1dlYWtNYXA8T2JqZWN0LCBGdW5jdGlvbj59XG4gKiBAcHJpdmF0ZVxuICovXG5jb25zdCB3cmFwcGVycyA9IG5ldyBXZWFrTWFwKCk7XG5cbi8qKlxuICogR2V0IHByaXZhdGUgZGF0YS5cbiAqIEBwYXJhbSB7RXZlbnR9IGV2ZW50IFRoZSBldmVudCBvYmplY3QgdG8gZ2V0IHByaXZhdGUgZGF0YS5cbiAqIEByZXR1cm5zIHtQcml2YXRlRGF0YX0gVGhlIHByaXZhdGUgZGF0YSBvZiB0aGUgZXZlbnQuXG4gKiBAcHJpdmF0ZVxuICovXG5mdW5jdGlvbiBwZChldmVudCkge1xuICAgIGNvbnN0IHJldHYgPSBwcml2YXRlRGF0YS5nZXQoZXZlbnQpO1xuICAgIGNvbnNvbGUuYXNzZXJ0KFxuICAgICAgICByZXR2ICE9IG51bGwsXG4gICAgICAgIFwiJ3RoaXMnIGlzIGV4cGVjdGVkIGFuIEV2ZW50IG9iamVjdCwgYnV0IGdvdFwiLFxuICAgICAgICBldmVudFxuICAgICk7XG4gICAgcmV0dXJuIHJldHZcbn1cblxuLyoqXG4gKiBodHRwczovL2RvbS5zcGVjLndoYXR3Zy5vcmcvI3NldC10aGUtY2FuY2VsZWQtZmxhZ1xuICogQHBhcmFtIGRhdGEge1ByaXZhdGVEYXRhfSBwcml2YXRlIGRhdGEuXG4gKi9cbmZ1bmN0aW9uIHNldENhbmNlbEZsYWcoZGF0YSkge1xuICAgIGlmIChkYXRhLnBhc3NpdmVMaXN0ZW5lciAhPSBudWxsKSB7XG4gICAgICAgIGlmIChcbiAgICAgICAgICAgIHR5cGVvZiBjb25zb2xlICE9PSBcInVuZGVmaW5lZFwiICYmXG4gICAgICAgICAgICB0eXBlb2YgY29uc29sZS5lcnJvciA9PT0gXCJmdW5jdGlvblwiXG4gICAgICAgICkge1xuICAgICAgICAgICAgY29uc29sZS5lcnJvcihcbiAgICAgICAgICAgICAgICBcIlVuYWJsZSB0byBwcmV2ZW50RGVmYXVsdCBpbnNpZGUgcGFzc2l2ZSBldmVudCBsaXN0ZW5lciBpbnZvY2F0aW9uLlwiLFxuICAgICAgICAgICAgICAgIGRhdGEucGFzc2l2ZUxpc3RlbmVyXG4gICAgICAgICAgICApO1xuICAgICAgICB9XG4gICAgICAgIHJldHVyblxuICAgIH1cbiAgICBpZiAoIWRhdGEuZXZlbnQuY2FuY2VsYWJsZSkge1xuICAgICAgICByZXR1cm5cbiAgICB9XG5cbiAgICBkYXRhLmNhbmNlbGVkID0gdHJ1ZTtcbiAgICBpZiAodHlwZW9mIGRhdGEuZXZlbnQucHJldmVudERlZmF1bHQgPT09IFwiZnVuY3Rpb25cIikge1xuICAgICAgICBkYXRhLmV2ZW50LnByZXZlbnREZWZhdWx0KCk7XG4gICAgfVxufVxuXG4vKipcbiAqIEBzZWUgaHR0cHM6Ly9kb20uc3BlYy53aGF0d2cub3JnLyNpbnRlcmZhY2UtZXZlbnRcbiAqIEBwcml2YXRlXG4gKi9cbi8qKlxuICogVGhlIGV2ZW50IHdyYXBwZXIuXG4gKiBAY29uc3RydWN0b3JcbiAqIEBwYXJhbSB7RXZlbnRUYXJnZXR9IGV2ZW50VGFyZ2V0IFRoZSBldmVudCB0YXJnZXQgb2YgdGhpcyBkaXNwYXRjaGluZy5cbiAqIEBwYXJhbSB7RXZlbnR8e3R5cGU6c3RyaW5nfX0gZXZlbnQgVGhlIG9yaWdpbmFsIGV2ZW50IHRvIHdyYXAuXG4gKi9cbmZ1bmN0aW9uIEV2ZW50KGV2ZW50VGFyZ2V0LCBldmVudCkge1xuICAgIHByaXZhdGVEYXRhLnNldCh0aGlzLCB7XG4gICAgICAgIGV2ZW50VGFyZ2V0LFxuICAgICAgICBldmVudCxcbiAgICAgICAgZXZlbnRQaGFzZTogMixcbiAgICAgICAgY3VycmVudFRhcmdldDogZXZlbnRUYXJnZXQsXG4gICAgICAgIGNhbmNlbGVkOiBmYWxzZSxcbiAgICAgICAgc3RvcHBlZDogZmFsc2UsXG4gICAgICAgIGltbWVkaWF0ZVN0b3BwZWQ6IGZhbHNlLFxuICAgICAgICBwYXNzaXZlTGlzdGVuZXI6IG51bGwsXG4gICAgICAgIHRpbWVTdGFtcDogZXZlbnQudGltZVN0YW1wIHx8IERhdGUubm93KCksXG4gICAgfSk7XG5cbiAgICAvLyBodHRwczovL2hleWNhbS5naXRodWIuaW8vd2ViaWRsLyNVbmZvcmdlYWJsZVxuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0aGlzLCBcImlzVHJ1c3RlZFwiLCB7IHZhbHVlOiBmYWxzZSwgZW51bWVyYWJsZTogdHJ1ZSB9KTtcblxuICAgIC8vIERlZmluZSBhY2Nlc3NvcnNcbiAgICBjb25zdCBrZXlzID0gT2JqZWN0LmtleXMoZXZlbnQpO1xuICAgIGZvciAobGV0IGkgPSAwOyBpIDwga2V5cy5sZW5ndGg7ICsraSkge1xuICAgICAgICBjb25zdCBrZXkgPSBrZXlzW2ldO1xuICAgICAgICBpZiAoIShrZXkgaW4gdGhpcykpIHtcbiAgICAgICAgICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0aGlzLCBrZXksIGRlZmluZVJlZGlyZWN0RGVzY3JpcHRvcihrZXkpKTtcbiAgICAgICAgfVxuICAgIH1cbn1cblxuLy8gU2hvdWxkIGJlIGVudW1lcmFibGUsIGJ1dCBjbGFzcyBtZXRob2RzIGFyZSBub3QgZW51bWVyYWJsZS5cbkV2ZW50LnByb3RvdHlwZSA9IHtcbiAgICAvKipcbiAgICAgKiBUaGUgdHlwZSBvZiB0aGlzIGV2ZW50LlxuICAgICAqIEB0eXBlIHtzdHJpbmd9XG4gICAgICovXG4gICAgZ2V0IHR5cGUoKSB7XG4gICAgICAgIHJldHVybiBwZCh0aGlzKS5ldmVudC50eXBlXG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSB0YXJnZXQgb2YgdGhpcyBldmVudC5cbiAgICAgKiBAdHlwZSB7RXZlbnRUYXJnZXR9XG4gICAgICovXG4gICAgZ2V0IHRhcmdldCgpIHtcbiAgICAgICAgcmV0dXJuIHBkKHRoaXMpLmV2ZW50VGFyZ2V0XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSB0YXJnZXQgb2YgdGhpcyBldmVudC5cbiAgICAgKiBAdHlwZSB7RXZlbnRUYXJnZXR9XG4gICAgICovXG4gICAgZ2V0IGN1cnJlbnRUYXJnZXQoKSB7XG4gICAgICAgIHJldHVybiBwZCh0aGlzKS5jdXJyZW50VGFyZ2V0XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIEByZXR1cm5zIHtFdmVudFRhcmdldFtdfSBUaGUgY29tcG9zZWQgcGF0aCBvZiB0aGlzIGV2ZW50LlxuICAgICAqL1xuICAgIGNvbXBvc2VkUGF0aCgpIHtcbiAgICAgICAgY29uc3QgY3VycmVudFRhcmdldCA9IHBkKHRoaXMpLmN1cnJlbnRUYXJnZXQ7XG4gICAgICAgIGlmIChjdXJyZW50VGFyZ2V0ID09IG51bGwpIHtcbiAgICAgICAgICAgIHJldHVybiBbXVxuICAgICAgICB9XG4gICAgICAgIHJldHVybiBbY3VycmVudFRhcmdldF1cbiAgICB9LFxuXG4gICAgLyoqXG4gICAgICogQ29uc3RhbnQgb2YgTk9ORS5cbiAgICAgKiBAdHlwZSB7bnVtYmVyfVxuICAgICAqL1xuICAgIGdldCBOT05FKCkge1xuICAgICAgICByZXR1cm4gMFxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBDb25zdGFudCBvZiBDQVBUVVJJTkdfUEhBU0UuXG4gICAgICogQHR5cGUge251bWJlcn1cbiAgICAgKi9cbiAgICBnZXQgQ0FQVFVSSU5HX1BIQVNFKCkge1xuICAgICAgICByZXR1cm4gMVxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBDb25zdGFudCBvZiBBVF9UQVJHRVQuXG4gICAgICogQHR5cGUge251bWJlcn1cbiAgICAgKi9cbiAgICBnZXQgQVRfVEFSR0VUKCkge1xuICAgICAgICByZXR1cm4gMlxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBDb25zdGFudCBvZiBCVUJCTElOR19QSEFTRS5cbiAgICAgKiBAdHlwZSB7bnVtYmVyfVxuICAgICAqL1xuICAgIGdldCBCVUJCTElOR19QSEFTRSgpIHtcbiAgICAgICAgcmV0dXJuIDNcbiAgICB9LFxuXG4gICAgLyoqXG4gICAgICogVGhlIHRhcmdldCBvZiB0aGlzIGV2ZW50LlxuICAgICAqIEB0eXBlIHtudW1iZXJ9XG4gICAgICovXG4gICAgZ2V0IGV2ZW50UGhhc2UoKSB7XG4gICAgICAgIHJldHVybiBwZCh0aGlzKS5ldmVudFBoYXNlXG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFN0b3AgZXZlbnQgYnViYmxpbmcuXG4gICAgICogQHJldHVybnMge3ZvaWR9XG4gICAgICovXG4gICAgc3RvcFByb3BhZ2F0aW9uKCkge1xuICAgICAgICBjb25zdCBkYXRhID0gcGQodGhpcyk7XG5cbiAgICAgICAgZGF0YS5zdG9wcGVkID0gdHJ1ZTtcbiAgICAgICAgaWYgKHR5cGVvZiBkYXRhLmV2ZW50LnN0b3BQcm9wYWdhdGlvbiA9PT0gXCJmdW5jdGlvblwiKSB7XG4gICAgICAgICAgICBkYXRhLmV2ZW50LnN0b3BQcm9wYWdhdGlvbigpO1xuICAgICAgICB9XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFN0b3AgZXZlbnQgYnViYmxpbmcuXG4gICAgICogQHJldHVybnMge3ZvaWR9XG4gICAgICovXG4gICAgc3RvcEltbWVkaWF0ZVByb3BhZ2F0aW9uKCkge1xuICAgICAgICBjb25zdCBkYXRhID0gcGQodGhpcyk7XG5cbiAgICAgICAgZGF0YS5zdG9wcGVkID0gdHJ1ZTtcbiAgICAgICAgZGF0YS5pbW1lZGlhdGVTdG9wcGVkID0gdHJ1ZTtcbiAgICAgICAgaWYgKHR5cGVvZiBkYXRhLmV2ZW50LnN0b3BJbW1lZGlhdGVQcm9wYWdhdGlvbiA9PT0gXCJmdW5jdGlvblwiKSB7XG4gICAgICAgICAgICBkYXRhLmV2ZW50LnN0b3BJbW1lZGlhdGVQcm9wYWdhdGlvbigpO1xuICAgICAgICB9XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSBmbGFnIHRvIGJlIGJ1YmJsaW5nLlxuICAgICAqIEB0eXBlIHtib29sZWFufVxuICAgICAqL1xuICAgIGdldCBidWJibGVzKCkge1xuICAgICAgICByZXR1cm4gQm9vbGVhbihwZCh0aGlzKS5ldmVudC5idWJibGVzKVxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBUaGUgZmxhZyB0byBiZSBjYW5jZWxhYmxlLlxuICAgICAqIEB0eXBlIHtib29sZWFufVxuICAgICAqL1xuICAgIGdldCBjYW5jZWxhYmxlKCkge1xuICAgICAgICByZXR1cm4gQm9vbGVhbihwZCh0aGlzKS5ldmVudC5jYW5jZWxhYmxlKVxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBDYW5jZWwgdGhpcyBldmVudC5cbiAgICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICAgKi9cbiAgICBwcmV2ZW50RGVmYXVsdCgpIHtcbiAgICAgICAgc2V0Q2FuY2VsRmxhZyhwZCh0aGlzKSk7XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSBmbGFnIHRvIGluZGljYXRlIGNhbmNlbGxhdGlvbiBzdGF0ZS5cbiAgICAgKiBAdHlwZSB7Ym9vbGVhbn1cbiAgICAgKi9cbiAgICBnZXQgZGVmYXVsdFByZXZlbnRlZCgpIHtcbiAgICAgICAgcmV0dXJuIHBkKHRoaXMpLmNhbmNlbGVkXG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSBmbGFnIHRvIGJlIGNvbXBvc2VkLlxuICAgICAqIEB0eXBlIHtib29sZWFufVxuICAgICAqL1xuICAgIGdldCBjb21wb3NlZCgpIHtcbiAgICAgICAgcmV0dXJuIEJvb2xlYW4ocGQodGhpcykuZXZlbnQuY29tcG9zZWQpXG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSB1bml4IHRpbWUgb2YgdGhpcyBldmVudC5cbiAgICAgKiBAdHlwZSB7bnVtYmVyfVxuICAgICAqL1xuICAgIGdldCB0aW1lU3RhbXAoKSB7XG4gICAgICAgIHJldHVybiBwZCh0aGlzKS50aW1lU3RhbXBcbiAgICB9LFxuXG4gICAgLyoqXG4gICAgICogVGhlIHRhcmdldCBvZiB0aGlzIGV2ZW50LlxuICAgICAqIEB0eXBlIHtFdmVudFRhcmdldH1cbiAgICAgKiBAZGVwcmVjYXRlZFxuICAgICAqL1xuICAgIGdldCBzcmNFbGVtZW50KCkge1xuICAgICAgICByZXR1cm4gcGQodGhpcykuZXZlbnRUYXJnZXRcbiAgICB9LFxuXG4gICAgLyoqXG4gICAgICogVGhlIGZsYWcgdG8gc3RvcCBldmVudCBidWJibGluZy5cbiAgICAgKiBAdHlwZSB7Ym9vbGVhbn1cbiAgICAgKiBAZGVwcmVjYXRlZFxuICAgICAqL1xuICAgIGdldCBjYW5jZWxCdWJibGUoKSB7XG4gICAgICAgIHJldHVybiBwZCh0aGlzKS5zdG9wcGVkXG4gICAgfSxcbiAgICBzZXQgY2FuY2VsQnViYmxlKHZhbHVlKSB7XG4gICAgICAgIGlmICghdmFsdWUpIHtcbiAgICAgICAgICAgIHJldHVyblxuICAgICAgICB9XG4gICAgICAgIGNvbnN0IGRhdGEgPSBwZCh0aGlzKTtcblxuICAgICAgICBkYXRhLnN0b3BwZWQgPSB0cnVlO1xuICAgICAgICBpZiAodHlwZW9mIGRhdGEuZXZlbnQuY2FuY2VsQnViYmxlID09PSBcImJvb2xlYW5cIikge1xuICAgICAgICAgICAgZGF0YS5ldmVudC5jYW5jZWxCdWJibGUgPSB0cnVlO1xuICAgICAgICB9XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSBmbGFnIHRvIGluZGljYXRlIGNhbmNlbGxhdGlvbiBzdGF0ZS5cbiAgICAgKiBAdHlwZSB7Ym9vbGVhbn1cbiAgICAgKiBAZGVwcmVjYXRlZFxuICAgICAqL1xuICAgIGdldCByZXR1cm5WYWx1ZSgpIHtcbiAgICAgICAgcmV0dXJuICFwZCh0aGlzKS5jYW5jZWxlZFxuICAgIH0sXG4gICAgc2V0IHJldHVyblZhbHVlKHZhbHVlKSB7XG4gICAgICAgIGlmICghdmFsdWUpIHtcbiAgICAgICAgICAgIHNldENhbmNlbEZsYWcocGQodGhpcykpO1xuICAgICAgICB9XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIEluaXRpYWxpemUgdGhpcyBldmVudCBvYmplY3QuIEJ1dCBkbyBub3RoaW5nIHVuZGVyIGV2ZW50IGRpc3BhdGNoaW5nLlxuICAgICAqIEBwYXJhbSB7c3RyaW5nfSB0eXBlIFRoZSBldmVudCB0eXBlLlxuICAgICAqIEBwYXJhbSB7Ym9vbGVhbn0gW2J1YmJsZXM9ZmFsc2VdIFRoZSBmbGFnIHRvIGJlIHBvc3NpYmxlIHRvIGJ1YmJsZSB1cC5cbiAgICAgKiBAcGFyYW0ge2Jvb2xlYW59IFtjYW5jZWxhYmxlPWZhbHNlXSBUaGUgZmxhZyB0byBiZSBwb3NzaWJsZSB0byBjYW5jZWwuXG4gICAgICogQGRlcHJlY2F0ZWRcbiAgICAgKi9cbiAgICBpbml0RXZlbnQoKSB7XG4gICAgICAgIC8vIERvIG5vdGhpbmcuXG4gICAgfSxcbn07XG5cbi8vIGBjb25zdHJ1Y3RvcmAgaXMgbm90IGVudW1lcmFibGUuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoRXZlbnQucHJvdG90eXBlLCBcImNvbnN0cnVjdG9yXCIsIHtcbiAgICB2YWx1ZTogRXZlbnQsXG4gICAgY29uZmlndXJhYmxlOiB0cnVlLFxuICAgIHdyaXRhYmxlOiB0cnVlLFxufSk7XG5cbi8vIEVuc3VyZSBgZXZlbnQgaW5zdGFuY2VvZiB3aW5kb3cuRXZlbnRgIGlzIGB0cnVlYC5cbmlmICh0eXBlb2Ygd2luZG93ICE9PSBcInVuZGVmaW5lZFwiICYmIHR5cGVvZiB3aW5kb3cuRXZlbnQgIT09IFwidW5kZWZpbmVkXCIpIHtcbiAgICBPYmplY3Quc2V0UHJvdG90eXBlT2YoRXZlbnQucHJvdG90eXBlLCB3aW5kb3cuRXZlbnQucHJvdG90eXBlKTtcblxuICAgIC8vIE1ha2UgYXNzb2NpYXRpb24gZm9yIHdyYXBwZXJzLlxuICAgIHdyYXBwZXJzLnNldCh3aW5kb3cuRXZlbnQucHJvdG90eXBlLCBFdmVudCk7XG59XG5cbi8qKlxuICogR2V0IHRoZSBwcm9wZXJ0eSBkZXNjcmlwdG9yIHRvIHJlZGlyZWN0IGEgZ2l2ZW4gcHJvcGVydHkuXG4gKiBAcGFyYW0ge3N0cmluZ30ga2V5IFByb3BlcnR5IG5hbWUgdG8gZGVmaW5lIHByb3BlcnR5IGRlc2NyaXB0b3IuXG4gKiBAcmV0dXJucyB7UHJvcGVydHlEZXNjcmlwdG9yfSBUaGUgcHJvcGVydHkgZGVzY3JpcHRvciB0byByZWRpcmVjdCB0aGUgcHJvcGVydHkuXG4gKiBAcHJpdmF0ZVxuICovXG5mdW5jdGlvbiBkZWZpbmVSZWRpcmVjdERlc2NyaXB0b3Ioa2V5KSB7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgZ2V0KCkge1xuICAgICAgICAgICAgcmV0dXJuIHBkKHRoaXMpLmV2ZW50W2tleV1cbiAgICAgICAgfSxcbiAgICAgICAgc2V0KHZhbHVlKSB7XG4gICAgICAgICAgICBwZCh0aGlzKS5ldmVudFtrZXldID0gdmFsdWU7XG4gICAgICAgIH0sXG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZSxcbiAgICAgICAgZW51bWVyYWJsZTogdHJ1ZSxcbiAgICB9XG59XG5cbi8qKlxuICogR2V0IHRoZSBwcm9wZXJ0eSBkZXNjcmlwdG9yIHRvIGNhbGwgYSBnaXZlbiBtZXRob2QgcHJvcGVydHkuXG4gKiBAcGFyYW0ge3N0cmluZ30ga2V5IFByb3BlcnR5IG5hbWUgdG8gZGVmaW5lIHByb3BlcnR5IGRlc2NyaXB0b3IuXG4gKiBAcmV0dXJucyB7UHJvcGVydHlEZXNjcmlwdG9yfSBUaGUgcHJvcGVydHkgZGVzY3JpcHRvciB0byBjYWxsIHRoZSBtZXRob2QgcHJvcGVydHkuXG4gKiBAcHJpdmF0ZVxuICovXG5mdW5jdGlvbiBkZWZpbmVDYWxsRGVzY3JpcHRvcihrZXkpIHtcbiAgICByZXR1cm4ge1xuICAgICAgICB2YWx1ZSgpIHtcbiAgICAgICAgICAgIGNvbnN0IGV2ZW50ID0gcGQodGhpcykuZXZlbnQ7XG4gICAgICAgICAgICByZXR1cm4gZXZlbnRba2V5XS5hcHBseShldmVudCwgYXJndW1lbnRzKVxuICAgICAgICB9LFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIGVudW1lcmFibGU6IHRydWUsXG4gICAgfVxufVxuXG4vKipcbiAqIERlZmluZSBuZXcgd3JhcHBlciBjbGFzcy5cbiAqIEBwYXJhbSB7RnVuY3Rpb259IEJhc2VFdmVudCBUaGUgYmFzZSB3cmFwcGVyIGNsYXNzLlxuICogQHBhcmFtIHtPYmplY3R9IHByb3RvIFRoZSBwcm90b3R5cGUgb2YgdGhlIG9yaWdpbmFsIGV2ZW50LlxuICogQHJldHVybnMge0Z1bmN0aW9ufSBUaGUgZGVmaW5lZCB3cmFwcGVyIGNsYXNzLlxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gZGVmaW5lV3JhcHBlcihCYXNlRXZlbnQsIHByb3RvKSB7XG4gICAgY29uc3Qga2V5cyA9IE9iamVjdC5rZXlzKHByb3RvKTtcbiAgICBpZiAoa2V5cy5sZW5ndGggPT09IDApIHtcbiAgICAgICAgcmV0dXJuIEJhc2VFdmVudFxuICAgIH1cblxuICAgIC8qKiBDdXN0b21FdmVudCAqL1xuICAgIGZ1bmN0aW9uIEN1c3RvbUV2ZW50KGV2ZW50VGFyZ2V0LCBldmVudCkge1xuICAgICAgICBCYXNlRXZlbnQuY2FsbCh0aGlzLCBldmVudFRhcmdldCwgZXZlbnQpO1xuICAgIH1cblxuICAgIEN1c3RvbUV2ZW50LnByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoQmFzZUV2ZW50LnByb3RvdHlwZSwge1xuICAgICAgICBjb25zdHJ1Y3RvcjogeyB2YWx1ZTogQ3VzdG9tRXZlbnQsIGNvbmZpZ3VyYWJsZTogdHJ1ZSwgd3JpdGFibGU6IHRydWUgfSxcbiAgICB9KTtcblxuICAgIC8vIERlZmluZSBhY2Nlc3NvcnMuXG4gICAgZm9yIChsZXQgaSA9IDA7IGkgPCBrZXlzLmxlbmd0aDsgKytpKSB7XG4gICAgICAgIGNvbnN0IGtleSA9IGtleXNbaV07XG4gICAgICAgIGlmICghKGtleSBpbiBCYXNlRXZlbnQucHJvdG90eXBlKSkge1xuICAgICAgICAgICAgY29uc3QgZGVzY3JpcHRvciA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3IocHJvdG8sIGtleSk7XG4gICAgICAgICAgICBjb25zdCBpc0Z1bmMgPSB0eXBlb2YgZGVzY3JpcHRvci52YWx1ZSA9PT0gXCJmdW5jdGlvblwiO1xuICAgICAgICAgICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFxuICAgICAgICAgICAgICAgIEN1c3RvbUV2ZW50LnByb3RvdHlwZSxcbiAgICAgICAgICAgICAgICBrZXksXG4gICAgICAgICAgICAgICAgaXNGdW5jXG4gICAgICAgICAgICAgICAgICAgID8gZGVmaW5lQ2FsbERlc2NyaXB0b3Ioa2V5KVxuICAgICAgICAgICAgICAgICAgICA6IGRlZmluZVJlZGlyZWN0RGVzY3JpcHRvcihrZXkpXG4gICAgICAgICAgICApO1xuICAgICAgICB9XG4gICAgfVxuXG4gICAgcmV0dXJuIEN1c3RvbUV2ZW50XG59XG5cbi8qKlxuICogR2V0IHRoZSB3cmFwcGVyIGNsYXNzIG9mIGEgZ2l2ZW4gcHJvdG90eXBlLlxuICogQHBhcmFtIHtPYmplY3R9IHByb3RvIFRoZSBwcm90b3R5cGUgb2YgdGhlIG9yaWdpbmFsIGV2ZW50IHRvIGdldCBpdHMgd3JhcHBlci5cbiAqIEByZXR1cm5zIHtGdW5jdGlvbn0gVGhlIHdyYXBwZXIgY2xhc3MuXG4gKiBAcHJpdmF0ZVxuICovXG5mdW5jdGlvbiBnZXRXcmFwcGVyKHByb3RvKSB7XG4gICAgaWYgKHByb3RvID09IG51bGwgfHwgcHJvdG8gPT09IE9iamVjdC5wcm90b3R5cGUpIHtcbiAgICAgICAgcmV0dXJuIEV2ZW50XG4gICAgfVxuXG4gICAgbGV0IHdyYXBwZXIgPSB3cmFwcGVycy5nZXQocHJvdG8pO1xuICAgIGlmICh3cmFwcGVyID09IG51bGwpIHtcbiAgICAgICAgd3JhcHBlciA9IGRlZmluZVdyYXBwZXIoZ2V0V3JhcHBlcihPYmplY3QuZ2V0UHJvdG90eXBlT2YocHJvdG8pKSwgcHJvdG8pO1xuICAgICAgICB3cmFwcGVycy5zZXQocHJvdG8sIHdyYXBwZXIpO1xuICAgIH1cbiAgICByZXR1cm4gd3JhcHBlclxufVxuXG4vKipcbiAqIFdyYXAgYSBnaXZlbiBldmVudCB0byBtYW5hZ2VtZW50IGEgZGlzcGF0Y2hpbmcuXG4gKiBAcGFyYW0ge0V2ZW50VGFyZ2V0fSBldmVudFRhcmdldCBUaGUgZXZlbnQgdGFyZ2V0IG9mIHRoaXMgZGlzcGF0Y2hpbmcuXG4gKiBAcGFyYW0ge09iamVjdH0gZXZlbnQgVGhlIGV2ZW50IHRvIHdyYXAuXG4gKiBAcmV0dXJucyB7RXZlbnR9IFRoZSB3cmFwcGVyIGluc3RhbmNlLlxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gd3JhcEV2ZW50KGV2ZW50VGFyZ2V0LCBldmVudCkge1xuICAgIGNvbnN0IFdyYXBwZXIgPSBnZXRXcmFwcGVyKE9iamVjdC5nZXRQcm90b3R5cGVPZihldmVudCkpO1xuICAgIHJldHVybiBuZXcgV3JhcHBlcihldmVudFRhcmdldCwgZXZlbnQpXG59XG5cbi8qKlxuICogR2V0IHRoZSBpbW1lZGlhdGVTdG9wcGVkIGZsYWcgb2YgYSBnaXZlbiBldmVudC5cbiAqIEBwYXJhbSB7RXZlbnR9IGV2ZW50IFRoZSBldmVudCB0byBnZXQuXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gVGhlIGZsYWcgdG8gc3RvcCBwcm9wYWdhdGlvbiBpbW1lZGlhdGVseS5cbiAqIEBwcml2YXRlXG4gKi9cbmZ1bmN0aW9uIGlzU3RvcHBlZChldmVudCkge1xuICAgIHJldHVybiBwZChldmVudCkuaW1tZWRpYXRlU3RvcHBlZFxufVxuXG4vKipcbiAqIFNldCB0aGUgY3VycmVudCBldmVudCBwaGFzZSBvZiBhIGdpdmVuIGV2ZW50LlxuICogQHBhcmFtIHtFdmVudH0gZXZlbnQgVGhlIGV2ZW50IHRvIHNldCBjdXJyZW50IHRhcmdldC5cbiAqIEBwYXJhbSB7bnVtYmVyfSBldmVudFBoYXNlIE5ldyBldmVudCBwaGFzZS5cbiAqIEByZXR1cm5zIHt2b2lkfVxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gc2V0RXZlbnRQaGFzZShldmVudCwgZXZlbnRQaGFzZSkge1xuICAgIHBkKGV2ZW50KS5ldmVudFBoYXNlID0gZXZlbnRQaGFzZTtcbn1cblxuLyoqXG4gKiBTZXQgdGhlIGN1cnJlbnQgdGFyZ2V0IG9mIGEgZ2l2ZW4gZXZlbnQuXG4gKiBAcGFyYW0ge0V2ZW50fSBldmVudCBUaGUgZXZlbnQgdG8gc2V0IGN1cnJlbnQgdGFyZ2V0LlxuICogQHBhcmFtIHtFdmVudFRhcmdldHxudWxsfSBjdXJyZW50VGFyZ2V0IE5ldyBjdXJyZW50IHRhcmdldC5cbiAqIEByZXR1cm5zIHt2b2lkfVxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gc2V0Q3VycmVudFRhcmdldChldmVudCwgY3VycmVudFRhcmdldCkge1xuICAgIHBkKGV2ZW50KS5jdXJyZW50VGFyZ2V0ID0gY3VycmVudFRhcmdldDtcbn1cblxuLyoqXG4gKiBTZXQgYSBwYXNzaXZlIGxpc3RlbmVyIG9mIGEgZ2l2ZW4gZXZlbnQuXG4gKiBAcGFyYW0ge0V2ZW50fSBldmVudCBUaGUgZXZlbnQgdG8gc2V0IGN1cnJlbnQgdGFyZ2V0LlxuICogQHBhcmFtIHtGdW5jdGlvbnxudWxsfSBwYXNzaXZlTGlzdGVuZXIgTmV3IHBhc3NpdmUgbGlzdGVuZXIuXG4gKiBAcmV0dXJucyB7dm9pZH1cbiAqIEBwcml2YXRlXG4gKi9cbmZ1bmN0aW9uIHNldFBhc3NpdmVMaXN0ZW5lcihldmVudCwgcGFzc2l2ZUxpc3RlbmVyKSB7XG4gICAgcGQoZXZlbnQpLnBhc3NpdmVMaXN0ZW5lciA9IHBhc3NpdmVMaXN0ZW5lcjtcbn1cblxuLyoqXG4gKiBAdHlwZWRlZiB7b2JqZWN0fSBMaXN0ZW5lck5vZGVcbiAqIEBwcm9wZXJ0eSB7RnVuY3Rpb259IGxpc3RlbmVyXG4gKiBAcHJvcGVydHkgezF8MnwzfSBsaXN0ZW5lclR5cGVcbiAqIEBwcm9wZXJ0eSB7Ym9vbGVhbn0gcGFzc2l2ZVxuICogQHByb3BlcnR5IHtib29sZWFufSBvbmNlXG4gKiBAcHJvcGVydHkge0xpc3RlbmVyTm9kZXxudWxsfSBuZXh0XG4gKiBAcHJpdmF0ZVxuICovXG5cbi8qKlxuICogQHR5cGUge1dlYWtNYXA8b2JqZWN0LCBNYXA8c3RyaW5nLCBMaXN0ZW5lck5vZGU+Pn1cbiAqIEBwcml2YXRlXG4gKi9cbmNvbnN0IGxpc3RlbmVyc01hcCA9IG5ldyBXZWFrTWFwKCk7XG5cbi8vIExpc3RlbmVyIHR5cGVzXG5jb25zdCBDQVBUVVJFID0gMTtcbmNvbnN0IEJVQkJMRSA9IDI7XG5jb25zdCBBVFRSSUJVVEUgPSAzO1xuXG4vKipcbiAqIENoZWNrIHdoZXRoZXIgYSBnaXZlbiB2YWx1ZSBpcyBhbiBvYmplY3Qgb3Igbm90LlxuICogQHBhcmFtIHthbnl9IHggVGhlIHZhbHVlIHRvIGNoZWNrLlxuICogQHJldHVybnMge2Jvb2xlYW59IGB0cnVlYCBpZiB0aGUgdmFsdWUgaXMgYW4gb2JqZWN0LlxuICovXG5mdW5jdGlvbiBpc09iamVjdCh4KSB7XG4gICAgcmV0dXJuIHggIT09IG51bGwgJiYgdHlwZW9mIHggPT09IFwib2JqZWN0XCIgLy9lc2xpbnQtZGlzYWJsZS1saW5lIG5vLXJlc3RyaWN0ZWQtc3ludGF4XG59XG5cbi8qKlxuICogR2V0IGxpc3RlbmVycy5cbiAqIEBwYXJhbSB7RXZlbnRUYXJnZXR9IGV2ZW50VGFyZ2V0IFRoZSBldmVudCB0YXJnZXQgdG8gZ2V0LlxuICogQHJldHVybnMge01hcDxzdHJpbmcsIExpc3RlbmVyTm9kZT59IFRoZSBsaXN0ZW5lcnMuXG4gKiBAcHJpdmF0ZVxuICovXG5mdW5jdGlvbiBnZXRMaXN0ZW5lcnMoZXZlbnRUYXJnZXQpIHtcbiAgICBjb25zdCBsaXN0ZW5lcnMgPSBsaXN0ZW5lcnNNYXAuZ2V0KGV2ZW50VGFyZ2V0KTtcbiAgICBpZiAobGlzdGVuZXJzID09IG51bGwpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcbiAgICAgICAgICAgIFwiJ3RoaXMnIGlzIGV4cGVjdGVkIGFuIEV2ZW50VGFyZ2V0IG9iamVjdCwgYnV0IGdvdCBhbm90aGVyIHZhbHVlLlwiXG4gICAgICAgIClcbiAgICB9XG4gICAgcmV0dXJuIGxpc3RlbmVyc1xufVxuXG4vKipcbiAqIEdldCB0aGUgcHJvcGVydHkgZGVzY3JpcHRvciBmb3IgdGhlIGV2ZW50IGF0dHJpYnV0ZSBvZiBhIGdpdmVuIGV2ZW50LlxuICogQHBhcmFtIHtzdHJpbmd9IGV2ZW50TmFtZSBUaGUgZXZlbnQgbmFtZSB0byBnZXQgcHJvcGVydHkgZGVzY3JpcHRvci5cbiAqIEByZXR1cm5zIHtQcm9wZXJ0eURlc2NyaXB0b3J9IFRoZSBwcm9wZXJ0eSBkZXNjcmlwdG9yLlxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gZGVmaW5lRXZlbnRBdHRyaWJ1dGVEZXNjcmlwdG9yKGV2ZW50TmFtZSkge1xuICAgIHJldHVybiB7XG4gICAgICAgIGdldCgpIHtcbiAgICAgICAgICAgIGNvbnN0IGxpc3RlbmVycyA9IGdldExpc3RlbmVycyh0aGlzKTtcbiAgICAgICAgICAgIGxldCBub2RlID0gbGlzdGVuZXJzLmdldChldmVudE5hbWUpO1xuICAgICAgICAgICAgd2hpbGUgKG5vZGUgIT0gbnVsbCkge1xuICAgICAgICAgICAgICAgIGlmIChub2RlLmxpc3RlbmVyVHlwZSA9PT0gQVRUUklCVVRFKSB7XG4gICAgICAgICAgICAgICAgICAgIHJldHVybiBub2RlLmxpc3RlbmVyXG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIG5vZGUgPSBub2RlLm5leHQ7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICByZXR1cm4gbnVsbFxuICAgICAgICB9LFxuXG4gICAgICAgIHNldChsaXN0ZW5lcikge1xuICAgICAgICAgICAgaWYgKHR5cGVvZiBsaXN0ZW5lciAhPT0gXCJmdW5jdGlvblwiICYmICFpc09iamVjdChsaXN0ZW5lcikpIHtcbiAgICAgICAgICAgICAgICBsaXN0ZW5lciA9IG51bGw7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tcGFyYW0tcmVhc3NpZ25cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGNvbnN0IGxpc3RlbmVycyA9IGdldExpc3RlbmVycyh0aGlzKTtcblxuICAgICAgICAgICAgLy8gVHJhdmVyc2UgdG8gdGhlIHRhaWwgd2hpbGUgcmVtb3Zpbmcgb2xkIHZhbHVlLlxuICAgICAgICAgICAgbGV0IHByZXYgPSBudWxsO1xuICAgICAgICAgICAgbGV0IG5vZGUgPSBsaXN0ZW5lcnMuZ2V0KGV2ZW50TmFtZSk7XG4gICAgICAgICAgICB3aGlsZSAobm9kZSAhPSBudWxsKSB7XG4gICAgICAgICAgICAgICAgaWYgKG5vZGUubGlzdGVuZXJUeXBlID09PSBBVFRSSUJVVEUpIHtcbiAgICAgICAgICAgICAgICAgICAgLy8gUmVtb3ZlIG9sZCB2YWx1ZS5cbiAgICAgICAgICAgICAgICAgICAgaWYgKHByZXYgIT09IG51bGwpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgIHByZXYubmV4dCA9IG5vZGUubmV4dDtcbiAgICAgICAgICAgICAgICAgICAgfSBlbHNlIGlmIChub2RlLm5leHQgIT09IG51bGwpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgIGxpc3RlbmVycy5zZXQoZXZlbnROYW1lLCBub2RlLm5leHQpO1xuICAgICAgICAgICAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgICAgICAgICAgICAgbGlzdGVuZXJzLmRlbGV0ZShldmVudE5hbWUpO1xuICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgICAgICAgICAgcHJldiA9IG5vZGU7XG4gICAgICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICAgICAgbm9kZSA9IG5vZGUubmV4dDtcbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLy8gQWRkIG5ldyB2YWx1ZS5cbiAgICAgICAgICAgIGlmIChsaXN0ZW5lciAhPT0gbnVsbCkge1xuICAgICAgICAgICAgICAgIGNvbnN0IG5ld05vZGUgPSB7XG4gICAgICAgICAgICAgICAgICAgIGxpc3RlbmVyLFxuICAgICAgICAgICAgICAgICAgICBsaXN0ZW5lclR5cGU6IEFUVFJJQlVURSxcbiAgICAgICAgICAgICAgICAgICAgcGFzc2l2ZTogZmFsc2UsXG4gICAgICAgICAgICAgICAgICAgIG9uY2U6IGZhbHNlLFxuICAgICAgICAgICAgICAgICAgICBuZXh0OiBudWxsLFxuICAgICAgICAgICAgICAgIH07XG4gICAgICAgICAgICAgICAgaWYgKHByZXYgPT09IG51bGwpIHtcbiAgICAgICAgICAgICAgICAgICAgbGlzdGVuZXJzLnNldChldmVudE5hbWUsIG5ld05vZGUpO1xuICAgICAgICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICAgICAgICAgIHByZXYubmV4dCA9IG5ld05vZGU7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIGVudW1lcmFibGU6IHRydWUsXG4gICAgfVxufVxuXG4vKipcbiAqIERlZmluZSBhbiBldmVudCBhdHRyaWJ1dGUgKGUuZy4gYGV2ZW50VGFyZ2V0Lm9uY2xpY2tgKS5cbiAqIEBwYXJhbSB7T2JqZWN0fSBldmVudFRhcmdldFByb3RvdHlwZSBUaGUgZXZlbnQgdGFyZ2V0IHByb3RvdHlwZSB0byBkZWZpbmUgYW4gZXZlbnQgYXR0cmJpdGUuXG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lIFRoZSBldmVudCBuYW1lIHRvIGRlZmluZS5cbiAqIEByZXR1cm5zIHt2b2lkfVxuICovXG5mdW5jdGlvbiBkZWZpbmVFdmVudEF0dHJpYnV0ZShldmVudFRhcmdldFByb3RvdHlwZSwgZXZlbnROYW1lKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFxuICAgICAgICBldmVudFRhcmdldFByb3RvdHlwZSxcbiAgICAgICAgYG9uJHtldmVudE5hbWV9YCxcbiAgICAgICAgZGVmaW5lRXZlbnRBdHRyaWJ1dGVEZXNjcmlwdG9yKGV2ZW50TmFtZSlcbiAgICApO1xufVxuXG4vKipcbiAqIERlZmluZSBhIGN1c3RvbSBFdmVudFRhcmdldCB3aXRoIGV2ZW50IGF0dHJpYnV0ZXMuXG4gKiBAcGFyYW0ge3N0cmluZ1tdfSBldmVudE5hbWVzIEV2ZW50IG5hbWVzIGZvciBldmVudCBhdHRyaWJ1dGVzLlxuICogQHJldHVybnMge0V2ZW50VGFyZ2V0fSBUaGUgY3VzdG9tIEV2ZW50VGFyZ2V0LlxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gZGVmaW5lQ3VzdG9tRXZlbnRUYXJnZXQoZXZlbnROYW1lcykge1xuICAgIC8qKiBDdXN0b21FdmVudFRhcmdldCAqL1xuICAgIGZ1bmN0aW9uIEN1c3RvbUV2ZW50VGFyZ2V0KCkge1xuICAgICAgICBFdmVudFRhcmdldC5jYWxsKHRoaXMpO1xuICAgIH1cblxuICAgIEN1c3RvbUV2ZW50VGFyZ2V0LnByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoRXZlbnRUYXJnZXQucHJvdG90eXBlLCB7XG4gICAgICAgIGNvbnN0cnVjdG9yOiB7XG4gICAgICAgICAgICB2YWx1ZTogQ3VzdG9tRXZlbnRUYXJnZXQsXG4gICAgICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgICAgICB3cml0YWJsZTogdHJ1ZSxcbiAgICAgICAgfSxcbiAgICB9KTtcblxuICAgIGZvciAobGV0IGkgPSAwOyBpIDwgZXZlbnROYW1lcy5sZW5ndGg7ICsraSkge1xuICAgICAgICBkZWZpbmVFdmVudEF0dHJpYnV0ZShDdXN0b21FdmVudFRhcmdldC5wcm90b3R5cGUsIGV2ZW50TmFtZXNbaV0pO1xuICAgIH1cblxuICAgIHJldHVybiBDdXN0b21FdmVudFRhcmdldFxufVxuXG4vKipcbiAqIEV2ZW50VGFyZ2V0LlxuICpcbiAqIC0gVGhpcyBpcyBjb25zdHJ1Y3RvciBpZiBubyBhcmd1bWVudHMuXG4gKiAtIFRoaXMgaXMgYSBmdW5jdGlvbiB3aGljaCByZXR1cm5zIGEgQ3VzdG9tRXZlbnRUYXJnZXQgY29uc3RydWN0b3IgaWYgdGhlcmUgYXJlIGFyZ3VtZW50cy5cbiAqXG4gKiBGb3IgZXhhbXBsZTpcbiAqXG4gKiAgICAgY2xhc3MgQSBleHRlbmRzIEV2ZW50VGFyZ2V0IHt9XG4gKiAgICAgY2xhc3MgQiBleHRlbmRzIEV2ZW50VGFyZ2V0KFwibWVzc2FnZVwiKSB7fVxuICogICAgIGNsYXNzIEMgZXh0ZW5kcyBFdmVudFRhcmdldChcIm1lc3NhZ2VcIiwgXCJlcnJvclwiKSB7fVxuICogICAgIGNsYXNzIEQgZXh0ZW5kcyBFdmVudFRhcmdldChbXCJtZXNzYWdlXCIsIFwiZXJyb3JcIl0pIHt9XG4gKi9cbmZ1bmN0aW9uIEV2ZW50VGFyZ2V0KCkge1xuICAgIC8qZXNsaW50LWRpc2FibGUgY29uc2lzdGVudC1yZXR1cm4gKi9cbiAgICBpZiAodGhpcyBpbnN0YW5jZW9mIEV2ZW50VGFyZ2V0KSB7XG4gICAgICAgIGxpc3RlbmVyc01hcC5zZXQodGhpcywgbmV3IE1hcCgpKTtcbiAgICAgICAgcmV0dXJuXG4gICAgfVxuICAgIGlmIChhcmd1bWVudHMubGVuZ3RoID09PSAxICYmIEFycmF5LmlzQXJyYXkoYXJndW1lbnRzWzBdKSkge1xuICAgICAgICByZXR1cm4gZGVmaW5lQ3VzdG9tRXZlbnRUYXJnZXQoYXJndW1lbnRzWzBdKVxuICAgIH1cbiAgICBpZiAoYXJndW1lbnRzLmxlbmd0aCA+IDApIHtcbiAgICAgICAgY29uc3QgdHlwZXMgPSBuZXcgQXJyYXkoYXJndW1lbnRzLmxlbmd0aCk7XG4gICAgICAgIGZvciAobGV0IGkgPSAwOyBpIDwgYXJndW1lbnRzLmxlbmd0aDsgKytpKSB7XG4gICAgICAgICAgICB0eXBlc1tpXSA9IGFyZ3VtZW50c1tpXTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gZGVmaW5lQ3VzdG9tRXZlbnRUYXJnZXQodHlwZXMpXG4gICAgfVxuICAgIHRocm93IG5ldyBUeXBlRXJyb3IoXCJDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb25cIilcbiAgICAvKmVzbGludC1lbmFibGUgY29uc2lzdGVudC1yZXR1cm4gKi9cbn1cblxuLy8gU2hvdWxkIGJlIGVudW1lcmFibGUsIGJ1dCBjbGFzcyBtZXRob2RzIGFyZSBub3QgZW51bWVyYWJsZS5cbkV2ZW50VGFyZ2V0LnByb3RvdHlwZSA9IHtcbiAgICAvKipcbiAgICAgKiBBZGQgYSBnaXZlbiBsaXN0ZW5lciB0byB0aGlzIGV2ZW50IHRhcmdldC5cbiAgICAgKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lIFRoZSBldmVudCBuYW1lIHRvIGFkZC5cbiAgICAgKiBAcGFyYW0ge0Z1bmN0aW9ufSBsaXN0ZW5lciBUaGUgbGlzdGVuZXIgdG8gYWRkLlxuICAgICAqIEBwYXJhbSB7Ym9vbGVhbnx7Y2FwdHVyZT86Ym9vbGVhbixwYXNzaXZlPzpib29sZWFuLG9uY2U/OmJvb2xlYW59fSBbb3B0aW9uc10gVGhlIG9wdGlvbnMgZm9yIHRoaXMgbGlzdGVuZXIuXG4gICAgICogQHJldHVybnMge3ZvaWR9XG4gICAgICovXG4gICAgYWRkRXZlbnRMaXN0ZW5lcihldmVudE5hbWUsIGxpc3RlbmVyLCBvcHRpb25zKSB7XG4gICAgICAgIGlmIChsaXN0ZW5lciA9PSBudWxsKSB7XG4gICAgICAgICAgICByZXR1cm5cbiAgICAgICAgfVxuICAgICAgICBpZiAodHlwZW9mIGxpc3RlbmVyICE9PSBcImZ1bmN0aW9uXCIgJiYgIWlzT2JqZWN0KGxpc3RlbmVyKSkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcIidsaXN0ZW5lcicgc2hvdWxkIGJlIGEgZnVuY3Rpb24gb3IgYW4gb2JqZWN0LlwiKVxuICAgICAgICB9XG5cbiAgICAgICAgY29uc3QgbGlzdGVuZXJzID0gZ2V0TGlzdGVuZXJzKHRoaXMpO1xuICAgICAgICBjb25zdCBvcHRpb25zSXNPYmogPSBpc09iamVjdChvcHRpb25zKTtcbiAgICAgICAgY29uc3QgY2FwdHVyZSA9IG9wdGlvbnNJc09ialxuICAgICAgICAgICAgPyBCb29sZWFuKG9wdGlvbnMuY2FwdHVyZSlcbiAgICAgICAgICAgIDogQm9vbGVhbihvcHRpb25zKTtcbiAgICAgICAgY29uc3QgbGlzdGVuZXJUeXBlID0gY2FwdHVyZSA/IENBUFRVUkUgOiBCVUJCTEU7XG4gICAgICAgIGNvbnN0IG5ld05vZGUgPSB7XG4gICAgICAgICAgICBsaXN0ZW5lcixcbiAgICAgICAgICAgIGxpc3RlbmVyVHlwZSxcbiAgICAgICAgICAgIHBhc3NpdmU6IG9wdGlvbnNJc09iaiAmJiBCb29sZWFuKG9wdGlvbnMucGFzc2l2ZSksXG4gICAgICAgICAgICBvbmNlOiBvcHRpb25zSXNPYmogJiYgQm9vbGVhbihvcHRpb25zLm9uY2UpLFxuICAgICAgICAgICAgbmV4dDogbnVsbCxcbiAgICAgICAgfTtcblxuICAgICAgICAvLyBTZXQgaXQgYXMgdGhlIGZpcnN0IG5vZGUgaWYgdGhlIGZpcnN0IG5vZGUgaXMgbnVsbC5cbiAgICAgICAgbGV0IG5vZGUgPSBsaXN0ZW5lcnMuZ2V0KGV2ZW50TmFtZSk7XG4gICAgICAgIGlmIChub2RlID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIGxpc3RlbmVycy5zZXQoZXZlbnROYW1lLCBuZXdOb2RlKTtcbiAgICAgICAgICAgIHJldHVyblxuICAgICAgICB9XG5cbiAgICAgICAgLy8gVHJhdmVyc2UgdG8gdGhlIHRhaWwgd2hpbGUgY2hlY2tpbmcgZHVwbGljYXRpb24uLlxuICAgICAgICBsZXQgcHJldiA9IG51bGw7XG4gICAgICAgIHdoaWxlIChub2RlICE9IG51bGwpIHtcbiAgICAgICAgICAgIGlmIChcbiAgICAgICAgICAgICAgICBub2RlLmxpc3RlbmVyID09PSBsaXN0ZW5lciAmJlxuICAgICAgICAgICAgICAgIG5vZGUubGlzdGVuZXJUeXBlID09PSBsaXN0ZW5lclR5cGVcbiAgICAgICAgICAgICkge1xuICAgICAgICAgICAgICAgIC8vIFNob3VsZCBpZ25vcmUgZHVwbGljYXRpb24uXG4gICAgICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBwcmV2ID0gbm9kZTtcbiAgICAgICAgICAgIG5vZGUgPSBub2RlLm5leHQ7XG4gICAgICAgIH1cblxuICAgICAgICAvLyBBZGQgaXQuXG4gICAgICAgIHByZXYubmV4dCA9IG5ld05vZGU7XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFJlbW92ZSBhIGdpdmVuIGxpc3RlbmVyIGZyb20gdGhpcyBldmVudCB0YXJnZXQuXG4gICAgICogQHBhcmFtIHtzdHJpbmd9IGV2ZW50TmFtZSBUaGUgZXZlbnQgbmFtZSB0byByZW1vdmUuXG4gICAgICogQHBhcmFtIHtGdW5jdGlvbn0gbGlzdGVuZXIgVGhlIGxpc3RlbmVyIHRvIHJlbW92ZS5cbiAgICAgKiBAcGFyYW0ge2Jvb2xlYW58e2NhcHR1cmU/OmJvb2xlYW4scGFzc2l2ZT86Ym9vbGVhbixvbmNlPzpib29sZWFufX0gW29wdGlvbnNdIFRoZSBvcHRpb25zIGZvciB0aGlzIGxpc3RlbmVyLlxuICAgICAqIEByZXR1cm5zIHt2b2lkfVxuICAgICAqL1xuICAgIHJlbW92ZUV2ZW50TGlzdGVuZXIoZXZlbnROYW1lLCBsaXN0ZW5lciwgb3B0aW9ucykge1xuICAgICAgICBpZiAobGlzdGVuZXIgPT0gbnVsbCkge1xuICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgIH1cblxuICAgICAgICBjb25zdCBsaXN0ZW5lcnMgPSBnZXRMaXN0ZW5lcnModGhpcyk7XG4gICAgICAgIGNvbnN0IGNhcHR1cmUgPSBpc09iamVjdChvcHRpb25zKVxuICAgICAgICAgICAgPyBCb29sZWFuKG9wdGlvbnMuY2FwdHVyZSlcbiAgICAgICAgICAgIDogQm9vbGVhbihvcHRpb25zKTtcbiAgICAgICAgY29uc3QgbGlzdGVuZXJUeXBlID0gY2FwdHVyZSA/IENBUFRVUkUgOiBCVUJCTEU7XG5cbiAgICAgICAgbGV0IHByZXYgPSBudWxsO1xuICAgICAgICBsZXQgbm9kZSA9IGxpc3RlbmVycy5nZXQoZXZlbnROYW1lKTtcbiAgICAgICAgd2hpbGUgKG5vZGUgIT0gbnVsbCkge1xuICAgICAgICAgICAgaWYgKFxuICAgICAgICAgICAgICAgIG5vZGUubGlzdGVuZXIgPT09IGxpc3RlbmVyICYmXG4gICAgICAgICAgICAgICAgbm9kZS5saXN0ZW5lclR5cGUgPT09IGxpc3RlbmVyVHlwZVxuICAgICAgICAgICAgKSB7XG4gICAgICAgICAgICAgICAgaWYgKHByZXYgIT09IG51bGwpIHtcbiAgICAgICAgICAgICAgICAgICAgcHJldi5uZXh0ID0gbm9kZS5uZXh0O1xuICAgICAgICAgICAgICAgIH0gZWxzZSBpZiAobm9kZS5uZXh0ICE9PSBudWxsKSB7XG4gICAgICAgICAgICAgICAgICAgIGxpc3RlbmVycy5zZXQoZXZlbnROYW1lLCBub2RlLm5leHQpO1xuICAgICAgICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICAgICAgICAgIGxpc3RlbmVycy5kZWxldGUoZXZlbnROYW1lKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIHByZXYgPSBub2RlO1xuICAgICAgICAgICAgbm9kZSA9IG5vZGUubmV4dDtcbiAgICAgICAgfVxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBEaXNwYXRjaCBhIGdpdmVuIGV2ZW50LlxuICAgICAqIEBwYXJhbSB7RXZlbnR8e3R5cGU6c3RyaW5nfX0gZXZlbnQgVGhlIGV2ZW50IHRvIGRpc3BhdGNoLlxuICAgICAqIEByZXR1cm5zIHtib29sZWFufSBgZmFsc2VgIGlmIGNhbmNlbGVkLlxuICAgICAqL1xuICAgIGRpc3BhdGNoRXZlbnQoZXZlbnQpIHtcbiAgICAgICAgaWYgKGV2ZW50ID09IG51bGwgfHwgdHlwZW9mIGV2ZW50LnR5cGUgIT09IFwic3RyaW5nXCIpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1wiZXZlbnQudHlwZVwiIHNob3VsZCBiZSBhIHN0cmluZy4nKVxuICAgICAgICB9XG5cbiAgICAgICAgLy8gSWYgbGlzdGVuZXJzIGFyZW4ndCByZWdpc3RlcmVkLCB0ZXJtaW5hdGUuXG4gICAgICAgIGNvbnN0IGxpc3RlbmVycyA9IGdldExpc3RlbmVycyh0aGlzKTtcbiAgICAgICAgY29uc3QgZXZlbnROYW1lID0gZXZlbnQudHlwZTtcbiAgICAgICAgbGV0IG5vZGUgPSBsaXN0ZW5lcnMuZ2V0KGV2ZW50TmFtZSk7XG4gICAgICAgIGlmIChub2RlID09IG51bGwpIHtcbiAgICAgICAgICAgIHJldHVybiB0cnVlXG4gICAgICAgIH1cblxuICAgICAgICAvLyBTaW5jZSB3ZSBjYW5ub3QgcmV3cml0ZSBzZXZlcmFsIHByb3BlcnRpZXMsIHNvIHdyYXAgb2JqZWN0LlxuICAgICAgICBjb25zdCB3cmFwcGVkRXZlbnQgPSB3cmFwRXZlbnQodGhpcywgZXZlbnQpO1xuXG4gICAgICAgIC8vIFRoaXMgZG9lc24ndCBwcm9jZXNzIGNhcHR1cmluZyBwaGFzZSBhbmQgYnViYmxpbmcgcGhhc2UuXG4gICAgICAgIC8vIFRoaXMgaXNuJ3QgcGFydGljaXBhdGluZyBpbiBhIHRyZWUuXG4gICAgICAgIGxldCBwcmV2ID0gbnVsbDtcbiAgICAgICAgd2hpbGUgKG5vZGUgIT0gbnVsbCkge1xuICAgICAgICAgICAgLy8gUmVtb3ZlIHRoaXMgbGlzdGVuZXIgaWYgaXQncyBvbmNlXG4gICAgICAgICAgICBpZiAobm9kZS5vbmNlKSB7XG4gICAgICAgICAgICAgICAgaWYgKHByZXYgIT09IG51bGwpIHtcbiAgICAgICAgICAgICAgICAgICAgcHJldi5uZXh0ID0gbm9kZS5uZXh0O1xuICAgICAgICAgICAgICAgIH0gZWxzZSBpZiAobm9kZS5uZXh0ICE9PSBudWxsKSB7XG4gICAgICAgICAgICAgICAgICAgIGxpc3RlbmVycy5zZXQoZXZlbnROYW1lLCBub2RlLm5leHQpO1xuICAgICAgICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICAgICAgICAgIGxpc3RlbmVycy5kZWxldGUoZXZlbnROYW1lKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgICAgIHByZXYgPSBub2RlO1xuICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICAvLyBDYWxsIHRoaXMgbGlzdGVuZXJcbiAgICAgICAgICAgIHNldFBhc3NpdmVMaXN0ZW5lcihcbiAgICAgICAgICAgICAgICB3cmFwcGVkRXZlbnQsXG4gICAgICAgICAgICAgICAgbm9kZS5wYXNzaXZlID8gbm9kZS5saXN0ZW5lciA6IG51bGxcbiAgICAgICAgICAgICk7XG4gICAgICAgICAgICBpZiAodHlwZW9mIG5vZGUubGlzdGVuZXIgPT09IFwiZnVuY3Rpb25cIikge1xuICAgICAgICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICAgICAgICAgIG5vZGUubGlzdGVuZXIuY2FsbCh0aGlzLCB3cmFwcGVkRXZlbnQpO1xuICAgICAgICAgICAgICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgICAgICAgICAgICAgICBpZiAoXG4gICAgICAgICAgICAgICAgICAgICAgICB0eXBlb2YgY29uc29sZSAhPT0gXCJ1bmRlZmluZWRcIiAmJlxuICAgICAgICAgICAgICAgICAgICAgICAgdHlwZW9mIGNvbnNvbGUuZXJyb3IgPT09IFwiZnVuY3Rpb25cIlxuICAgICAgICAgICAgICAgICAgICApIHtcbiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoZXJyKTtcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH0gZWxzZSBpZiAoXG4gICAgICAgICAgICAgICAgbm9kZS5saXN0ZW5lclR5cGUgIT09IEFUVFJJQlVURSAmJlxuICAgICAgICAgICAgICAgIHR5cGVvZiBub2RlLmxpc3RlbmVyLmhhbmRsZUV2ZW50ID09PSBcImZ1bmN0aW9uXCJcbiAgICAgICAgICAgICkge1xuICAgICAgICAgICAgICAgIG5vZGUubGlzdGVuZXIuaGFuZGxlRXZlbnQod3JhcHBlZEV2ZW50KTtcbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLy8gQnJlYWsgaWYgYGV2ZW50LnN0b3BJbW1lZGlhdGVQcm9wYWdhdGlvbmAgd2FzIGNhbGxlZC5cbiAgICAgICAgICAgIGlmIChpc1N0b3BwZWQod3JhcHBlZEV2ZW50KSkge1xuICAgICAgICAgICAgICAgIGJyZWFrXG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIG5vZGUgPSBub2RlLm5leHQ7XG4gICAgICAgIH1cbiAgICAgICAgc2V0UGFzc2l2ZUxpc3RlbmVyKHdyYXBwZWRFdmVudCwgbnVsbCk7XG4gICAgICAgIHNldEV2ZW50UGhhc2Uod3JhcHBlZEV2ZW50LCAwKTtcbiAgICAgICAgc2V0Q3VycmVudFRhcmdldCh3cmFwcGVkRXZlbnQsIG51bGwpO1xuXG4gICAgICAgIHJldHVybiAhd3JhcHBlZEV2ZW50LmRlZmF1bHRQcmV2ZW50ZWRcbiAgICB9LFxufTtcblxuLy8gYGNvbnN0cnVjdG9yYCBpcyBub3QgZW51bWVyYWJsZS5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShFdmVudFRhcmdldC5wcm90b3R5cGUsIFwiY29uc3RydWN0b3JcIiwge1xuICAgIHZhbHVlOiBFdmVudFRhcmdldCxcbiAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgd3JpdGFibGU6IHRydWUsXG59KTtcblxuLy8gRW5zdXJlIGBldmVudFRhcmdldCBpbnN0YW5jZW9mIHdpbmRvdy5FdmVudFRhcmdldGAgaXMgYHRydWVgLlxuaWYgKFxuICAgIHR5cGVvZiB3aW5kb3cgIT09IFwidW5kZWZpbmVkXCIgJiZcbiAgICB0eXBlb2Ygd2luZG93LkV2ZW50VGFyZ2V0ICE9PSBcInVuZGVmaW5lZFwiXG4pIHtcbiAgICBPYmplY3Quc2V0UHJvdG90eXBlT2YoRXZlbnRUYXJnZXQucHJvdG90eXBlLCB3aW5kb3cuRXZlbnRUYXJnZXQucHJvdG90eXBlKTtcbn1cblxuZXhwb3J0cy5kZWZpbmVFdmVudEF0dHJpYnV0ZSA9IGRlZmluZUV2ZW50QXR0cmlidXRlO1xuZXhwb3J0cy5FdmVudFRhcmdldCA9IEV2ZW50VGFyZ2V0O1xuZXhwb3J0cy5kZWZhdWx0ID0gRXZlbnRUYXJnZXQ7XG5cbm1vZHVsZS5leHBvcnRzID0gRXZlbnRUYXJnZXRcbm1vZHVsZS5leHBvcnRzLkV2ZW50VGFyZ2V0ID0gbW9kdWxlLmV4cG9ydHNbXCJkZWZhdWx0XCJdID0gRXZlbnRUYXJnZXRcbm1vZHVsZS5leHBvcnRzLmRlZmluZUV2ZW50QXR0cmlidXRlID0gZGVmaW5lRXZlbnRBdHRyaWJ1dGVcbi8vIyBzb3VyY2VNYXBwaW5nVVJMPWV2ZW50LXRhcmdldC1zaGltLmpzLm1hcFxuIiwiY29uc3Qge1JlYWRhYmxlfSA9IHJlcXVpcmUoJ3N0cmVhbScpO1xuXG4vKipcbiAqIEB0eXBlIHtXZWFrTWFwPEJsb2IsIHt0eXBlOiBzdHJpbmcsIHNpemU6IG51bWJlciwgcGFydHM6IChCbG9iIHwgQnVmZmVyKVtdIH0+fVxuICovXG5jb25zdCB3bSA9IG5ldyBXZWFrTWFwKCk7XG5cbmFzeW5jIGZ1bmN0aW9uICogcmVhZChwYXJ0cykge1xuXHRmb3IgKGNvbnN0IHBhcnQgb2YgcGFydHMpIHtcblx0XHRpZiAoJ3N0cmVhbScgaW4gcGFydCkge1xuXHRcdFx0eWllbGQgKiBwYXJ0LnN0cmVhbSgpO1xuXHRcdH0gZWxzZSB7XG5cdFx0XHR5aWVsZCBwYXJ0O1xuXHRcdH1cblx0fVxufVxuXG5jbGFzcyBCbG9iIHtcblx0LyoqXG5cdCAqIFRoZSBCbG9iKCkgY29uc3RydWN0b3IgcmV0dXJucyBhIG5ldyBCbG9iIG9iamVjdC4gVGhlIGNvbnRlbnRcblx0ICogb2YgdGhlIGJsb2IgY29uc2lzdHMgb2YgdGhlIGNvbmNhdGVuYXRpb24gb2YgdGhlIHZhbHVlcyBnaXZlblxuXHQgKiBpbiB0aGUgcGFyYW1ldGVyIGFycmF5LlxuXHQgKlxuXHQgKiBAcGFyYW0geyhBcnJheUJ1ZmZlckxpa2UgfCBBcnJheUJ1ZmZlclZpZXcgfCBCbG9iIHwgQnVmZmVyIHwgc3RyaW5nKVtdfSBibG9iUGFydHNcblx0ICogQHBhcmFtIHt7IHR5cGU/OiBzdHJpbmcgfX0gW29wdGlvbnNdXG5cdCAqL1xuXHRjb25zdHJ1Y3RvcihibG9iUGFydHMgPSBbXSwgb3B0aW9ucyA9IHt0eXBlOiAnJ30pIHtcblx0XHRsZXQgc2l6ZSA9IDA7XG5cblx0XHRjb25zdCBwYXJ0cyA9IGJsb2JQYXJ0cy5tYXAoZWxlbWVudCA9PiB7XG5cdFx0XHRsZXQgYnVmZmVyO1xuXHRcdFx0aWYgKGVsZW1lbnQgaW5zdGFuY2VvZiBCdWZmZXIpIHtcblx0XHRcdFx0YnVmZmVyID0gZWxlbWVudDtcblx0XHRcdH0gZWxzZSBpZiAoQXJyYXlCdWZmZXIuaXNWaWV3KGVsZW1lbnQpKSB7XG5cdFx0XHRcdGJ1ZmZlciA9IEJ1ZmZlci5mcm9tKGVsZW1lbnQuYnVmZmVyLCBlbGVtZW50LmJ5dGVPZmZzZXQsIGVsZW1lbnQuYnl0ZUxlbmd0aCk7XG5cdFx0XHR9IGVsc2UgaWYgKGVsZW1lbnQgaW5zdGFuY2VvZiBBcnJheUJ1ZmZlcikge1xuXHRcdFx0XHRidWZmZXIgPSBCdWZmZXIuZnJvbShlbGVtZW50KTtcblx0XHRcdH0gZWxzZSBpZiAoZWxlbWVudCBpbnN0YW5jZW9mIEJsb2IpIHtcblx0XHRcdFx0YnVmZmVyID0gZWxlbWVudDtcblx0XHRcdH0gZWxzZSB7XG5cdFx0XHRcdGJ1ZmZlciA9IEJ1ZmZlci5mcm9tKHR5cGVvZiBlbGVtZW50ID09PSAnc3RyaW5nJyA/IGVsZW1lbnQgOiBTdHJpbmcoZWxlbWVudCkpO1xuXHRcdFx0fVxuXG5cdFx0XHRzaXplICs9IGJ1ZmZlci5sZW5ndGggfHwgYnVmZmVyLnNpemUgfHwgMDtcblx0XHRcdHJldHVybiBidWZmZXI7XG5cdFx0fSk7XG5cblx0XHRjb25zdCB0eXBlID0gb3B0aW9ucy50eXBlID09PSB1bmRlZmluZWQgPyAnJyA6IFN0cmluZyhvcHRpb25zLnR5cGUpLnRvTG93ZXJDYXNlKCk7XG5cblx0XHR3bS5zZXQodGhpcywge1xuXHRcdFx0dHlwZTogL1teXFx1MDAyMC1cXHUwMDdFXS8udGVzdCh0eXBlKSA/ICcnIDogdHlwZSxcblx0XHRcdHNpemUsXG5cdFx0XHRwYXJ0c1xuXHRcdH0pO1xuXHR9XG5cblx0LyoqXG5cdCAqIFRoZSBCbG9iIGludGVyZmFjZSdzIHNpemUgcHJvcGVydHkgcmV0dXJucyB0aGVcblx0ICogc2l6ZSBvZiB0aGUgQmxvYiBpbiBieXRlcy5cblx0ICovXG5cdGdldCBzaXplKCkge1xuXHRcdHJldHVybiB3bS5nZXQodGhpcykuc2l6ZTtcblx0fVxuXG5cdC8qKlxuXHQgKiBUaGUgdHlwZSBwcm9wZXJ0eSBvZiBhIEJsb2Igb2JqZWN0IHJldHVybnMgdGhlIE1JTUUgdHlwZSBvZiB0aGUgZmlsZS5cblx0ICovXG5cdGdldCB0eXBlKCkge1xuXHRcdHJldHVybiB3bS5nZXQodGhpcykudHlwZTtcblx0fVxuXG5cdC8qKlxuXHQgKiBUaGUgdGV4dCgpIG1ldGhvZCBpbiB0aGUgQmxvYiBpbnRlcmZhY2UgcmV0dXJucyBhIFByb21pc2Vcblx0ICogdGhhdCByZXNvbHZlcyB3aXRoIGEgc3RyaW5nIGNvbnRhaW5pbmcgdGhlIGNvbnRlbnRzIG9mXG5cdCAqIHRoZSBibG9iLCBpbnRlcnByZXRlZCBhcyBVVEYtOC5cblx0ICpcblx0ICogQHJldHVybiB7UHJvbWlzZTxzdHJpbmc+fVxuXHQgKi9cblx0YXN5bmMgdGV4dCgpIHtcblx0XHRyZXR1cm4gQnVmZmVyLmZyb20oYXdhaXQgdGhpcy5hcnJheUJ1ZmZlcigpKS50b1N0cmluZygpO1xuXHR9XG5cblx0LyoqXG5cdCAqIFRoZSBhcnJheUJ1ZmZlcigpIG1ldGhvZCBpbiB0aGUgQmxvYiBpbnRlcmZhY2UgcmV0dXJucyBhXG5cdCAqIFByb21pc2UgdGhhdCByZXNvbHZlcyB3aXRoIHRoZSBjb250ZW50cyBvZiB0aGUgYmxvYiBhc1xuXHQgKiBiaW5hcnkgZGF0YSBjb250YWluZWQgaW4gYW4gQXJyYXlCdWZmZXIuXG5cdCAqXG5cdCAqIEByZXR1cm4ge1Byb21pc2U8QXJyYXlCdWZmZXI+fVxuXHQgKi9cblx0YXN5bmMgYXJyYXlCdWZmZXIoKSB7XG5cdFx0Y29uc3QgZGF0YSA9IG5ldyBVaW50OEFycmF5KHRoaXMuc2l6ZSk7XG5cdFx0bGV0IG9mZnNldCA9IDA7XG5cdFx0Zm9yIGF3YWl0IChjb25zdCBjaHVuayBvZiB0aGlzLnN0cmVhbSgpKSB7XG5cdFx0XHRkYXRhLnNldChjaHVuaywgb2Zmc2V0KTtcblx0XHRcdG9mZnNldCArPSBjaHVuay5sZW5ndGg7XG5cdFx0fVxuXG5cdFx0cmV0dXJuIGRhdGEuYnVmZmVyO1xuXHR9XG5cblx0LyoqXG5cdCAqIFRoZSBCbG9iIGludGVyZmFjZSdzIHN0cmVhbSgpIG1ldGhvZCBpcyBkaWZmZXJlbmNlIGZyb20gbmF0aXZlXG5cdCAqIGFuZCB1c2VzIG5vZGUgc3RyZWFtcyBpbnN0ZWFkIG9mIHdoYXR3ZyBzdHJlYW1zLlxuXHQgKlxuXHQgKiBAcmV0dXJucyB7UmVhZGFibGV9IE5vZGUgcmVhZGFibGUgc3RyZWFtXG5cdCAqL1xuXHRzdHJlYW0oKSB7XG5cdFx0cmV0dXJuIFJlYWRhYmxlLmZyb20ocmVhZCh3bS5nZXQodGhpcykucGFydHMpKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBUaGUgQmxvYiBpbnRlcmZhY2UncyBzbGljZSgpIG1ldGhvZCBjcmVhdGVzIGFuZCByZXR1cm5zIGFcblx0ICogbmV3IEJsb2Igb2JqZWN0IHdoaWNoIGNvbnRhaW5zIGRhdGEgZnJvbSBhIHN1YnNldCBvZiB0aGVcblx0ICogYmxvYiBvbiB3aGljaCBpdCdzIGNhbGxlZC5cblx0ICpcblx0ICogQHBhcmFtIHtudW1iZXJ9IFtzdGFydF1cblx0ICogQHBhcmFtIHtudW1iZXJ9IFtlbmRdXG5cdCAqIEBwYXJhbSB7c3RyaW5nfSBbdHlwZV1cblx0ICovXG5cdHNsaWNlKHN0YXJ0ID0gMCwgZW5kID0gdGhpcy5zaXplLCB0eXBlID0gJycpIHtcblx0XHRjb25zdCB7c2l6ZX0gPSB0aGlzO1xuXG5cdFx0bGV0IHJlbGF0aXZlU3RhcnQgPSBzdGFydCA8IDAgPyBNYXRoLm1heChzaXplICsgc3RhcnQsIDApIDogTWF0aC5taW4oc3RhcnQsIHNpemUpO1xuXHRcdGxldCByZWxhdGl2ZUVuZCA9IGVuZCA8IDAgPyBNYXRoLm1heChzaXplICsgZW5kLCAwKSA6IE1hdGgubWluKGVuZCwgc2l6ZSk7XG5cblx0XHRjb25zdCBzcGFuID0gTWF0aC5tYXgocmVsYXRpdmVFbmQgLSByZWxhdGl2ZVN0YXJ0LCAwKTtcblx0XHRjb25zdCBwYXJ0cyA9IHdtLmdldCh0aGlzKS5wYXJ0cy52YWx1ZXMoKTtcblx0XHRjb25zdCBibG9iUGFydHMgPSBbXTtcblx0XHRsZXQgYWRkZWQgPSAwO1xuXG5cdFx0Zm9yIChjb25zdCBwYXJ0IG9mIHBhcnRzKSB7XG5cdFx0XHRjb25zdCBzaXplID0gQXJyYXlCdWZmZXIuaXNWaWV3KHBhcnQpID8gcGFydC5ieXRlTGVuZ3RoIDogcGFydC5zaXplO1xuXHRcdFx0aWYgKHJlbGF0aXZlU3RhcnQgJiYgc2l6ZSA8PSByZWxhdGl2ZVN0YXJ0KSB7XG5cdFx0XHRcdC8vIFNraXAgdGhlIGJlZ2lubmluZyBhbmQgY2hhbmdlIHRoZSByZWxhdGl2ZVxuXHRcdFx0XHQvLyBzdGFydCAmIGVuZCBwb3NpdGlvbiBhcyB3ZSBza2lwIHRoZSB1bndhbnRlZCBwYXJ0c1xuXHRcdFx0XHRyZWxhdGl2ZVN0YXJ0IC09IHNpemU7XG5cdFx0XHRcdHJlbGF0aXZlRW5kIC09IHNpemU7XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRjb25zdCBjaHVuayA9IHBhcnQuc2xpY2UocmVsYXRpdmVTdGFydCwgTWF0aC5taW4oc2l6ZSwgcmVsYXRpdmVFbmQpKTtcblx0XHRcdFx0YmxvYlBhcnRzLnB1c2goY2h1bmspO1xuXHRcdFx0XHRhZGRlZCArPSBBcnJheUJ1ZmZlci5pc1ZpZXcoY2h1bmspID8gY2h1bmsuYnl0ZUxlbmd0aCA6IGNodW5rLnNpemU7XG5cdFx0XHRcdHJlbGF0aXZlU3RhcnQgPSAwOyAvLyBBbGwgbmV4dCBzZXF1ZW50YWwgcGFydHMgc2hvdWxkIHN0YXJ0IGF0IDBcblxuXHRcdFx0XHQvLyBkb24ndCBhZGQgdGhlIG92ZXJmbG93IHRvIG5ldyBibG9iUGFydHNcblx0XHRcdFx0aWYgKGFkZGVkID49IHNwYW4pIHtcblx0XHRcdFx0XHRicmVhaztcblx0XHRcdFx0fVxuXHRcdFx0fVxuXHRcdH1cblxuXHRcdGNvbnN0IGJsb2IgPSBuZXcgQmxvYihbXSwge3R5cGV9KTtcblx0XHRPYmplY3QuYXNzaWduKHdtLmdldChibG9iKSwge3NpemU6IHNwYW4sIHBhcnRzOiBibG9iUGFydHN9KTtcblxuXHRcdHJldHVybiBibG9iO1xuXHR9XG5cblx0Z2V0IFtTeW1ib2wudG9TdHJpbmdUYWddKCkge1xuXHRcdHJldHVybiAnQmxvYic7XG5cdH1cblxuXHRzdGF0aWMgW1N5bWJvbC5oYXNJbnN0YW5jZV0ob2JqZWN0KSB7XG5cdFx0cmV0dXJuIChcblx0XHRcdHR5cGVvZiBvYmplY3QgPT09ICdvYmplY3QnICYmXG5cdFx0XHR0eXBlb2Ygb2JqZWN0LnN0cmVhbSA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdFx0b2JqZWN0LnN0cmVhbS5sZW5ndGggPT09IDAgJiZcblx0XHRcdHR5cGVvZiBvYmplY3QuY29uc3RydWN0b3IgPT09ICdmdW5jdGlvbicgJiZcblx0XHRcdC9eKEJsb2J8RmlsZSkkLy50ZXN0KG9iamVjdFtTeW1ib2wudG9TdHJpbmdUYWddKVxuXHRcdCk7XG5cdH1cbn1cblxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoQmxvYi5wcm90b3R5cGUsIHtcblx0c2l6ZToge2VudW1lcmFibGU6IHRydWV9LFxuXHR0eXBlOiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdHNsaWNlOiB7ZW51bWVyYWJsZTogdHJ1ZX1cbn0pO1xuXG5tb2R1bGUuZXhwb3J0cyA9IEJsb2I7XG4iLCIndXNlIHN0cmljdCc7XG5jb25zdCBmZXRjaCA9IHJlcXVpcmUoJ25vZGUtZmV0Y2gnKTtcbmNvbnN0IEFib3J0Q29udHJvbGxlciA9IHJlcXVpcmUoJ2Fib3J0LWNvbnRyb2xsZXInKTtcblxuY29uc3QgVEVOX01FR0FCWVRFUyA9IDEwMDAgKiAxMDAwICogMTA7XG5cbmlmICghZ2xvYmFsLmZldGNoKSB7XG5cdGdsb2JhbC5mZXRjaCA9ICh1cmwsIG9wdGlvbnMpID0+IGZldGNoKHVybCwge2hpZ2hXYXRlck1hcms6IFRFTl9NRUdBQllURVMsIC4uLm9wdGlvbnN9KTtcbn1cblxuaWYgKCFnbG9iYWwuSGVhZGVycykge1xuXHRnbG9iYWwuSGVhZGVycyA9IGZldGNoLkhlYWRlcnM7XG59XG5cbmlmICghZ2xvYmFsLlJlcXVlc3QpIHtcblx0Z2xvYmFsLlJlcXVlc3QgPSBmZXRjaC5SZXF1ZXN0O1xufVxuXG5pZiAoIWdsb2JhbC5SZXNwb25zZSkge1xuXHRnbG9iYWwuUmVzcG9uc2UgPSBmZXRjaC5SZXNwb25zZTtcbn1cblxuaWYgKCFnbG9iYWwuQWJvcnRDb250cm9sbGVyKSB7XG5cdGdsb2JhbC5BYm9ydENvbnRyb2xsZXIgPSBBYm9ydENvbnRyb2xsZXI7XG59XG5cbmlmICghZ2xvYmFsLlJlYWRhYmxlU3RyZWFtKSB7XG5cdHRyeSB7XG5cdFx0Z2xvYmFsLlJlYWRhYmxlU3RyZWFtID0gcmVxdWlyZSgnd2ViLXN0cmVhbXMtcG9seWZpbGwvcG9ueWZpbGwvZXMyMDE4Jyk7XG5cdH0gY2F0Y2ggKF8pIHt9XG59XG5cbm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZSgna3kvdW1kJyk7XG4iLCIoZnVuY3Rpb24gKGdsb2JhbCwgZmFjdG9yeSkge1xuXHR0eXBlb2YgZXhwb3J0cyA9PT0gJ29iamVjdCcgJiYgdHlwZW9mIG1vZHVsZSAhPT0gJ3VuZGVmaW5lZCcgPyBtb2R1bGUuZXhwb3J0cyA9IGZhY3RvcnkoKSA6XG5cdHR5cGVvZiBkZWZpbmUgPT09ICdmdW5jdGlvbicgJiYgZGVmaW5lLmFtZCA/IGRlZmluZShmYWN0b3J5KSA6XG5cdChnbG9iYWwgPSB0eXBlb2YgZ2xvYmFsVGhpcyAhPT0gJ3VuZGVmaW5lZCcgPyBnbG9iYWxUaGlzIDogZ2xvYmFsIHx8IHNlbGYsIGdsb2JhbC5reSA9IGZhY3RvcnkoKSk7XG59KHRoaXMsIChmdW5jdGlvbiAoKSB7ICd1c2Ugc3RyaWN0JztcblxuXHQvKiEgTUlUIExpY2Vuc2UgwqkgU2luZHJlIFNvcmh1cyAqL1xuXG5cdGNvbnN0IGdsb2JhbHMgPSB7fTtcblxuXHRjb25zdCBnZXRHbG9iYWwgPSBwcm9wZXJ0eSA9PiB7XG5cdFx0LyogaXN0YW5idWwgaWdub3JlIG5leHQgKi9cblx0XHRpZiAodHlwZW9mIHNlbGYgIT09ICd1bmRlZmluZWQnICYmIHNlbGYgJiYgcHJvcGVydHkgaW4gc2VsZikge1xuXHRcdFx0cmV0dXJuIHNlbGY7XG5cdFx0fVxuXG5cdFx0LyogaXN0YW5idWwgaWdub3JlIG5leHQgKi9cblx0XHRpZiAodHlwZW9mIHdpbmRvdyAhPT0gJ3VuZGVmaW5lZCcgJiYgd2luZG93ICYmIHByb3BlcnR5IGluIHdpbmRvdykge1xuXHRcdFx0cmV0dXJuIHdpbmRvdztcblx0XHR9XG5cblx0XHRpZiAodHlwZW9mIGdsb2JhbCAhPT0gJ3VuZGVmaW5lZCcgJiYgZ2xvYmFsICYmIHByb3BlcnR5IGluIGdsb2JhbCkge1xuXHRcdFx0cmV0dXJuIGdsb2JhbDtcblx0XHR9XG5cblx0XHQvKiBpc3RhbmJ1bCBpZ25vcmUgbmV4dCAqL1xuXHRcdGlmICh0eXBlb2YgZ2xvYmFsVGhpcyAhPT0gJ3VuZGVmaW5lZCcgJiYgZ2xvYmFsVGhpcykge1xuXHRcdFx0cmV0dXJuIGdsb2JhbFRoaXM7XG5cdFx0fVxuXHR9O1xuXG5cdGNvbnN0IGdsb2JhbFByb3BlcnRpZXMgPSBbXG5cdFx0J0hlYWRlcnMnLFxuXHRcdCdSZXF1ZXN0Jyxcblx0XHQnUmVzcG9uc2UnLFxuXHRcdCdSZWFkYWJsZVN0cmVhbScsXG5cdFx0J2ZldGNoJyxcblx0XHQnQWJvcnRDb250cm9sbGVyJyxcblx0XHQnRm9ybURhdGEnXG5cdF07XG5cblx0Zm9yIChjb25zdCBwcm9wZXJ0eSBvZiBnbG9iYWxQcm9wZXJ0aWVzKSB7XG5cdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGdsb2JhbHMsIHByb3BlcnR5LCB7XG5cdFx0XHRnZXQoKSB7XG5cdFx0XHRcdGNvbnN0IGdsb2JhbE9iamVjdCA9IGdldEdsb2JhbChwcm9wZXJ0eSk7XG5cdFx0XHRcdGNvbnN0IHZhbHVlID0gZ2xvYmFsT2JqZWN0ICYmIGdsb2JhbE9iamVjdFtwcm9wZXJ0eV07XG5cdFx0XHRcdHJldHVybiB0eXBlb2YgdmFsdWUgPT09ICdmdW5jdGlvbicgPyB2YWx1ZS5iaW5kKGdsb2JhbE9iamVjdCkgOiB2YWx1ZTtcblx0XHRcdH1cblx0XHR9KTtcblx0fVxuXG5cdGNvbnN0IGlzT2JqZWN0ID0gdmFsdWUgPT4gdmFsdWUgIT09IG51bGwgJiYgdHlwZW9mIHZhbHVlID09PSAnb2JqZWN0Jztcblx0Y29uc3Qgc3VwcG9ydHNBYm9ydENvbnRyb2xsZXIgPSB0eXBlb2YgZ2xvYmFscy5BYm9ydENvbnRyb2xsZXIgPT09ICdmdW5jdGlvbic7XG5cdGNvbnN0IHN1cHBvcnRzU3RyZWFtcyA9IHR5cGVvZiBnbG9iYWxzLlJlYWRhYmxlU3RyZWFtID09PSAnZnVuY3Rpb24nO1xuXHRjb25zdCBzdXBwb3J0c0Zvcm1EYXRhID0gdHlwZW9mIGdsb2JhbHMuRm9ybURhdGEgPT09ICdmdW5jdGlvbic7XG5cblx0Y29uc3QgbWVyZ2VIZWFkZXJzID0gKHNvdXJjZTEsIHNvdXJjZTIpID0+IHtcblx0XHRjb25zdCByZXN1bHQgPSBuZXcgZ2xvYmFscy5IZWFkZXJzKHNvdXJjZTEgfHwge30pO1xuXHRcdGNvbnN0IGlzSGVhZGVyc0luc3RhbmNlID0gc291cmNlMiBpbnN0YW5jZW9mIGdsb2JhbHMuSGVhZGVycztcblx0XHRjb25zdCBzb3VyY2UgPSBuZXcgZ2xvYmFscy5IZWFkZXJzKHNvdXJjZTIgfHwge30pO1xuXG5cdFx0Zm9yIChjb25zdCBba2V5LCB2YWx1ZV0gb2Ygc291cmNlKSB7XG5cdFx0XHRpZiAoKGlzSGVhZGVyc0luc3RhbmNlICYmIHZhbHVlID09PSAndW5kZWZpbmVkJykgfHwgdmFsdWUgPT09IHVuZGVmaW5lZCkge1xuXHRcdFx0XHRyZXN1bHQuZGVsZXRlKGtleSk7XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRyZXN1bHQuc2V0KGtleSwgdmFsdWUpO1xuXHRcdFx0fVxuXHRcdH1cblxuXHRcdHJldHVybiByZXN1bHQ7XG5cdH07XG5cblx0Y29uc3QgZGVlcE1lcmdlID0gKC4uLnNvdXJjZXMpID0+IHtcblx0XHRsZXQgcmV0dXJuVmFsdWUgPSB7fTtcblx0XHRsZXQgaGVhZGVycyA9IHt9O1xuXG5cdFx0Zm9yIChjb25zdCBzb3VyY2Ugb2Ygc291cmNlcykge1xuXHRcdFx0aWYgKEFycmF5LmlzQXJyYXkoc291cmNlKSkge1xuXHRcdFx0XHRpZiAoIShBcnJheS5pc0FycmF5KHJldHVyblZhbHVlKSkpIHtcblx0XHRcdFx0XHRyZXR1cm5WYWx1ZSA9IFtdO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0cmV0dXJuVmFsdWUgPSBbLi4ucmV0dXJuVmFsdWUsIC4uLnNvdXJjZV07XG5cdFx0XHR9IGVsc2UgaWYgKGlzT2JqZWN0KHNvdXJjZSkpIHtcblx0XHRcdFx0Zm9yIChsZXQgW2tleSwgdmFsdWVdIG9mIE9iamVjdC5lbnRyaWVzKHNvdXJjZSkpIHtcblx0XHRcdFx0XHRpZiAoaXNPYmplY3QodmFsdWUpICYmIChrZXkgaW4gcmV0dXJuVmFsdWUpKSB7XG5cdFx0XHRcdFx0XHR2YWx1ZSA9IGRlZXBNZXJnZShyZXR1cm5WYWx1ZVtrZXldLCB2YWx1ZSk7XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0cmV0dXJuVmFsdWUgPSB7Li4ucmV0dXJuVmFsdWUsIFtrZXldOiB2YWx1ZX07XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRpZiAoaXNPYmplY3Qoc291cmNlLmhlYWRlcnMpKSB7XG5cdFx0XHRcdFx0aGVhZGVycyA9IG1lcmdlSGVhZGVycyhoZWFkZXJzLCBzb3VyY2UuaGVhZGVycyk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblxuXHRcdFx0cmV0dXJuVmFsdWUuaGVhZGVycyA9IGhlYWRlcnM7XG5cdFx0fVxuXG5cdFx0cmV0dXJuIHJldHVyblZhbHVlO1xuXHR9O1xuXG5cdGNvbnN0IHJlcXVlc3RNZXRob2RzID0gW1xuXHRcdCdnZXQnLFxuXHRcdCdwb3N0Jyxcblx0XHQncHV0Jyxcblx0XHQncGF0Y2gnLFxuXHRcdCdoZWFkJyxcblx0XHQnZGVsZXRlJ1xuXHRdO1xuXG5cdGNvbnN0IHJlc3BvbnNlVHlwZXMgPSB7XG5cdFx0anNvbjogJ2FwcGxpY2F0aW9uL2pzb24nLFxuXHRcdHRleHQ6ICd0ZXh0LyonLFxuXHRcdGZvcm1EYXRhOiAnbXVsdGlwYXJ0L2Zvcm0tZGF0YScsXG5cdFx0YXJyYXlCdWZmZXI6ICcqLyonLFxuXHRcdGJsb2I6ICcqLyonXG5cdH07XG5cblx0Y29uc3QgcmV0cnlNZXRob2RzID0gW1xuXHRcdCdnZXQnLFxuXHRcdCdwdXQnLFxuXHRcdCdoZWFkJyxcblx0XHQnZGVsZXRlJyxcblx0XHQnb3B0aW9ucycsXG5cdFx0J3RyYWNlJ1xuXHRdO1xuXG5cdGNvbnN0IHJldHJ5U3RhdHVzQ29kZXMgPSBbXG5cdFx0NDA4LFxuXHRcdDQxMyxcblx0XHQ0MjksXG5cdFx0NTAwLFxuXHRcdDUwMixcblx0XHQ1MDMsXG5cdFx0NTA0XG5cdF07XG5cblx0Y29uc3QgcmV0cnlBZnRlclN0YXR1c0NvZGVzID0gW1xuXHRcdDQxMyxcblx0XHQ0MjksXG5cdFx0NTAzXG5cdF07XG5cblx0Y29uc3Qgc3RvcCA9IFN5bWJvbCgnc3RvcCcpO1xuXG5cdGNsYXNzIEhUVFBFcnJvciBleHRlbmRzIEVycm9yIHtcblx0XHRjb25zdHJ1Y3RvcihyZXNwb25zZSkge1xuXHRcdFx0Ly8gU2V0IHRoZSBtZXNzYWdlIHRvIHRoZSBzdGF0dXMgdGV4dCwgc3VjaCBhcyBVbmF1dGhvcml6ZWQsXG5cdFx0XHQvLyB3aXRoIHNvbWUgZmFsbGJhY2tzLiBUaGlzIG1lc3NhZ2Ugc2hvdWxkIG5ldmVyIGJlIHVuZGVmaW5lZC5cblx0XHRcdHN1cGVyKFxuXHRcdFx0XHRyZXNwb25zZS5zdGF0dXNUZXh0IHx8XG5cdFx0XHRcdFN0cmluZyhcblx0XHRcdFx0XHQocmVzcG9uc2Uuc3RhdHVzID09PSAwIHx8IHJlc3BvbnNlLnN0YXR1cykgP1xuXHRcdFx0XHRcdFx0cmVzcG9uc2Uuc3RhdHVzIDogJ1Vua25vd24gcmVzcG9uc2UgZXJyb3InXG5cdFx0XHRcdClcblx0XHRcdCk7XG5cdFx0XHR0aGlzLm5hbWUgPSAnSFRUUEVycm9yJztcblx0XHRcdHRoaXMucmVzcG9uc2UgPSByZXNwb25zZTtcblx0XHR9XG5cdH1cblxuXHRjbGFzcyBUaW1lb3V0RXJyb3IgZXh0ZW5kcyBFcnJvciB7XG5cdFx0Y29uc3RydWN0b3IocmVxdWVzdCkge1xuXHRcdFx0c3VwZXIoJ1JlcXVlc3QgdGltZWQgb3V0Jyk7XG5cdFx0XHR0aGlzLm5hbWUgPSAnVGltZW91dEVycm9yJztcblx0XHRcdHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG5cdFx0fVxuXHR9XG5cblx0Y29uc3QgZGVsYXkgPSBtcyA9PiBuZXcgUHJvbWlzZShyZXNvbHZlID0+IHNldFRpbWVvdXQocmVzb2x2ZSwgbXMpKTtcblxuXHQvLyBgUHJvbWlzZS5yYWNlKClgIHdvcmthcm91bmQgKCM5MSlcblx0Y29uc3QgdGltZW91dCA9IChyZXF1ZXN0LCBhYm9ydENvbnRyb2xsZXIsIG9wdGlvbnMpID0+XG5cdFx0bmV3IFByb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuXHRcdFx0Y29uc3QgdGltZW91dElEID0gc2V0VGltZW91dCgoKSA9PiB7XG5cdFx0XHRcdGlmIChhYm9ydENvbnRyb2xsZXIpIHtcblx0XHRcdFx0XHRhYm9ydENvbnRyb2xsZXIuYWJvcnQoKTtcblx0XHRcdFx0fVxuXG5cdFx0XHRcdHJlamVjdChuZXcgVGltZW91dEVycm9yKHJlcXVlc3QpKTtcblx0XHRcdH0sIG9wdGlvbnMudGltZW91dCk7XG5cblx0XHRcdC8qIGVzbGludC1kaXNhYmxlIHByb21pc2UvcHJlZmVyLWF3YWl0LXRvLXRoZW4gKi9cblx0XHRcdG9wdGlvbnMuZmV0Y2gocmVxdWVzdClcblx0XHRcdFx0LnRoZW4ocmVzb2x2ZSlcblx0XHRcdFx0LmNhdGNoKHJlamVjdClcblx0XHRcdFx0LnRoZW4oKCkgPT4ge1xuXHRcdFx0XHRcdGNsZWFyVGltZW91dCh0aW1lb3V0SUQpO1xuXHRcdFx0XHR9KTtcblx0XHRcdC8qIGVzbGludC1lbmFibGUgcHJvbWlzZS9wcmVmZXItYXdhaXQtdG8tdGhlbiAqL1xuXHRcdH0pO1xuXG5cdGNvbnN0IG5vcm1hbGl6ZVJlcXVlc3RNZXRob2QgPSBpbnB1dCA9PiByZXF1ZXN0TWV0aG9kcy5pbmNsdWRlcyhpbnB1dCkgPyBpbnB1dC50b1VwcGVyQ2FzZSgpIDogaW5wdXQ7XG5cblx0Y29uc3QgZGVmYXVsdFJldHJ5T3B0aW9ucyA9IHtcblx0XHRsaW1pdDogMixcblx0XHRtZXRob2RzOiByZXRyeU1ldGhvZHMsXG5cdFx0c3RhdHVzQ29kZXM6IHJldHJ5U3RhdHVzQ29kZXMsXG5cdFx0YWZ0ZXJTdGF0dXNDb2RlczogcmV0cnlBZnRlclN0YXR1c0NvZGVzXG5cdH07XG5cblx0Y29uc3Qgbm9ybWFsaXplUmV0cnlPcHRpb25zID0gKHJldHJ5ID0ge30pID0+IHtcblx0XHRpZiAodHlwZW9mIHJldHJ5ID09PSAnbnVtYmVyJykge1xuXHRcdFx0cmV0dXJuIHtcblx0XHRcdFx0Li4uZGVmYXVsdFJldHJ5T3B0aW9ucyxcblx0XHRcdFx0bGltaXQ6IHJldHJ5XG5cdFx0XHR9O1xuXHRcdH1cblxuXHRcdGlmIChyZXRyeS5tZXRob2RzICYmICFBcnJheS5pc0FycmF5KHJldHJ5Lm1ldGhvZHMpKSB7XG5cdFx0XHR0aHJvdyBuZXcgRXJyb3IoJ3JldHJ5Lm1ldGhvZHMgbXVzdCBiZSBhbiBhcnJheScpO1xuXHRcdH1cblxuXHRcdGlmIChyZXRyeS5zdGF0dXNDb2RlcyAmJiAhQXJyYXkuaXNBcnJheShyZXRyeS5zdGF0dXNDb2RlcykpIHtcblx0XHRcdHRocm93IG5ldyBFcnJvcigncmV0cnkuc3RhdHVzQ29kZXMgbXVzdCBiZSBhbiBhcnJheScpO1xuXHRcdH1cblxuXHRcdHJldHVybiB7XG5cdFx0XHQuLi5kZWZhdWx0UmV0cnlPcHRpb25zLFxuXHRcdFx0Li4ucmV0cnksXG5cdFx0XHRhZnRlclN0YXR1c0NvZGVzOiByZXRyeUFmdGVyU3RhdHVzQ29kZXNcblx0XHR9O1xuXHR9O1xuXG5cdC8vIFRoZSBtYXhpbXVtIHZhbHVlIG9mIGEgMzJiaXQgaW50IChzZWUgaXNzdWUgIzExNylcblx0Y29uc3QgbWF4U2FmZVRpbWVvdXQgPSAyMTQ3NDgzNjQ3O1xuXG5cdGNsYXNzIEt5IHtcblx0XHRjb25zdHJ1Y3RvcihpbnB1dCwgb3B0aW9ucyA9IHt9KSB7XG5cdFx0XHR0aGlzLl9yZXRyeUNvdW50ID0gMDtcblx0XHRcdHRoaXMuX2lucHV0ID0gaW5wdXQ7XG5cdFx0XHR0aGlzLl9vcHRpb25zID0ge1xuXHRcdFx0XHQvLyBUT0RPOiBjcmVkZW50aWFscyBjYW4gYmUgcmVtb3ZlZCB3aGVuIHRoZSBzcGVjIGNoYW5nZSBpcyBpbXBsZW1lbnRlZCBpbiBhbGwgYnJvd3NlcnMuIENvbnRleHQ6IGh0dHBzOi8vd3d3LmNocm9tZXN0YXR1cy5jb20vZmVhdHVyZS80NTM5NDczMzEyMzUwMjA4XG5cdFx0XHRcdGNyZWRlbnRpYWxzOiB0aGlzLl9pbnB1dC5jcmVkZW50aWFscyB8fCAnc2FtZS1vcmlnaW4nLFxuXHRcdFx0XHQuLi5vcHRpb25zLFxuXHRcdFx0XHRoZWFkZXJzOiBtZXJnZUhlYWRlcnModGhpcy5faW5wdXQuaGVhZGVycywgb3B0aW9ucy5oZWFkZXJzKSxcblx0XHRcdFx0aG9va3M6IGRlZXBNZXJnZSh7XG5cdFx0XHRcdFx0YmVmb3JlUmVxdWVzdDogW10sXG5cdFx0XHRcdFx0YmVmb3JlUmV0cnk6IFtdLFxuXHRcdFx0XHRcdGFmdGVyUmVzcG9uc2U6IFtdXG5cdFx0XHRcdH0sIG9wdGlvbnMuaG9va3MpLFxuXHRcdFx0XHRtZXRob2Q6IG5vcm1hbGl6ZVJlcXVlc3RNZXRob2Qob3B0aW9ucy5tZXRob2QgfHwgdGhpcy5faW5wdXQubWV0aG9kKSxcblx0XHRcdFx0cHJlZml4VXJsOiBTdHJpbmcob3B0aW9ucy5wcmVmaXhVcmwgfHwgJycpLFxuXHRcdFx0XHRyZXRyeTogbm9ybWFsaXplUmV0cnlPcHRpb25zKG9wdGlvbnMucmV0cnkpLFxuXHRcdFx0XHR0aHJvd0h0dHBFcnJvcnM6IG9wdGlvbnMudGhyb3dIdHRwRXJyb3JzICE9PSBmYWxzZSxcblx0XHRcdFx0dGltZW91dDogdHlwZW9mIG9wdGlvbnMudGltZW91dCA9PT0gJ3VuZGVmaW5lZCcgPyAxMDAwMCA6IG9wdGlvbnMudGltZW91dCxcblx0XHRcdFx0ZmV0Y2g6IG9wdGlvbnMuZmV0Y2ggfHwgZ2xvYmFscy5mZXRjaFxuXHRcdFx0fTtcblxuXHRcdFx0aWYgKHR5cGVvZiB0aGlzLl9pbnB1dCAhPT0gJ3N0cmluZycgJiYgISh0aGlzLl9pbnB1dCBpbnN0YW5jZW9mIFVSTCB8fCB0aGlzLl9pbnB1dCBpbnN0YW5jZW9mIGdsb2JhbHMuUmVxdWVzdCkpIHtcblx0XHRcdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignYGlucHV0YCBtdXN0IGJlIGEgc3RyaW5nLCBVUkwsIG9yIFJlcXVlc3QnKTtcblx0XHRcdH1cblxuXHRcdFx0aWYgKHRoaXMuX29wdGlvbnMucHJlZml4VXJsICYmIHR5cGVvZiB0aGlzLl9pbnB1dCA9PT0gJ3N0cmluZycpIHtcblx0XHRcdFx0aWYgKHRoaXMuX2lucHV0LnN0YXJ0c1dpdGgoJy8nKSkge1xuXHRcdFx0XHRcdHRocm93IG5ldyBFcnJvcignYGlucHV0YCBtdXN0IG5vdCBiZWdpbiB3aXRoIGEgc2xhc2ggd2hlbiB1c2luZyBgcHJlZml4VXJsYCcpO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0aWYgKCF0aGlzLl9vcHRpb25zLnByZWZpeFVybC5lbmRzV2l0aCgnLycpKSB7XG5cdFx0XHRcdFx0dGhpcy5fb3B0aW9ucy5wcmVmaXhVcmwgKz0gJy8nO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0dGhpcy5faW5wdXQgPSB0aGlzLl9vcHRpb25zLnByZWZpeFVybCArIHRoaXMuX2lucHV0O1xuXHRcdFx0fVxuXG5cdFx0XHRpZiAoc3VwcG9ydHNBYm9ydENvbnRyb2xsZXIpIHtcblx0XHRcdFx0dGhpcy5hYm9ydENvbnRyb2xsZXIgPSBuZXcgZ2xvYmFscy5BYm9ydENvbnRyb2xsZXIoKTtcblx0XHRcdFx0aWYgKHRoaXMuX29wdGlvbnMuc2lnbmFsKSB7XG5cdFx0XHRcdFx0dGhpcy5fb3B0aW9ucy5zaWduYWwuYWRkRXZlbnRMaXN0ZW5lcignYWJvcnQnLCAoKSA9PiB7XG5cdFx0XHRcdFx0XHR0aGlzLmFib3J0Q29udHJvbGxlci5hYm9ydCgpO1xuXHRcdFx0XHRcdH0pO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0dGhpcy5fb3B0aW9ucy5zaWduYWwgPSB0aGlzLmFib3J0Q29udHJvbGxlci5zaWduYWw7XG5cdFx0XHR9XG5cblx0XHRcdHRoaXMucmVxdWVzdCA9IG5ldyBnbG9iYWxzLlJlcXVlc3QodGhpcy5faW5wdXQsIHRoaXMuX29wdGlvbnMpO1xuXG5cdFx0XHRpZiAodGhpcy5fb3B0aW9ucy5zZWFyY2hQYXJhbXMpIHtcblx0XHRcdFx0Y29uc3Qgc2VhcmNoUGFyYW1zID0gJz8nICsgbmV3IFVSTFNlYXJjaFBhcmFtcyh0aGlzLl9vcHRpb25zLnNlYXJjaFBhcmFtcykudG9TdHJpbmcoKTtcblx0XHRcdFx0Y29uc3QgdXJsID0gdGhpcy5yZXF1ZXN0LnVybC5yZXBsYWNlKC8oPzpcXD8uKj8pPyg/PSN8JCkvLCBzZWFyY2hQYXJhbXMpO1xuXG5cdFx0XHRcdC8vIFRvIHByb3ZpZGUgY29ycmVjdCBmb3JtIGJvdW5kYXJ5LCBDb250ZW50LVR5cGUgaGVhZGVyIHNob3VsZCBiZSBkZWxldGVkIGVhY2ggdGltZSB3aGVuIG5ldyBSZXF1ZXN0IGluc3RhbnRpYXRlZCBmcm9tIGFub3RoZXIgb25lXG5cdFx0XHRcdGlmICgoKHN1cHBvcnRzRm9ybURhdGEgJiYgdGhpcy5fb3B0aW9ucy5ib2R5IGluc3RhbmNlb2YgZ2xvYmFscy5Gb3JtRGF0YSkgfHwgdGhpcy5fb3B0aW9ucy5ib2R5IGluc3RhbmNlb2YgVVJMU2VhcmNoUGFyYW1zKSAmJiAhKHRoaXMuX29wdGlvbnMuaGVhZGVycyAmJiB0aGlzLl9vcHRpb25zLmhlYWRlcnNbJ2NvbnRlbnQtdHlwZSddKSkge1xuXHRcdFx0XHRcdHRoaXMucmVxdWVzdC5oZWFkZXJzLmRlbGV0ZSgnY29udGVudC10eXBlJyk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHR0aGlzLnJlcXVlc3QgPSBuZXcgZ2xvYmFscy5SZXF1ZXN0KG5ldyBnbG9iYWxzLlJlcXVlc3QodXJsLCB0aGlzLnJlcXVlc3QpLCB0aGlzLl9vcHRpb25zKTtcblx0XHRcdH1cblxuXHRcdFx0aWYgKHRoaXMuX29wdGlvbnMuanNvbiAhPT0gdW5kZWZpbmVkKSB7XG5cdFx0XHRcdHRoaXMuX29wdGlvbnMuYm9keSA9IEpTT04uc3RyaW5naWZ5KHRoaXMuX29wdGlvbnMuanNvbik7XG5cdFx0XHRcdHRoaXMucmVxdWVzdC5oZWFkZXJzLnNldCgnY29udGVudC10eXBlJywgJ2FwcGxpY2F0aW9uL2pzb24nKTtcblx0XHRcdFx0dGhpcy5yZXF1ZXN0ID0gbmV3IGdsb2JhbHMuUmVxdWVzdCh0aGlzLnJlcXVlc3QsIHtib2R5OiB0aGlzLl9vcHRpb25zLmJvZHl9KTtcblx0XHRcdH1cblxuXHRcdFx0Y29uc3QgZm4gPSBhc3luYyAoKSA9PiB7XG5cdFx0XHRcdGlmICh0aGlzLl9vcHRpb25zLnRpbWVvdXQgPiBtYXhTYWZlVGltZW91dCkge1xuXHRcdFx0XHRcdHRocm93IG5ldyBSYW5nZUVycm9yKGBUaGUgXFxgdGltZW91dFxcYCBvcHRpb24gY2Fubm90IGJlIGdyZWF0ZXIgdGhhbiAke21heFNhZmVUaW1lb3V0fWApO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0YXdhaXQgZGVsYXkoMSk7XG5cdFx0XHRcdGxldCByZXNwb25zZSA9IGF3YWl0IHRoaXMuX2ZldGNoKCk7XG5cblx0XHRcdFx0Zm9yIChjb25zdCBob29rIG9mIHRoaXMuX29wdGlvbnMuaG9va3MuYWZ0ZXJSZXNwb25zZSkge1xuXHRcdFx0XHRcdC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1hd2FpdC1pbi1sb29wXG5cdFx0XHRcdFx0Y29uc3QgbW9kaWZpZWRSZXNwb25zZSA9IGF3YWl0IGhvb2soXG5cdFx0XHRcdFx0XHR0aGlzLnJlcXVlc3QsXG5cdFx0XHRcdFx0XHR0aGlzLl9vcHRpb25zLFxuXHRcdFx0XHRcdFx0dGhpcy5fZGVjb3JhdGVSZXNwb25zZShyZXNwb25zZS5jbG9uZSgpKVxuXHRcdFx0XHRcdCk7XG5cblx0XHRcdFx0XHRpZiAobW9kaWZpZWRSZXNwb25zZSBpbnN0YW5jZW9mIGdsb2JhbHMuUmVzcG9uc2UpIHtcblx0XHRcdFx0XHRcdHJlc3BvbnNlID0gbW9kaWZpZWRSZXNwb25zZTtcblx0XHRcdFx0XHR9XG5cdFx0XHRcdH1cblxuXHRcdFx0XHR0aGlzLl9kZWNvcmF0ZVJlc3BvbnNlKHJlc3BvbnNlKTtcblxuXHRcdFx0XHRpZiAoIXJlc3BvbnNlLm9rICYmIHRoaXMuX29wdGlvbnMudGhyb3dIdHRwRXJyb3JzKSB7XG5cdFx0XHRcdFx0dGhyb3cgbmV3IEhUVFBFcnJvcihyZXNwb25zZSk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHQvLyBJZiBgb25Eb3dubG9hZFByb2dyZXNzYCBpcyBwYXNzZWQsIGl0IHVzZXMgdGhlIHN0cmVhbSBBUEkgaW50ZXJuYWxseVxuXHRcdFx0XHQvKiBpc3RhbmJ1bCBpZ25vcmUgbmV4dCAqL1xuXHRcdFx0XHRpZiAodGhpcy5fb3B0aW9ucy5vbkRvd25sb2FkUHJvZ3Jlc3MpIHtcblx0XHRcdFx0XHRpZiAodHlwZW9mIHRoaXMuX29wdGlvbnMub25Eb3dubG9hZFByb2dyZXNzICE9PSAnZnVuY3Rpb24nKSB7XG5cdFx0XHRcdFx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGUgYG9uRG93bmxvYWRQcm9ncmVzc2Agb3B0aW9uIG11c3QgYmUgYSBmdW5jdGlvbicpO1xuXHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdGlmICghc3VwcG9ydHNTdHJlYW1zKSB7XG5cdFx0XHRcdFx0XHR0aHJvdyBuZXcgRXJyb3IoJ1N0cmVhbXMgYXJlIG5vdCBzdXBwb3J0ZWQgaW4geW91ciBlbnZpcm9ubWVudC4gYFJlYWRhYmxlU3RyZWFtYCBpcyBtaXNzaW5nLicpO1xuXHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdHJldHVybiB0aGlzLl9zdHJlYW0ocmVzcG9uc2UuY2xvbmUoKSwgdGhpcy5fb3B0aW9ucy5vbkRvd25sb2FkUHJvZ3Jlc3MpO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0cmV0dXJuIHJlc3BvbnNlO1xuXHRcdFx0fTtcblxuXHRcdFx0Y29uc3QgaXNSZXRyaWFibGVNZXRob2QgPSB0aGlzLl9vcHRpb25zLnJldHJ5Lm1ldGhvZHMuaW5jbHVkZXModGhpcy5yZXF1ZXN0Lm1ldGhvZC50b0xvd2VyQ2FzZSgpKTtcblx0XHRcdGNvbnN0IHJlc3VsdCA9IGlzUmV0cmlhYmxlTWV0aG9kID8gdGhpcy5fcmV0cnkoZm4pIDogZm4oKTtcblxuXHRcdFx0Zm9yIChjb25zdCBbdHlwZSwgbWltZVR5cGVdIG9mIE9iamVjdC5lbnRyaWVzKHJlc3BvbnNlVHlwZXMpKSB7XG5cdFx0XHRcdHJlc3VsdFt0eXBlXSA9IGFzeW5jICgpID0+IHtcblx0XHRcdFx0XHR0aGlzLnJlcXVlc3QuaGVhZGVycy5zZXQoJ2FjY2VwdCcsIHRoaXMucmVxdWVzdC5oZWFkZXJzLmdldCgnYWNjZXB0JykgfHwgbWltZVR5cGUpO1xuXG5cdFx0XHRcdFx0Y29uc3QgcmVzcG9uc2UgPSAoYXdhaXQgcmVzdWx0KS5jbG9uZSgpO1xuXG5cdFx0XHRcdFx0aWYgKHR5cGUgPT09ICdqc29uJykge1xuXHRcdFx0XHRcdFx0aWYgKHJlc3BvbnNlLnN0YXR1cyA9PT0gMjA0KSB7XG5cdFx0XHRcdFx0XHRcdHJldHVybiAnJztcblx0XHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdFx0aWYgKG9wdGlvbnMucGFyc2VKc29uKSB7XG5cdFx0XHRcdFx0XHRcdHJldHVybiBvcHRpb25zLnBhcnNlSnNvbihhd2FpdCByZXNwb25zZS50ZXh0KCkpO1xuXHRcdFx0XHRcdFx0fVxuXHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdHJldHVybiByZXNwb25zZVt0eXBlXSgpO1xuXHRcdFx0XHR9O1xuXHRcdFx0fVxuXG5cdFx0XHRyZXR1cm4gcmVzdWx0O1xuXHRcdH1cblxuXHRcdF9jYWxjdWxhdGVSZXRyeURlbGF5KGVycm9yKSB7XG5cdFx0XHR0aGlzLl9yZXRyeUNvdW50Kys7XG5cblx0XHRcdGlmICh0aGlzLl9yZXRyeUNvdW50IDwgdGhpcy5fb3B0aW9ucy5yZXRyeS5saW1pdCAmJiAhKGVycm9yIGluc3RhbmNlb2YgVGltZW91dEVycm9yKSkge1xuXHRcdFx0XHRpZiAoZXJyb3IgaW5zdGFuY2VvZiBIVFRQRXJyb3IpIHtcblx0XHRcdFx0XHRpZiAoIXRoaXMuX29wdGlvbnMucmV0cnkuc3RhdHVzQ29kZXMuaW5jbHVkZXMoZXJyb3IucmVzcG9uc2Uuc3RhdHVzKSkge1xuXHRcdFx0XHRcdFx0cmV0dXJuIDA7XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0Y29uc3QgcmV0cnlBZnRlciA9IGVycm9yLnJlc3BvbnNlLmhlYWRlcnMuZ2V0KCdSZXRyeS1BZnRlcicpO1xuXHRcdFx0XHRcdGlmIChyZXRyeUFmdGVyICYmIHRoaXMuX29wdGlvbnMucmV0cnkuYWZ0ZXJTdGF0dXNDb2Rlcy5pbmNsdWRlcyhlcnJvci5yZXNwb25zZS5zdGF0dXMpKSB7XG5cdFx0XHRcdFx0XHRsZXQgYWZ0ZXIgPSBOdW1iZXIocmV0cnlBZnRlcik7XG5cdFx0XHRcdFx0XHRpZiAoTnVtYmVyLmlzTmFOKGFmdGVyKSkge1xuXHRcdFx0XHRcdFx0XHRhZnRlciA9IERhdGUucGFyc2UocmV0cnlBZnRlcikgLSBEYXRlLm5vdygpO1xuXHRcdFx0XHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0XHRcdFx0YWZ0ZXIgKj0gMTAwMDtcblx0XHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdFx0aWYgKHR5cGVvZiB0aGlzLl9vcHRpb25zLnJldHJ5Lm1heFJldHJ5QWZ0ZXIgIT09ICd1bmRlZmluZWQnICYmIGFmdGVyID4gdGhpcy5fb3B0aW9ucy5yZXRyeS5tYXhSZXRyeUFmdGVyKSB7XG5cdFx0XHRcdFx0XHRcdHJldHVybiAwO1xuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHRyZXR1cm4gYWZ0ZXI7XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0aWYgKGVycm9yLnJlc3BvbnNlLnN0YXR1cyA9PT0gNDEzKSB7XG5cdFx0XHRcdFx0XHRyZXR1cm4gMDtcblx0XHRcdFx0XHR9XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRjb25zdCBCQUNLT0ZGX0ZBQ1RPUiA9IDAuMztcblx0XHRcdFx0cmV0dXJuIEJBQ0tPRkZfRkFDVE9SICogKDIgKiogKHRoaXMuX3JldHJ5Q291bnQgLSAxKSkgKiAxMDAwO1xuXHRcdFx0fVxuXG5cdFx0XHRyZXR1cm4gMDtcblx0XHR9XG5cblx0XHRfZGVjb3JhdGVSZXNwb25zZShyZXNwb25zZSkge1xuXHRcdFx0aWYgKHRoaXMuX29wdGlvbnMucGFyc2VKc29uKSB7XG5cdFx0XHRcdHJlc3BvbnNlLmpzb24gPSBhc3luYyAoKSA9PiB7XG5cdFx0XHRcdFx0cmV0dXJuIHRoaXMuX29wdGlvbnMucGFyc2VKc29uKGF3YWl0IHJlc3BvbnNlLnRleHQoKSk7XG5cdFx0XHRcdH07XG5cdFx0XHR9XG5cblx0XHRcdHJldHVybiByZXNwb25zZTtcblx0XHR9XG5cblx0XHRhc3luYyBfcmV0cnkoZm4pIHtcblx0XHRcdHRyeSB7XG5cdFx0XHRcdHJldHVybiBhd2FpdCBmbigpO1xuXHRcdFx0fSBjYXRjaCAoZXJyb3IpIHtcblx0XHRcdFx0Y29uc3QgbXMgPSBNYXRoLm1pbih0aGlzLl9jYWxjdWxhdGVSZXRyeURlbGF5KGVycm9yKSwgbWF4U2FmZVRpbWVvdXQpO1xuXHRcdFx0XHRpZiAobXMgIT09IDAgJiYgdGhpcy5fcmV0cnlDb3VudCA+IDApIHtcblx0XHRcdFx0XHRhd2FpdCBkZWxheShtcyk7XG5cblx0XHRcdFx0XHRmb3IgKGNvbnN0IGhvb2sgb2YgdGhpcy5fb3B0aW9ucy5ob29rcy5iZWZvcmVSZXRyeSkge1xuXHRcdFx0XHRcdFx0Ly8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLWF3YWl0LWluLWxvb3Bcblx0XHRcdFx0XHRcdGNvbnN0IGhvb2tSZXN1bHQgPSBhd2FpdCBob29rKHtcblx0XHRcdFx0XHRcdFx0cmVxdWVzdDogdGhpcy5yZXF1ZXN0LFxuXHRcdFx0XHRcdFx0XHRvcHRpb25zOiB0aGlzLl9vcHRpb25zLFxuXHRcdFx0XHRcdFx0XHRlcnJvcixcblx0XHRcdFx0XHRcdFx0cmV0cnlDb3VudDogdGhpcy5fcmV0cnlDb3VudFxuXHRcdFx0XHRcdFx0fSk7XG5cblx0XHRcdFx0XHRcdC8vIElmIGBzdG9wYCBpcyByZXR1cm5lZCBmcm9tIHRoZSBob29rLCB0aGUgcmV0cnkgcHJvY2VzcyBpcyBzdG9wcGVkXG5cdFx0XHRcdFx0XHRpZiAoaG9va1Jlc3VsdCA9PT0gc3RvcCkge1xuXHRcdFx0XHRcdFx0XHRyZXR1cm47XG5cdFx0XHRcdFx0XHR9XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0cmV0dXJuIHRoaXMuX3JldHJ5KGZuKTtcblx0XHRcdFx0fVxuXG5cdFx0XHRcdGlmICh0aGlzLl9vcHRpb25zLnRocm93SHR0cEVycm9ycykge1xuXHRcdFx0XHRcdHRocm93IGVycm9yO1xuXHRcdFx0XHR9XG5cdFx0XHR9XG5cdFx0fVxuXG5cdFx0YXN5bmMgX2ZldGNoKCkge1xuXHRcdFx0Zm9yIChjb25zdCBob29rIG9mIHRoaXMuX29wdGlvbnMuaG9va3MuYmVmb3JlUmVxdWVzdCkge1xuXHRcdFx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tYXdhaXQtaW4tbG9vcFxuXHRcdFx0XHRjb25zdCByZXN1bHQgPSBhd2FpdCBob29rKHRoaXMucmVxdWVzdCwgdGhpcy5fb3B0aW9ucyk7XG5cblx0XHRcdFx0aWYgKHJlc3VsdCBpbnN0YW5jZW9mIFJlcXVlc3QpIHtcblx0XHRcdFx0XHR0aGlzLnJlcXVlc3QgPSByZXN1bHQ7XG5cdFx0XHRcdFx0YnJlYWs7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRpZiAocmVzdWx0IGluc3RhbmNlb2YgUmVzcG9uc2UpIHtcblx0XHRcdFx0XHRyZXR1cm4gcmVzdWx0O1xuXHRcdFx0XHR9XG5cdFx0XHR9XG5cblx0XHRcdGlmICh0aGlzLl9vcHRpb25zLnRpbWVvdXQgPT09IGZhbHNlKSB7XG5cdFx0XHRcdHJldHVybiB0aGlzLl9vcHRpb25zLmZldGNoKHRoaXMucmVxdWVzdC5jbG9uZSgpKTtcblx0XHRcdH1cblxuXHRcdFx0cmV0dXJuIHRpbWVvdXQodGhpcy5yZXF1ZXN0LmNsb25lKCksIHRoaXMuYWJvcnRDb250cm9sbGVyLCB0aGlzLl9vcHRpb25zKTtcblx0XHR9XG5cblx0XHQvKiBpc3RhbmJ1bCBpZ25vcmUgbmV4dCAqL1xuXHRcdF9zdHJlYW0ocmVzcG9uc2UsIG9uRG93bmxvYWRQcm9ncmVzcykge1xuXHRcdFx0Y29uc3QgdG90YWxCeXRlcyA9IE51bWJlcihyZXNwb25zZS5oZWFkZXJzLmdldCgnY29udGVudC1sZW5ndGgnKSkgfHwgMDtcblx0XHRcdGxldCB0cmFuc2ZlcnJlZEJ5dGVzID0gMDtcblxuXHRcdFx0cmV0dXJuIG5ldyBnbG9iYWxzLlJlc3BvbnNlKFxuXHRcdFx0XHRuZXcgZ2xvYmFscy5SZWFkYWJsZVN0cmVhbSh7XG5cdFx0XHRcdFx0c3RhcnQoY29udHJvbGxlcikge1xuXHRcdFx0XHRcdFx0Y29uc3QgcmVhZGVyID0gcmVzcG9uc2UuYm9keS5nZXRSZWFkZXIoKTtcblxuXHRcdFx0XHRcdFx0aWYgKG9uRG93bmxvYWRQcm9ncmVzcykge1xuXHRcdFx0XHRcdFx0XHRvbkRvd25sb2FkUHJvZ3Jlc3Moe3BlcmNlbnQ6IDAsIHRyYW5zZmVycmVkQnl0ZXM6IDAsIHRvdGFsQnl0ZXN9LCBuZXcgVWludDhBcnJheSgpKTtcblx0XHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdFx0YXN5bmMgZnVuY3Rpb24gcmVhZCgpIHtcblx0XHRcdFx0XHRcdFx0Y29uc3Qge2RvbmUsIHZhbHVlfSA9IGF3YWl0IHJlYWRlci5yZWFkKCk7XG5cdFx0XHRcdFx0XHRcdGlmIChkb25lKSB7XG5cdFx0XHRcdFx0XHRcdFx0Y29udHJvbGxlci5jbG9zZSgpO1xuXHRcdFx0XHRcdFx0XHRcdHJldHVybjtcblx0XHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHRcdGlmIChvbkRvd25sb2FkUHJvZ3Jlc3MpIHtcblx0XHRcdFx0XHRcdFx0XHR0cmFuc2ZlcnJlZEJ5dGVzICs9IHZhbHVlLmJ5dGVMZW5ndGg7XG5cdFx0XHRcdFx0XHRcdFx0Y29uc3QgcGVyY2VudCA9IHRvdGFsQnl0ZXMgPT09IDAgPyAwIDogdHJhbnNmZXJyZWRCeXRlcyAvIHRvdGFsQnl0ZXM7XG5cdFx0XHRcdFx0XHRcdFx0b25Eb3dubG9hZFByb2dyZXNzKHtwZXJjZW50LCB0cmFuc2ZlcnJlZEJ5dGVzLCB0b3RhbEJ5dGVzfSwgdmFsdWUpO1xuXHRcdFx0XHRcdFx0XHR9XG5cblx0XHRcdFx0XHRcdFx0Y29udHJvbGxlci5lbnF1ZXVlKHZhbHVlKTtcblx0XHRcdFx0XHRcdFx0cmVhZCgpO1xuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHRyZWFkKCk7XG5cdFx0XHRcdFx0fVxuXHRcdFx0XHR9KVxuXHRcdFx0KTtcblx0XHR9XG5cdH1cblxuXHRjb25zdCB2YWxpZGF0ZUFuZE1lcmdlID0gKC4uLnNvdXJjZXMpID0+IHtcblx0XHRmb3IgKGNvbnN0IHNvdXJjZSBvZiBzb3VyY2VzKSB7XG5cdFx0XHRpZiAoKCFpc09iamVjdChzb3VyY2UpIHx8IEFycmF5LmlzQXJyYXkoc291cmNlKSkgJiYgdHlwZW9mIHNvdXJjZSAhPT0gJ3VuZGVmaW5lZCcpIHtcblx0XHRcdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignVGhlIGBvcHRpb25zYCBhcmd1bWVudCBtdXN0IGJlIGFuIG9iamVjdCcpO1xuXHRcdFx0fVxuXHRcdH1cblxuXHRcdHJldHVybiBkZWVwTWVyZ2Uoe30sIC4uLnNvdXJjZXMpO1xuXHR9O1xuXG5cdGNvbnN0IGNyZWF0ZUluc3RhbmNlID0gZGVmYXVsdHMgPT4ge1xuXHRcdGNvbnN0IGt5ID0gKGlucHV0LCBvcHRpb25zKSA9PiBuZXcgS3koaW5wdXQsIHZhbGlkYXRlQW5kTWVyZ2UoZGVmYXVsdHMsIG9wdGlvbnMpKTtcblxuXHRcdGZvciAoY29uc3QgbWV0aG9kIG9mIHJlcXVlc3RNZXRob2RzKSB7XG5cdFx0XHRreVttZXRob2RdID0gKGlucHV0LCBvcHRpb25zKSA9PiBuZXcgS3koaW5wdXQsIHZhbGlkYXRlQW5kTWVyZ2UoZGVmYXVsdHMsIG9wdGlvbnMsIHttZXRob2R9KSk7XG5cdFx0fVxuXG5cdFx0a3kuSFRUUEVycm9yID0gSFRUUEVycm9yO1xuXHRcdGt5LlRpbWVvdXRFcnJvciA9IFRpbWVvdXRFcnJvcjtcblx0XHRreS5jcmVhdGUgPSBuZXdEZWZhdWx0cyA9PiBjcmVhdGVJbnN0YW5jZSh2YWxpZGF0ZUFuZE1lcmdlKG5ld0RlZmF1bHRzKSk7XG5cdFx0a3kuZXh0ZW5kID0gbmV3RGVmYXVsdHMgPT4gY3JlYXRlSW5zdGFuY2UodmFsaWRhdGVBbmRNZXJnZShkZWZhdWx0cywgbmV3RGVmYXVsdHMpKTtcblx0XHRreS5zdG9wID0gc3RvcDtcblxuXHRcdHJldHVybiBreTtcblx0fTtcblxuXHR2YXIgaW5kZXggPSBjcmVhdGVJbnN0YW5jZSgpO1xuXG5cdHJldHVybiBpbmRleDtcblxufSkpKTtcbiIsIid1c2Ugc3RyaWN0JztcblxuZXhwb3J0cyA9IG1vZHVsZS5leHBvcnRzID0gZmV0Y2g7XG5cbmNvbnN0IGh0dHAgPSByZXF1aXJlKCdodHRwJyk7XG5jb25zdCBodHRwcyA9IHJlcXVpcmUoJ2h0dHBzJyk7XG5jb25zdCB6bGliID0gcmVxdWlyZSgnemxpYicpO1xuY29uc3QgU3RyZWFtID0gcmVxdWlyZSgnc3RyZWFtJyk7XG5jb25zdCBkYXRhVXJpVG9CdWZmZXIgPSByZXF1aXJlKCdkYXRhLXVyaS10by1idWZmZXInKTtcbmNvbnN0IHV0aWwgPSByZXF1aXJlKCd1dGlsJyk7XG5jb25zdCBCbG9iID0gcmVxdWlyZSgnZmV0Y2gtYmxvYicpO1xuY29uc3QgY3J5cHRvID0gcmVxdWlyZSgnY3J5cHRvJyk7XG5jb25zdCB1cmwgPSByZXF1aXJlKCd1cmwnKTtcblxuY2xhc3MgRmV0Y2hCYXNlRXJyb3IgZXh0ZW5kcyBFcnJvciB7XG5cdGNvbnN0cnVjdG9yKG1lc3NhZ2UsIHR5cGUpIHtcblx0XHRzdXBlcihtZXNzYWdlKTtcblx0XHQvLyBIaWRlIGN1c3RvbSBlcnJvciBpbXBsZW1lbnRhdGlvbiBkZXRhaWxzIGZyb20gZW5kLXVzZXJzXG5cdFx0RXJyb3IuY2FwdHVyZVN0YWNrVHJhY2UodGhpcywgdGhpcy5jb25zdHJ1Y3Rvcik7XG5cblx0XHR0aGlzLnR5cGUgPSB0eXBlO1xuXHR9XG5cblx0Z2V0IG5hbWUoKSB7XG5cdFx0cmV0dXJuIHRoaXMuY29uc3RydWN0b3IubmFtZTtcblx0fVxuXG5cdGdldCBbU3ltYm9sLnRvU3RyaW5nVGFnXSgpIHtcblx0XHRyZXR1cm4gdGhpcy5jb25zdHJ1Y3Rvci5uYW1lO1xuXHR9XG59XG5cbi8qKlxuICogQHR5cGVkZWYge3sgYWRkcmVzcz86IHN0cmluZywgY29kZTogc3RyaW5nLCBkZXN0Pzogc3RyaW5nLCBlcnJubzogbnVtYmVyLCBpbmZvPzogb2JqZWN0LCBtZXNzYWdlOiBzdHJpbmcsIHBhdGg/OiBzdHJpbmcsIHBvcnQ/OiBudW1iZXIsIHN5c2NhbGw6IHN0cmluZ319IFN5c3RlbUVycm9yXG4qL1xuXG4vKipcbiAqIEZldGNoRXJyb3IgaW50ZXJmYWNlIGZvciBvcGVyYXRpb25hbCBlcnJvcnNcbiAqL1xuY2xhc3MgRmV0Y2hFcnJvciBleHRlbmRzIEZldGNoQmFzZUVycm9yIHtcblx0LyoqXG5cdCAqIEBwYXJhbSAge3N0cmluZ30gbWVzc2FnZSAtICAgICAgRXJyb3IgbWVzc2FnZSBmb3IgaHVtYW5cblx0ICogQHBhcmFtICB7c3RyaW5nfSBbdHlwZV0gLSAgICAgICAgRXJyb3IgdHlwZSBmb3IgbWFjaGluZVxuXHQgKiBAcGFyYW0gIHtTeXN0ZW1FcnJvcn0gW3N5c3RlbUVycm9yXSAtIEZvciBOb2RlLmpzIHN5c3RlbSBlcnJvclxuXHQgKi9cblx0Y29uc3RydWN0b3IobWVzc2FnZSwgdHlwZSwgc3lzdGVtRXJyb3IpIHtcblx0XHRzdXBlcihtZXNzYWdlLCB0eXBlKTtcblx0XHQvLyBXaGVuIGVyci50eXBlIGlzIGBzeXN0ZW1gLCBlcnIuZXJyb3JlZFN5c0NhbGwgY29udGFpbnMgc3lzdGVtIGVycm9yIGFuZCBlcnIuY29kZSBjb250YWlucyBzeXN0ZW0gZXJyb3IgY29kZVxuXHRcdGlmIChzeXN0ZW1FcnJvcikge1xuXHRcdFx0Ly8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLW11bHRpLWFzc2lnblxuXHRcdFx0dGhpcy5jb2RlID0gdGhpcy5lcnJubyA9IHN5c3RlbUVycm9yLmNvZGU7XG5cdFx0XHR0aGlzLmVycm9yZWRTeXNDYWxsID0gc3lzdGVtRXJyb3Iuc3lzY2FsbDtcblx0XHR9XG5cdH1cbn1cblxuLyoqXG4gKiBJcy5qc1xuICpcbiAqIE9iamVjdCB0eXBlIGNoZWNrcy5cbiAqL1xuXG5jb25zdCBOQU1FID0gU3ltYm9sLnRvU3RyaW5nVGFnO1xuXG4vKipcbiAqIENoZWNrIGlmIGBvYmpgIGlzIGEgVVJMU2VhcmNoUGFyYW1zIG9iamVjdFxuICogcmVmOiBodHRwczovL2dpdGh1Yi5jb20vbm9kZS1mZXRjaC9ub2RlLWZldGNoL2lzc3Vlcy8yOTYjaXNzdWVjb21tZW50LTMwNzU5ODE0M1xuICpcbiAqIEBwYXJhbSAgeyp9IG9ialxuICogQHJldHVybiB7Ym9vbGVhbn1cbiAqL1xuY29uc3QgaXNVUkxTZWFyY2hQYXJhbWV0ZXJzID0gb2JqZWN0ID0+IHtcblx0cmV0dXJuIChcblx0XHR0eXBlb2Ygb2JqZWN0ID09PSAnb2JqZWN0JyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuYXBwZW5kID09PSAnZnVuY3Rpb24nICYmXG5cdFx0dHlwZW9mIG9iamVjdC5kZWxldGUgPT09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LmdldCA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuZ2V0QWxsID09PSAnZnVuY3Rpb24nICYmXG5cdFx0dHlwZW9mIG9iamVjdC5oYXMgPT09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LnNldCA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3Quc29ydCA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdG9iamVjdFtOQU1FXSA9PT0gJ1VSTFNlYXJjaFBhcmFtcydcblx0KTtcbn07XG5cbi8qKlxuICogQ2hlY2sgaWYgYG9iamVjdGAgaXMgYSBXM0MgYEJsb2JgIG9iamVjdCAod2hpY2ggYEZpbGVgIGluaGVyaXRzIGZyb20pXG4gKlxuICogQHBhcmFtICB7Kn0gb2JqXG4gKiBAcmV0dXJuIHtib29sZWFufVxuICovXG5jb25zdCBpc0Jsb2IgPSBvYmplY3QgPT4ge1xuXHRyZXR1cm4gKFxuXHRcdHR5cGVvZiBvYmplY3QgPT09ICdvYmplY3QnICYmXG5cdFx0dHlwZW9mIG9iamVjdC5hcnJheUJ1ZmZlciA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QudHlwZSA9PT0gJ3N0cmluZycgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LnN0cmVhbSA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuY29uc3RydWN0b3IgPT09ICdmdW5jdGlvbicgJiZcblx0XHQvXihCbG9ifEZpbGUpJC8udGVzdChvYmplY3RbTkFNRV0pXG5cdCk7XG59O1xuXG4vKipcbiAqIENoZWNrIGlmIGBvYmpgIGlzIGEgc3BlYy1jb21wbGlhbnQgYEZvcm1EYXRhYCBvYmplY3RcbiAqXG4gKiBAcGFyYW0geyp9IG9iamVjdFxuICogQHJldHVybiB7Ym9vbGVhbn1cbiAqL1xuZnVuY3Rpb24gaXNGb3JtRGF0YShvYmplY3QpIHtcblx0cmV0dXJuIChcblx0XHR0eXBlb2Ygb2JqZWN0ID09PSAnb2JqZWN0JyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuYXBwZW5kID09PSAnZnVuY3Rpb24nICYmXG5cdFx0dHlwZW9mIG9iamVjdC5zZXQgPT09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LmdldCA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuZ2V0QWxsID09PSAnZnVuY3Rpb24nICYmXG5cdFx0dHlwZW9mIG9iamVjdC5kZWxldGUgPT09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LmtleXMgPT09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LnZhbHVlcyA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuZW50cmllcyA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuY29uc3RydWN0b3IgPT09ICdmdW5jdGlvbicgJiZcblx0XHRvYmplY3RbTkFNRV0gPT09ICdGb3JtRGF0YSdcblx0KTtcbn1cblxuLyoqXG4gKiBDaGVjayBpZiBgb2JqYCBpcyBhbiBpbnN0YW5jZSBvZiBBYm9ydFNpZ25hbC5cbiAqXG4gKiBAcGFyYW0gIHsqfSBvYmpcbiAqIEByZXR1cm4ge2Jvb2xlYW59XG4gKi9cbmNvbnN0IGlzQWJvcnRTaWduYWwgPSBvYmplY3QgPT4ge1xuXHRyZXR1cm4gKFxuXHRcdHR5cGVvZiBvYmplY3QgPT09ICdvYmplY3QnICYmXG5cdFx0b2JqZWN0W05BTUVdID09PSAnQWJvcnRTaWduYWwnXG5cdCk7XG59O1xuXG5jb25zdCBjYXJyaWFnZSA9ICdcXHJcXG4nO1xuY29uc3QgZGFzaGVzID0gJy0nLnJlcGVhdCgyKTtcbmNvbnN0IGNhcnJpYWdlTGVuZ3RoID0gQnVmZmVyLmJ5dGVMZW5ndGgoY2FycmlhZ2UpO1xuXG4vKipcbiAqIEBwYXJhbSB7c3RyaW5nfSBib3VuZGFyeVxuICovXG5jb25zdCBnZXRGb290ZXIgPSBib3VuZGFyeSA9PiBgJHtkYXNoZXN9JHtib3VuZGFyeX0ke2Rhc2hlc30ke2NhcnJpYWdlLnJlcGVhdCgyKX1gO1xuXG4vKipcbiAqIEBwYXJhbSB7c3RyaW5nfSBib3VuZGFyeVxuICogQHBhcmFtIHtzdHJpbmd9IG5hbWVcbiAqIEBwYXJhbSB7Kn0gZmllbGRcbiAqXG4gKiBAcmV0dXJuIHtzdHJpbmd9XG4gKi9cbmZ1bmN0aW9uIGdldEhlYWRlcihib3VuZGFyeSwgbmFtZSwgZmllbGQpIHtcblx0bGV0IGhlYWRlciA9ICcnO1xuXG5cdGhlYWRlciArPSBgJHtkYXNoZXN9JHtib3VuZGFyeX0ke2NhcnJpYWdlfWA7XG5cdGhlYWRlciArPSBgQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPVwiJHtuYW1lfVwiYDtcblxuXHRpZiAoaXNCbG9iKGZpZWxkKSkge1xuXHRcdGhlYWRlciArPSBgOyBmaWxlbmFtZT1cIiR7ZmllbGQubmFtZX1cIiR7Y2FycmlhZ2V9YDtcblx0XHRoZWFkZXIgKz0gYENvbnRlbnQtVHlwZTogJHtmaWVsZC50eXBlIHx8ICdhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0nfWA7XG5cdH1cblxuXHRyZXR1cm4gYCR7aGVhZGVyfSR7Y2FycmlhZ2UucmVwZWF0KDIpfWA7XG59XG5cbi8qKlxuICogQHJldHVybiB7c3RyaW5nfVxuICovXG5jb25zdCBnZXRCb3VuZGFyeSA9ICgpID0+IGNyeXB0by5yYW5kb21CeXRlcyg4KS50b1N0cmluZygnaGV4Jyk7XG5cbi8qKlxuICogQHBhcmFtIHtGb3JtRGF0YX0gZm9ybVxuICogQHBhcmFtIHtzdHJpbmd9IGJvdW5kYXJ5XG4gKi9cbmFzeW5jIGZ1bmN0aW9uICogZm9ybURhdGFJdGVyYXRvcihmb3JtLCBib3VuZGFyeSkge1xuXHRmb3IgKGNvbnN0IFtuYW1lLCB2YWx1ZV0gb2YgZm9ybSkge1xuXHRcdHlpZWxkIGdldEhlYWRlcihib3VuZGFyeSwgbmFtZSwgdmFsdWUpO1xuXG5cdFx0aWYgKGlzQmxvYih2YWx1ZSkpIHtcblx0XHRcdHlpZWxkICogdmFsdWUuc3RyZWFtKCk7XG5cdFx0fSBlbHNlIHtcblx0XHRcdHlpZWxkIHZhbHVlO1xuXHRcdH1cblxuXHRcdHlpZWxkIGNhcnJpYWdlO1xuXHR9XG5cblx0eWllbGQgZ2V0Rm9vdGVyKGJvdW5kYXJ5KTtcbn1cblxuLyoqXG4gKiBAcGFyYW0ge0Zvcm1EYXRhfSBmb3JtXG4gKiBAcGFyYW0ge3N0cmluZ30gYm91bmRhcnlcbiAqL1xuZnVuY3Rpb24gZ2V0Rm9ybURhdGFMZW5ndGgoZm9ybSwgYm91bmRhcnkpIHtcblx0bGV0IGxlbmd0aCA9IDA7XG5cblx0Zm9yIChjb25zdCBbbmFtZSwgdmFsdWVdIG9mIGZvcm0pIHtcblx0XHRsZW5ndGggKz0gQnVmZmVyLmJ5dGVMZW5ndGgoZ2V0SGVhZGVyKGJvdW5kYXJ5LCBuYW1lLCB2YWx1ZSkpO1xuXG5cdFx0aWYgKGlzQmxvYih2YWx1ZSkpIHtcblx0XHRcdGxlbmd0aCArPSB2YWx1ZS5zaXplO1xuXHRcdH0gZWxzZSB7XG5cdFx0XHRsZW5ndGggKz0gQnVmZmVyLmJ5dGVMZW5ndGgoU3RyaW5nKHZhbHVlKSk7XG5cdFx0fVxuXG5cdFx0bGVuZ3RoICs9IGNhcnJpYWdlTGVuZ3RoO1xuXHR9XG5cblx0bGVuZ3RoICs9IEJ1ZmZlci5ieXRlTGVuZ3RoKGdldEZvb3Rlcihib3VuZGFyeSkpO1xuXG5cdHJldHVybiBsZW5ndGg7XG59XG5cbmNvbnN0IElOVEVSTkFMUyA9IFN5bWJvbCgnQm9keSBpbnRlcm5hbHMnKTtcblxuLyoqXG4gKiBCb2R5IG1peGluXG4gKlxuICogUmVmOiBodHRwczovL2ZldGNoLnNwZWMud2hhdHdnLm9yZy8jYm9keVxuICpcbiAqIEBwYXJhbSAgIFN0cmVhbSAgYm9keSAgUmVhZGFibGUgc3RyZWFtXG4gKiBAcGFyYW0gICBPYmplY3QgIG9wdHMgIFJlc3BvbnNlIG9wdGlvbnNcbiAqIEByZXR1cm4gIFZvaWRcbiAqL1xuY2xhc3MgQm9keSB7XG5cdGNvbnN0cnVjdG9yKGJvZHksIHtcblx0XHRzaXplID0gMFxuXHR9ID0ge30pIHtcblx0XHRsZXQgYm91bmRhcnkgPSBudWxsO1xuXG5cdFx0aWYgKGJvZHkgPT09IG51bGwpIHtcblx0XHRcdC8vIEJvZHkgaXMgdW5kZWZpbmVkIG9yIG51bGxcblx0XHRcdGJvZHkgPSBudWxsO1xuXHRcdH0gZWxzZSBpZiAoaXNVUkxTZWFyY2hQYXJhbWV0ZXJzKGJvZHkpKSB7XG5cdFx0Ly8gQm9keSBpcyBhIFVSTFNlYXJjaFBhcmFtc1xuXHRcdFx0Ym9keSA9IEJ1ZmZlci5mcm9tKGJvZHkudG9TdHJpbmcoKSk7XG5cdFx0fSBlbHNlIGlmIChpc0Jsb2IoYm9keSkpIDsgZWxzZSBpZiAoQnVmZmVyLmlzQnVmZmVyKGJvZHkpKSA7IGVsc2UgaWYgKHV0aWwudHlwZXMuaXNBbnlBcnJheUJ1ZmZlcihib2R5KSkge1xuXHRcdFx0Ly8gQm9keSBpcyBBcnJheUJ1ZmZlclxuXHRcdFx0Ym9keSA9IEJ1ZmZlci5mcm9tKGJvZHkpO1xuXHRcdH0gZWxzZSBpZiAoQXJyYXlCdWZmZXIuaXNWaWV3KGJvZHkpKSB7XG5cdFx0XHQvLyBCb2R5IGlzIEFycmF5QnVmZmVyVmlld1xuXHRcdFx0Ym9keSA9IEJ1ZmZlci5mcm9tKGJvZHkuYnVmZmVyLCBib2R5LmJ5dGVPZmZzZXQsIGJvZHkuYnl0ZUxlbmd0aCk7XG5cdFx0fSBlbHNlIGlmIChib2R5IGluc3RhbmNlb2YgU3RyZWFtKSA7IGVsc2UgaWYgKGlzRm9ybURhdGEoYm9keSkpIHtcblx0XHRcdC8vIEJvZHkgaXMgYW4gaW5zdGFuY2Ugb2YgZm9ybWRhdGEtbm9kZVxuXHRcdFx0Ym91bmRhcnkgPSBgTm9kZUZldGNoRm9ybURhdGFCb3VuZGFyeSR7Z2V0Qm91bmRhcnkoKX1gO1xuXHRcdFx0Ym9keSA9IFN0cmVhbS5SZWFkYWJsZS5mcm9tKGZvcm1EYXRhSXRlcmF0b3IoYm9keSwgYm91bmRhcnkpKTtcblx0XHR9IGVsc2Uge1xuXHRcdFx0Ly8gTm9uZSBvZiB0aGUgYWJvdmVcblx0XHRcdC8vIGNvZXJjZSB0byBzdHJpbmcgdGhlbiBidWZmZXJcblx0XHRcdGJvZHkgPSBCdWZmZXIuZnJvbShTdHJpbmcoYm9keSkpO1xuXHRcdH1cblxuXHRcdHRoaXNbSU5URVJOQUxTXSA9IHtcblx0XHRcdGJvZHksXG5cdFx0XHRib3VuZGFyeSxcblx0XHRcdGRpc3R1cmJlZDogZmFsc2UsXG5cdFx0XHRlcnJvcjogbnVsbFxuXHRcdH07XG5cdFx0dGhpcy5zaXplID0gc2l6ZTtcblxuXHRcdGlmIChib2R5IGluc3RhbmNlb2YgU3RyZWFtKSB7XG5cdFx0XHRib2R5Lm9uKCdlcnJvcicsIGVyciA9PiB7XG5cdFx0XHRcdGNvbnN0IGVycm9yID0gZXJyIGluc3RhbmNlb2YgRmV0Y2hCYXNlRXJyb3IgP1xuXHRcdFx0XHRcdGVyciA6XG5cdFx0XHRcdFx0bmV3IEZldGNoRXJyb3IoYEludmFsaWQgcmVzcG9uc2UgYm9keSB3aGlsZSB0cnlpbmcgdG8gZmV0Y2ggJHt0aGlzLnVybH06ICR7ZXJyLm1lc3NhZ2V9YCwgJ3N5c3RlbScsIGVycik7XG5cdFx0XHRcdHRoaXNbSU5URVJOQUxTXS5lcnJvciA9IGVycm9yO1xuXHRcdFx0fSk7XG5cdFx0fVxuXHR9XG5cblx0Z2V0IGJvZHkoKSB7XG5cdFx0cmV0dXJuIHRoaXNbSU5URVJOQUxTXS5ib2R5O1xuXHR9XG5cblx0Z2V0IGJvZHlVc2VkKCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMU10uZGlzdHVyYmVkO1xuXHR9XG5cblx0LyoqXG5cdCAqIERlY29kZSByZXNwb25zZSBhcyBBcnJheUJ1ZmZlclxuXHQgKlxuXHQgKiBAcmV0dXJuICBQcm9taXNlXG5cdCAqL1xuXHRhc3luYyBhcnJheUJ1ZmZlcigpIHtcblx0XHRjb25zdCB7YnVmZmVyLCBieXRlT2Zmc2V0LCBieXRlTGVuZ3RofSA9IGF3YWl0IGNvbnN1bWVCb2R5KHRoaXMpO1xuXHRcdHJldHVybiBidWZmZXIuc2xpY2UoYnl0ZU9mZnNldCwgYnl0ZU9mZnNldCArIGJ5dGVMZW5ndGgpO1xuXHR9XG5cblx0LyoqXG5cdCAqIFJldHVybiByYXcgcmVzcG9uc2UgYXMgQmxvYlxuXHQgKlxuXHQgKiBAcmV0dXJuIFByb21pc2Vcblx0ICovXG5cdGFzeW5jIGJsb2IoKSB7XG5cdFx0Y29uc3QgY3QgPSAodGhpcy5oZWFkZXJzICYmIHRoaXMuaGVhZGVycy5nZXQoJ2NvbnRlbnQtdHlwZScpKSB8fCAodGhpc1tJTlRFUk5BTFNdLmJvZHkgJiYgdGhpc1tJTlRFUk5BTFNdLmJvZHkudHlwZSkgfHwgJyc7XG5cdFx0Y29uc3QgYnVmID0gYXdhaXQgdGhpcy5idWZmZXIoKTtcblxuXHRcdHJldHVybiBuZXcgQmxvYihbYnVmXSwge1xuXHRcdFx0dHlwZTogY3Rcblx0XHR9KTtcblx0fVxuXG5cdC8qKlxuXHQgKiBEZWNvZGUgcmVzcG9uc2UgYXMganNvblxuXHQgKlxuXHQgKiBAcmV0dXJuICBQcm9taXNlXG5cdCAqL1xuXHRhc3luYyBqc29uKCkge1xuXHRcdGNvbnN0IGJ1ZmZlciA9IGF3YWl0IGNvbnN1bWVCb2R5KHRoaXMpO1xuXHRcdHJldHVybiBKU09OLnBhcnNlKGJ1ZmZlci50b1N0cmluZygpKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBEZWNvZGUgcmVzcG9uc2UgYXMgdGV4dFxuXHQgKlxuXHQgKiBAcmV0dXJuICBQcm9taXNlXG5cdCAqL1xuXHRhc3luYyB0ZXh0KCkge1xuXHRcdGNvbnN0IGJ1ZmZlciA9IGF3YWl0IGNvbnN1bWVCb2R5KHRoaXMpO1xuXHRcdHJldHVybiBidWZmZXIudG9TdHJpbmcoKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBEZWNvZGUgcmVzcG9uc2UgYXMgYnVmZmVyIChub24tc3BlYyBhcGkpXG5cdCAqXG5cdCAqIEByZXR1cm4gIFByb21pc2Vcblx0ICovXG5cdGJ1ZmZlcigpIHtcblx0XHRyZXR1cm4gY29uc3VtZUJvZHkodGhpcyk7XG5cdH1cbn1cblxuLy8gSW4gYnJvd3NlcnMsIGFsbCBwcm9wZXJ0aWVzIGFyZSBlbnVtZXJhYmxlLlxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoQm9keS5wcm90b3R5cGUsIHtcblx0Ym9keToge2VudW1lcmFibGU6IHRydWV9LFxuXHRib2R5VXNlZDoge2VudW1lcmFibGU6IHRydWV9LFxuXHRhcnJheUJ1ZmZlcjoge2VudW1lcmFibGU6IHRydWV9LFxuXHRibG9iOiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdGpzb246IHtlbnVtZXJhYmxlOiB0cnVlfSxcblx0dGV4dDoge2VudW1lcmFibGU6IHRydWV9XG59KTtcblxuLyoqXG4gKiBDb25zdW1lIGFuZCBjb252ZXJ0IGFuIGVudGlyZSBCb2R5IHRvIGEgQnVmZmVyLlxuICpcbiAqIFJlZjogaHR0cHM6Ly9mZXRjaC5zcGVjLndoYXR3Zy5vcmcvI2NvbmNlcHQtYm9keS1jb25zdW1lLWJvZHlcbiAqXG4gKiBAcmV0dXJuIFByb21pc2VcbiAqL1xuYXN5bmMgZnVuY3Rpb24gY29uc3VtZUJvZHkoZGF0YSkge1xuXHRpZiAoZGF0YVtJTlRFUk5BTFNdLmRpc3R1cmJlZCkge1xuXHRcdHRocm93IG5ldyBUeXBlRXJyb3IoYGJvZHkgdXNlZCBhbHJlYWR5IGZvcjogJHtkYXRhLnVybH1gKTtcblx0fVxuXG5cdGRhdGFbSU5URVJOQUxTXS5kaXN0dXJiZWQgPSB0cnVlO1xuXG5cdGlmIChkYXRhW0lOVEVSTkFMU10uZXJyb3IpIHtcblx0XHR0aHJvdyBkYXRhW0lOVEVSTkFMU10uZXJyb3I7XG5cdH1cblxuXHRsZXQge2JvZHl9ID0gZGF0YTtcblxuXHQvLyBCb2R5IGlzIG51bGxcblx0aWYgKGJvZHkgPT09IG51bGwpIHtcblx0XHRyZXR1cm4gQnVmZmVyLmFsbG9jKDApO1xuXHR9XG5cblx0Ly8gQm9keSBpcyBibG9iXG5cdGlmIChpc0Jsb2IoYm9keSkpIHtcblx0XHRib2R5ID0gYm9keS5zdHJlYW0oKTtcblx0fVxuXG5cdC8vIEJvZHkgaXMgYnVmZmVyXG5cdGlmIChCdWZmZXIuaXNCdWZmZXIoYm9keSkpIHtcblx0XHRyZXR1cm4gYm9keTtcblx0fVxuXG5cdC8qIGM4IGlnbm9yZSBuZXh0IDMgKi9cblx0aWYgKCEoYm9keSBpbnN0YW5jZW9mIFN0cmVhbSkpIHtcblx0XHRyZXR1cm4gQnVmZmVyLmFsbG9jKDApO1xuXHR9XG5cblx0Ly8gQm9keSBpcyBzdHJlYW1cblx0Ly8gZ2V0IHJlYWR5IHRvIGFjdHVhbGx5IGNvbnN1bWUgdGhlIGJvZHlcblx0Y29uc3QgYWNjdW0gPSBbXTtcblx0bGV0IGFjY3VtQnl0ZXMgPSAwO1xuXG5cdHRyeSB7XG5cdFx0Zm9yIGF3YWl0IChjb25zdCBjaHVuayBvZiBib2R5KSB7XG5cdFx0XHRpZiAoZGF0YS5zaXplID4gMCAmJiBhY2N1bUJ5dGVzICsgY2h1bmsubGVuZ3RoID4gZGF0YS5zaXplKSB7XG5cdFx0XHRcdGNvbnN0IGVyciA9IG5ldyBGZXRjaEVycm9yKGBjb250ZW50IHNpemUgYXQgJHtkYXRhLnVybH0gb3ZlciBsaW1pdDogJHtkYXRhLnNpemV9YCwgJ21heC1zaXplJyk7XG5cdFx0XHRcdGJvZHkuZGVzdHJveShlcnIpO1xuXHRcdFx0XHR0aHJvdyBlcnI7XG5cdFx0XHR9XG5cblx0XHRcdGFjY3VtQnl0ZXMgKz0gY2h1bmsubGVuZ3RoO1xuXHRcdFx0YWNjdW0ucHVzaChjaHVuayk7XG5cdFx0fVxuXHR9IGNhdGNoIChlcnJvcikge1xuXHRcdGlmIChlcnJvciBpbnN0YW5jZW9mIEZldGNoQmFzZUVycm9yKSB7XG5cdFx0XHR0aHJvdyBlcnJvcjtcblx0XHR9IGVsc2Uge1xuXHRcdFx0Ly8gT3RoZXIgZXJyb3JzLCBzdWNoIGFzIGluY29ycmVjdCBjb250ZW50LWVuY29kaW5nXG5cdFx0XHR0aHJvdyBuZXcgRmV0Y2hFcnJvcihgSW52YWxpZCByZXNwb25zZSBib2R5IHdoaWxlIHRyeWluZyB0byBmZXRjaCAke2RhdGEudXJsfTogJHtlcnJvci5tZXNzYWdlfWAsICdzeXN0ZW0nLCBlcnJvcik7XG5cdFx0fVxuXHR9XG5cblx0aWYgKGJvZHkucmVhZGFibGVFbmRlZCA9PT0gdHJ1ZSB8fCBib2R5Ll9yZWFkYWJsZVN0YXRlLmVuZGVkID09PSB0cnVlKSB7XG5cdFx0dHJ5IHtcblx0XHRcdGlmIChhY2N1bS5ldmVyeShjID0+IHR5cGVvZiBjID09PSAnc3RyaW5nJykpIHtcblx0XHRcdFx0cmV0dXJuIEJ1ZmZlci5mcm9tKGFjY3VtLmpvaW4oJycpKTtcblx0XHRcdH1cblxuXHRcdFx0cmV0dXJuIEJ1ZmZlci5jb25jYXQoYWNjdW0sIGFjY3VtQnl0ZXMpO1xuXHRcdH0gY2F0Y2ggKGVycm9yKSB7XG5cdFx0XHR0aHJvdyBuZXcgRmV0Y2hFcnJvcihgQ291bGQgbm90IGNyZWF0ZSBCdWZmZXIgZnJvbSByZXNwb25zZSBib2R5IGZvciAke2RhdGEudXJsfTogJHtlcnJvci5tZXNzYWdlfWAsICdzeXN0ZW0nLCBlcnJvcik7XG5cdFx0fVxuXHR9IGVsc2Uge1xuXHRcdHRocm93IG5ldyBGZXRjaEVycm9yKGBQcmVtYXR1cmUgY2xvc2Ugb2Ygc2VydmVyIHJlc3BvbnNlIHdoaWxlIHRyeWluZyB0byBmZXRjaCAke2RhdGEudXJsfWApO1xuXHR9XG59XG5cbi8qKlxuICogQ2xvbmUgYm9keSBnaXZlbiBSZXMvUmVxIGluc3RhbmNlXG4gKlxuICogQHBhcmFtICAgTWl4ZWQgICBpbnN0YW5jZSAgICAgICBSZXNwb25zZSBvciBSZXF1ZXN0IGluc3RhbmNlXG4gKiBAcGFyYW0gICBTdHJpbmcgIGhpZ2hXYXRlck1hcmsgIGhpZ2hXYXRlck1hcmsgZm9yIGJvdGggUGFzc1Rocm91Z2ggYm9keSBzdHJlYW1zXG4gKiBAcmV0dXJuICBNaXhlZFxuICovXG5jb25zdCBjbG9uZSA9IChpbnN0YW5jZSwgaGlnaFdhdGVyTWFyaykgPT4ge1xuXHRsZXQgcDE7XG5cdGxldCBwMjtcblx0bGV0IHtib2R5fSA9IGluc3RhbmNlO1xuXG5cdC8vIERvbid0IGFsbG93IGNsb25pbmcgYSB1c2VkIGJvZHlcblx0aWYgKGluc3RhbmNlLmJvZHlVc2VkKSB7XG5cdFx0dGhyb3cgbmV3IEVycm9yKCdjYW5ub3QgY2xvbmUgYm9keSBhZnRlciBpdCBpcyB1c2VkJyk7XG5cdH1cblxuXHQvLyBDaGVjayB0aGF0IGJvZHkgaXMgYSBzdHJlYW0gYW5kIG5vdCBmb3JtLWRhdGEgb2JqZWN0XG5cdC8vIG5vdGU6IHdlIGNhbid0IGNsb25lIHRoZSBmb3JtLWRhdGEgb2JqZWN0IHdpdGhvdXQgaGF2aW5nIGl0IGFzIGEgZGVwZW5kZW5jeVxuXHRpZiAoKGJvZHkgaW5zdGFuY2VvZiBTdHJlYW0pICYmICh0eXBlb2YgYm9keS5nZXRCb3VuZGFyeSAhPT0gJ2Z1bmN0aW9uJykpIHtcblx0XHQvLyBUZWUgaW5zdGFuY2UgYm9keVxuXHRcdHAxID0gbmV3IFN0cmVhbS5QYXNzVGhyb3VnaCh7aGlnaFdhdGVyTWFya30pO1xuXHRcdHAyID0gbmV3IFN0cmVhbS5QYXNzVGhyb3VnaCh7aGlnaFdhdGVyTWFya30pO1xuXHRcdGJvZHkucGlwZShwMSk7XG5cdFx0Ym9keS5waXBlKHAyKTtcblx0XHQvLyBTZXQgaW5zdGFuY2UgYm9keSB0byB0ZWVkIGJvZHkgYW5kIHJldHVybiB0aGUgb3RoZXIgdGVlZCBib2R5XG5cdFx0aW5zdGFuY2VbSU5URVJOQUxTXS5ib2R5ID0gcDE7XG5cdFx0Ym9keSA9IHAyO1xuXHR9XG5cblx0cmV0dXJuIGJvZHk7XG59O1xuXG4vKipcbiAqIFBlcmZvcm1zIHRoZSBvcGVyYXRpb24gXCJleHRyYWN0IGEgYENvbnRlbnQtVHlwZWAgdmFsdWUgZnJvbSB8b2JqZWN0fFwiIGFzXG4gKiBzcGVjaWZpZWQgaW4gdGhlIHNwZWNpZmljYXRpb246XG4gKiBodHRwczovL2ZldGNoLnNwZWMud2hhdHdnLm9yZy8jY29uY2VwdC1ib2R5aW5pdC1leHRyYWN0XG4gKlxuICogVGhpcyBmdW5jdGlvbiBhc3N1bWVzIHRoYXQgaW5zdGFuY2UuYm9keSBpcyBwcmVzZW50LlxuICpcbiAqIEBwYXJhbSB7YW55fSBib2R5IEFueSBvcHRpb25zLmJvZHkgaW5wdXRcbiAqIEByZXR1cm5zIHtzdHJpbmcgfCBudWxsfVxuICovXG5jb25zdCBleHRyYWN0Q29udGVudFR5cGUgPSAoYm9keSwgcmVxdWVzdCkgPT4ge1xuXHQvLyBCb2R5IGlzIG51bGwgb3IgdW5kZWZpbmVkXG5cdGlmIChib2R5ID09PSBudWxsKSB7XG5cdFx0cmV0dXJuIG51bGw7XG5cdH1cblxuXHQvLyBCb2R5IGlzIHN0cmluZ1xuXHRpZiAodHlwZW9mIGJvZHkgPT09ICdzdHJpbmcnKSB7XG5cdFx0cmV0dXJuICd0ZXh0L3BsYWluO2NoYXJzZXQ9VVRGLTgnO1xuXHR9XG5cblx0Ly8gQm9keSBpcyBhIFVSTFNlYXJjaFBhcmFtc1xuXHRpZiAoaXNVUkxTZWFyY2hQYXJhbWV0ZXJzKGJvZHkpKSB7XG5cdFx0cmV0dXJuICdhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQ7Y2hhcnNldD1VVEYtOCc7XG5cdH1cblxuXHQvLyBCb2R5IGlzIGJsb2Jcblx0aWYgKGlzQmxvYihib2R5KSkge1xuXHRcdHJldHVybiBib2R5LnR5cGUgfHwgbnVsbDtcblx0fVxuXG5cdC8vIEJvZHkgaXMgYSBCdWZmZXIgKEJ1ZmZlciwgQXJyYXlCdWZmZXIgb3IgQXJyYXlCdWZmZXJWaWV3KVxuXHRpZiAoQnVmZmVyLmlzQnVmZmVyKGJvZHkpIHx8IHV0aWwudHlwZXMuaXNBbnlBcnJheUJ1ZmZlcihib2R5KSB8fCBBcnJheUJ1ZmZlci5pc1ZpZXcoYm9keSkpIHtcblx0XHRyZXR1cm4gbnVsbDtcblx0fVxuXG5cdC8vIERldGVjdCBmb3JtIGRhdGEgaW5wdXQgZnJvbSBmb3JtLWRhdGEgbW9kdWxlXG5cdGlmIChib2R5ICYmIHR5cGVvZiBib2R5LmdldEJvdW5kYXJ5ID09PSAnZnVuY3Rpb24nKSB7XG5cdFx0cmV0dXJuIGBtdWx0aXBhcnQvZm9ybS1kYXRhO2JvdW5kYXJ5PSR7Ym9keS5nZXRCb3VuZGFyeSgpfWA7XG5cdH1cblxuXHRpZiAoaXNGb3JtRGF0YShib2R5KSkge1xuXHRcdHJldHVybiBgbXVsdGlwYXJ0L2Zvcm0tZGF0YTsgYm91bmRhcnk9JHtyZXF1ZXN0W0lOVEVSTkFMU10uYm91bmRhcnl9YDtcblx0fVxuXG5cdC8vIEJvZHkgaXMgc3RyZWFtIC0gY2FuJ3QgcmVhbGx5IGRvIG11Y2ggYWJvdXQgdGhpc1xuXHRpZiAoYm9keSBpbnN0YW5jZW9mIFN0cmVhbSkge1xuXHRcdHJldHVybiBudWxsO1xuXHR9XG5cblx0Ly8gQm9keSBjb25zdHJ1Y3RvciBkZWZhdWx0cyBvdGhlciB0aGluZ3MgdG8gc3RyaW5nXG5cdHJldHVybiAndGV4dC9wbGFpbjtjaGFyc2V0PVVURi04Jztcbn07XG5cbi8qKlxuICogVGhlIEZldGNoIFN0YW5kYXJkIHRyZWF0cyB0aGlzIGFzIGlmIFwidG90YWwgYnl0ZXNcIiBpcyBhIHByb3BlcnR5IG9uIHRoZSBib2R5LlxuICogRm9yIHVzLCB3ZSBoYXZlIHRvIGV4cGxpY2l0bHkgZ2V0IGl0IHdpdGggYSBmdW5jdGlvbi5cbiAqXG4gKiByZWY6IGh0dHBzOi8vZmV0Y2guc3BlYy53aGF0d2cub3JnLyNjb25jZXB0LWJvZHktdG90YWwtYnl0ZXNcbiAqXG4gKiBAcGFyYW0ge2FueX0gb2JqLmJvZHkgQm9keSBvYmplY3QgZnJvbSB0aGUgQm9keSBpbnN0YW5jZS5cbiAqIEByZXR1cm5zIHtudW1iZXIgfCBudWxsfVxuICovXG5jb25zdCBnZXRUb3RhbEJ5dGVzID0gcmVxdWVzdCA9PiB7XG5cdGNvbnN0IHtib2R5fSA9IHJlcXVlc3Q7XG5cblx0Ly8gQm9keSBpcyBudWxsIG9yIHVuZGVmaW5lZFxuXHRpZiAoYm9keSA9PT0gbnVsbCkge1xuXHRcdHJldHVybiAwO1xuXHR9XG5cblx0Ly8gQm9keSBpcyBCbG9iXG5cdGlmIChpc0Jsb2IoYm9keSkpIHtcblx0XHRyZXR1cm4gYm9keS5zaXplO1xuXHR9XG5cblx0Ly8gQm9keSBpcyBCdWZmZXJcblx0aWYgKEJ1ZmZlci5pc0J1ZmZlcihib2R5KSkge1xuXHRcdHJldHVybiBib2R5Lmxlbmd0aDtcblx0fVxuXG5cdC8vIERldGVjdCBmb3JtIGRhdGEgaW5wdXQgZnJvbSBmb3JtLWRhdGEgbW9kdWxlXG5cdGlmIChib2R5ICYmIHR5cGVvZiBib2R5LmdldExlbmd0aFN5bmMgPT09ICdmdW5jdGlvbicpIHtcblx0XHRyZXR1cm4gYm9keS5oYXNLbm93bkxlbmd0aCAmJiBib2R5Lmhhc0tub3duTGVuZ3RoKCkgPyBib2R5LmdldExlbmd0aFN5bmMoKSA6IG51bGw7XG5cdH1cblxuXHQvLyBCb2R5IGlzIGEgc3BlYy1jb21wbGlhbnQgZm9ybS1kYXRhXG5cdGlmIChpc0Zvcm1EYXRhKGJvZHkpKSB7XG5cdFx0cmV0dXJuIGdldEZvcm1EYXRhTGVuZ3RoKHJlcXVlc3RbSU5URVJOQUxTXS5ib3VuZGFyeSk7XG5cdH1cblxuXHQvLyBCb2R5IGlzIHN0cmVhbVxuXHRyZXR1cm4gbnVsbDtcbn07XG5cbi8qKlxuICogV3JpdGUgYSBCb2R5IHRvIGEgTm9kZS5qcyBXcml0YWJsZVN0cmVhbSAoZS5nLiBodHRwLlJlcXVlc3QpIG9iamVjdC5cbiAqXG4gKiBAcGFyYW0ge1N0cmVhbS5Xcml0YWJsZX0gZGVzdCBUaGUgc3RyZWFtIHRvIHdyaXRlIHRvLlxuICogQHBhcmFtIG9iai5ib2R5IEJvZHkgb2JqZWN0IGZyb20gdGhlIEJvZHkgaW5zdGFuY2UuXG4gKiBAcmV0dXJucyB7dm9pZH1cbiAqL1xuY29uc3Qgd3JpdGVUb1N0cmVhbSA9IChkZXN0LCB7Ym9keX0pID0+IHtcblx0aWYgKGJvZHkgPT09IG51bGwpIHtcblx0XHQvLyBCb2R5IGlzIG51bGxcblx0XHRkZXN0LmVuZCgpO1xuXHR9IGVsc2UgaWYgKGlzQmxvYihib2R5KSkge1xuXHRcdC8vIEJvZHkgaXMgQmxvYlxuXHRcdGJvZHkuc3RyZWFtKCkucGlwZShkZXN0KTtcblx0fSBlbHNlIGlmIChCdWZmZXIuaXNCdWZmZXIoYm9keSkpIHtcblx0XHQvLyBCb2R5IGlzIGJ1ZmZlclxuXHRcdGRlc3Qud3JpdGUoYm9keSk7XG5cdFx0ZGVzdC5lbmQoKTtcblx0fSBlbHNlIHtcblx0XHQvLyBCb2R5IGlzIHN0cmVhbVxuXHRcdGJvZHkucGlwZShkZXN0KTtcblx0fVxufTtcblxuLyoqXG4gKiBIZWFkZXJzLmpzXG4gKlxuICogSGVhZGVycyBjbGFzcyBvZmZlcnMgY29udmVuaWVudCBoZWxwZXJzXG4gKi9cblxuY29uc3QgdmFsaWRhdGVIZWFkZXJOYW1lID0gdHlwZW9mIGh0dHAudmFsaWRhdGVIZWFkZXJOYW1lID09PSAnZnVuY3Rpb24nID9cblx0aHR0cC52YWxpZGF0ZUhlYWRlck5hbWUgOlxuXHRuYW1lID0+IHtcblx0XHRpZiAoIS9eW1xcXmBcXC1cXHchIyQlJicqKy58fl0rJC8udGVzdChuYW1lKSkge1xuXHRcdFx0Y29uc3QgZXJyID0gbmV3IFR5cGVFcnJvcihgSGVhZGVyIG5hbWUgbXVzdCBiZSBhIHZhbGlkIEhUVFAgdG9rZW4gWyR7bmFtZX1dYCk7XG5cdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXJyLCAnY29kZScsIHt2YWx1ZTogJ0VSUl9JTlZBTElEX0hUVFBfVE9LRU4nfSk7XG5cdFx0XHR0aHJvdyBlcnI7XG5cdFx0fVxuXHR9O1xuXG5jb25zdCB2YWxpZGF0ZUhlYWRlclZhbHVlID0gdHlwZW9mIGh0dHAudmFsaWRhdGVIZWFkZXJWYWx1ZSA9PT0gJ2Z1bmN0aW9uJyA/XG5cdGh0dHAudmFsaWRhdGVIZWFkZXJWYWx1ZSA6XG5cdChuYW1lLCB2YWx1ZSkgPT4ge1xuXHRcdGlmICgvW15cXHRcXHUwMDIwLVxcdTAwN0VcXHUwMDgwLVxcdTAwRkZdLy50ZXN0KHZhbHVlKSkge1xuXHRcdFx0Y29uc3QgZXJyID0gbmV3IFR5cGVFcnJvcihgSW52YWxpZCBjaGFyYWN0ZXIgaW4gaGVhZGVyIGNvbnRlbnQgW1wiJHtuYW1lfVwiXWApO1xuXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGVyciwgJ2NvZGUnLCB7dmFsdWU6ICdFUlJfSU5WQUxJRF9DSEFSJ30pO1xuXHRcdFx0dGhyb3cgZXJyO1xuXHRcdH1cblx0fTtcblxuLyoqXG4gKiBAdHlwZWRlZiB7SGVhZGVycyB8IFJlY29yZDxzdHJpbmcsIHN0cmluZz4gfCBJdGVyYWJsZTxyZWFkb25seSBbc3RyaW5nLCBzdHJpbmddPiB8IEl0ZXJhYmxlPEl0ZXJhYmxlPHN0cmluZz4+fSBIZWFkZXJzSW5pdFxuICovXG5cbi8qKlxuICogVGhpcyBGZXRjaCBBUEkgaW50ZXJmYWNlIGFsbG93cyB5b3UgdG8gcGVyZm9ybSB2YXJpb3VzIGFjdGlvbnMgb24gSFRUUCByZXF1ZXN0IGFuZCByZXNwb25zZSBoZWFkZXJzLlxuICogVGhlc2UgYWN0aW9ucyBpbmNsdWRlIHJldHJpZXZpbmcsIHNldHRpbmcsIGFkZGluZyB0bywgYW5kIHJlbW92aW5nLlxuICogQSBIZWFkZXJzIG9iamVjdCBoYXMgYW4gYXNzb2NpYXRlZCBoZWFkZXIgbGlzdCwgd2hpY2ggaXMgaW5pdGlhbGx5IGVtcHR5IGFuZCBjb25zaXN0cyBvZiB6ZXJvIG9yIG1vcmUgbmFtZSBhbmQgdmFsdWUgcGFpcnMuXG4gKiBZb3UgY2FuIGFkZCB0byB0aGlzIHVzaW5nIG1ldGhvZHMgbGlrZSBhcHBlbmQoKSAoc2VlIEV4YW1wbGVzLilcbiAqIEluIGFsbCBtZXRob2RzIG9mIHRoaXMgaW50ZXJmYWNlLCBoZWFkZXIgbmFtZXMgYXJlIG1hdGNoZWQgYnkgY2FzZS1pbnNlbnNpdGl2ZSBieXRlIHNlcXVlbmNlLlxuICpcbiAqL1xuY2xhc3MgSGVhZGVycyBleHRlbmRzIFVSTFNlYXJjaFBhcmFtcyB7XG5cdC8qKlxuXHQgKiBIZWFkZXJzIGNsYXNzXG5cdCAqXG5cdCAqIEBjb25zdHJ1Y3RvclxuXHQgKiBAcGFyYW0ge0hlYWRlcnNJbml0fSBbaW5pdF0gLSBSZXNwb25zZSBoZWFkZXJzXG5cdCAqL1xuXHRjb25zdHJ1Y3Rvcihpbml0KSB7XG5cdFx0Ly8gVmFsaWRhdGUgYW5kIG5vcm1hbGl6ZSBpbml0IG9iamVjdCBpbiBbbmFtZSwgdmFsdWUocyldW11cblx0XHQvKiogQHR5cGUge3N0cmluZ1tdW119ICovXG5cdFx0bGV0IHJlc3VsdCA9IFtdO1xuXHRcdGlmIChpbml0IGluc3RhbmNlb2YgSGVhZGVycykge1xuXHRcdFx0Y29uc3QgcmF3ID0gaW5pdC5yYXcoKTtcblx0XHRcdGZvciAoY29uc3QgW25hbWUsIHZhbHVlc10gb2YgT2JqZWN0LmVudHJpZXMocmF3KSkge1xuXHRcdFx0XHRyZXN1bHQucHVzaCguLi52YWx1ZXMubWFwKHZhbHVlID0+IFtuYW1lLCB2YWx1ZV0pKTtcblx0XHRcdH1cblx0XHR9IGVsc2UgaWYgKGluaXQgPT0gbnVsbCkgOyBlbHNlIGlmICh0eXBlb2YgaW5pdCA9PT0gJ29iamVjdCcgJiYgIXV0aWwudHlwZXMuaXNCb3hlZFByaW1pdGl2ZShpbml0KSkge1xuXHRcdFx0Y29uc3QgbWV0aG9kID0gaW5pdFtTeW1ib2wuaXRlcmF0b3JdO1xuXHRcdFx0Ly8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLWVxLW51bGwsIGVxZXFlcVxuXHRcdFx0aWYgKG1ldGhvZCA9PSBudWxsKSB7XG5cdFx0XHRcdC8vIFJlY29yZDxCeXRlU3RyaW5nLCBCeXRlU3RyaW5nPlxuXHRcdFx0XHRyZXN1bHQucHVzaCguLi5PYmplY3QuZW50cmllcyhpbml0KSk7XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRpZiAodHlwZW9mIG1ldGhvZCAhPT0gJ2Z1bmN0aW9uJykge1xuXHRcdFx0XHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ0hlYWRlciBwYWlycyBtdXN0IGJlIGl0ZXJhYmxlJyk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHQvLyBTZXF1ZW5jZTxzZXF1ZW5jZTxCeXRlU3RyaW5nPj5cblx0XHRcdFx0Ly8gTm90ZTogcGVyIHNwZWMgd2UgaGF2ZSB0byBmaXJzdCBleGhhdXN0IHRoZSBsaXN0cyB0aGVuIHByb2Nlc3MgdGhlbVxuXHRcdFx0XHRyZXN1bHQgPSBbLi4uaW5pdF1cblx0XHRcdFx0XHQubWFwKHBhaXIgPT4ge1xuXHRcdFx0XHRcdFx0aWYgKFxuXHRcdFx0XHRcdFx0XHR0eXBlb2YgcGFpciAhPT0gJ29iamVjdCcgfHwgdXRpbC50eXBlcy5pc0JveGVkUHJpbWl0aXZlKHBhaXIpXG5cdFx0XHRcdFx0XHQpIHtcblx0XHRcdFx0XHRcdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignRWFjaCBoZWFkZXIgcGFpciBtdXN0IGJlIGFuIGl0ZXJhYmxlIG9iamVjdCcpO1xuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHRyZXR1cm4gWy4uLnBhaXJdO1xuXHRcdFx0XHRcdH0pLm1hcChwYWlyID0+IHtcblx0XHRcdFx0XHRcdGlmIChwYWlyLmxlbmd0aCAhPT0gMikge1xuXHRcdFx0XHRcdFx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKCdFYWNoIGhlYWRlciBwYWlyIG11c3QgYmUgYSBuYW1lL3ZhbHVlIHR1cGxlJyk7XG5cdFx0XHRcdFx0XHR9XG5cblx0XHRcdFx0XHRcdHJldHVybiBbLi4ucGFpcl07XG5cdFx0XHRcdFx0fSk7XG5cdFx0XHR9XG5cdFx0fSBlbHNlIHtcblx0XHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ0ZhaWxlZCB0byBjb25zdHJ1Y3QgXFwnSGVhZGVyc1xcJzogVGhlIHByb3ZpZGVkIHZhbHVlIGlzIG5vdCBvZiB0eXBlIFxcJyhzZXF1ZW5jZTxzZXF1ZW5jZTxCeXRlU3RyaW5nPj4gb3IgcmVjb3JkPEJ5dGVTdHJpbmcsIEJ5dGVTdHJpbmc+KScpO1xuXHRcdH1cblxuXHRcdC8vIFZhbGlkYXRlIGFuZCBsb3dlcmNhc2Vcblx0XHRyZXN1bHQgPVxuXHRcdFx0cmVzdWx0Lmxlbmd0aCA+IDAgP1xuXHRcdFx0XHRyZXN1bHQubWFwKChbbmFtZSwgdmFsdWVdKSA9PiB7XG5cdFx0XHRcdFx0dmFsaWRhdGVIZWFkZXJOYW1lKG5hbWUpO1xuXHRcdFx0XHRcdHZhbGlkYXRlSGVhZGVyVmFsdWUobmFtZSwgU3RyaW5nKHZhbHVlKSk7XG5cdFx0XHRcdFx0cmV0dXJuIFtTdHJpbmcobmFtZSkudG9Mb3dlckNhc2UoKSwgU3RyaW5nKHZhbHVlKV07XG5cdFx0XHRcdH0pIDpcblx0XHRcdFx0dW5kZWZpbmVkO1xuXG5cdFx0c3VwZXIocmVzdWx0KTtcblxuXHRcdC8vIFJldHVybmluZyBhIFByb3h5IHRoYXQgd2lsbCBsb3dlcmNhc2Uga2V5IG5hbWVzLCB2YWxpZGF0ZSBwYXJhbWV0ZXJzIGFuZCBzb3J0IGtleXNcblx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tY29uc3RydWN0b3ItcmV0dXJuXG5cdFx0cmV0dXJuIG5ldyBQcm94eSh0aGlzLCB7XG5cdFx0XHRnZXQodGFyZ2V0LCBwLCByZWNlaXZlcikge1xuXHRcdFx0XHRzd2l0Y2ggKHApIHtcblx0XHRcdFx0XHRjYXNlICdhcHBlbmQnOlxuXHRcdFx0XHRcdGNhc2UgJ3NldCc6XG5cdFx0XHRcdFx0XHRyZXR1cm4gKG5hbWUsIHZhbHVlKSA9PiB7XG5cdFx0XHRcdFx0XHRcdHZhbGlkYXRlSGVhZGVyTmFtZShuYW1lKTtcblx0XHRcdFx0XHRcdFx0dmFsaWRhdGVIZWFkZXJWYWx1ZShuYW1lLCBTdHJpbmcodmFsdWUpKTtcblx0XHRcdFx0XHRcdFx0cmV0dXJuIFVSTFNlYXJjaFBhcmFtcy5wcm90b3R5cGVbcF0uY2FsbChcblx0XHRcdFx0XHRcdFx0XHRyZWNlaXZlcixcblx0XHRcdFx0XHRcdFx0XHRTdHJpbmcobmFtZSkudG9Mb3dlckNhc2UoKSxcblx0XHRcdFx0XHRcdFx0XHRTdHJpbmcodmFsdWUpXG5cdFx0XHRcdFx0XHRcdCk7XG5cdFx0XHRcdFx0XHR9O1xuXG5cdFx0XHRcdFx0Y2FzZSAnZGVsZXRlJzpcblx0XHRcdFx0XHRjYXNlICdoYXMnOlxuXHRcdFx0XHRcdGNhc2UgJ2dldEFsbCc6XG5cdFx0XHRcdFx0XHRyZXR1cm4gbmFtZSA9PiB7XG5cdFx0XHRcdFx0XHRcdHZhbGlkYXRlSGVhZGVyTmFtZShuYW1lKTtcblx0XHRcdFx0XHRcdFx0cmV0dXJuIFVSTFNlYXJjaFBhcmFtcy5wcm90b3R5cGVbcF0uY2FsbChcblx0XHRcdFx0XHRcdFx0XHRyZWNlaXZlcixcblx0XHRcdFx0XHRcdFx0XHRTdHJpbmcobmFtZSkudG9Mb3dlckNhc2UoKVxuXHRcdFx0XHRcdFx0XHQpO1xuXHRcdFx0XHRcdFx0fTtcblxuXHRcdFx0XHRcdGNhc2UgJ2tleXMnOlxuXHRcdFx0XHRcdFx0cmV0dXJuICgpID0+IHtcblx0XHRcdFx0XHRcdFx0dGFyZ2V0LnNvcnQoKTtcblx0XHRcdFx0XHRcdFx0cmV0dXJuIG5ldyBTZXQoVVJMU2VhcmNoUGFyYW1zLnByb3RvdHlwZS5rZXlzLmNhbGwodGFyZ2V0KSkua2V5cygpO1xuXHRcdFx0XHRcdFx0fTtcblxuXHRcdFx0XHRcdGRlZmF1bHQ6XG5cdFx0XHRcdFx0XHRyZXR1cm4gUmVmbGVjdC5nZXQodGFyZ2V0LCBwLCByZWNlaXZlcik7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHRcdC8qIGM4IGlnbm9yZSBuZXh0ICovXG5cdFx0fSk7XG5cdH1cblxuXHRnZXQgW1N5bWJvbC50b1N0cmluZ1RhZ10oKSB7XG5cdFx0cmV0dXJuIHRoaXMuY29uc3RydWN0b3IubmFtZTtcblx0fVxuXG5cdHRvU3RyaW5nKCkge1xuXHRcdHJldHVybiBPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nLmNhbGwodGhpcyk7XG5cdH1cblxuXHRnZXQobmFtZSkge1xuXHRcdGNvbnN0IHZhbHVlcyA9IHRoaXMuZ2V0QWxsKG5hbWUpO1xuXHRcdGlmICh2YWx1ZXMubGVuZ3RoID09PSAwKSB7XG5cdFx0XHRyZXR1cm4gbnVsbDtcblx0XHR9XG5cblx0XHRsZXQgdmFsdWUgPSB2YWx1ZXMuam9pbignLCAnKTtcblx0XHRpZiAoL15jb250ZW50LWVuY29kaW5nJC9pLnRlc3QobmFtZSkpIHtcblx0XHRcdHZhbHVlID0gdmFsdWUudG9Mb3dlckNhc2UoKTtcblx0XHR9XG5cblx0XHRyZXR1cm4gdmFsdWU7XG5cdH1cblxuXHRmb3JFYWNoKGNhbGxiYWNrKSB7XG5cdFx0Zm9yIChjb25zdCBuYW1lIG9mIHRoaXMua2V5cygpKSB7XG5cdFx0XHRjYWxsYmFjayh0aGlzLmdldChuYW1lKSwgbmFtZSk7XG5cdFx0fVxuXHR9XG5cblx0KiB2YWx1ZXMoKSB7XG5cdFx0Zm9yIChjb25zdCBuYW1lIG9mIHRoaXMua2V5cygpKSB7XG5cdFx0XHR5aWVsZCB0aGlzLmdldChuYW1lKTtcblx0XHR9XG5cdH1cblxuXHQvKipcblx0ICogQHR5cGUgeygpID0+IEl0ZXJhYmxlSXRlcmF0b3I8W3N0cmluZywgc3RyaW5nXT59XG5cdCAqL1xuXHQqIGVudHJpZXMoKSB7XG5cdFx0Zm9yIChjb25zdCBuYW1lIG9mIHRoaXMua2V5cygpKSB7XG5cdFx0XHR5aWVsZCBbbmFtZSwgdGhpcy5nZXQobmFtZSldO1xuXHRcdH1cblx0fVxuXG5cdFtTeW1ib2wuaXRlcmF0b3JdKCkge1xuXHRcdHJldHVybiB0aGlzLmVudHJpZXMoKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBOb2RlLWZldGNoIG5vbi1zcGVjIG1ldGhvZFxuXHQgKiByZXR1cm5pbmcgYWxsIGhlYWRlcnMgYW5kIHRoZWlyIHZhbHVlcyBhcyBhcnJheVxuXHQgKiBAcmV0dXJucyB7UmVjb3JkPHN0cmluZywgc3RyaW5nW10+fVxuXHQgKi9cblx0cmF3KCkge1xuXHRcdHJldHVybiBbLi4udGhpcy5rZXlzKCldLnJlZHVjZSgocmVzdWx0LCBrZXkpID0+IHtcblx0XHRcdHJlc3VsdFtrZXldID0gdGhpcy5nZXRBbGwoa2V5KTtcblx0XHRcdHJldHVybiByZXN1bHQ7XG5cdFx0fSwge30pO1xuXHR9XG5cblx0LyoqXG5cdCAqIEZvciBiZXR0ZXIgY29uc29sZS5sb2coaGVhZGVycykgYW5kIGFsc28gdG8gY29udmVydCBIZWFkZXJzIGludG8gTm9kZS5qcyBSZXF1ZXN0IGNvbXBhdGlibGUgZm9ybWF0XG5cdCAqL1xuXHRbU3ltYm9sLmZvcignbm9kZWpzLnV0aWwuaW5zcGVjdC5jdXN0b20nKV0oKSB7XG5cdFx0cmV0dXJuIFsuLi50aGlzLmtleXMoKV0ucmVkdWNlKChyZXN1bHQsIGtleSkgPT4ge1xuXHRcdFx0Y29uc3QgdmFsdWVzID0gdGhpcy5nZXRBbGwoa2V5KTtcblx0XHRcdC8vIEh0dHAucmVxdWVzdCgpIG9ubHkgc3VwcG9ydHMgc3RyaW5nIGFzIEhvc3QgaGVhZGVyLlxuXHRcdFx0Ly8gVGhpcyBoYWNrIG1ha2VzIHNwZWNpZnlpbmcgY3VzdG9tIEhvc3QgaGVhZGVyIHBvc3NpYmxlLlxuXHRcdFx0aWYgKGtleSA9PT0gJ2hvc3QnKSB7XG5cdFx0XHRcdHJlc3VsdFtrZXldID0gdmFsdWVzWzBdO1xuXHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0cmVzdWx0W2tleV0gPSB2YWx1ZXMubGVuZ3RoID4gMSA/IHZhbHVlcyA6IHZhbHVlc1swXTtcblx0XHRcdH1cblxuXHRcdFx0cmV0dXJuIHJlc3VsdDtcblx0XHR9LCB7fSk7XG5cdH1cbn1cblxuLyoqXG4gKiBSZS1zaGFwaW5nIG9iamVjdCBmb3IgV2ViIElETCB0ZXN0c1xuICogT25seSBuZWVkIHRvIGRvIGl0IGZvciBvdmVycmlkZGVuIG1ldGhvZHNcbiAqL1xuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoXG5cdEhlYWRlcnMucHJvdG90eXBlLFxuXHRbJ2dldCcsICdlbnRyaWVzJywgJ2ZvckVhY2gnLCAndmFsdWVzJ10ucmVkdWNlKChyZXN1bHQsIHByb3BlcnR5KSA9PiB7XG5cdFx0cmVzdWx0W3Byb3BlcnR5XSA9IHtlbnVtZXJhYmxlOiB0cnVlfTtcblx0XHRyZXR1cm4gcmVzdWx0O1xuXHR9LCB7fSlcbik7XG5cbi8qKlxuICogQ3JlYXRlIGEgSGVhZGVycyBvYmplY3QgZnJvbSBhbiBodHRwLkluY29taW5nTWVzc2FnZS5yYXdIZWFkZXJzLCBpZ25vcmluZyB0aG9zZSB0aGF0IGRvXG4gKiBub3QgY29uZm9ybSB0byBIVFRQIGdyYW1tYXIgcHJvZHVjdGlvbnMuXG4gKiBAcGFyYW0ge2ltcG9ydCgnaHR0cCcpLkluY29taW5nTWVzc2FnZVsncmF3SGVhZGVycyddfSBoZWFkZXJzXG4gKi9cbmZ1bmN0aW9uIGZyb21SYXdIZWFkZXJzKGhlYWRlcnMgPSBbXSkge1xuXHRyZXR1cm4gbmV3IEhlYWRlcnMoXG5cdFx0aGVhZGVyc1xuXHRcdFx0Ly8gU3BsaXQgaW50byBwYWlyc1xuXHRcdFx0LnJlZHVjZSgocmVzdWx0LCB2YWx1ZSwgaW5kZXgsIGFycmF5KSA9PiB7XG5cdFx0XHRcdGlmIChpbmRleCAlIDIgPT09IDApIHtcblx0XHRcdFx0XHRyZXN1bHQucHVzaChhcnJheS5zbGljZShpbmRleCwgaW5kZXggKyAyKSk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRyZXR1cm4gcmVzdWx0O1xuXHRcdFx0fSwgW10pXG5cdFx0XHQuZmlsdGVyKChbbmFtZSwgdmFsdWVdKSA9PiB7XG5cdFx0XHRcdHRyeSB7XG5cdFx0XHRcdFx0dmFsaWRhdGVIZWFkZXJOYW1lKG5hbWUpO1xuXHRcdFx0XHRcdHZhbGlkYXRlSGVhZGVyVmFsdWUobmFtZSwgU3RyaW5nKHZhbHVlKSk7XG5cdFx0XHRcdFx0cmV0dXJuIHRydWU7XG5cdFx0XHRcdH0gY2F0Y2gge1xuXHRcdFx0XHRcdHJldHVybiBmYWxzZTtcblx0XHRcdFx0fVxuXHRcdFx0fSlcblxuXHQpO1xufVxuXG5jb25zdCByZWRpcmVjdFN0YXR1cyA9IG5ldyBTZXQoWzMwMSwgMzAyLCAzMDMsIDMwNywgMzA4XSk7XG5cbi8qKlxuICogUmVkaXJlY3QgY29kZSBtYXRjaGluZ1xuICpcbiAqIEBwYXJhbSB7bnVtYmVyfSBjb2RlIC0gU3RhdHVzIGNvZGVcbiAqIEByZXR1cm4ge2Jvb2xlYW59XG4gKi9cbmNvbnN0IGlzUmVkaXJlY3QgPSBjb2RlID0+IHtcblx0cmV0dXJuIHJlZGlyZWN0U3RhdHVzLmhhcyhjb2RlKTtcbn07XG5cbi8qKlxuICogUmVzcG9uc2UuanNcbiAqXG4gKiBSZXNwb25zZSBjbGFzcyBwcm92aWRlcyBjb250ZW50IGRlY29kaW5nXG4gKi9cblxuY29uc3QgSU5URVJOQUxTJDEgPSBTeW1ib2woJ1Jlc3BvbnNlIGludGVybmFscycpO1xuXG4vKipcbiAqIFJlc3BvbnNlIGNsYXNzXG4gKlxuICogQHBhcmFtICAgU3RyZWFtICBib2R5ICBSZWFkYWJsZSBzdHJlYW1cbiAqIEBwYXJhbSAgIE9iamVjdCAgb3B0cyAgUmVzcG9uc2Ugb3B0aW9uc1xuICogQHJldHVybiAgVm9pZFxuICovXG5jbGFzcyBSZXNwb25zZSBleHRlbmRzIEJvZHkge1xuXHRjb25zdHJ1Y3Rvcihib2R5ID0gbnVsbCwgb3B0aW9ucyA9IHt9KSB7XG5cdFx0c3VwZXIoYm9keSwgb3B0aW9ucyk7XG5cblx0XHRjb25zdCBzdGF0dXMgPSBvcHRpb25zLnN0YXR1cyB8fCAyMDA7XG5cdFx0Y29uc3QgaGVhZGVycyA9IG5ldyBIZWFkZXJzKG9wdGlvbnMuaGVhZGVycyk7XG5cblx0XHRpZiAoYm9keSAhPT0gbnVsbCAmJiAhaGVhZGVycy5oYXMoJ0NvbnRlbnQtVHlwZScpKSB7XG5cdFx0XHRjb25zdCBjb250ZW50VHlwZSA9IGV4dHJhY3RDb250ZW50VHlwZShib2R5KTtcblx0XHRcdGlmIChjb250ZW50VHlwZSkge1xuXHRcdFx0XHRoZWFkZXJzLmFwcGVuZCgnQ29udGVudC1UeXBlJywgY29udGVudFR5cGUpO1xuXHRcdFx0fVxuXHRcdH1cblxuXHRcdHRoaXNbSU5URVJOQUxTJDFdID0ge1xuXHRcdFx0dXJsOiBvcHRpb25zLnVybCxcblx0XHRcdHN0YXR1cyxcblx0XHRcdHN0YXR1c1RleHQ6IG9wdGlvbnMuc3RhdHVzVGV4dCB8fCAnJyxcblx0XHRcdGhlYWRlcnMsXG5cdFx0XHRjb3VudGVyOiBvcHRpb25zLmNvdW50ZXIsXG5cdFx0XHRoaWdoV2F0ZXJNYXJrOiBvcHRpb25zLmhpZ2hXYXRlck1hcmtcblx0XHR9O1xuXHR9XG5cblx0Z2V0IHVybCgpIHtcblx0XHRyZXR1cm4gdGhpc1tJTlRFUk5BTFMkMV0udXJsIHx8ICcnO1xuXHR9XG5cblx0Z2V0IHN0YXR1cygpIHtcblx0XHRyZXR1cm4gdGhpc1tJTlRFUk5BTFMkMV0uc3RhdHVzO1xuXHR9XG5cblx0LyoqXG5cdCAqIENvbnZlbmllbmNlIHByb3BlcnR5IHJlcHJlc2VudGluZyBpZiB0aGUgcmVxdWVzdCBlbmRlZCBub3JtYWxseVxuXHQgKi9cblx0Z2V0IG9rKCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMUyQxXS5zdGF0dXMgPj0gMjAwICYmIHRoaXNbSU5URVJOQUxTJDFdLnN0YXR1cyA8IDMwMDtcblx0fVxuXG5cdGdldCByZWRpcmVjdGVkKCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMUyQxXS5jb3VudGVyID4gMDtcblx0fVxuXG5cdGdldCBzdGF0dXNUZXh0KCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMUyQxXS5zdGF0dXNUZXh0O1xuXHR9XG5cblx0Z2V0IGhlYWRlcnMoKSB7XG5cdFx0cmV0dXJuIHRoaXNbSU5URVJOQUxTJDFdLmhlYWRlcnM7XG5cdH1cblxuXHRnZXQgaGlnaFdhdGVyTWFyaygpIHtcblx0XHRyZXR1cm4gdGhpc1tJTlRFUk5BTFMkMV0uaGlnaFdhdGVyTWFyaztcblx0fVxuXG5cdC8qKlxuXHQgKiBDbG9uZSB0aGlzIHJlc3BvbnNlXG5cdCAqXG5cdCAqIEByZXR1cm4gIFJlc3BvbnNlXG5cdCAqL1xuXHRjbG9uZSgpIHtcblx0XHRyZXR1cm4gbmV3IFJlc3BvbnNlKGNsb25lKHRoaXMsIHRoaXMuaGlnaFdhdGVyTWFyayksIHtcblx0XHRcdHVybDogdGhpcy51cmwsXG5cdFx0XHRzdGF0dXM6IHRoaXMuc3RhdHVzLFxuXHRcdFx0c3RhdHVzVGV4dDogdGhpcy5zdGF0dXNUZXh0LFxuXHRcdFx0aGVhZGVyczogdGhpcy5oZWFkZXJzLFxuXHRcdFx0b2s6IHRoaXMub2ssXG5cdFx0XHRyZWRpcmVjdGVkOiB0aGlzLnJlZGlyZWN0ZWQsXG5cdFx0XHRzaXplOiB0aGlzLnNpemVcblx0XHR9KTtcblx0fVxuXG5cdC8qKlxuXHQgKiBAcGFyYW0ge3N0cmluZ30gdXJsICAgIFRoZSBVUkwgdGhhdCB0aGUgbmV3IHJlc3BvbnNlIGlzIHRvIG9yaWdpbmF0ZSBmcm9tLlxuXHQgKiBAcGFyYW0ge251bWJlcn0gc3RhdHVzIEFuIG9wdGlvbmFsIHN0YXR1cyBjb2RlIGZvciB0aGUgcmVzcG9uc2UgKGUuZy4sIDMwMi4pXG5cdCAqIEByZXR1cm5zIHtSZXNwb25zZX0gICAgQSBSZXNwb25zZSBvYmplY3QuXG5cdCAqL1xuXHRzdGF0aWMgcmVkaXJlY3QodXJsLCBzdGF0dXMgPSAzMDIpIHtcblx0XHRpZiAoIWlzUmVkaXJlY3Qoc3RhdHVzKSkge1xuXHRcdFx0dGhyb3cgbmV3IFJhbmdlRXJyb3IoJ0ZhaWxlZCB0byBleGVjdXRlIFwicmVkaXJlY3RcIiBvbiBcInJlc3BvbnNlXCI6IEludmFsaWQgc3RhdHVzIGNvZGUnKTtcblx0XHR9XG5cblx0XHRyZXR1cm4gbmV3IFJlc3BvbnNlKG51bGwsIHtcblx0XHRcdGhlYWRlcnM6IHtcblx0XHRcdFx0bG9jYXRpb246IG5ldyBVUkwodXJsKS50b1N0cmluZygpXG5cdFx0XHR9LFxuXHRcdFx0c3RhdHVzXG5cdFx0fSk7XG5cdH1cblxuXHRnZXQgW1N5bWJvbC50b1N0cmluZ1RhZ10oKSB7XG5cdFx0cmV0dXJuICdSZXNwb25zZSc7XG5cdH1cbn1cblxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoUmVzcG9uc2UucHJvdG90eXBlLCB7XG5cdHVybDoge2VudW1lcmFibGU6IHRydWV9LFxuXHRzdGF0dXM6IHtlbnVtZXJhYmxlOiB0cnVlfSxcblx0b2s6IHtlbnVtZXJhYmxlOiB0cnVlfSxcblx0cmVkaXJlY3RlZDoge2VudW1lcmFibGU6IHRydWV9LFxuXHRzdGF0dXNUZXh0OiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdGhlYWRlcnM6IHtlbnVtZXJhYmxlOiB0cnVlfSxcblx0Y2xvbmU6IHtlbnVtZXJhYmxlOiB0cnVlfVxufSk7XG5cbmNvbnN0IGdldFNlYXJjaCA9IHBhcnNlZFVSTCA9PiB7XG5cdGlmIChwYXJzZWRVUkwuc2VhcmNoKSB7XG5cdFx0cmV0dXJuIHBhcnNlZFVSTC5zZWFyY2g7XG5cdH1cblxuXHRjb25zdCBsYXN0T2Zmc2V0ID0gcGFyc2VkVVJMLmhyZWYubGVuZ3RoIC0gMTtcblx0Y29uc3QgaGFzaCA9IHBhcnNlZFVSTC5oYXNoIHx8IChwYXJzZWRVUkwuaHJlZltsYXN0T2Zmc2V0XSA9PT0gJyMnID8gJyMnIDogJycpO1xuXHRyZXR1cm4gcGFyc2VkVVJMLmhyZWZbbGFzdE9mZnNldCAtIGhhc2gubGVuZ3RoXSA9PT0gJz8nID8gJz8nIDogJyc7XG59O1xuXG5jb25zdCBJTlRFUk5BTFMkMiA9IFN5bWJvbCgnUmVxdWVzdCBpbnRlcm5hbHMnKTtcblxuLyoqXG4gKiBDaGVjayBpZiBgb2JqYCBpcyBhbiBpbnN0YW5jZSBvZiBSZXF1ZXN0LlxuICpcbiAqIEBwYXJhbSAgeyp9IG9ialxuICogQHJldHVybiB7Ym9vbGVhbn1cbiAqL1xuY29uc3QgaXNSZXF1ZXN0ID0gb2JqZWN0ID0+IHtcblx0cmV0dXJuIChcblx0XHR0eXBlb2Ygb2JqZWN0ID09PSAnb2JqZWN0JyAmJlxuXHRcdHR5cGVvZiBvYmplY3RbSU5URVJOQUxTJDJdID09PSAnb2JqZWN0J1xuXHQpO1xufTtcblxuLyoqXG4gKiBSZXF1ZXN0IGNsYXNzXG4gKlxuICogQHBhcmFtICAgTWl4ZWQgICBpbnB1dCAgVXJsIG9yIFJlcXVlc3QgaW5zdGFuY2VcbiAqIEBwYXJhbSAgIE9iamVjdCAgaW5pdCAgIEN1c3RvbSBvcHRpb25zXG4gKiBAcmV0dXJuICBWb2lkXG4gKi9cbmNsYXNzIFJlcXVlc3QgZXh0ZW5kcyBCb2R5IHtcblx0Y29uc3RydWN0b3IoaW5wdXQsIGluaXQgPSB7fSkge1xuXHRcdGxldCBwYXJzZWRVUkw7XG5cblx0XHQvLyBOb3JtYWxpemUgaW5wdXQgYW5kIGZvcmNlIFVSTCB0byBiZSBlbmNvZGVkIGFzIFVURi04IChodHRwczovL2dpdGh1Yi5jb20vbm9kZS1mZXRjaC9ub2RlLWZldGNoL2lzc3Vlcy8yNDUpXG5cdFx0aWYgKGlzUmVxdWVzdChpbnB1dCkpIHtcblx0XHRcdHBhcnNlZFVSTCA9IG5ldyBVUkwoaW5wdXQudXJsKTtcblx0XHR9IGVsc2Uge1xuXHRcdFx0cGFyc2VkVVJMID0gbmV3IFVSTChpbnB1dCk7XG5cdFx0XHRpbnB1dCA9IHt9O1xuXHRcdH1cblxuXHRcdGxldCBtZXRob2QgPSBpbml0Lm1ldGhvZCB8fCBpbnB1dC5tZXRob2QgfHwgJ0dFVCc7XG5cdFx0bWV0aG9kID0gbWV0aG9kLnRvVXBwZXJDYXNlKCk7XG5cblx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tZXEtbnVsbCwgZXFlcWVxXG5cdFx0aWYgKCgoaW5pdC5ib2R5ICE9IG51bGwgfHwgaXNSZXF1ZXN0KGlucHV0KSkgJiYgaW5wdXQuYm9keSAhPT0gbnVsbCkgJiZcblx0XHRcdChtZXRob2QgPT09ICdHRVQnIHx8IG1ldGhvZCA9PT0gJ0hFQUQnKSkge1xuXHRcdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignUmVxdWVzdCB3aXRoIEdFVC9IRUFEIG1ldGhvZCBjYW5ub3QgaGF2ZSBib2R5Jyk7XG5cdFx0fVxuXG5cdFx0Y29uc3QgaW5wdXRCb2R5ID0gaW5pdC5ib2R5ID9cblx0XHRcdGluaXQuYm9keSA6XG5cdFx0XHQoaXNSZXF1ZXN0KGlucHV0KSAmJiBpbnB1dC5ib2R5ICE9PSBudWxsID9cblx0XHRcdFx0Y2xvbmUoaW5wdXQpIDpcblx0XHRcdFx0bnVsbCk7XG5cblx0XHRzdXBlcihpbnB1dEJvZHksIHtcblx0XHRcdHNpemU6IGluaXQuc2l6ZSB8fCBpbnB1dC5zaXplIHx8IDBcblx0XHR9KTtcblxuXHRcdGNvbnN0IGhlYWRlcnMgPSBuZXcgSGVhZGVycyhpbml0LmhlYWRlcnMgfHwgaW5wdXQuaGVhZGVycyB8fCB7fSk7XG5cblx0XHRpZiAoaW5wdXRCb2R5ICE9PSBudWxsICYmICFoZWFkZXJzLmhhcygnQ29udGVudC1UeXBlJykpIHtcblx0XHRcdGNvbnN0IGNvbnRlbnRUeXBlID0gZXh0cmFjdENvbnRlbnRUeXBlKGlucHV0Qm9keSwgdGhpcyk7XG5cdFx0XHRpZiAoY29udGVudFR5cGUpIHtcblx0XHRcdFx0aGVhZGVycy5hcHBlbmQoJ0NvbnRlbnQtVHlwZScsIGNvbnRlbnRUeXBlKTtcblx0XHRcdH1cblx0XHR9XG5cblx0XHRsZXQgc2lnbmFsID0gaXNSZXF1ZXN0KGlucHV0KSA/XG5cdFx0XHRpbnB1dC5zaWduYWwgOlxuXHRcdFx0bnVsbDtcblx0XHRpZiAoJ3NpZ25hbCcgaW4gaW5pdCkge1xuXHRcdFx0c2lnbmFsID0gaW5pdC5zaWduYWw7XG5cdFx0fVxuXG5cdFx0aWYgKHNpZ25hbCAhPT0gbnVsbCAmJiAhaXNBYm9ydFNpZ25hbChzaWduYWwpKSB7XG5cdFx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKCdFeHBlY3RlZCBzaWduYWwgdG8gYmUgYW4gaW5zdGFuY2VvZiBBYm9ydFNpZ25hbCcpO1xuXHRcdH1cblxuXHRcdHRoaXNbSU5URVJOQUxTJDJdID0ge1xuXHRcdFx0bWV0aG9kLFxuXHRcdFx0cmVkaXJlY3Q6IGluaXQucmVkaXJlY3QgfHwgaW5wdXQucmVkaXJlY3QgfHwgJ2ZvbGxvdycsXG5cdFx0XHRoZWFkZXJzLFxuXHRcdFx0cGFyc2VkVVJMLFxuXHRcdFx0c2lnbmFsXG5cdFx0fTtcblxuXHRcdC8vIE5vZGUtZmV0Y2gtb25seSBvcHRpb25zXG5cdFx0dGhpcy5mb2xsb3cgPSBpbml0LmZvbGxvdyA9PT0gdW5kZWZpbmVkID8gKGlucHV0LmZvbGxvdyA9PT0gdW5kZWZpbmVkID8gMjAgOiBpbnB1dC5mb2xsb3cpIDogaW5pdC5mb2xsb3c7XG5cdFx0dGhpcy5jb21wcmVzcyA9IGluaXQuY29tcHJlc3MgPT09IHVuZGVmaW5lZCA/IChpbnB1dC5jb21wcmVzcyA9PT0gdW5kZWZpbmVkID8gdHJ1ZSA6IGlucHV0LmNvbXByZXNzKSA6IGluaXQuY29tcHJlc3M7XG5cdFx0dGhpcy5jb3VudGVyID0gaW5pdC5jb3VudGVyIHx8IGlucHV0LmNvdW50ZXIgfHwgMDtcblx0XHR0aGlzLmFnZW50ID0gaW5pdC5hZ2VudCB8fCBpbnB1dC5hZ2VudDtcblx0XHR0aGlzLmhpZ2hXYXRlck1hcmsgPSBpbml0LmhpZ2hXYXRlck1hcmsgfHwgaW5wdXQuaGlnaFdhdGVyTWFyayB8fCAxNjM4NDtcblx0XHR0aGlzLmluc2VjdXJlSFRUUFBhcnNlciA9IGluaXQuaW5zZWN1cmVIVFRQUGFyc2VyIHx8IGlucHV0Lmluc2VjdXJlSFRUUFBhcnNlciB8fCBmYWxzZTtcblx0fVxuXG5cdGdldCBtZXRob2QoKSB7XG5cdFx0cmV0dXJuIHRoaXNbSU5URVJOQUxTJDJdLm1ldGhvZDtcblx0fVxuXG5cdGdldCB1cmwoKSB7XG5cdFx0cmV0dXJuIHVybC5mb3JtYXQodGhpc1tJTlRFUk5BTFMkMl0ucGFyc2VkVVJMKTtcblx0fVxuXG5cdGdldCBoZWFkZXJzKCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMUyQyXS5oZWFkZXJzO1xuXHR9XG5cblx0Z2V0IHJlZGlyZWN0KCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMUyQyXS5yZWRpcmVjdDtcblx0fVxuXG5cdGdldCBzaWduYWwoKSB7XG5cdFx0cmV0dXJuIHRoaXNbSU5URVJOQUxTJDJdLnNpZ25hbDtcblx0fVxuXG5cdC8qKlxuXHQgKiBDbG9uZSB0aGlzIHJlcXVlc3Rcblx0ICpcblx0ICogQHJldHVybiAgUmVxdWVzdFxuXHQgKi9cblx0Y2xvbmUoKSB7XG5cdFx0cmV0dXJuIG5ldyBSZXF1ZXN0KHRoaXMpO1xuXHR9XG5cblx0Z2V0IFtTeW1ib2wudG9TdHJpbmdUYWddKCkge1xuXHRcdHJldHVybiAnUmVxdWVzdCc7XG5cdH1cbn1cblxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoUmVxdWVzdC5wcm90b3R5cGUsIHtcblx0bWV0aG9kOiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdHVybDoge2VudW1lcmFibGU6IHRydWV9LFxuXHRoZWFkZXJzOiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdHJlZGlyZWN0OiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdGNsb25lOiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdHNpZ25hbDoge2VudW1lcmFibGU6IHRydWV9XG59KTtcblxuLyoqXG4gKiBDb252ZXJ0IGEgUmVxdWVzdCB0byBOb2RlLmpzIGh0dHAgcmVxdWVzdCBvcHRpb25zLlxuICpcbiAqIEBwYXJhbSAgIFJlcXVlc3QgIEEgUmVxdWVzdCBpbnN0YW5jZVxuICogQHJldHVybiAgT2JqZWN0ICAgVGhlIG9wdGlvbnMgb2JqZWN0IHRvIGJlIHBhc3NlZCB0byBodHRwLnJlcXVlc3RcbiAqL1xuY29uc3QgZ2V0Tm9kZVJlcXVlc3RPcHRpb25zID0gcmVxdWVzdCA9PiB7XG5cdGNvbnN0IHtwYXJzZWRVUkx9ID0gcmVxdWVzdFtJTlRFUk5BTFMkMl07XG5cdGNvbnN0IGhlYWRlcnMgPSBuZXcgSGVhZGVycyhyZXF1ZXN0W0lOVEVSTkFMUyQyXS5oZWFkZXJzKTtcblxuXHQvLyBGZXRjaCBzdGVwIDEuM1xuXHRpZiAoIWhlYWRlcnMuaGFzKCdBY2NlcHQnKSkge1xuXHRcdGhlYWRlcnMuc2V0KCdBY2NlcHQnLCAnKi8qJyk7XG5cdH1cblxuXHQvLyBIVFRQLW5ldHdvcmstb3ItY2FjaGUgZmV0Y2ggc3RlcHMgMi40LTIuN1xuXHRsZXQgY29udGVudExlbmd0aFZhbHVlID0gbnVsbDtcblx0aWYgKHJlcXVlc3QuYm9keSA9PT0gbnVsbCAmJiAvXihwb3N0fHB1dCkkL2kudGVzdChyZXF1ZXN0Lm1ldGhvZCkpIHtcblx0XHRjb250ZW50TGVuZ3RoVmFsdWUgPSAnMCc7XG5cdH1cblxuXHRpZiAocmVxdWVzdC5ib2R5ICE9PSBudWxsKSB7XG5cdFx0Y29uc3QgdG90YWxCeXRlcyA9IGdldFRvdGFsQnl0ZXMocmVxdWVzdCk7XG5cdFx0Ly8gU2V0IENvbnRlbnQtTGVuZ3RoIGlmIHRvdGFsQnl0ZXMgaXMgYSBudW1iZXIgKHRoYXQgaXMgbm90IE5hTilcblx0XHRpZiAodHlwZW9mIHRvdGFsQnl0ZXMgPT09ICdudW1iZXInICYmICFOdW1iZXIuaXNOYU4odG90YWxCeXRlcykpIHtcblx0XHRcdGNvbnRlbnRMZW5ndGhWYWx1ZSA9IFN0cmluZyh0b3RhbEJ5dGVzKTtcblx0XHR9XG5cdH1cblxuXHRpZiAoY29udGVudExlbmd0aFZhbHVlKSB7XG5cdFx0aGVhZGVycy5zZXQoJ0NvbnRlbnQtTGVuZ3RoJywgY29udGVudExlbmd0aFZhbHVlKTtcblx0fVxuXG5cdC8vIEhUVFAtbmV0d29yay1vci1jYWNoZSBmZXRjaCBzdGVwIDIuMTFcblx0aWYgKCFoZWFkZXJzLmhhcygnVXNlci1BZ2VudCcpKSB7XG5cdFx0aGVhZGVycy5zZXQoJ1VzZXItQWdlbnQnLCAnbm9kZS1mZXRjaCcpO1xuXHR9XG5cblx0Ly8gSFRUUC1uZXR3b3JrLW9yLWNhY2hlIGZldGNoIHN0ZXAgMi4xNVxuXHRpZiAocmVxdWVzdC5jb21wcmVzcyAmJiAhaGVhZGVycy5oYXMoJ0FjY2VwdC1FbmNvZGluZycpKSB7XG5cdFx0aGVhZGVycy5zZXQoJ0FjY2VwdC1FbmNvZGluZycsICdnemlwLGRlZmxhdGUsYnInKTtcblx0fVxuXG5cdGxldCB7YWdlbnR9ID0gcmVxdWVzdDtcblx0aWYgKHR5cGVvZiBhZ2VudCA9PT0gJ2Z1bmN0aW9uJykge1xuXHRcdGFnZW50ID0gYWdlbnQocGFyc2VkVVJMKTtcblx0fVxuXG5cdGlmICghaGVhZGVycy5oYXMoJ0Nvbm5lY3Rpb24nKSAmJiAhYWdlbnQpIHtcblx0XHRoZWFkZXJzLnNldCgnQ29ubmVjdGlvbicsICdjbG9zZScpO1xuXHR9XG5cblx0Ly8gSFRUUC1uZXR3b3JrIGZldGNoIHN0ZXAgNC4yXG5cdC8vIGNodW5rZWQgZW5jb2RpbmcgaXMgaGFuZGxlZCBieSBOb2RlLmpzXG5cblx0Y29uc3Qgc2VhcmNoID0gZ2V0U2VhcmNoKHBhcnNlZFVSTCk7XG5cblx0Ly8gTWFudWFsbHkgc3ByZWFkIHRoZSBVUkwgb2JqZWN0IGluc3RlYWQgb2Ygc3ByZWFkIHN5bnRheFxuXHRjb25zdCByZXF1ZXN0T3B0aW9ucyA9IHtcblx0XHRwYXRoOiBwYXJzZWRVUkwucGF0aG5hbWUgKyBzZWFyY2gsXG5cdFx0cGF0aG5hbWU6IHBhcnNlZFVSTC5wYXRobmFtZSxcblx0XHRob3N0bmFtZTogcGFyc2VkVVJMLmhvc3RuYW1lLFxuXHRcdHByb3RvY29sOiBwYXJzZWRVUkwucHJvdG9jb2wsXG5cdFx0cG9ydDogcGFyc2VkVVJMLnBvcnQsXG5cdFx0aGFzaDogcGFyc2VkVVJMLmhhc2gsXG5cdFx0c2VhcmNoOiBwYXJzZWRVUkwuc2VhcmNoLFxuXHRcdHF1ZXJ5OiBwYXJzZWRVUkwucXVlcnksXG5cdFx0aHJlZjogcGFyc2VkVVJMLmhyZWYsXG5cdFx0bWV0aG9kOiByZXF1ZXN0Lm1ldGhvZCxcblx0XHRoZWFkZXJzOiBoZWFkZXJzW1N5bWJvbC5mb3IoJ25vZGVqcy51dGlsLmluc3BlY3QuY3VzdG9tJyldKCksXG5cdFx0aW5zZWN1cmVIVFRQUGFyc2VyOiByZXF1ZXN0Lmluc2VjdXJlSFRUUFBhcnNlcixcblx0XHRhZ2VudFxuXHR9O1xuXG5cdHJldHVybiByZXF1ZXN0T3B0aW9ucztcbn07XG5cbi8qKlxuICogQWJvcnRFcnJvciBpbnRlcmZhY2UgZm9yIGNhbmNlbGxlZCByZXF1ZXN0c1xuICovXG5jbGFzcyBBYm9ydEVycm9yIGV4dGVuZHMgRmV0Y2hCYXNlRXJyb3Ige1xuXHRjb25zdHJ1Y3RvcihtZXNzYWdlLCB0eXBlID0gJ2Fib3J0ZWQnKSB7XG5cdFx0c3VwZXIobWVzc2FnZSwgdHlwZSk7XG5cdH1cbn1cblxuLyoqXG4gKiBJbmRleC5qc1xuICpcbiAqIGEgcmVxdWVzdCBBUEkgY29tcGF0aWJsZSB3aXRoIHdpbmRvdy5mZXRjaFxuICpcbiAqIEFsbCBzcGVjIGFsZ29yaXRobSBzdGVwIG51bWJlcnMgYXJlIGJhc2VkIG9uIGh0dHBzOi8vZmV0Y2guc3BlYy53aGF0d2cub3JnL2NvbW1pdC1zbmFwc2hvdHMvYWU3MTY4MjJjYjNhNjE4NDMyMjZjZDA5MGVlZmM2NTg5NDQ2YzFkMi8uXG4gKi9cblxuY29uc3Qgc3VwcG9ydGVkU2NoZW1hcyA9IG5ldyBTZXQoWydkYXRhOicsICdodHRwOicsICdodHRwczonXSk7XG5cbi8qKlxuICogRmV0Y2ggZnVuY3Rpb25cbiAqXG4gKiBAcGFyYW0gICB7c3RyaW5nIHwgVVJMIHwgaW1wb3J0KCcuL3JlcXVlc3QnKS5kZWZhdWx0fSB1cmwgLSBBYnNvbHV0ZSB1cmwgb3IgUmVxdWVzdCBpbnN0YW5jZVxuICogQHBhcmFtICAgeyp9IFtvcHRpb25zX10gLSBGZXRjaCBvcHRpb25zXG4gKiBAcmV0dXJuICB7UHJvbWlzZTxpbXBvcnQoJy4vcmVzcG9uc2UnKS5kZWZhdWx0Pn1cbiAqL1xuYXN5bmMgZnVuY3Rpb24gZmV0Y2godXJsLCBvcHRpb25zXykge1xuXHRyZXR1cm4gbmV3IFByb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuXHRcdC8vIEJ1aWxkIHJlcXVlc3Qgb2JqZWN0XG5cdFx0Y29uc3QgcmVxdWVzdCA9IG5ldyBSZXF1ZXN0KHVybCwgb3B0aW9uc18pO1xuXHRcdGNvbnN0IG9wdGlvbnMgPSBnZXROb2RlUmVxdWVzdE9wdGlvbnMocmVxdWVzdCk7XG5cdFx0aWYgKCFzdXBwb3J0ZWRTY2hlbWFzLmhhcyhvcHRpb25zLnByb3RvY29sKSkge1xuXHRcdFx0dGhyb3cgbmV3IFR5cGVFcnJvcihgbm9kZS1mZXRjaCBjYW5ub3QgbG9hZCAke3VybH0uIFVSTCBzY2hlbWUgXCIke29wdGlvbnMucHJvdG9jb2wucmVwbGFjZSgvOiQvLCAnJyl9XCIgaXMgbm90IHN1cHBvcnRlZC5gKTtcblx0XHR9XG5cblx0XHRpZiAob3B0aW9ucy5wcm90b2NvbCA9PT0gJ2RhdGE6Jykge1xuXHRcdFx0Y29uc3QgZGF0YSA9IGRhdGFVcmlUb0J1ZmZlcihyZXF1ZXN0LnVybCk7XG5cdFx0XHRjb25zdCByZXNwb25zZSA9IG5ldyBSZXNwb25zZShkYXRhLCB7aGVhZGVyczogeydDb250ZW50LVR5cGUnOiBkYXRhLnR5cGVGdWxsfX0pO1xuXHRcdFx0cmVzb2x2ZShyZXNwb25zZSk7XG5cdFx0XHRyZXR1cm47XG5cdFx0fVxuXG5cdFx0Ly8gV3JhcCBodHRwLnJlcXVlc3QgaW50byBmZXRjaFxuXHRcdGNvbnN0IHNlbmQgPSAob3B0aW9ucy5wcm90b2NvbCA9PT0gJ2h0dHBzOicgPyBodHRwcyA6IGh0dHApLnJlcXVlc3Q7XG5cdFx0Y29uc3Qge3NpZ25hbH0gPSByZXF1ZXN0O1xuXHRcdGxldCByZXNwb25zZSA9IG51bGw7XG5cblx0XHRjb25zdCBhYm9ydCA9ICgpID0+IHtcblx0XHRcdGNvbnN0IGVycm9yID0gbmV3IEFib3J0RXJyb3IoJ1RoZSBvcGVyYXRpb24gd2FzIGFib3J0ZWQuJyk7XG5cdFx0XHRyZWplY3QoZXJyb3IpO1xuXHRcdFx0aWYgKHJlcXVlc3QuYm9keSAmJiByZXF1ZXN0LmJvZHkgaW5zdGFuY2VvZiBTdHJlYW0uUmVhZGFibGUpIHtcblx0XHRcdFx0cmVxdWVzdC5ib2R5LmRlc3Ryb3koZXJyb3IpO1xuXHRcdFx0fVxuXG5cdFx0XHRpZiAoIXJlc3BvbnNlIHx8ICFyZXNwb25zZS5ib2R5KSB7XG5cdFx0XHRcdHJldHVybjtcblx0XHRcdH1cblxuXHRcdFx0cmVzcG9uc2UuYm9keS5lbWl0KCdlcnJvcicsIGVycm9yKTtcblx0XHR9O1xuXG5cdFx0aWYgKHNpZ25hbCAmJiBzaWduYWwuYWJvcnRlZCkge1xuXHRcdFx0YWJvcnQoKTtcblx0XHRcdHJldHVybjtcblx0XHR9XG5cblx0XHRjb25zdCBhYm9ydEFuZEZpbmFsaXplID0gKCkgPT4ge1xuXHRcdFx0YWJvcnQoKTtcblx0XHRcdGZpbmFsaXplKCk7XG5cdFx0fTtcblxuXHRcdC8vIFNlbmQgcmVxdWVzdFxuXHRcdGNvbnN0IHJlcXVlc3RfID0gc2VuZChvcHRpb25zKTtcblxuXHRcdGlmIChzaWduYWwpIHtcblx0XHRcdHNpZ25hbC5hZGRFdmVudExpc3RlbmVyKCdhYm9ydCcsIGFib3J0QW5kRmluYWxpemUpO1xuXHRcdH1cblxuXHRcdGNvbnN0IGZpbmFsaXplID0gKCkgPT4ge1xuXHRcdFx0cmVxdWVzdF8uYWJvcnQoKTtcblx0XHRcdGlmIChzaWduYWwpIHtcblx0XHRcdFx0c2lnbmFsLnJlbW92ZUV2ZW50TGlzdGVuZXIoJ2Fib3J0JywgYWJvcnRBbmRGaW5hbGl6ZSk7XG5cdFx0XHR9XG5cdFx0fTtcblxuXHRcdHJlcXVlc3RfLm9uKCdlcnJvcicsIGVyciA9PiB7XG5cdFx0XHRyZWplY3QobmV3IEZldGNoRXJyb3IoYHJlcXVlc3QgdG8gJHtyZXF1ZXN0LnVybH0gZmFpbGVkLCByZWFzb246ICR7ZXJyLm1lc3NhZ2V9YCwgJ3N5c3RlbScsIGVycikpO1xuXHRcdFx0ZmluYWxpemUoKTtcblx0XHR9KTtcblxuXHRcdHJlcXVlc3RfLm9uKCdyZXNwb25zZScsIHJlc3BvbnNlXyA9PiB7XG5cdFx0XHRyZXF1ZXN0Xy5zZXRUaW1lb3V0KDApO1xuXHRcdFx0Y29uc3QgaGVhZGVycyA9IGZyb21SYXdIZWFkZXJzKHJlc3BvbnNlXy5yYXdIZWFkZXJzKTtcblxuXHRcdFx0Ly8gSFRUUCBmZXRjaCBzdGVwIDVcblx0XHRcdGlmIChpc1JlZGlyZWN0KHJlc3BvbnNlXy5zdGF0dXNDb2RlKSkge1xuXHRcdFx0XHQvLyBIVFRQIGZldGNoIHN0ZXAgNS4yXG5cdFx0XHRcdGNvbnN0IGxvY2F0aW9uID0gaGVhZGVycy5nZXQoJ0xvY2F0aW9uJyk7XG5cblx0XHRcdFx0Ly8gSFRUUCBmZXRjaCBzdGVwIDUuM1xuXHRcdFx0XHRjb25zdCBsb2NhdGlvblVSTCA9IGxvY2F0aW9uID09PSBudWxsID8gbnVsbCA6IG5ldyBVUkwobG9jYXRpb24sIHJlcXVlc3QudXJsKTtcblxuXHRcdFx0XHQvLyBIVFRQIGZldGNoIHN0ZXAgNS41XG5cdFx0XHRcdHN3aXRjaCAocmVxdWVzdC5yZWRpcmVjdCkge1xuXHRcdFx0XHRcdGNhc2UgJ2Vycm9yJzpcblx0XHRcdFx0XHRcdHJlamVjdChuZXcgRmV0Y2hFcnJvcihgdXJpIHJlcXVlc3RlZCByZXNwb25kcyB3aXRoIGEgcmVkaXJlY3QsIHJlZGlyZWN0IG1vZGUgaXMgc2V0IHRvIGVycm9yOiAke3JlcXVlc3QudXJsfWAsICduby1yZWRpcmVjdCcpKTtcblx0XHRcdFx0XHRcdGZpbmFsaXplKCk7XG5cdFx0XHRcdFx0XHRyZXR1cm47XG5cdFx0XHRcdFx0Y2FzZSAnbWFudWFsJzpcblx0XHRcdFx0XHRcdC8vIE5vZGUtZmV0Y2gtc3BlY2lmaWMgc3RlcDogbWFrZSBtYW51YWwgcmVkaXJlY3QgYSBiaXQgZWFzaWVyIHRvIHVzZSBieSBzZXR0aW5nIHRoZSBMb2NhdGlvbiBoZWFkZXIgdmFsdWUgdG8gdGhlIHJlc29sdmVkIFVSTC5cblx0XHRcdFx0XHRcdGlmIChsb2NhdGlvblVSTCAhPT0gbnVsbCkge1xuXHRcdFx0XHRcdFx0XHQvLyBIYW5kbGUgY29ycnVwdGVkIGhlYWRlclxuXHRcdFx0XHRcdFx0XHR0cnkge1xuXHRcdFx0XHRcdFx0XHRcdGhlYWRlcnMuc2V0KCdMb2NhdGlvbicsIGxvY2F0aW9uVVJMKTtcblx0XHRcdFx0XHRcdFx0XHQvKiBjOCBpZ25vcmUgbmV4dCAzICovXG5cdFx0XHRcdFx0XHRcdH0gY2F0Y2ggKGVycm9yKSB7XG5cdFx0XHRcdFx0XHRcdFx0cmVqZWN0KGVycm9yKTtcblx0XHRcdFx0XHRcdFx0fVxuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHRicmVhaztcblx0XHRcdFx0XHRjYXNlICdmb2xsb3cnOiB7XG5cdFx0XHRcdFx0XHQvLyBIVFRQLXJlZGlyZWN0IGZldGNoIHN0ZXAgMlxuXHRcdFx0XHRcdFx0aWYgKGxvY2F0aW9uVVJMID09PSBudWxsKSB7XG5cdFx0XHRcdFx0XHRcdGJyZWFrO1xuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHQvLyBIVFRQLXJlZGlyZWN0IGZldGNoIHN0ZXAgNVxuXHRcdFx0XHRcdFx0aWYgKHJlcXVlc3QuY291bnRlciA+PSByZXF1ZXN0LmZvbGxvdykge1xuXHRcdFx0XHRcdFx0XHRyZWplY3QobmV3IEZldGNoRXJyb3IoYG1heGltdW0gcmVkaXJlY3QgcmVhY2hlZCBhdDogJHtyZXF1ZXN0LnVybH1gLCAnbWF4LXJlZGlyZWN0JykpO1xuXHRcdFx0XHRcdFx0XHRmaW5hbGl6ZSgpO1xuXHRcdFx0XHRcdFx0XHRyZXR1cm47XG5cdFx0XHRcdFx0XHR9XG5cblx0XHRcdFx0XHRcdC8vIEhUVFAtcmVkaXJlY3QgZmV0Y2ggc3RlcCA2IChjb3VudGVyIGluY3JlbWVudClcblx0XHRcdFx0XHRcdC8vIENyZWF0ZSBhIG5ldyBSZXF1ZXN0IG9iamVjdC5cblx0XHRcdFx0XHRcdGNvbnN0IHJlcXVlc3RPcHRpb25zID0ge1xuXHRcdFx0XHRcdFx0XHRoZWFkZXJzOiBuZXcgSGVhZGVycyhyZXF1ZXN0LmhlYWRlcnMpLFxuXHRcdFx0XHRcdFx0XHRmb2xsb3c6IHJlcXVlc3QuZm9sbG93LFxuXHRcdFx0XHRcdFx0XHRjb3VudGVyOiByZXF1ZXN0LmNvdW50ZXIgKyAxLFxuXHRcdFx0XHRcdFx0XHRhZ2VudDogcmVxdWVzdC5hZ2VudCxcblx0XHRcdFx0XHRcdFx0Y29tcHJlc3M6IHJlcXVlc3QuY29tcHJlc3MsXG5cdFx0XHRcdFx0XHRcdG1ldGhvZDogcmVxdWVzdC5tZXRob2QsXG5cdFx0XHRcdFx0XHRcdGJvZHk6IHJlcXVlc3QuYm9keSxcblx0XHRcdFx0XHRcdFx0c2lnbmFsOiByZXF1ZXN0LnNpZ25hbCxcblx0XHRcdFx0XHRcdFx0c2l6ZTogcmVxdWVzdC5zaXplXG5cdFx0XHRcdFx0XHR9O1xuXG5cdFx0XHRcdFx0XHQvLyBIVFRQLXJlZGlyZWN0IGZldGNoIHN0ZXAgOVxuXHRcdFx0XHRcdFx0aWYgKHJlc3BvbnNlXy5zdGF0dXNDb2RlICE9PSAzMDMgJiYgcmVxdWVzdC5ib2R5ICYmIG9wdGlvbnNfLmJvZHkgaW5zdGFuY2VvZiBTdHJlYW0uUmVhZGFibGUpIHtcblx0XHRcdFx0XHRcdFx0cmVqZWN0KG5ldyBGZXRjaEVycm9yKCdDYW5ub3QgZm9sbG93IHJlZGlyZWN0IHdpdGggYm9keSBiZWluZyBhIHJlYWRhYmxlIHN0cmVhbScsICd1bnN1cHBvcnRlZC1yZWRpcmVjdCcpKTtcblx0XHRcdFx0XHRcdFx0ZmluYWxpemUoKTtcblx0XHRcdFx0XHRcdFx0cmV0dXJuO1xuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHQvLyBIVFRQLXJlZGlyZWN0IGZldGNoIHN0ZXAgMTFcblx0XHRcdFx0XHRcdGlmIChyZXNwb25zZV8uc3RhdHVzQ29kZSA9PT0gMzAzIHx8ICgocmVzcG9uc2VfLnN0YXR1c0NvZGUgPT09IDMwMSB8fCByZXNwb25zZV8uc3RhdHVzQ29kZSA9PT0gMzAyKSAmJiByZXF1ZXN0Lm1ldGhvZCA9PT0gJ1BPU1QnKSkge1xuXHRcdFx0XHRcdFx0XHRyZXF1ZXN0T3B0aW9ucy5tZXRob2QgPSAnR0VUJztcblx0XHRcdFx0XHRcdFx0cmVxdWVzdE9wdGlvbnMuYm9keSA9IHVuZGVmaW5lZDtcblx0XHRcdFx0XHRcdFx0cmVxdWVzdE9wdGlvbnMuaGVhZGVycy5kZWxldGUoJ2NvbnRlbnQtbGVuZ3RoJyk7XG5cdFx0XHRcdFx0XHR9XG5cblx0XHRcdFx0XHRcdC8vIEhUVFAtcmVkaXJlY3QgZmV0Y2ggc3RlcCAxNVxuXHRcdFx0XHRcdFx0cmVzb2x2ZShmZXRjaChuZXcgUmVxdWVzdChsb2NhdGlvblVSTCwgcmVxdWVzdE9wdGlvbnMpKSk7XG5cdFx0XHRcdFx0XHRmaW5hbGl6ZSgpO1xuXHRcdFx0XHRcdFx0cmV0dXJuO1xuXHRcdFx0XHRcdH1cblx0XHRcdFx0XHQvLyBEbyBub3RoaW5nXG5cdFx0XHRcdH1cblx0XHRcdH1cblxuXHRcdFx0Ly8gUHJlcGFyZSByZXNwb25zZVxuXHRcdFx0cmVzcG9uc2VfLm9uY2UoJ2VuZCcsICgpID0+IHtcblx0XHRcdFx0aWYgKHNpZ25hbCkge1xuXHRcdFx0XHRcdHNpZ25hbC5yZW1vdmVFdmVudExpc3RlbmVyKCdhYm9ydCcsIGFib3J0QW5kRmluYWxpemUpO1xuXHRcdFx0XHR9XG5cdFx0XHR9KTtcblxuXHRcdFx0bGV0IGJvZHkgPSBTdHJlYW0ucGlwZWxpbmUocmVzcG9uc2VfLCBuZXcgU3RyZWFtLlBhc3NUaHJvdWdoKCksIGVycm9yID0+IHtcblx0XHRcdFx0cmVqZWN0KGVycm9yKTtcblx0XHRcdH0pO1xuXHRcdFx0Ly8gc2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9ub2RlanMvbm9kZS9wdWxsLzI5Mzc2XG5cdFx0XHRpZiAocHJvY2Vzcy52ZXJzaW9uIDwgJ3YxMi4xMCcpIHtcblx0XHRcdFx0cmVzcG9uc2VfLm9uKCdhYm9ydGVkJywgYWJvcnRBbmRGaW5hbGl6ZSk7XG5cdFx0XHR9XG5cblx0XHRcdGNvbnN0IHJlc3BvbnNlT3B0aW9ucyA9IHtcblx0XHRcdFx0dXJsOiByZXF1ZXN0LnVybCxcblx0XHRcdFx0c3RhdHVzOiByZXNwb25zZV8uc3RhdHVzQ29kZSxcblx0XHRcdFx0c3RhdHVzVGV4dDogcmVzcG9uc2VfLnN0YXR1c01lc3NhZ2UsXG5cdFx0XHRcdGhlYWRlcnMsXG5cdFx0XHRcdHNpemU6IHJlcXVlc3Quc2l6ZSxcblx0XHRcdFx0Y291bnRlcjogcmVxdWVzdC5jb3VudGVyLFxuXHRcdFx0XHRoaWdoV2F0ZXJNYXJrOiByZXF1ZXN0LmhpZ2hXYXRlck1hcmtcblx0XHRcdH07XG5cblx0XHRcdC8vIEhUVFAtbmV0d29yayBmZXRjaCBzdGVwIDEyLjEuMS4zXG5cdFx0XHRjb25zdCBjb2RpbmdzID0gaGVhZGVycy5nZXQoJ0NvbnRlbnQtRW5jb2RpbmcnKTtcblxuXHRcdFx0Ly8gSFRUUC1uZXR3b3JrIGZldGNoIHN0ZXAgMTIuMS4xLjQ6IGhhbmRsZSBjb250ZW50IGNvZGluZ3NcblxuXHRcdFx0Ly8gaW4gZm9sbG93aW5nIHNjZW5hcmlvcyB3ZSBpZ25vcmUgY29tcHJlc3Npb24gc3VwcG9ydFxuXHRcdFx0Ly8gMS4gY29tcHJlc3Npb24gc3VwcG9ydCBpcyBkaXNhYmxlZFxuXHRcdFx0Ly8gMi4gSEVBRCByZXF1ZXN0XG5cdFx0XHQvLyAzLiBubyBDb250ZW50LUVuY29kaW5nIGhlYWRlclxuXHRcdFx0Ly8gNC4gbm8gY29udGVudCByZXNwb25zZSAoMjA0KVxuXHRcdFx0Ly8gNS4gY29udGVudCBub3QgbW9kaWZpZWQgcmVzcG9uc2UgKDMwNClcblx0XHRcdGlmICghcmVxdWVzdC5jb21wcmVzcyB8fCByZXF1ZXN0Lm1ldGhvZCA9PT0gJ0hFQUQnIHx8IGNvZGluZ3MgPT09IG51bGwgfHwgcmVzcG9uc2VfLnN0YXR1c0NvZGUgPT09IDIwNCB8fCByZXNwb25zZV8uc3RhdHVzQ29kZSA9PT0gMzA0KSB7XG5cdFx0XHRcdHJlc3BvbnNlID0gbmV3IFJlc3BvbnNlKGJvZHksIHJlc3BvbnNlT3B0aW9ucyk7XG5cdFx0XHRcdHJlc29sdmUocmVzcG9uc2UpO1xuXHRcdFx0XHRyZXR1cm47XG5cdFx0XHR9XG5cblx0XHRcdC8vIEZvciBOb2RlIHY2K1xuXHRcdFx0Ly8gQmUgbGVzcyBzdHJpY3Qgd2hlbiBkZWNvZGluZyBjb21wcmVzc2VkIHJlc3BvbnNlcywgc2luY2Ugc29tZXRpbWVzXG5cdFx0XHQvLyBzZXJ2ZXJzIHNlbmQgc2xpZ2h0bHkgaW52YWxpZCByZXNwb25zZXMgdGhhdCBhcmUgc3RpbGwgYWNjZXB0ZWRcblx0XHRcdC8vIGJ5IGNvbW1vbiBicm93c2Vycy5cblx0XHRcdC8vIEFsd2F5cyB1c2luZyBaX1NZTkNfRkxVU0ggaXMgd2hhdCBjVVJMIGRvZXMuXG5cdFx0XHRjb25zdCB6bGliT3B0aW9ucyA9IHtcblx0XHRcdFx0Zmx1c2g6IHpsaWIuWl9TWU5DX0ZMVVNILFxuXHRcdFx0XHRmaW5pc2hGbHVzaDogemxpYi5aX1NZTkNfRkxVU0hcblx0XHRcdH07XG5cblx0XHRcdC8vIEZvciBnemlwXG5cdFx0XHRpZiAoY29kaW5ncyA9PT0gJ2d6aXAnIHx8IGNvZGluZ3MgPT09ICd4LWd6aXAnKSB7XG5cdFx0XHRcdGJvZHkgPSBTdHJlYW0ucGlwZWxpbmUoYm9keSwgemxpYi5jcmVhdGVHdW56aXAoemxpYk9wdGlvbnMpLCBlcnJvciA9PiB7XG5cdFx0XHRcdFx0cmVqZWN0KGVycm9yKTtcblx0XHRcdFx0fSk7XG5cdFx0XHRcdHJlc3BvbnNlID0gbmV3IFJlc3BvbnNlKGJvZHksIHJlc3BvbnNlT3B0aW9ucyk7XG5cdFx0XHRcdHJlc29sdmUocmVzcG9uc2UpO1xuXHRcdFx0XHRyZXR1cm47XG5cdFx0XHR9XG5cblx0XHRcdC8vIEZvciBkZWZsYXRlXG5cdFx0XHRpZiAoY29kaW5ncyA9PT0gJ2RlZmxhdGUnIHx8IGNvZGluZ3MgPT09ICd4LWRlZmxhdGUnKSB7XG5cdFx0XHRcdC8vIEhhbmRsZSB0aGUgaW5mYW1vdXMgcmF3IGRlZmxhdGUgcmVzcG9uc2UgZnJvbSBvbGQgc2VydmVyc1xuXHRcdFx0XHQvLyBhIGhhY2sgZm9yIG9sZCBJSVMgYW5kIEFwYWNoZSBzZXJ2ZXJzXG5cdFx0XHRcdGNvbnN0IHJhdyA9IFN0cmVhbS5waXBlbGluZShyZXNwb25zZV8sIG5ldyBTdHJlYW0uUGFzc1Rocm91Z2goKSwgZXJyb3IgPT4ge1xuXHRcdFx0XHRcdHJlamVjdChlcnJvcik7XG5cdFx0XHRcdH0pO1xuXHRcdFx0XHRyYXcub25jZSgnZGF0YScsIGNodW5rID0+IHtcblx0XHRcdFx0XHQvLyBTZWUgaHR0cDovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy8zNzUxOTgyOFxuXHRcdFx0XHRcdGlmICgoY2h1bmtbMF0gJiAweDBGKSA9PT0gMHgwOCkge1xuXHRcdFx0XHRcdFx0Ym9keSA9IFN0cmVhbS5waXBlbGluZShib2R5LCB6bGliLmNyZWF0ZUluZmxhdGUoKSwgZXJyb3IgPT4ge1xuXHRcdFx0XHRcdFx0XHRyZWplY3QoZXJyb3IpO1xuXHRcdFx0XHRcdFx0fSk7XG5cdFx0XHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0XHRcdGJvZHkgPSBTdHJlYW0ucGlwZWxpbmUoYm9keSwgemxpYi5jcmVhdGVJbmZsYXRlUmF3KCksIGVycm9yID0+IHtcblx0XHRcdFx0XHRcdFx0cmVqZWN0KGVycm9yKTtcblx0XHRcdFx0XHRcdH0pO1xuXHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdHJlc3BvbnNlID0gbmV3IFJlc3BvbnNlKGJvZHksIHJlc3BvbnNlT3B0aW9ucyk7XG5cdFx0XHRcdFx0cmVzb2x2ZShyZXNwb25zZSk7XG5cdFx0XHRcdH0pO1xuXHRcdFx0XHRyZXR1cm47XG5cdFx0XHR9XG5cblx0XHRcdC8vIEZvciBiclxuXHRcdFx0aWYgKGNvZGluZ3MgPT09ICdicicpIHtcblx0XHRcdFx0Ym9keSA9IFN0cmVhbS5waXBlbGluZShib2R5LCB6bGliLmNyZWF0ZUJyb3RsaURlY29tcHJlc3MoKSwgZXJyb3IgPT4ge1xuXHRcdFx0XHRcdHJlamVjdChlcnJvcik7XG5cdFx0XHRcdH0pO1xuXHRcdFx0XHRyZXNwb25zZSA9IG5ldyBSZXNwb25zZShib2R5LCByZXNwb25zZU9wdGlvbnMpO1xuXHRcdFx0XHRyZXNvbHZlKHJlc3BvbnNlKTtcblx0XHRcdFx0cmV0dXJuO1xuXHRcdFx0fVxuXG5cdFx0XHQvLyBPdGhlcndpc2UsIHVzZSByZXNwb25zZSBhcy1pc1xuXHRcdFx0cmVzcG9uc2UgPSBuZXcgUmVzcG9uc2UoYm9keSwgcmVzcG9uc2VPcHRpb25zKTtcblx0XHRcdHJlc29sdmUocmVzcG9uc2UpO1xuXHRcdH0pO1xuXG5cdFx0d3JpdGVUb1N0cmVhbShyZXF1ZXN0XywgcmVxdWVzdCk7XG5cdH0pO1xufVxuXG5leHBvcnRzLkFib3J0RXJyb3IgPSBBYm9ydEVycm9yO1xuZXhwb3J0cy5GZXRjaEVycm9yID0gRmV0Y2hFcnJvcjtcbmV4cG9ydHMuSGVhZGVycyA9IEhlYWRlcnM7XG5leHBvcnRzLlJlcXVlc3QgPSBSZXF1ZXN0O1xuZXhwb3J0cy5SZXNwb25zZSA9IFJlc3BvbnNlO1xuZXhwb3J0cy5kZWZhdWx0ID0gZmV0Y2g7XG5leHBvcnRzLmlzUmVkaXJlY3QgPSBpc1JlZGlyZWN0O1xuLy8jIHNvdXJjZU1hcHBpbmdVUkw9aW5kZXguY2pzLm1hcFxuIiwiZnVuY3Rpb24gbm9ybWFsaXplIChzdHIpIHtcbiAgcmV0dXJuIHN0clxuICAgICAgICAgIC5yZXBsYWNlKC9bXFwvXSsvZywgJy8nKVxuICAgICAgICAgIC5yZXBsYWNlKC9cXC9cXD8vZywgJz8nKVxuICAgICAgICAgIC5yZXBsYWNlKC9cXC9cXCMvZywgJyMnKVxuICAgICAgICAgIC5yZXBsYWNlKC9cXDpcXC8vZywgJzovLycpO1xufVxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uICgpIHtcbiAgdmFyIGpvaW5lZCA9IFtdLnNsaWNlLmNhbGwoYXJndW1lbnRzLCAwKS5qb2luKCcvJyk7XG4gIHJldHVybiBub3JtYWxpemUoam9pbmVkKTtcbn07IiwiLyoqXG4gKiB3ZWItc3RyZWFtcy1wb2x5ZmlsbCB2My4wLjFcbiAqL1xuLy8vIDxyZWZlcmVuY2UgbGliPVwiZXMyMDE1LnN5bWJvbFwiIC8+XG5jb25zdCBTeW1ib2xQb2x5ZmlsbCA9IHR5cGVvZiBTeW1ib2wgPT09ICdmdW5jdGlvbicgJiYgdHlwZW9mIFN5bWJvbC5pdGVyYXRvciA9PT0gJ3N5bWJvbCcgP1xuICAgIFN5bWJvbCA6XG4gICAgZGVzY3JpcHRpb24gPT4gYFN5bWJvbCgke2Rlc2NyaXB0aW9ufSlgO1xuXG4vLy8gPHJlZmVyZW5jZSBsaWI9XCJkb21cIiAvPlxuZnVuY3Rpb24gbm9vcCgpIHtcbiAgICAvLyBkbyBub3RoaW5nXG59XG5mdW5jdGlvbiBnZXRHbG9iYWxzKCkge1xuICAgIGlmICh0eXBlb2Ygc2VsZiAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgcmV0dXJuIHNlbGY7XG4gICAgfVxuICAgIGVsc2UgaWYgKHR5cGVvZiB3aW5kb3cgIT09ICd1bmRlZmluZWQnKSB7XG4gICAgICAgIHJldHVybiB3aW5kb3c7XG4gICAgfVxuICAgIGVsc2UgaWYgKHR5cGVvZiBnbG9iYWwgIT09ICd1bmRlZmluZWQnKSB7XG4gICAgICAgIHJldHVybiBnbG9iYWw7XG4gICAgfVxuICAgIHJldHVybiB1bmRlZmluZWQ7XG59XG5jb25zdCBnbG9iYWxzID0gZ2V0R2xvYmFscygpO1xuXG5mdW5jdGlvbiB0eXBlSXNPYmplY3QoeCkge1xuICAgIHJldHVybiAodHlwZW9mIHggPT09ICdvYmplY3QnICYmIHggIT09IG51bGwpIHx8IHR5cGVvZiB4ID09PSAnZnVuY3Rpb24nO1xufVxuY29uc3QgcmV0aHJvd0Fzc2VydGlvbkVycm9yUmVqZWN0aW9uID0gIG5vb3A7XG5cbmNvbnN0IG9yaWdpbmFsUHJvbWlzZSA9IFByb21pc2U7XG5jb25zdCBvcmlnaW5hbFByb21pc2VUaGVuID0gUHJvbWlzZS5wcm90b3R5cGUudGhlbjtcbmNvbnN0IG9yaWdpbmFsUHJvbWlzZVJlc29sdmUgPSBQcm9taXNlLnJlc29sdmUuYmluZChvcmlnaW5hbFByb21pc2UpO1xuY29uc3Qgb3JpZ2luYWxQcm9taXNlUmVqZWN0ID0gUHJvbWlzZS5yZWplY3QuYmluZChvcmlnaW5hbFByb21pc2UpO1xuZnVuY3Rpb24gbmV3UHJvbWlzZShleGVjdXRvcikge1xuICAgIHJldHVybiBuZXcgb3JpZ2luYWxQcm9taXNlKGV4ZWN1dG9yKTtcbn1cbmZ1bmN0aW9uIHByb21pc2VSZXNvbHZlZFdpdGgodmFsdWUpIHtcbiAgICByZXR1cm4gb3JpZ2luYWxQcm9taXNlUmVzb2x2ZSh2YWx1ZSk7XG59XG5mdW5jdGlvbiBwcm9taXNlUmVqZWN0ZWRXaXRoKHJlYXNvbikge1xuICAgIHJldHVybiBvcmlnaW5hbFByb21pc2VSZWplY3QocmVhc29uKTtcbn1cbmZ1bmN0aW9uIFBlcmZvcm1Qcm9taXNlVGhlbihwcm9taXNlLCBvbkZ1bGZpbGxlZCwgb25SZWplY3RlZCkge1xuICAgIC8vIFRoZXJlIGRvZXNuJ3QgYXBwZWFyIHRvIGJlIGFueSB3YXkgdG8gY29ycmVjdGx5IGVtdWxhdGUgdGhlIGJlaGF2aW91ciBmcm9tIEphdmFTY3JpcHQsIHNvIHRoaXMgaXMganVzdCBhblxuICAgIC8vIGFwcHJveGltYXRpb24uXG4gICAgcmV0dXJuIG9yaWdpbmFsUHJvbWlzZVRoZW4uY2FsbChwcm9taXNlLCBvbkZ1bGZpbGxlZCwgb25SZWplY3RlZCk7XG59XG5mdW5jdGlvbiB1cG9uUHJvbWlzZShwcm9taXNlLCBvbkZ1bGZpbGxlZCwgb25SZWplY3RlZCkge1xuICAgIFBlcmZvcm1Qcm9taXNlVGhlbihQZXJmb3JtUHJvbWlzZVRoZW4ocHJvbWlzZSwgb25GdWxmaWxsZWQsIG9uUmVqZWN0ZWQpLCB1bmRlZmluZWQsIHJldGhyb3dBc3NlcnRpb25FcnJvclJlamVjdGlvbik7XG59XG5mdW5jdGlvbiB1cG9uRnVsZmlsbG1lbnQocHJvbWlzZSwgb25GdWxmaWxsZWQpIHtcbiAgICB1cG9uUHJvbWlzZShwcm9taXNlLCBvbkZ1bGZpbGxlZCk7XG59XG5mdW5jdGlvbiB1cG9uUmVqZWN0aW9uKHByb21pc2UsIG9uUmVqZWN0ZWQpIHtcbiAgICB1cG9uUHJvbWlzZShwcm9taXNlLCB1bmRlZmluZWQsIG9uUmVqZWN0ZWQpO1xufVxuZnVuY3Rpb24gdHJhbnNmb3JtUHJvbWlzZVdpdGgocHJvbWlzZSwgZnVsZmlsbG1lbnRIYW5kbGVyLCByZWplY3Rpb25IYW5kbGVyKSB7XG4gICAgcmV0dXJuIFBlcmZvcm1Qcm9taXNlVGhlbihwcm9taXNlLCBmdWxmaWxsbWVudEhhbmRsZXIsIHJlamVjdGlvbkhhbmRsZXIpO1xufVxuZnVuY3Rpb24gc2V0UHJvbWlzZUlzSGFuZGxlZFRvVHJ1ZShwcm9taXNlKSB7XG4gICAgUGVyZm9ybVByb21pc2VUaGVuKHByb21pc2UsIHVuZGVmaW5lZCwgcmV0aHJvd0Fzc2VydGlvbkVycm9yUmVqZWN0aW9uKTtcbn1cbmNvbnN0IHF1ZXVlTWljcm90YXNrID0gKCgpID0+IHtcbiAgICBjb25zdCBnbG9iYWxRdWV1ZU1pY3JvdGFzayA9IGdsb2JhbHMgJiYgZ2xvYmFscy5xdWV1ZU1pY3JvdGFzaztcbiAgICBpZiAodHlwZW9mIGdsb2JhbFF1ZXVlTWljcm90YXNrID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICAgIHJldHVybiBnbG9iYWxRdWV1ZU1pY3JvdGFzaztcbiAgICB9XG4gICAgY29uc3QgcmVzb2x2ZWRQcm9taXNlID0gcHJvbWlzZVJlc29sdmVkV2l0aCh1bmRlZmluZWQpO1xuICAgIHJldHVybiAoZm4pID0+IFBlcmZvcm1Qcm9taXNlVGhlbihyZXNvbHZlZFByb21pc2UsIGZuKTtcbn0pKCk7XG5mdW5jdGlvbiByZWZsZWN0Q2FsbChGLCBWLCBhcmdzKSB7XG4gICAgaWYgKHR5cGVvZiBGICE9PSAnZnVuY3Rpb24nKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0FyZ3VtZW50IGlzIG5vdCBhIGZ1bmN0aW9uJyk7XG4gICAgfVxuICAgIHJldHVybiBGdW5jdGlvbi5wcm90b3R5cGUuYXBwbHkuY2FsbChGLCBWLCBhcmdzKTtcbn1cbmZ1bmN0aW9uIHByb21pc2VDYWxsKEYsIFYsIGFyZ3MpIHtcbiAgICB0cnkge1xuICAgICAgICByZXR1cm4gcHJvbWlzZVJlc29sdmVkV2l0aChyZWZsZWN0Q2FsbChGLCBWLCBhcmdzKSk7XG4gICAgfVxuICAgIGNhdGNoICh2YWx1ZSkge1xuICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aCh2YWx1ZSk7XG4gICAgfVxufVxuXG4vLyBPcmlnaW5hbCBmcm9tIENocm9taXVtXG4vLyBodHRwczovL2Nocm9taXVtLmdvb2dsZXNvdXJjZS5jb20vY2hyb21pdW0vc3JjLysvMGFlZTQ0MzRhNGRiYTQyYTQyYWJhZWE5YmZiYzBjZDE5NmE2M2JjMS90aGlyZF9wYXJ0eS9ibGluay9yZW5kZXJlci9jb3JlL3N0cmVhbXMvU2ltcGxlUXVldWUuanNcbmNvbnN0IFFVRVVFX01BWF9BUlJBWV9TSVpFID0gMTYzODQ7XG4vKipcbiAqIFNpbXBsZSBxdWV1ZSBzdHJ1Y3R1cmUuXG4gKlxuICogQXZvaWRzIHNjYWxhYmlsaXR5IGlzc3VlcyB3aXRoIHVzaW5nIGEgcGFja2VkIGFycmF5IGRpcmVjdGx5IGJ5IHVzaW5nXG4gKiBtdWx0aXBsZSBhcnJheXMgaW4gYSBsaW5rZWQgbGlzdCBhbmQga2VlcGluZyB0aGUgYXJyYXkgc2l6ZSBib3VuZGVkLlxuICovXG5jbGFzcyBTaW1wbGVRdWV1ZSB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICAgIHRoaXMuX2N1cnNvciA9IDA7XG4gICAgICAgIHRoaXMuX3NpemUgPSAwO1xuICAgICAgICAvLyBfZnJvbnQgYW5kIF9iYWNrIGFyZSBhbHdheXMgZGVmaW5lZC5cbiAgICAgICAgdGhpcy5fZnJvbnQgPSB7XG4gICAgICAgICAgICBfZWxlbWVudHM6IFtdLFxuICAgICAgICAgICAgX25leHQ6IHVuZGVmaW5lZFxuICAgICAgICB9O1xuICAgICAgICB0aGlzLl9iYWNrID0gdGhpcy5fZnJvbnQ7XG4gICAgICAgIC8vIFRoZSBjdXJzb3IgaXMgdXNlZCB0byBhdm9pZCBjYWxsaW5nIEFycmF5LnNoaWZ0KCkuXG4gICAgICAgIC8vIEl0IGNvbnRhaW5zIHRoZSBpbmRleCBvZiB0aGUgZnJvbnQgZWxlbWVudCBvZiB0aGUgYXJyYXkgaW5zaWRlIHRoZVxuICAgICAgICAvLyBmcm9udC1tb3N0IG5vZGUuIEl0IGlzIGFsd2F5cyBpbiB0aGUgcmFuZ2UgWzAsIFFVRVVFX01BWF9BUlJBWV9TSVpFKS5cbiAgICAgICAgdGhpcy5fY3Vyc29yID0gMDtcbiAgICAgICAgLy8gV2hlbiB0aGVyZSBpcyBvbmx5IG9uZSBub2RlLCBzaXplID09PSBlbGVtZW50cy5sZW5ndGggLSBjdXJzb3IuXG4gICAgICAgIHRoaXMuX3NpemUgPSAwO1xuICAgIH1cbiAgICBnZXQgbGVuZ3RoKCkge1xuICAgICAgICByZXR1cm4gdGhpcy5fc2l6ZTtcbiAgICB9XG4gICAgLy8gRm9yIGV4Y2VwdGlvbiBzYWZldHksIHRoaXMgbWV0aG9kIGlzIHN0cnVjdHVyZWQgaW4gb3JkZXI6XG4gICAgLy8gMS4gUmVhZCBzdGF0ZVxuICAgIC8vIDIuIENhbGN1bGF0ZSByZXF1aXJlZCBzdGF0ZSBtdXRhdGlvbnNcbiAgICAvLyAzLiBQZXJmb3JtIHN0YXRlIG11dGF0aW9uc1xuICAgIHB1c2goZWxlbWVudCkge1xuICAgICAgICBjb25zdCBvbGRCYWNrID0gdGhpcy5fYmFjaztcbiAgICAgICAgbGV0IG5ld0JhY2sgPSBvbGRCYWNrO1xuICAgICAgICBpZiAob2xkQmFjay5fZWxlbWVudHMubGVuZ3RoID09PSBRVUVVRV9NQVhfQVJSQVlfU0laRSAtIDEpIHtcbiAgICAgICAgICAgIG5ld0JhY2sgPSB7XG4gICAgICAgICAgICAgICAgX2VsZW1lbnRzOiBbXSxcbiAgICAgICAgICAgICAgICBfbmV4dDogdW5kZWZpbmVkXG4gICAgICAgICAgICB9O1xuICAgICAgICB9XG4gICAgICAgIC8vIHB1c2goKSBpcyB0aGUgbXV0YXRpb24gbW9zdCBsaWtlbHkgdG8gdGhyb3cgYW4gZXhjZXB0aW9uLCBzbyBpdFxuICAgICAgICAvLyBnb2VzIGZpcnN0LlxuICAgICAgICBvbGRCYWNrLl9lbGVtZW50cy5wdXNoKGVsZW1lbnQpO1xuICAgICAgICBpZiAobmV3QmFjayAhPT0gb2xkQmFjaykge1xuICAgICAgICAgICAgdGhpcy5fYmFjayA9IG5ld0JhY2s7XG4gICAgICAgICAgICBvbGRCYWNrLl9uZXh0ID0gbmV3QmFjaztcbiAgICAgICAgfVxuICAgICAgICArK3RoaXMuX3NpemU7XG4gICAgfVxuICAgIC8vIExpa2UgcHVzaCgpLCBzaGlmdCgpIGZvbGxvd3MgdGhlIHJlYWQgLT4gY2FsY3VsYXRlIC0+IG11dGF0ZSBwYXR0ZXJuIGZvclxuICAgIC8vIGV4Y2VwdGlvbiBzYWZldHkuXG4gICAgc2hpZnQoKSB7IC8vIG11c3Qgbm90IGJlIGNhbGxlZCBvbiBhbiBlbXB0eSBxdWV1ZVxuICAgICAgICBjb25zdCBvbGRGcm9udCA9IHRoaXMuX2Zyb250O1xuICAgICAgICBsZXQgbmV3RnJvbnQgPSBvbGRGcm9udDtcbiAgICAgICAgY29uc3Qgb2xkQ3Vyc29yID0gdGhpcy5fY3Vyc29yO1xuICAgICAgICBsZXQgbmV3Q3Vyc29yID0gb2xkQ3Vyc29yICsgMTtcbiAgICAgICAgY29uc3QgZWxlbWVudHMgPSBvbGRGcm9udC5fZWxlbWVudHM7XG4gICAgICAgIGNvbnN0IGVsZW1lbnQgPSBlbGVtZW50c1tvbGRDdXJzb3JdO1xuICAgICAgICBpZiAobmV3Q3Vyc29yID09PSBRVUVVRV9NQVhfQVJSQVlfU0laRSkge1xuICAgICAgICAgICAgbmV3RnJvbnQgPSBvbGRGcm9udC5fbmV4dDtcbiAgICAgICAgICAgIG5ld0N1cnNvciA9IDA7XG4gICAgICAgIH1cbiAgICAgICAgLy8gTm8gbXV0YXRpb25zIGJlZm9yZSB0aGlzIHBvaW50LlxuICAgICAgICAtLXRoaXMuX3NpemU7XG4gICAgICAgIHRoaXMuX2N1cnNvciA9IG5ld0N1cnNvcjtcbiAgICAgICAgaWYgKG9sZEZyb250ICE9PSBuZXdGcm9udCkge1xuICAgICAgICAgICAgdGhpcy5fZnJvbnQgPSBuZXdGcm9udDtcbiAgICAgICAgfVxuICAgICAgICAvLyBQZXJtaXQgc2hpZnRlZCBlbGVtZW50IHRvIGJlIGdhcmJhZ2UgY29sbGVjdGVkLlxuICAgICAgICBlbGVtZW50c1tvbGRDdXJzb3JdID0gdW5kZWZpbmVkO1xuICAgICAgICByZXR1cm4gZWxlbWVudDtcbiAgICB9XG4gICAgLy8gVGhlIHRyaWNreSB0aGluZyBhYm91dCBmb3JFYWNoKCkgaXMgdGhhdCBpdCBjYW4gYmUgY2FsbGVkXG4gICAgLy8gcmUtZW50cmFudGx5LiBUaGUgcXVldWUgbWF5IGJlIG11dGF0ZWQgaW5zaWRlIHRoZSBjYWxsYmFjay4gSXQgaXMgZWFzeSB0b1xuICAgIC8vIHNlZSB0aGF0IHB1c2goKSB3aXRoaW4gdGhlIGNhbGxiYWNrIGhhcyBubyBuZWdhdGl2ZSBlZmZlY3RzIHNpbmNlIHRoZSBlbmRcbiAgICAvLyBvZiB0aGUgcXVldWUgaXMgY2hlY2tlZCBmb3Igb24gZXZlcnkgaXRlcmF0aW9uLiBJZiBzaGlmdCgpIGlzIGNhbGxlZFxuICAgIC8vIHJlcGVhdGVkbHkgd2l0aGluIHRoZSBjYWxsYmFjayB0aGVuIHRoZSBuZXh0IGl0ZXJhdGlvbiBtYXkgcmV0dXJuIGFuXG4gICAgLy8gZWxlbWVudCB0aGF0IGhhcyBiZWVuIHJlbW92ZWQuIEluIHRoaXMgY2FzZSB0aGUgY2FsbGJhY2sgd2lsbCBiZSBjYWxsZWRcbiAgICAvLyB3aXRoIHVuZGVmaW5lZCB2YWx1ZXMgdW50aWwgd2UgZWl0aGVyIFwiY2F0Y2ggdXBcIiB3aXRoIGVsZW1lbnRzIHRoYXQgc3RpbGxcbiAgICAvLyBleGlzdCBvciByZWFjaCB0aGUgYmFjayBvZiB0aGUgcXVldWUuXG4gICAgZm9yRWFjaChjYWxsYmFjaykge1xuICAgICAgICBsZXQgaSA9IHRoaXMuX2N1cnNvcjtcbiAgICAgICAgbGV0IG5vZGUgPSB0aGlzLl9mcm9udDtcbiAgICAgICAgbGV0IGVsZW1lbnRzID0gbm9kZS5fZWxlbWVudHM7XG4gICAgICAgIHdoaWxlIChpICE9PSBlbGVtZW50cy5sZW5ndGggfHwgbm9kZS5fbmV4dCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICBpZiAoaSA9PT0gZWxlbWVudHMubGVuZ3RoKSB7XG4gICAgICAgICAgICAgICAgbm9kZSA9IG5vZGUuX25leHQ7XG4gICAgICAgICAgICAgICAgZWxlbWVudHMgPSBub2RlLl9lbGVtZW50cztcbiAgICAgICAgICAgICAgICBpID0gMDtcbiAgICAgICAgICAgICAgICBpZiAoZWxlbWVudHMubGVuZ3RoID09PSAwKSB7XG4gICAgICAgICAgICAgICAgICAgIGJyZWFrO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGNhbGxiYWNrKGVsZW1lbnRzW2ldKTtcbiAgICAgICAgICAgICsraTtcbiAgICAgICAgfVxuICAgIH1cbiAgICAvLyBSZXR1cm4gdGhlIGVsZW1lbnQgdGhhdCB3b3VsZCBiZSByZXR1cm5lZCBpZiBzaGlmdCgpIHdhcyBjYWxsZWQgbm93LFxuICAgIC8vIHdpdGhvdXQgbW9kaWZ5aW5nIHRoZSBxdWV1ZS5cbiAgICBwZWVrKCkgeyAvLyBtdXN0IG5vdCBiZSBjYWxsZWQgb24gYW4gZW1wdHkgcXVldWVcbiAgICAgICAgY29uc3QgZnJvbnQgPSB0aGlzLl9mcm9udDtcbiAgICAgICAgY29uc3QgY3Vyc29yID0gdGhpcy5fY3Vyc29yO1xuICAgICAgICByZXR1cm4gZnJvbnQuX2VsZW1lbnRzW2N1cnNvcl07XG4gICAgfVxufVxuXG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNJbml0aWFsaXplKHJlYWRlciwgc3RyZWFtKSB7XG4gICAgcmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtID0gc3RyZWFtO1xuICAgIHN0cmVhbS5fcmVhZGVyID0gcmVhZGVyO1xuICAgIGlmIChzdHJlYW0uX3N0YXRlID09PSAncmVhZGFibGUnKSB7XG4gICAgICAgIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZShyZWFkZXIpO1xuICAgIH1cbiAgICBlbHNlIGlmIChzdHJlYW0uX3N0YXRlID09PSAnY2xvc2VkJykge1xuICAgICAgICBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemVBc1Jlc29sdmVkKHJlYWRlcik7XG4gICAgfVxuICAgIGVsc2Uge1xuICAgICAgICBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemVBc1JlamVjdGVkKHJlYWRlciwgc3RyZWFtLl9zdG9yZWRFcnJvcik7XG4gICAgfVxufVxuLy8gQSBjbGllbnQgb2YgUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyIGFuZCBSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIgbWF5IHVzZSB0aGVzZSBmdW5jdGlvbnMgZGlyZWN0bHkgdG8gYnlwYXNzIHN0YXRlXG4vLyBjaGVjay5cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtUmVhZGVyR2VuZXJpY0NhbmNlbChyZWFkZXIsIHJlYXNvbikge1xuICAgIGNvbnN0IHN0cmVhbSA9IHJlYWRlci5fb3duZXJSZWFkYWJsZVN0cmVhbTtcbiAgICByZXR1cm4gUmVhZGFibGVTdHJlYW1DYW5jZWwoc3RyZWFtLCByZWFzb24pO1xufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljUmVsZWFzZShyZWFkZXIpIHtcbiAgICBpZiAocmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtLl9zdGF0ZSA9PT0gJ3JlYWRhYmxlJykge1xuICAgICAgICBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZVJlamVjdChyZWFkZXIsIG5ldyBUeXBlRXJyb3IoYFJlYWRlciB3YXMgcmVsZWFzZWQgYW5kIGNhbiBubyBsb25nZXIgYmUgdXNlZCB0byBtb25pdG9yIHRoZSBzdHJlYW0ncyBjbG9zZWRuZXNzYCkpO1xuICAgIH1cbiAgICBlbHNlIHtcbiAgICAgICAgZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VSZXNldFRvUmVqZWN0ZWQocmVhZGVyLCBuZXcgVHlwZUVycm9yKGBSZWFkZXIgd2FzIHJlbGVhc2VkIGFuZCBjYW4gbm8gbG9uZ2VyIGJlIHVzZWQgdG8gbW9uaXRvciB0aGUgc3RyZWFtJ3MgY2xvc2VkbmVzc2ApKTtcbiAgICB9XG4gICAgcmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtLl9yZWFkZXIgPSB1bmRlZmluZWQ7XG4gICAgcmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtID0gdW5kZWZpbmVkO1xufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIHJlYWRlcnMuXG5mdW5jdGlvbiByZWFkZXJMb2NrRXhjZXB0aW9uKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcignQ2Fubm90ICcgKyBuYW1lICsgJyBhIHN0cmVhbSB1c2luZyBhIHJlbGVhc2VkIHJlYWRlcicpO1xufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlci5cbmZ1bmN0aW9uIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZShyZWFkZXIpIHtcbiAgICByZWFkZXIuX2Nsb3NlZFByb21pc2UgPSBuZXdQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgcmVhZGVyLl9jbG9zZWRQcm9taXNlX3Jlc29sdmUgPSByZXNvbHZlO1xuICAgICAgICByZWFkZXIuX2Nsb3NlZFByb21pc2VfcmVqZWN0ID0gcmVqZWN0O1xuICAgIH0pO1xufVxuZnVuY3Rpb24gZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VJbml0aWFsaXplQXNSZWplY3RlZChyZWFkZXIsIHJlYXNvbikge1xuICAgIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZShyZWFkZXIpO1xuICAgIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlUmVqZWN0KHJlYWRlciwgcmVhc29uKTtcbn1cbmZ1bmN0aW9uIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZUFzUmVzb2x2ZWQocmVhZGVyKSB7XG4gICAgZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VJbml0aWFsaXplKHJlYWRlcik7XG4gICAgZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VSZXNvbHZlKHJlYWRlcik7XG59XG5mdW5jdGlvbiBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZVJlamVjdChyZWFkZXIsIHJlYXNvbikge1xuICAgIGlmIChyZWFkZXIuX2Nsb3NlZFByb21pc2VfcmVqZWN0ID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBzZXRQcm9taXNlSXNIYW5kbGVkVG9UcnVlKHJlYWRlci5fY2xvc2VkUHJvbWlzZSk7XG4gICAgcmVhZGVyLl9jbG9zZWRQcm9taXNlX3JlamVjdChyZWFzb24pO1xuICAgIHJlYWRlci5fY2xvc2VkUHJvbWlzZV9yZXNvbHZlID0gdW5kZWZpbmVkO1xuICAgIHJlYWRlci5fY2xvc2VkUHJvbWlzZV9yZWplY3QgPSB1bmRlZmluZWQ7XG59XG5mdW5jdGlvbiBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZVJlc2V0VG9SZWplY3RlZChyZWFkZXIsIHJlYXNvbikge1xuICAgIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZUFzUmVqZWN0ZWQocmVhZGVyLCByZWFzb24pO1xufVxuZnVuY3Rpb24gZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VSZXNvbHZlKHJlYWRlcikge1xuICAgIGlmIChyZWFkZXIuX2Nsb3NlZFByb21pc2VfcmVzb2x2ZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgcmVhZGVyLl9jbG9zZWRQcm9taXNlX3Jlc29sdmUodW5kZWZpbmVkKTtcbiAgICByZWFkZXIuX2Nsb3NlZFByb21pc2VfcmVzb2x2ZSA9IHVuZGVmaW5lZDtcbiAgICByZWFkZXIuX2Nsb3NlZFByb21pc2VfcmVqZWN0ID0gdW5kZWZpbmVkO1xufVxuXG5jb25zdCBBYm9ydFN0ZXBzID0gU3ltYm9sUG9seWZpbGwoJ1tbQWJvcnRTdGVwc11dJyk7XG5jb25zdCBFcnJvclN0ZXBzID0gU3ltYm9sUG9seWZpbGwoJ1tbRXJyb3JTdGVwc11dJyk7XG5jb25zdCBDYW5jZWxTdGVwcyA9IFN5bWJvbFBvbHlmaWxsKCdbW0NhbmNlbFN0ZXBzXV0nKTtcbmNvbnN0IFB1bGxTdGVwcyA9IFN5bWJvbFBvbHlmaWxsKCdbW1B1bGxTdGVwc11dJyk7XG5cbi8vLyA8cmVmZXJlbmNlIGxpYj1cImVzMjAxNS5jb3JlXCIgLz5cbi8vIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0phdmFTY3JpcHQvUmVmZXJlbmNlL0dsb2JhbF9PYmplY3RzL051bWJlci9pc0Zpbml0ZSNQb2x5ZmlsbFxuY29uc3QgTnVtYmVySXNGaW5pdGUgPSBOdW1iZXIuaXNGaW5pdGUgfHwgZnVuY3Rpb24gKHgpIHtcbiAgICByZXR1cm4gdHlwZW9mIHggPT09ICdudW1iZXInICYmIGlzRmluaXRlKHgpO1xufTtcblxuLy8vIDxyZWZlcmVuY2UgbGliPVwiZXMyMDE1LmNvcmVcIiAvPlxuLy8gaHR0cHM6Ly9kZXZlbG9wZXIubW96aWxsYS5vcmcvZW4tVVMvZG9jcy9XZWIvSmF2YVNjcmlwdC9SZWZlcmVuY2UvR2xvYmFsX09iamVjdHMvTWF0aC90cnVuYyNQb2x5ZmlsbFxuY29uc3QgTWF0aFRydW5jID0gTWF0aC50cnVuYyB8fCBmdW5jdGlvbiAodikge1xuICAgIHJldHVybiB2IDwgMCA/IE1hdGguY2VpbCh2KSA6IE1hdGguZmxvb3Iodik7XG59O1xuXG4vLyBodHRwczovL2hleWNhbS5naXRodWIuaW8vd2ViaWRsLyNpZGwtZGljdGlvbmFyaWVzXG5mdW5jdGlvbiBpc0RpY3Rpb25hcnkoeCkge1xuICAgIHJldHVybiB0eXBlb2YgeCA9PT0gJ29iamVjdCcgfHwgdHlwZW9mIHggPT09ICdmdW5jdGlvbic7XG59XG5mdW5jdGlvbiBhc3NlcnREaWN0aW9uYXJ5KG9iaiwgY29udGV4dCkge1xuICAgIGlmIChvYmogIT09IHVuZGVmaW5lZCAmJiAhaXNEaWN0aW9uYXJ5KG9iaikpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSBpcyBub3QgYW4gb2JqZWN0LmApO1xuICAgIH1cbn1cbi8vIGh0dHBzOi8vaGV5Y2FtLmdpdGh1Yi5pby93ZWJpZGwvI2lkbC1jYWxsYmFjay1mdW5jdGlvbnNcbmZ1bmN0aW9uIGFzc2VydEZ1bmN0aW9uKHgsIGNvbnRleHQpIHtcbiAgICBpZiAodHlwZW9mIHggIT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSBpcyBub3QgYSBmdW5jdGlvbi5gKTtcbiAgICB9XG59XG4vLyBodHRwczovL2hleWNhbS5naXRodWIuaW8vd2ViaWRsLyNpZGwtb2JqZWN0XG5mdW5jdGlvbiBpc09iamVjdCh4KSB7XG4gICAgcmV0dXJuICh0eXBlb2YgeCA9PT0gJ29iamVjdCcgJiYgeCAhPT0gbnVsbCkgfHwgdHlwZW9mIHggPT09ICdmdW5jdGlvbic7XG59XG5mdW5jdGlvbiBhc3NlcnRPYmplY3QoeCwgY29udGV4dCkge1xuICAgIGlmICghaXNPYmplY3QoeCkpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSBpcyBub3QgYW4gb2JqZWN0LmApO1xuICAgIH1cbn1cbmZ1bmN0aW9uIGFzc2VydFJlcXVpcmVkQXJndW1lbnQoeCwgcG9zaXRpb24sIGNvbnRleHQpIHtcbiAgICBpZiAoeCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoYFBhcmFtZXRlciAke3Bvc2l0aW9ufSBpcyByZXF1aXJlZCBpbiAnJHtjb250ZXh0fScuYCk7XG4gICAgfVxufVxuZnVuY3Rpb24gYXNzZXJ0UmVxdWlyZWRGaWVsZCh4LCBmaWVsZCwgY29udGV4dCkge1xuICAgIGlmICh4ID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtmaWVsZH0gaXMgcmVxdWlyZWQgaW4gJyR7Y29udGV4dH0nLmApO1xuICAgIH1cbn1cbi8vIGh0dHBzOi8vaGV5Y2FtLmdpdGh1Yi5pby93ZWJpZGwvI2lkbC11bnJlc3RyaWN0ZWQtZG91YmxlXG5mdW5jdGlvbiBjb252ZXJ0VW5yZXN0cmljdGVkRG91YmxlKHZhbHVlKSB7XG4gICAgcmV0dXJuIE51bWJlcih2YWx1ZSk7XG59XG5mdW5jdGlvbiBjZW5zb3JOZWdhdGl2ZVplcm8oeCkge1xuICAgIHJldHVybiB4ID09PSAwID8gMCA6IHg7XG59XG5mdW5jdGlvbiBpbnRlZ2VyUGFydCh4KSB7XG4gICAgcmV0dXJuIGNlbnNvck5lZ2F0aXZlWmVybyhNYXRoVHJ1bmMoeCkpO1xufVxuLy8gaHR0cHM6Ly9oZXljYW0uZ2l0aHViLmlvL3dlYmlkbC8jaWRsLXVuc2lnbmVkLWxvbmctbG9uZ1xuZnVuY3Rpb24gY29udmVydFVuc2lnbmVkTG9uZ0xvbmdXaXRoRW5mb3JjZVJhbmdlKHZhbHVlLCBjb250ZXh0KSB7XG4gICAgY29uc3QgbG93ZXJCb3VuZCA9IDA7XG4gICAgY29uc3QgdXBwZXJCb3VuZCA9IE51bWJlci5NQVhfU0FGRV9JTlRFR0VSO1xuICAgIGxldCB4ID0gTnVtYmVyKHZhbHVlKTtcbiAgICB4ID0gY2Vuc29yTmVnYXRpdmVaZXJvKHgpO1xuICAgIGlmICghTnVtYmVySXNGaW5pdGUoeCkpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSBpcyBub3QgYSBmaW5pdGUgbnVtYmVyYCk7XG4gICAgfVxuICAgIHggPSBpbnRlZ2VyUGFydCh4KTtcbiAgICBpZiAoeCA8IGxvd2VyQm91bmQgfHwgeCA+IHVwcGVyQm91bmQpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSBpcyBvdXRzaWRlIHRoZSBhY2NlcHRlZCByYW5nZSBvZiAke2xvd2VyQm91bmR9IHRvICR7dXBwZXJCb3VuZH0sIGluY2x1c2l2ZWApO1xuICAgIH1cbiAgICBpZiAoIU51bWJlcklzRmluaXRlKHgpIHx8IHggPT09IDApIHtcbiAgICAgICAgcmV0dXJuIDA7XG4gICAgfVxuICAgIC8vIFRPRE8gVXNlIEJpZ0ludCBpZiBzdXBwb3J0ZWQ/XG4gICAgLy8gbGV0IHhCaWdJbnQgPSBCaWdJbnQoaW50ZWdlclBhcnQoeCkpO1xuICAgIC8vIHhCaWdJbnQgPSBCaWdJbnQuYXNVaW50Tig2NCwgeEJpZ0ludCk7XG4gICAgLy8gcmV0dXJuIE51bWJlcih4QmlnSW50KTtcbiAgICByZXR1cm4geDtcbn1cblxuZnVuY3Rpb24gYXNzZXJ0UmVhZGFibGVTdHJlYW0oeCwgY29udGV4dCkge1xuICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbSh4KSkge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGAke2NvbnRleHR9IGlzIG5vdCBhIFJlYWRhYmxlU3RyZWFtLmApO1xuICAgIH1cbn1cblxuLy8gQWJzdHJhY3Qgb3BlcmF0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtLlxuZnVuY3Rpb24gQWNxdWlyZVJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcihzdHJlYW0pIHtcbiAgICByZXR1cm4gbmV3IFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcihzdHJlYW0pO1xufVxuLy8gUmVhZGFibGVTdHJlYW0gQVBJIGV4cG9zZWQgZm9yIGNvbnRyb2xsZXJzLlxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1BZGRSZWFkUmVxdWVzdChzdHJlYW0sIHJlYWRSZXF1ZXN0KSB7XG4gICAgc3RyZWFtLl9yZWFkZXIuX3JlYWRSZXF1ZXN0cy5wdXNoKHJlYWRSZXF1ZXN0KTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRSZXF1ZXN0KHN0cmVhbSwgY2h1bmssIGRvbmUpIHtcbiAgICBjb25zdCByZWFkZXIgPSBzdHJlYW0uX3JlYWRlcjtcbiAgICBjb25zdCByZWFkUmVxdWVzdCA9IHJlYWRlci5fcmVhZFJlcXVlc3RzLnNoaWZ0KCk7XG4gICAgaWYgKGRvbmUpIHtcbiAgICAgICAgcmVhZFJlcXVlc3QuX2Nsb3NlU3RlcHMoKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIHJlYWRSZXF1ZXN0Ll9jaHVua1N0ZXBzKGNodW5rKTtcbiAgICB9XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbUdldE51bVJlYWRSZXF1ZXN0cyhzdHJlYW0pIHtcbiAgICByZXR1cm4gc3RyZWFtLl9yZWFkZXIuX3JlYWRSZXF1ZXN0cy5sZW5ndGg7XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbUhhc0RlZmF1bHRSZWFkZXIoc3RyZWFtKSB7XG4gICAgY29uc3QgcmVhZGVyID0gc3RyZWFtLl9yZWFkZXI7XG4gICAgaWYgKHJlYWRlciA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcihyZWFkZXIpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG4vKipcbiAqIEEgZGVmYXVsdCByZWFkZXIgdmVuZGVkIGJ5IGEge0BsaW5rIFJlYWRhYmxlU3RyZWFtfS5cbiAqXG4gKiBAcHVibGljXG4gKi9cbmNsYXNzIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlciB7XG4gICAgY29uc3RydWN0b3Ioc3RyZWFtKSB7XG4gICAgICAgIGFzc2VydFJlcXVpcmVkQXJndW1lbnQoc3RyZWFtLCAxLCAnUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyJyk7XG4gICAgICAgIGFzc2VydFJlYWRhYmxlU3RyZWFtKHN0cmVhbSwgJ0ZpcnN0IHBhcmFtZXRlcicpO1xuICAgICAgICBpZiAoSXNSZWFkYWJsZVN0cmVhbUxvY2tlZChzdHJlYW0pKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGlzIHN0cmVhbSBoYXMgYWxyZWFkeSBiZWVuIGxvY2tlZCBmb3IgZXhjbHVzaXZlIHJlYWRpbmcgYnkgYW5vdGhlciByZWFkZXInKTtcbiAgICAgICAgfVxuICAgICAgICBSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNJbml0aWFsaXplKHRoaXMsIHN0cmVhbSk7XG4gICAgICAgIHRoaXMuX3JlYWRSZXF1ZXN0cyA9IG5ldyBTaW1wbGVRdWV1ZSgpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIGEgcHJvbWlzZSB0aGF0IHdpbGwgYmUgZnVsZmlsbGVkIHdoZW4gdGhlIHN0cmVhbSBiZWNvbWVzIGNsb3NlZCxcbiAgICAgKiBvciByZWplY3RlZCBpZiB0aGUgc3RyZWFtIGV2ZXIgZXJyb3JzIG9yIHRoZSByZWFkZXIncyBsb2NrIGlzIHJlbGVhc2VkIGJlZm9yZSB0aGUgc3RyZWFtIGZpbmlzaGVzIGNsb3NpbmcuXG4gICAgICovXG4gICAgZ2V0IGNsb3NlZCgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoZGVmYXVsdFJlYWRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Nsb3NlZCcpKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gdGhpcy5fY2xvc2VkUHJvbWlzZTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogSWYgdGhlIHJlYWRlciBpcyBhY3RpdmUsIGJlaGF2ZXMgdGhlIHNhbWUgYXMge0BsaW5rIFJlYWRhYmxlU3RyZWFtLmNhbmNlbCB8IHN0cmVhbS5jYW5jZWwocmVhc29uKX0uXG4gICAgICovXG4gICAgY2FuY2VsKHJlYXNvbiA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChkZWZhdWx0UmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbignY2FuY2VsJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmICh0aGlzLl9vd25lclJlYWRhYmxlU3RyZWFtID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKHJlYWRlckxvY2tFeGNlcHRpb24oJ2NhbmNlbCcpKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljQ2FuY2VsKHRoaXMsIHJlYXNvbik7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFJldHVybnMgYSBwcm9taXNlIHRoYXQgYWxsb3dzIGFjY2VzcyB0byB0aGUgbmV4dCBjaHVuayBmcm9tIHRoZSBzdHJlYW0ncyBpbnRlcm5hbCBxdWV1ZSwgaWYgYXZhaWxhYmxlLlxuICAgICAqXG4gICAgICogSWYgcmVhZGluZyBhIGNodW5rIGNhdXNlcyB0aGUgcXVldWUgdG8gYmVjb21lIGVtcHR5LCBtb3JlIGRhdGEgd2lsbCBiZSBwdWxsZWQgZnJvbSB0aGUgdW5kZXJseWluZyBzb3VyY2UuXG4gICAgICovXG4gICAgcmVhZCgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoZGVmYXVsdFJlYWRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ3JlYWQnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX293bmVyUmVhZGFibGVTdHJlYW0gPT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgocmVhZGVyTG9ja0V4Y2VwdGlvbigncmVhZCBmcm9tJykpO1xuICAgICAgICB9XG4gICAgICAgIGxldCByZXNvbHZlUHJvbWlzZTtcbiAgICAgICAgbGV0IHJlamVjdFByb21pc2U7XG4gICAgICAgIGNvbnN0IHByb21pc2UgPSBuZXdQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgICAgIHJlc29sdmVQcm9taXNlID0gcmVzb2x2ZTtcbiAgICAgICAgICAgIHJlamVjdFByb21pc2UgPSByZWplY3Q7XG4gICAgICAgIH0pO1xuICAgICAgICBjb25zdCByZWFkUmVxdWVzdCA9IHtcbiAgICAgICAgICAgIF9jaHVua1N0ZXBzOiBjaHVuayA9PiByZXNvbHZlUHJvbWlzZSh7IHZhbHVlOiBjaHVuaywgZG9uZTogZmFsc2UgfSksXG4gICAgICAgICAgICBfY2xvc2VTdGVwczogKCkgPT4gcmVzb2x2ZVByb21pc2UoeyB2YWx1ZTogdW5kZWZpbmVkLCBkb25lOiB0cnVlIH0pLFxuICAgICAgICAgICAgX2Vycm9yU3RlcHM6IGUgPT4gcmVqZWN0UHJvbWlzZShlKVxuICAgICAgICB9O1xuICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXJSZWFkKHRoaXMsIHJlYWRSZXF1ZXN0KTtcbiAgICAgICAgcmV0dXJuIHByb21pc2U7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFJlbGVhc2VzIHRoZSByZWFkZXIncyBsb2NrIG9uIHRoZSBjb3JyZXNwb25kaW5nIHN0cmVhbS4gQWZ0ZXIgdGhlIGxvY2sgaXMgcmVsZWFzZWQsIHRoZSByZWFkZXIgaXMgbm8gbG9uZ2VyIGFjdGl2ZS5cbiAgICAgKiBJZiB0aGUgYXNzb2NpYXRlZCBzdHJlYW0gaXMgZXJyb3JlZCB3aGVuIHRoZSBsb2NrIGlzIHJlbGVhc2VkLCB0aGUgcmVhZGVyIHdpbGwgYXBwZWFyIGVycm9yZWQgaW4gdGhlIHNhbWUgd2F5XG4gICAgICogZnJvbSBub3cgb247IG90aGVyd2lzZSwgdGhlIHJlYWRlciB3aWxsIGFwcGVhciBjbG9zZWQuXG4gICAgICpcbiAgICAgKiBBIHJlYWRlcidzIGxvY2sgY2Fubm90IGJlIHJlbGVhc2VkIHdoaWxlIGl0IHN0aWxsIGhhcyBhIHBlbmRpbmcgcmVhZCByZXF1ZXN0LCBpLmUuLCBpZiBhIHByb21pc2UgcmV0dXJuZWQgYnlcbiAgICAgKiB0aGUgcmVhZGVyJ3Mge0BsaW5rIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlci5yZWFkIHwgcmVhZCgpfSBtZXRob2QgaGFzIG5vdCB5ZXQgYmVlbiBzZXR0bGVkLiBBdHRlbXB0aW5nIHRvXG4gICAgICogZG8gc28gd2lsbCB0aHJvdyBhIGBUeXBlRXJyb3JgIGFuZCBsZWF2ZSB0aGUgcmVhZGVyIGxvY2tlZCB0byB0aGUgc3RyZWFtLlxuICAgICAqL1xuICAgIHJlbGVhc2VMb2NrKCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBkZWZhdWx0UmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbigncmVsZWFzZUxvY2snKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAodGhpcy5fb3duZXJSZWFkYWJsZVN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX3JlYWRSZXF1ZXN0cy5sZW5ndGggPiAwKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUcmllZCB0byByZWxlYXNlIGEgcmVhZGVyIGxvY2sgd2hlbiB0aGF0IHJlYWRlciBoYXMgcGVuZGluZyByZWFkKCkgY2FsbHMgdW4tc2V0dGxlZCcpO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtUmVhZGVyR2VuZXJpY1JlbGVhc2UodGhpcyk7XG4gICAgfVxufVxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyLnByb3RvdHlwZSwge1xuICAgIGNhbmNlbDogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgcmVhZDogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgcmVsZWFzZUxvY2s6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGNsb3NlZDogeyBlbnVtZXJhYmxlOiB0cnVlIH1cbn0pO1xuaWYgKHR5cGVvZiBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZyA9PT0gJ3N5bWJvbCcpIHtcbiAgICBPYmplY3QuZGVmaW5lUHJvcGVydHkoUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyLnByb3RvdHlwZSwgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcsIHtcbiAgICAgICAgdmFsdWU6ICdSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXInLFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICB9KTtcbn1cbi8vIEFic3RyYWN0IG9wZXJhdGlvbnMgZm9yIHRoZSByZWFkZXJzLlxuZnVuY3Rpb24gSXNSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXIoeCkge1xuICAgIGlmICghdHlwZUlzT2JqZWN0KHgpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgaWYgKCFPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoeCwgJ19yZWFkUmVxdWVzdHMnKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyUmVhZChyZWFkZXIsIHJlYWRSZXF1ZXN0KSB7XG4gICAgY29uc3Qgc3RyZWFtID0gcmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtO1xuICAgIHN0cmVhbS5fZGlzdHVyYmVkID0gdHJ1ZTtcbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgcmVhZFJlcXVlc3QuX2Nsb3NlU3RlcHMoKTtcbiAgICB9XG4gICAgZWxzZSBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgIHJlYWRSZXF1ZXN0Ll9lcnJvclN0ZXBzKHN0cmVhbS5fc3RvcmVkRXJyb3IpO1xuICAgIH1cbiAgICBlbHNlIHtcbiAgICAgICAgc3RyZWFtLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXJbUHVsbFN0ZXBzXShyZWFkUmVxdWVzdCk7XG4gICAgfVxufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlci5cbmZ1bmN0aW9uIGRlZmF1bHRSZWFkZXJCcmFuZENoZWNrRXhjZXB0aW9uKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyLnByb3RvdHlwZS4ke25hbWV9IGNhbiBvbmx5IGJlIHVzZWQgb24gYSBSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXJgKTtcbn1cblxuLy8vIDxyZWZlcmVuY2UgbGliPVwiZXMyMDE4LmFzeW5jaXRlcmFibGVcIiAvPlxuLyogZXNsaW50LWRpc2FibGUgQHR5cGVzY3JpcHQtZXNsaW50L25vLWVtcHR5LWZ1bmN0aW9uICovXG5jb25zdCBBc3luY0l0ZXJhdG9yUHJvdG90eXBlID0gT2JqZWN0LmdldFByb3RvdHlwZU9mKE9iamVjdC5nZXRQcm90b3R5cGVPZihhc3luYyBmdW5jdGlvbiogKCkgeyB9KS5wcm90b3R5cGUpO1xuXG4vLy8gPHJlZmVyZW5jZSBsaWI9XCJlczIwMTguYXN5bmNpdGVyYWJsZVwiIC8+XG5jbGFzcyBSZWFkYWJsZVN0cmVhbUFzeW5jSXRlcmF0b3JJbXBsIHtcbiAgICBjb25zdHJ1Y3RvcihyZWFkZXIsIHByZXZlbnRDYW5jZWwpIHtcbiAgICAgICAgdGhpcy5fb25nb2luZ1Byb21pc2UgPSB1bmRlZmluZWQ7XG4gICAgICAgIHRoaXMuX2lzRmluaXNoZWQgPSBmYWxzZTtcbiAgICAgICAgdGhpcy5fcmVhZGVyID0gcmVhZGVyO1xuICAgICAgICB0aGlzLl9wcmV2ZW50Q2FuY2VsID0gcHJldmVudENhbmNlbDtcbiAgICB9XG4gICAgbmV4dCgpIHtcbiAgICAgICAgY29uc3QgbmV4dFN0ZXBzID0gKCkgPT4gdGhpcy5fbmV4dFN0ZXBzKCk7XG4gICAgICAgIHRoaXMuX29uZ29pbmdQcm9taXNlID0gdGhpcy5fb25nb2luZ1Byb21pc2UgP1xuICAgICAgICAgICAgdHJhbnNmb3JtUHJvbWlzZVdpdGgodGhpcy5fb25nb2luZ1Byb21pc2UsIG5leHRTdGVwcywgbmV4dFN0ZXBzKSA6XG4gICAgICAgICAgICBuZXh0U3RlcHMoKTtcbiAgICAgICAgcmV0dXJuIHRoaXMuX29uZ29pbmdQcm9taXNlO1xuICAgIH1cbiAgICByZXR1cm4odmFsdWUpIHtcbiAgICAgICAgY29uc3QgcmV0dXJuU3RlcHMgPSAoKSA9PiB0aGlzLl9yZXR1cm5TdGVwcyh2YWx1ZSk7XG4gICAgICAgIHJldHVybiB0aGlzLl9vbmdvaW5nUHJvbWlzZSA/XG4gICAgICAgICAgICB0cmFuc2Zvcm1Qcm9taXNlV2l0aCh0aGlzLl9vbmdvaW5nUHJvbWlzZSwgcmV0dXJuU3RlcHMsIHJldHVyblN0ZXBzKSA6XG4gICAgICAgICAgICByZXR1cm5TdGVwcygpO1xuICAgIH1cbiAgICBfbmV4dFN0ZXBzKCkge1xuICAgICAgICBpZiAodGhpcy5faXNGaW5pc2hlZCkge1xuICAgICAgICAgICAgcmV0dXJuIFByb21pc2UucmVzb2x2ZSh7IHZhbHVlOiB1bmRlZmluZWQsIGRvbmU6IHRydWUgfSk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3QgcmVhZGVyID0gdGhpcy5fcmVhZGVyO1xuICAgICAgICBpZiAocmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKHJlYWRlckxvY2tFeGNlcHRpb24oJ2l0ZXJhdGUnKSk7XG4gICAgICAgIH1cbiAgICAgICAgbGV0IHJlc29sdmVQcm9taXNlO1xuICAgICAgICBsZXQgcmVqZWN0UHJvbWlzZTtcbiAgICAgICAgY29uc3QgcHJvbWlzZSA9IG5ld1Byb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgICAgICAgICAgcmVzb2x2ZVByb21pc2UgPSByZXNvbHZlO1xuICAgICAgICAgICAgcmVqZWN0UHJvbWlzZSA9IHJlamVjdDtcbiAgICAgICAgfSk7XG4gICAgICAgIGNvbnN0IHJlYWRSZXF1ZXN0ID0ge1xuICAgICAgICAgICAgX2NodW5rU3RlcHM6IGNodW5rID0+IHtcbiAgICAgICAgICAgICAgICB0aGlzLl9vbmdvaW5nUHJvbWlzZSA9IHVuZGVmaW5lZDtcbiAgICAgICAgICAgICAgICAvLyBUaGlzIG5lZWRzIHRvIGJlIGRlbGF5ZWQgYnkgb25lIG1pY3JvdGFzaywgb3RoZXJ3aXNlIHdlIHN0b3AgcHVsbGluZyB0b28gZWFybHkgd2hpY2ggYnJlYWtzIGEgdGVzdC5cbiAgICAgICAgICAgICAgICAvLyBGSVhNRSBJcyB0aGlzIGEgYnVnIGluIHRoZSBzcGVjaWZpY2F0aW9uLCBvciBpbiB0aGUgdGVzdD9cbiAgICAgICAgICAgICAgICBxdWV1ZU1pY3JvdGFzaygoKSA9PiByZXNvbHZlUHJvbWlzZSh7IHZhbHVlOiBjaHVuaywgZG9uZTogZmFsc2UgfSkpO1xuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIF9jbG9zZVN0ZXBzOiAoKSA9PiB7XG4gICAgICAgICAgICAgICAgdGhpcy5fb25nb2luZ1Byb21pc2UgPSB1bmRlZmluZWQ7XG4gICAgICAgICAgICAgICAgdGhpcy5faXNGaW5pc2hlZCA9IHRydWU7XG4gICAgICAgICAgICAgICAgUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljUmVsZWFzZShyZWFkZXIpO1xuICAgICAgICAgICAgICAgIHJlc29sdmVQcm9taXNlKHsgdmFsdWU6IHVuZGVmaW5lZCwgZG9uZTogdHJ1ZSB9KTtcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBfZXJyb3JTdGVwczogcmVhc29uID0+IHtcbiAgICAgICAgICAgICAgICB0aGlzLl9vbmdvaW5nUHJvbWlzZSA9IHVuZGVmaW5lZDtcbiAgICAgICAgICAgICAgICB0aGlzLl9pc0ZpbmlzaGVkID0gdHJ1ZTtcbiAgICAgICAgICAgICAgICBSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNSZWxlYXNlKHJlYWRlcik7XG4gICAgICAgICAgICAgICAgcmVqZWN0UHJvbWlzZShyZWFzb24pO1xuICAgICAgICAgICAgfVxuICAgICAgICB9O1xuICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXJSZWFkKHJlYWRlciwgcmVhZFJlcXVlc3QpO1xuICAgICAgICByZXR1cm4gcHJvbWlzZTtcbiAgICB9XG4gICAgX3JldHVyblN0ZXBzKHZhbHVlKSB7XG4gICAgICAgIGlmICh0aGlzLl9pc0ZpbmlzaGVkKSB7XG4gICAgICAgICAgICByZXR1cm4gUHJvbWlzZS5yZXNvbHZlKHsgdmFsdWUsIGRvbmU6IHRydWUgfSk7XG4gICAgICAgIH1cbiAgICAgICAgdGhpcy5faXNGaW5pc2hlZCA9IHRydWU7XG4gICAgICAgIGNvbnN0IHJlYWRlciA9IHRoaXMuX3JlYWRlcjtcbiAgICAgICAgaWYgKHJlYWRlci5fb3duZXJSZWFkYWJsZVN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChyZWFkZXJMb2NrRXhjZXB0aW9uKCdmaW5pc2ggaXRlcmF0aW5nJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmICghdGhpcy5fcHJldmVudENhbmNlbCkge1xuICAgICAgICAgICAgY29uc3QgcmVzdWx0ID0gUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljQ2FuY2VsKHJlYWRlciwgdmFsdWUpO1xuICAgICAgICAgICAgUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljUmVsZWFzZShyZWFkZXIpO1xuICAgICAgICAgICAgcmV0dXJuIHRyYW5zZm9ybVByb21pc2VXaXRoKHJlc3VsdCwgKCkgPT4gKHsgdmFsdWUsIGRvbmU6IHRydWUgfSkpO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtUmVhZGVyR2VuZXJpY1JlbGVhc2UocmVhZGVyKTtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZXNvbHZlZFdpdGgoeyB2YWx1ZSwgZG9uZTogdHJ1ZSB9KTtcbiAgICB9XG59XG5jb25zdCBSZWFkYWJsZVN0cmVhbUFzeW5jSXRlcmF0b3JQcm90b3R5cGUgPSB7XG4gICAgbmV4dCgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtQXN5bmNJdGVyYXRvcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoc3RyZWFtQXN5bmNJdGVyYXRvckJyYW5kQ2hlY2tFeGNlcHRpb24oJ25leHQnKSk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIHRoaXMuX2FzeW5jSXRlcmF0b3JJbXBsLm5leHQoKTtcbiAgICB9LFxuICAgIHJldHVybih2YWx1ZSkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9yKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChzdHJlYW1Bc3luY0l0ZXJhdG9yQnJhbmRDaGVja0V4Y2VwdGlvbigncmV0dXJuJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9hc3luY0l0ZXJhdG9ySW1wbC5yZXR1cm4odmFsdWUpO1xuICAgIH1cbn07XG5pZiAoQXN5bmNJdGVyYXRvclByb3RvdHlwZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgT2JqZWN0LnNldFByb3RvdHlwZU9mKFJlYWRhYmxlU3RyZWFtQXN5bmNJdGVyYXRvclByb3RvdHlwZSwgQXN5bmNJdGVyYXRvclByb3RvdHlwZSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgUmVhZGFibGVTdHJlYW0uXG5mdW5jdGlvbiBBY3F1aXJlUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9yKHN0cmVhbSwgcHJldmVudENhbmNlbCkge1xuICAgIGNvbnN0IHJlYWRlciA9IEFjcXVpcmVSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXIoc3RyZWFtKTtcbiAgICBjb25zdCBpbXBsID0gbmV3IFJlYWRhYmxlU3RyZWFtQXN5bmNJdGVyYXRvckltcGwocmVhZGVyLCBwcmV2ZW50Q2FuY2VsKTtcbiAgICBjb25zdCBpdGVyYXRvciA9IE9iamVjdC5jcmVhdGUoUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9yUHJvdG90eXBlKTtcbiAgICBpdGVyYXRvci5fYXN5bmNJdGVyYXRvckltcGwgPSBpbXBsO1xuICAgIHJldHVybiBpdGVyYXRvcjtcbn1cbmZ1bmN0aW9uIElzUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9yKHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfYXN5bmNJdGVyYXRvckltcGwnKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtLlxuZnVuY3Rpb24gc3RyZWFtQXN5bmNJdGVyYXRvckJyYW5kQ2hlY2tFeGNlcHRpb24obmFtZSkge1xuICAgIHJldHVybiBuZXcgVHlwZUVycm9yKGBSZWFkYWJsZVN0cmVhbUFzeW5jSXRlcmF0b3IuJHtuYW1lfSBjYW4gb25seSBiZSB1c2VkIG9uIGEgUmVhZGFibGVTdGVhbUFzeW5jSXRlcmF0b3JgKTtcbn1cblxuLy8vIDxyZWZlcmVuY2UgbGliPVwiZXMyMDE1LmNvcmVcIiAvPlxuLy8gaHR0cHM6Ly9kZXZlbG9wZXIubW96aWxsYS5vcmcvZW4tVVMvZG9jcy9XZWIvSmF2YVNjcmlwdC9SZWZlcmVuY2UvR2xvYmFsX09iamVjdHMvTnVtYmVyL2lzTmFOI1BvbHlmaWxsXG5jb25zdCBOdW1iZXJJc05hTiA9IE51bWJlci5pc05hTiB8fCBmdW5jdGlvbiAoeCkge1xuICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1zZWxmLWNvbXBhcmVcbiAgICByZXR1cm4geCAhPT0geDtcbn07XG5cbmZ1bmN0aW9uIElzRmluaXRlTm9uTmVnYXRpdmVOdW1iZXIodikge1xuICAgIGlmICghSXNOb25OZWdhdGl2ZU51bWJlcih2KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICh2ID09PSBJbmZpbml0eSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gSXNOb25OZWdhdGl2ZU51bWJlcih2KSB7XG4gICAgaWYgKHR5cGVvZiB2ICE9PSAnbnVtYmVyJykge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmIChOdW1iZXJJc05hTih2KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICh2IDwgMCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuXG5mdW5jdGlvbiBEZXF1ZXVlVmFsdWUoY29udGFpbmVyKSB7XG4gICAgY29uc3QgcGFpciA9IGNvbnRhaW5lci5fcXVldWUuc2hpZnQoKTtcbiAgICBjb250YWluZXIuX3F1ZXVlVG90YWxTaXplIC09IHBhaXIuc2l6ZTtcbiAgICBpZiAoY29udGFpbmVyLl9xdWV1ZVRvdGFsU2l6ZSA8IDApIHtcbiAgICAgICAgY29udGFpbmVyLl9xdWV1ZVRvdGFsU2l6ZSA9IDA7XG4gICAgfVxuICAgIHJldHVybiBwYWlyLnZhbHVlO1xufVxuZnVuY3Rpb24gRW5xdWV1ZVZhbHVlV2l0aFNpemUoY29udGFpbmVyLCB2YWx1ZSwgc2l6ZSkge1xuICAgIHNpemUgPSBOdW1iZXIoc2l6ZSk7XG4gICAgaWYgKCFJc0Zpbml0ZU5vbk5lZ2F0aXZlTnVtYmVyKHNpemUpKSB7XG4gICAgICAgIHRocm93IG5ldyBSYW5nZUVycm9yKCdTaXplIG11c3QgYmUgYSBmaW5pdGUsIG5vbi1OYU4sIG5vbi1uZWdhdGl2ZSBudW1iZXIuJyk7XG4gICAgfVxuICAgIGNvbnRhaW5lci5fcXVldWUucHVzaCh7IHZhbHVlLCBzaXplIH0pO1xuICAgIGNvbnRhaW5lci5fcXVldWVUb3RhbFNpemUgKz0gc2l6ZTtcbn1cbmZ1bmN0aW9uIFBlZWtRdWV1ZVZhbHVlKGNvbnRhaW5lcikge1xuICAgIGNvbnN0IHBhaXIgPSBjb250YWluZXIuX3F1ZXVlLnBlZWsoKTtcbiAgICByZXR1cm4gcGFpci52YWx1ZTtcbn1cbmZ1bmN0aW9uIFJlc2V0UXVldWUoY29udGFpbmVyKSB7XG4gICAgY29udGFpbmVyLl9xdWV1ZSA9IG5ldyBTaW1wbGVRdWV1ZSgpO1xuICAgIGNvbnRhaW5lci5fcXVldWVUb3RhbFNpemUgPSAwO1xufVxuXG5mdW5jdGlvbiBDcmVhdGVBcnJheUZyb21MaXN0KGVsZW1lbnRzKSB7XG4gICAgLy8gV2UgdXNlIGFycmF5cyB0byByZXByZXNlbnQgbGlzdHMsIHNvIHRoaXMgaXMgYmFzaWNhbGx5IGEgbm8tb3AuXG4gICAgLy8gRG8gYSBzbGljZSB0aG91Z2gganVzdCBpbiBjYXNlIHdlIGhhcHBlbiB0byBkZXBlbmQgb24gdGhlIHVuaXF1ZS1uZXNzLlxuICAgIHJldHVybiBlbGVtZW50cy5zbGljZSgpO1xufVxuZnVuY3Rpb24gQ29weURhdGFCbG9ja0J5dGVzKGRlc3QsIGRlc3RPZmZzZXQsIHNyYywgc3JjT2Zmc2V0LCBuKSB7XG4gICAgbmV3IFVpbnQ4QXJyYXkoZGVzdCkuc2V0KG5ldyBVaW50OEFycmF5KHNyYywgc3JjT2Zmc2V0LCBuKSwgZGVzdE9mZnNldCk7XG59XG4vLyBOb3QgaW1wbGVtZW50ZWQgY29ycmVjdGx5XG5mdW5jdGlvbiBUcmFuc2ZlckFycmF5QnVmZmVyKE8pIHtcbiAgICByZXR1cm4gTztcbn1cbi8vIE5vdCBpbXBsZW1lbnRlZCBjb3JyZWN0bHlcbmZ1bmN0aW9uIElzRGV0YWNoZWRCdWZmZXIoTykge1xuICAgIHJldHVybiBmYWxzZTtcbn1cblxuLyoqXG4gKiBBIHB1bGwtaW50byByZXF1ZXN0IGluIGEge0BsaW5rIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJ9LlxuICpcbiAqIEBwdWJsaWNcbiAqL1xuY2xhc3MgUmVhZGFibGVTdHJlYW1CWU9CUmVxdWVzdCB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0lsbGVnYWwgY29uc3RydWN0b3InKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB0aGUgdmlldyBmb3Igd3JpdGluZyBpbiB0bywgb3IgYG51bGxgIGlmIHRoZSBCWU9CIHJlcXVlc3QgaGFzIGFscmVhZHkgYmVlbiByZXNwb25kZWQgdG8uXG4gICAgICovXG4gICAgZ2V0IHZpZXcoKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbUJZT0JSZXF1ZXN0KHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBieW9iUmVxdWVzdEJyYW5kQ2hlY2tFeGNlcHRpb24oJ3ZpZXcnKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gdGhpcy5fdmlldztcbiAgICB9XG4gICAgcmVzcG9uZChieXRlc1dyaXR0ZW4pIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGJ5b2JSZXF1ZXN0QnJhbmRDaGVja0V4Y2VwdGlvbigncmVzcG9uZCcpO1xuICAgICAgICB9XG4gICAgICAgIGFzc2VydFJlcXVpcmVkQXJndW1lbnQoYnl0ZXNXcml0dGVuLCAxLCAncmVzcG9uZCcpO1xuICAgICAgICBieXRlc1dyaXR0ZW4gPSBjb252ZXJ0VW5zaWduZWRMb25nTG9uZ1dpdGhFbmZvcmNlUmFuZ2UoYnl0ZXNXcml0dGVuLCAnRmlyc3QgcGFyYW1ldGVyJyk7XG4gICAgICAgIGlmICh0aGlzLl9hc3NvY2lhdGVkUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlciA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGlzIEJZT0IgcmVxdWVzdCBoYXMgYmVlbiBpbnZhbGlkYXRlZCcpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChJc0RldGFjaGVkQnVmZmVyKHRoaXMuX3ZpZXcuYnVmZmVyKSkgO1xuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZCh0aGlzLl9hc3NvY2lhdGVkUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlciwgYnl0ZXNXcml0dGVuKTtcbiAgICB9XG4gICAgcmVzcG9uZFdpdGhOZXdWaWV3KHZpZXcpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGJ5b2JSZXF1ZXN0QnJhbmRDaGVja0V4Y2VwdGlvbigncmVzcG9uZFdpdGhOZXdWaWV3Jyk7XG4gICAgICAgIH1cbiAgICAgICAgYXNzZXJ0UmVxdWlyZWRBcmd1bWVudCh2aWV3LCAxLCAncmVzcG9uZFdpdGhOZXdWaWV3Jyk7XG4gICAgICAgIGlmICghQXJyYXlCdWZmZXIuaXNWaWV3KHZpZXcpKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdZb3UgY2FuIG9ubHkgcmVzcG9uZCB3aXRoIGFycmF5IGJ1ZmZlciB2aWV3cycpO1xuICAgICAgICB9XG4gICAgICAgIGlmICh2aWV3LmJ5dGVMZW5ndGggPT09IDApIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ2NodW5rIG11c3QgaGF2ZSBub24temVybyBieXRlTGVuZ3RoJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHZpZXcuYnVmZmVyLmJ5dGVMZW5ndGggPT09IDApIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoYGNodW5rJ3MgYnVmZmVyIG11c3QgaGF2ZSBub24temVybyBieXRlTGVuZ3RoYCk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX2Fzc29jaWF0ZWRSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1RoaXMgQllPQiByZXF1ZXN0IGhhcyBiZWVuIGludmFsaWRhdGVkJyk7XG4gICAgICAgIH1cbiAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRXaXRoTmV3Vmlldyh0aGlzLl9hc3NvY2lhdGVkUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlciwgdmlldyk7XG4gICAgfVxufVxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoUmVhZGFibGVTdHJlYW1CWU9CUmVxdWVzdC5wcm90b3R5cGUsIHtcbiAgICByZXNwb25kOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICByZXNwb25kV2l0aE5ld1ZpZXc6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIHZpZXc6IHsgZW51bWVyYWJsZTogdHJ1ZSB9XG59KTtcbmlmICh0eXBlb2YgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcgPT09ICdzeW1ib2wnKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFJlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QucHJvdG90eXBlLCBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZywge1xuICAgICAgICB2YWx1ZTogJ1JlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QnLFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICB9KTtcbn1cbi8qKlxuICogQWxsb3dzIGNvbnRyb2wgb2YgYSB7QGxpbmsgUmVhZGFibGVTdHJlYW0gfCByZWFkYWJsZSBieXRlIHN0cmVhbX0ncyBzdGF0ZSBhbmQgaW50ZXJuYWwgcXVldWUuXG4gKlxuICogQHB1YmxpY1xuICovXG5jbGFzcyBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyIHtcbiAgICBjb25zdHJ1Y3RvcigpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignSWxsZWdhbCBjb25zdHJ1Y3RvcicpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIHRoZSBjdXJyZW50IEJZT0IgcHVsbCByZXF1ZXN0LCBvciBgbnVsbGAgaWYgdGhlcmUgaXNuJ3Qgb25lLlxuICAgICAqL1xuICAgIGdldCBieW9iUmVxdWVzdCgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGJ5dGVTdHJlYW1Db250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbignYnlvYlJlcXVlc3QnKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAodGhpcy5fYnlvYlJlcXVlc3QgPT09IG51bGwgJiYgdGhpcy5fcGVuZGluZ1B1bGxJbnRvcy5sZW5ndGggPiAwKSB7XG4gICAgICAgICAgICBjb25zdCBmaXJzdERlc2NyaXB0b3IgPSB0aGlzLl9wZW5kaW5nUHVsbEludG9zLnBlZWsoKTtcbiAgICAgICAgICAgIGNvbnN0IHZpZXcgPSBuZXcgVWludDhBcnJheShmaXJzdERlc2NyaXB0b3IuYnVmZmVyLCBmaXJzdERlc2NyaXB0b3IuYnl0ZU9mZnNldCArIGZpcnN0RGVzY3JpcHRvci5ieXRlc0ZpbGxlZCwgZmlyc3REZXNjcmlwdG9yLmJ5dGVMZW5ndGggLSBmaXJzdERlc2NyaXB0b3IuYnl0ZXNGaWxsZWQpO1xuICAgICAgICAgICAgY29uc3QgYnlvYlJlcXVlc3QgPSBPYmplY3QuY3JlYXRlKFJlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QucHJvdG90eXBlKTtcbiAgICAgICAgICAgIFNldFVwUmVhZGFibGVTdHJlYW1CWU9CUmVxdWVzdChieW9iUmVxdWVzdCwgdGhpcywgdmlldyk7XG4gICAgICAgICAgICB0aGlzLl9ieW9iUmVxdWVzdCA9IGJ5b2JSZXF1ZXN0O1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9ieW9iUmVxdWVzdDtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB0aGUgZGVzaXJlZCBzaXplIHRvIGZpbGwgdGhlIGNvbnRyb2xsZWQgc3RyZWFtJ3MgaW50ZXJuYWwgcXVldWUuIEl0IGNhbiBiZSBuZWdhdGl2ZSwgaWYgdGhlIHF1ZXVlIGlzXG4gICAgICogb3Zlci1mdWxsLiBBbiB1bmRlcmx5aW5nIGJ5dGUgc291cmNlIG91Z2h0IHRvIHVzZSB0aGlzIGluZm9ybWF0aW9uIHRvIGRldGVybWluZSB3aGVuIGFuZCBob3cgdG8gYXBwbHkgYmFja3ByZXNzdXJlLlxuICAgICAqL1xuICAgIGdldCBkZXNpcmVkU2l6ZSgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGJ5dGVTdHJlYW1Db250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbignZGVzaXJlZFNpemUnKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckdldERlc2lyZWRTaXplKHRoaXMpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBDbG9zZXMgdGhlIGNvbnRyb2xsZWQgcmVhZGFibGUgc3RyZWFtLiBDb25zdW1lcnMgd2lsbCBzdGlsbCBiZSBhYmxlIHRvIHJlYWQgYW55IHByZXZpb3VzbHktZW5xdWV1ZWQgY2h1bmtzIGZyb21cbiAgICAgKiB0aGUgc3RyZWFtLCBidXQgb25jZSB0aG9zZSBhcmUgcmVhZCwgdGhlIHN0cmVhbSB3aWxsIGJlY29tZSBjbG9zZWQuXG4gICAgICovXG4gICAgY2xvc2UoKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBieXRlU3RyZWFtQ29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Nsb3NlJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX2Nsb3NlUmVxdWVzdGVkKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGUgc3RyZWFtIGhhcyBhbHJlYWR5IGJlZW4gY2xvc2VkOyBkbyBub3QgY2xvc2UgaXQgYWdhaW4hJyk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3Qgc3RhdGUgPSB0aGlzLl9jb250cm9sbGVkUmVhZGFibGVCeXRlU3RyZWFtLl9zdGF0ZTtcbiAgICAgICAgaWYgKHN0YXRlICE9PSAncmVhZGFibGUnKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGBUaGUgc3RyZWFtIChpbiAke3N0YXRlfSBzdGF0ZSkgaXMgbm90IGluIHRoZSByZWFkYWJsZSBzdGF0ZSBhbmQgY2Fubm90IGJlIGNsb3NlZGApO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDbG9zZSh0aGlzKTtcbiAgICB9XG4gICAgZW5xdWV1ZShjaHVuaykge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgYnl0ZVN0cmVhbUNvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdlbnF1ZXVlJyk7XG4gICAgICAgIH1cbiAgICAgICAgYXNzZXJ0UmVxdWlyZWRBcmd1bWVudChjaHVuaywgMSwgJ2VucXVldWUnKTtcbiAgICAgICAgaWYgKCFBcnJheUJ1ZmZlci5pc1ZpZXcoY2h1bmspKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdjaHVuayBtdXN0IGJlIGFuIGFycmF5IGJ1ZmZlciB2aWV3Jyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKGNodW5rLmJ5dGVMZW5ndGggPT09IDApIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ2NodW5rIG11c3QgaGF2ZSBub24temVybyBieXRlTGVuZ3RoJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKGNodW5rLmJ1ZmZlci5ieXRlTGVuZ3RoID09PSAwKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGBjaHVuaydzIGJ1ZmZlciBtdXN0IGhhdmUgbm9uLXplcm8gYnl0ZUxlbmd0aGApO1xuICAgICAgICB9XG4gICAgICAgIGlmICh0aGlzLl9jbG9zZVJlcXVlc3RlZCkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignc3RyZWFtIGlzIGNsb3NlZCBvciBkcmFpbmluZycpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IHN0YXRlID0gdGhpcy5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbS5fc3RhdGU7XG4gICAgICAgIGlmIChzdGF0ZSAhPT0gJ3JlYWRhYmxlJykge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgVGhlIHN0cmVhbSAoaW4gJHtzdGF0ZX0gc3RhdGUpIGlzIG5vdCBpbiB0aGUgcmVhZGFibGUgc3RhdGUgYW5kIGNhbm5vdCBiZSBlbnF1ZXVlZCB0b2ApO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFbnF1ZXVlKHRoaXMsIGNodW5rKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogRXJyb3JzIHRoZSBjb250cm9sbGVkIHJlYWRhYmxlIHN0cmVhbSwgbWFraW5nIGFsbCBmdXR1cmUgaW50ZXJhY3Rpb25zIHdpdGggaXQgZmFpbCB3aXRoIHRoZSBnaXZlbiBlcnJvciBgZWAuXG4gICAgICovXG4gICAgZXJyb3IoZSA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgYnl0ZVN0cmVhbUNvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdlcnJvcicpO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFcnJvcih0aGlzLCBlKTtcbiAgICB9XG4gICAgLyoqIEBpbnRlcm5hbCAqL1xuICAgIFtDYW5jZWxTdGVwc10ocmVhc29uKSB7XG4gICAgICAgIGlmICh0aGlzLl9wZW5kaW5nUHVsbEludG9zLmxlbmd0aCA+IDApIHtcbiAgICAgICAgICAgIGNvbnN0IGZpcnN0RGVzY3JpcHRvciA9IHRoaXMuX3BlbmRpbmdQdWxsSW50b3MucGVlaygpO1xuICAgICAgICAgICAgZmlyc3REZXNjcmlwdG9yLmJ5dGVzRmlsbGVkID0gMDtcbiAgICAgICAgfVxuICAgICAgICBSZXNldFF1ZXVlKHRoaXMpO1xuICAgICAgICBjb25zdCByZXN1bHQgPSB0aGlzLl9jYW5jZWxBbGdvcml0aG0ocmVhc29uKTtcbiAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyh0aGlzKTtcbiAgICAgICAgcmV0dXJuIHJlc3VsdDtcbiAgICB9XG4gICAgLyoqIEBpbnRlcm5hbCAqL1xuICAgIFtQdWxsU3RlcHNdKHJlYWRSZXF1ZXN0KSB7XG4gICAgICAgIGNvbnN0IHN0cmVhbSA9IHRoaXMuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW07XG4gICAgICAgIGlmICh0aGlzLl9xdWV1ZVRvdGFsU2l6ZSA+IDApIHtcbiAgICAgICAgICAgIGNvbnN0IGVudHJ5ID0gdGhpcy5fcXVldWUuc2hpZnQoKTtcbiAgICAgICAgICAgIHRoaXMuX3F1ZXVlVG90YWxTaXplIC09IGVudHJ5LmJ5dGVMZW5ndGg7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVySGFuZGxlUXVldWVEcmFpbih0aGlzKTtcbiAgICAgICAgICAgIGNvbnN0IHZpZXcgPSBuZXcgVWludDhBcnJheShlbnRyeS5idWZmZXIsIGVudHJ5LmJ5dGVPZmZzZXQsIGVudHJ5LmJ5dGVMZW5ndGgpO1xuICAgICAgICAgICAgcmVhZFJlcXVlc3QuX2NodW5rU3RlcHModmlldyk7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgY29uc3QgYXV0b0FsbG9jYXRlQ2h1bmtTaXplID0gdGhpcy5fYXV0b0FsbG9jYXRlQ2h1bmtTaXplO1xuICAgICAgICBpZiAoYXV0b0FsbG9jYXRlQ2h1bmtTaXplICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIGxldCBidWZmZXI7XG4gICAgICAgICAgICB0cnkge1xuICAgICAgICAgICAgICAgIGJ1ZmZlciA9IG5ldyBBcnJheUJ1ZmZlcihhdXRvQWxsb2NhdGVDaHVua1NpemUpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgY2F0Y2ggKGJ1ZmZlckUpIHtcbiAgICAgICAgICAgICAgICByZWFkUmVxdWVzdC5fZXJyb3JTdGVwcyhidWZmZXJFKTtcbiAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBjb25zdCBwdWxsSW50b0Rlc2NyaXB0b3IgPSB7XG4gICAgICAgICAgICAgICAgYnVmZmVyLFxuICAgICAgICAgICAgICAgIGJ5dGVPZmZzZXQ6IDAsXG4gICAgICAgICAgICAgICAgYnl0ZUxlbmd0aDogYXV0b0FsbG9jYXRlQ2h1bmtTaXplLFxuICAgICAgICAgICAgICAgIGJ5dGVzRmlsbGVkOiAwLFxuICAgICAgICAgICAgICAgIGVsZW1lbnRTaXplOiAxLFxuICAgICAgICAgICAgICAgIHZpZXdDb25zdHJ1Y3RvcjogVWludDhBcnJheSxcbiAgICAgICAgICAgICAgICByZWFkZXJUeXBlOiAnZGVmYXVsdCdcbiAgICAgICAgICAgIH07XG4gICAgICAgICAgICB0aGlzLl9wZW5kaW5nUHVsbEludG9zLnB1c2gocHVsbEludG9EZXNjcmlwdG9yKTtcbiAgICAgICAgfVxuICAgICAgICBSZWFkYWJsZVN0cmVhbUFkZFJlYWRSZXF1ZXN0KHN0cmVhbSwgcmVhZFJlcXVlc3QpO1xuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZCh0aGlzKTtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyLnByb3RvdHlwZSwge1xuICAgIGNsb3NlOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBlbnF1ZXVlOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBlcnJvcjogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgYnlvYlJlcXVlc3Q6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGRlc2lyZWRTaXplOiB7IGVudW1lcmFibGU6IHRydWUgfVxufSk7XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnID09PSAnc3ltYm9sJykge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyLnByb3RvdHlwZSwgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcsIHtcbiAgICAgICAgdmFsdWU6ICdSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlci5cbmZ1bmN0aW9uIElzUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcih4KSB7XG4gICAgaWYgKCF0eXBlSXNPYmplY3QoeCkpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICBpZiAoIU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbCh4LCAnX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW0nKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gSXNSZWFkYWJsZVN0cmVhbUJZT0JSZXF1ZXN0KHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfYXNzb2NpYXRlZFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXInKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNhbGxQdWxsSWZOZWVkZWQoY29udHJvbGxlcikge1xuICAgIGNvbnN0IHNob3VsZFB1bGwgPSBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyU2hvdWxkQ2FsbFB1bGwoY29udHJvbGxlcik7XG4gICAgaWYgKCFzaG91bGRQdWxsKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKGNvbnRyb2xsZXIuX3B1bGxpbmcpIHtcbiAgICAgICAgY29udHJvbGxlci5fcHVsbEFnYWluID0gdHJ1ZTtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBjb250cm9sbGVyLl9wdWxsaW5nID0gdHJ1ZTtcbiAgICAvLyBUT0RPOiBUZXN0IGNvbnRyb2xsZXIgYXJndW1lbnRcbiAgICBjb25zdCBwdWxsUHJvbWlzZSA9IGNvbnRyb2xsZXIuX3B1bGxBbGdvcml0aG0oKTtcbiAgICB1cG9uUHJvbWlzZShwdWxsUHJvbWlzZSwgKCkgPT4ge1xuICAgICAgICBjb250cm9sbGVyLl9wdWxsaW5nID0gZmFsc2U7XG4gICAgICAgIGlmIChjb250cm9sbGVyLl9wdWxsQWdhaW4pIHtcbiAgICAgICAgICAgIGNvbnRyb2xsZXIuX3B1bGxBZ2FpbiA9IGZhbHNlO1xuICAgICAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNhbGxQdWxsSWZOZWVkZWQoY29udHJvbGxlcik7XG4gICAgICAgIH1cbiAgICB9LCBlID0+IHtcbiAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVycm9yKGNvbnRyb2xsZXIsIGUpO1xuICAgIH0pO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyUGVuZGluZ1B1bGxJbnRvcyhjb250cm9sbGVyKSB7XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckludmFsaWRhdGVCWU9CUmVxdWVzdChjb250cm9sbGVyKTtcbiAgICBjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zID0gbmV3IFNpbXBsZVF1ZXVlKCk7XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ29tbWl0UHVsbEludG9EZXNjcmlwdG9yKHN0cmVhbSwgcHVsbEludG9EZXNjcmlwdG9yKSB7XG4gICAgbGV0IGRvbmUgPSBmYWxzZTtcbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgZG9uZSA9IHRydWU7XG4gICAgfVxuICAgIGNvbnN0IGZpbGxlZFZpZXcgPSBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ29udmVydFB1bGxJbnRvRGVzY3JpcHRvcihwdWxsSW50b0Rlc2NyaXB0b3IpO1xuICAgIGlmIChwdWxsSW50b0Rlc2NyaXB0b3IucmVhZGVyVHlwZSA9PT0gJ2RlZmF1bHQnKSB7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRSZXF1ZXN0KHN0cmVhbSwgZmlsbGVkVmlldywgZG9uZSk7XG4gICAgfVxuICAgIGVsc2Uge1xuICAgICAgICBSZWFkYWJsZVN0cmVhbUZ1bGZpbGxSZWFkSW50b1JlcXVlc3Qoc3RyZWFtLCBmaWxsZWRWaWV3LCBkb25lKTtcbiAgICB9XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ29udmVydFB1bGxJbnRvRGVzY3JpcHRvcihwdWxsSW50b0Rlc2NyaXB0b3IpIHtcbiAgICBjb25zdCBieXRlc0ZpbGxlZCA9IHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlc0ZpbGxlZDtcbiAgICBjb25zdCBlbGVtZW50U2l6ZSA9IHB1bGxJbnRvRGVzY3JpcHRvci5lbGVtZW50U2l6ZTtcbiAgICByZXR1cm4gbmV3IHB1bGxJbnRvRGVzY3JpcHRvci52aWV3Q29uc3RydWN0b3IocHVsbEludG9EZXNjcmlwdG9yLmJ1ZmZlciwgcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVPZmZzZXQsIGJ5dGVzRmlsbGVkIC8gZWxlbWVudFNpemUpO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVucXVldWVDaHVua1RvUXVldWUoY29udHJvbGxlciwgYnVmZmVyLCBieXRlT2Zmc2V0LCBieXRlTGVuZ3RoKSB7XG4gICAgY29udHJvbGxlci5fcXVldWUucHVzaCh7IGJ1ZmZlciwgYnl0ZU9mZnNldCwgYnl0ZUxlbmd0aCB9KTtcbiAgICBjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZSArPSBieXRlTGVuZ3RoO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckZpbGxQdWxsSW50b0Rlc2NyaXB0b3JGcm9tUXVldWUoY29udHJvbGxlciwgcHVsbEludG9EZXNjcmlwdG9yKSB7XG4gICAgY29uc3QgZWxlbWVudFNpemUgPSBwdWxsSW50b0Rlc2NyaXB0b3IuZWxlbWVudFNpemU7XG4gICAgY29uc3QgY3VycmVudEFsaWduZWRCeXRlcyA9IHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlc0ZpbGxlZCAtIHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlc0ZpbGxlZCAlIGVsZW1lbnRTaXplO1xuICAgIGNvbnN0IG1heEJ5dGVzVG9Db3B5ID0gTWF0aC5taW4oY29udHJvbGxlci5fcXVldWVUb3RhbFNpemUsIHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlTGVuZ3RoIC0gcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkKTtcbiAgICBjb25zdCBtYXhCeXRlc0ZpbGxlZCA9IHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlc0ZpbGxlZCArIG1heEJ5dGVzVG9Db3B5O1xuICAgIGNvbnN0IG1heEFsaWduZWRCeXRlcyA9IG1heEJ5dGVzRmlsbGVkIC0gbWF4Qnl0ZXNGaWxsZWQgJSBlbGVtZW50U2l6ZTtcbiAgICBsZXQgdG90YWxCeXRlc1RvQ29weVJlbWFpbmluZyA9IG1heEJ5dGVzVG9Db3B5O1xuICAgIGxldCByZWFkeSA9IGZhbHNlO1xuICAgIGlmIChtYXhBbGlnbmVkQnl0ZXMgPiBjdXJyZW50QWxpZ25lZEJ5dGVzKSB7XG4gICAgICAgIHRvdGFsQnl0ZXNUb0NvcHlSZW1haW5pbmcgPSBtYXhBbGlnbmVkQnl0ZXMgLSBwdWxsSW50b0Rlc2NyaXB0b3IuYnl0ZXNGaWxsZWQ7XG4gICAgICAgIHJlYWR5ID0gdHJ1ZTtcbiAgICB9XG4gICAgY29uc3QgcXVldWUgPSBjb250cm9sbGVyLl9xdWV1ZTtcbiAgICB3aGlsZSAodG90YWxCeXRlc1RvQ29weVJlbWFpbmluZyA+IDApIHtcbiAgICAgICAgY29uc3QgaGVhZE9mUXVldWUgPSBxdWV1ZS5wZWVrKCk7XG4gICAgICAgIGNvbnN0IGJ5dGVzVG9Db3B5ID0gTWF0aC5taW4odG90YWxCeXRlc1RvQ29weVJlbWFpbmluZywgaGVhZE9mUXVldWUuYnl0ZUxlbmd0aCk7XG4gICAgICAgIGNvbnN0IGRlc3RTdGFydCA9IHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlT2Zmc2V0ICsgcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkO1xuICAgICAgICBDb3B5RGF0YUJsb2NrQnl0ZXMocHVsbEludG9EZXNjcmlwdG9yLmJ1ZmZlciwgZGVzdFN0YXJ0LCBoZWFkT2ZRdWV1ZS5idWZmZXIsIGhlYWRPZlF1ZXVlLmJ5dGVPZmZzZXQsIGJ5dGVzVG9Db3B5KTtcbiAgICAgICAgaWYgKGhlYWRPZlF1ZXVlLmJ5dGVMZW5ndGggPT09IGJ5dGVzVG9Db3B5KSB7XG4gICAgICAgICAgICBxdWV1ZS5zaGlmdCgpO1xuICAgICAgICB9XG4gICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgaGVhZE9mUXVldWUuYnl0ZU9mZnNldCArPSBieXRlc1RvQ29weTtcbiAgICAgICAgICAgIGhlYWRPZlF1ZXVlLmJ5dGVMZW5ndGggLT0gYnl0ZXNUb0NvcHk7XG4gICAgICAgIH1cbiAgICAgICAgY29udHJvbGxlci5fcXVldWVUb3RhbFNpemUgLT0gYnl0ZXNUb0NvcHk7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJGaWxsSGVhZFB1bGxJbnRvRGVzY3JpcHRvcihjb250cm9sbGVyLCBieXRlc1RvQ29weSwgcHVsbEludG9EZXNjcmlwdG9yKTtcbiAgICAgICAgdG90YWxCeXRlc1RvQ29weVJlbWFpbmluZyAtPSBieXRlc1RvQ29weTtcbiAgICB9XG4gICAgcmV0dXJuIHJlYWR5O1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckZpbGxIZWFkUHVsbEludG9EZXNjcmlwdG9yKGNvbnRyb2xsZXIsIHNpemUsIHB1bGxJbnRvRGVzY3JpcHRvcikge1xuICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJJbnZhbGlkYXRlQllPQlJlcXVlc3QoY29udHJvbGxlcik7XG4gICAgcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkICs9IHNpemU7XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVySGFuZGxlUXVldWVEcmFpbihjb250cm9sbGVyKSB7XG4gICAgaWYgKGNvbnRyb2xsZXIuX3F1ZXVlVG90YWxTaXplID09PSAwICYmIGNvbnRyb2xsZXIuX2Nsb3NlUmVxdWVzdGVkKSB7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoY29udHJvbGxlcik7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtQ2xvc2UoY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbSk7XG4gICAgfVxuICAgIGVsc2Uge1xuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZChjb250cm9sbGVyKTtcbiAgICB9XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVySW52YWxpZGF0ZUJZT0JSZXF1ZXN0KGNvbnRyb2xsZXIpIHtcbiAgICBpZiAoY29udHJvbGxlci5fYnlvYlJlcXVlc3QgPT09IG51bGwpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBjb250cm9sbGVyLl9ieW9iUmVxdWVzdC5fYXNzb2NpYXRlZFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIgPSB1bmRlZmluZWQ7XG4gICAgY29udHJvbGxlci5fYnlvYlJlcXVlc3QuX3ZpZXcgPSBudWxsO1xuICAgIGNvbnRyb2xsZXIuX2J5b2JSZXF1ZXN0ID0gbnVsbDtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJQcm9jZXNzUHVsbEludG9EZXNjcmlwdG9yc1VzaW5nUXVldWUoY29udHJvbGxlcikge1xuICAgIHdoaWxlIChjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zLmxlbmd0aCA+IDApIHtcbiAgICAgICAgaWYgKGNvbnRyb2xsZXIuX3F1ZXVlVG90YWxTaXplID09PSAwKSB7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgY29uc3QgcHVsbEludG9EZXNjcmlwdG9yID0gY29udHJvbGxlci5fcGVuZGluZ1B1bGxJbnRvcy5wZWVrKCk7XG4gICAgICAgIGlmIChSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyRmlsbFB1bGxJbnRvRGVzY3JpcHRvckZyb21RdWV1ZShjb250cm9sbGVyLCBwdWxsSW50b0Rlc2NyaXB0b3IpKSB7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyU2hpZnRQZW5kaW5nUHVsbEludG8oY29udHJvbGxlcik7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ29tbWl0UHVsbEludG9EZXNjcmlwdG9yKGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW0sIHB1bGxJbnRvRGVzY3JpcHRvcik7XG4gICAgICAgIH1cbiAgICB9XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUHVsbEludG8oY29udHJvbGxlciwgdmlldywgcmVhZEludG9SZXF1ZXN0KSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbTtcbiAgICBsZXQgZWxlbWVudFNpemUgPSAxO1xuICAgIGlmICh2aWV3LmNvbnN0cnVjdG9yICE9PSBEYXRhVmlldykge1xuICAgICAgICBlbGVtZW50U2l6ZSA9IHZpZXcuY29uc3RydWN0b3IuQllURVNfUEVSX0VMRU1FTlQ7XG4gICAgfVxuICAgIGNvbnN0IGN0b3IgPSB2aWV3LmNvbnN0cnVjdG9yO1xuICAgIGNvbnN0IGJ1ZmZlciA9IFRyYW5zZmVyQXJyYXlCdWZmZXIodmlldy5idWZmZXIpO1xuICAgIGNvbnN0IHB1bGxJbnRvRGVzY3JpcHRvciA9IHtcbiAgICAgICAgYnVmZmVyLFxuICAgICAgICBieXRlT2Zmc2V0OiB2aWV3LmJ5dGVPZmZzZXQsXG4gICAgICAgIGJ5dGVMZW5ndGg6IHZpZXcuYnl0ZUxlbmd0aCxcbiAgICAgICAgYnl0ZXNGaWxsZWQ6IDAsXG4gICAgICAgIGVsZW1lbnRTaXplLFxuICAgICAgICB2aWV3Q29uc3RydWN0b3I6IGN0b3IsXG4gICAgICAgIHJlYWRlclR5cGU6ICdieW9iJ1xuICAgIH07XG4gICAgaWYgKGNvbnRyb2xsZXIuX3BlbmRpbmdQdWxsSW50b3MubGVuZ3RoID4gMCkge1xuICAgICAgICBjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zLnB1c2gocHVsbEludG9EZXNjcmlwdG9yKTtcbiAgICAgICAgLy8gTm8gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNhbGxQdWxsSWZOZWVkZWQoKSBjYWxsIHNpbmNlOlxuICAgICAgICAvLyAtIE5vIGNoYW5nZSBoYXBwZW5zIG9uIGRlc2lyZWRTaXplXG4gICAgICAgIC8vIC0gVGhlIHNvdXJjZSBoYXMgYWxyZWFkeSBiZWVuIG5vdGlmaWVkIG9mIHRoYXQgdGhlcmUncyBhdCBsZWFzdCAxIHBlbmRpbmcgcmVhZCh2aWV3KVxuICAgICAgICBSZWFkYWJsZVN0cmVhbUFkZFJlYWRJbnRvUmVxdWVzdChzdHJlYW0sIHJlYWRJbnRvUmVxdWVzdCk7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKHN0cmVhbS5fc3RhdGUgPT09ICdjbG9zZWQnKSB7XG4gICAgICAgIGNvbnN0IGVtcHR5VmlldyA9IG5ldyBjdG9yKHB1bGxJbnRvRGVzY3JpcHRvci5idWZmZXIsIHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlT2Zmc2V0LCAwKTtcbiAgICAgICAgcmVhZEludG9SZXF1ZXN0Ll9jbG9zZVN0ZXBzKGVtcHR5Vmlldyk7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKGNvbnRyb2xsZXIuX3F1ZXVlVG90YWxTaXplID4gMCkge1xuICAgICAgICBpZiAoUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckZpbGxQdWxsSW50b0Rlc2NyaXB0b3JGcm9tUXVldWUoY29udHJvbGxlciwgcHVsbEludG9EZXNjcmlwdG9yKSkge1xuICAgICAgICAgICAgY29uc3QgZmlsbGVkVmlldyA9IFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDb252ZXJ0UHVsbEludG9EZXNjcmlwdG9yKHB1bGxJbnRvRGVzY3JpcHRvcik7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVySGFuZGxlUXVldWVEcmFpbihjb250cm9sbGVyKTtcbiAgICAgICAgICAgIHJlYWRJbnRvUmVxdWVzdC5fY2h1bmtTdGVwcyhmaWxsZWRWaWV3KTtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfVxuICAgICAgICBpZiAoY29udHJvbGxlci5fY2xvc2VSZXF1ZXN0ZWQpIHtcbiAgICAgICAgICAgIGNvbnN0IGUgPSBuZXcgVHlwZUVycm9yKCdJbnN1ZmZpY2llbnQgYnl0ZXMgdG8gZmlsbCBlbGVtZW50cyBpbiB0aGUgZ2l2ZW4gYnVmZmVyJyk7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyRXJyb3IoY29udHJvbGxlciwgZSk7XG4gICAgICAgICAgICByZWFkSW50b1JlcXVlc3QuX2Vycm9yU3RlcHMoZSk7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICB9XG4gICAgY29udHJvbGxlci5fcGVuZGluZ1B1bGxJbnRvcy5wdXNoKHB1bGxJbnRvRGVzY3JpcHRvcik7XG4gICAgUmVhZGFibGVTdHJlYW1BZGRSZWFkSW50b1JlcXVlc3Qoc3RyZWFtLCByZWFkSW50b1JlcXVlc3QpO1xuICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDYWxsUHVsbElmTmVlZGVkKGNvbnRyb2xsZXIpO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRJbkNsb3NlZFN0YXRlKGNvbnRyb2xsZXIsIGZpcnN0RGVzY3JpcHRvcikge1xuICAgIGZpcnN0RGVzY3JpcHRvci5idWZmZXIgPSBUcmFuc2ZlckFycmF5QnVmZmVyKGZpcnN0RGVzY3JpcHRvci5idWZmZXIpO1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW07XG4gICAgaWYgKFJlYWRhYmxlU3RyZWFtSGFzQllPQlJlYWRlcihzdHJlYW0pKSB7XG4gICAgICAgIHdoaWxlIChSZWFkYWJsZVN0cmVhbUdldE51bVJlYWRJbnRvUmVxdWVzdHMoc3RyZWFtKSA+IDApIHtcbiAgICAgICAgICAgIGNvbnN0IHB1bGxJbnRvRGVzY3JpcHRvciA9IFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJTaGlmdFBlbmRpbmdQdWxsSW50byhjb250cm9sbGVyKTtcbiAgICAgICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDb21taXRQdWxsSW50b0Rlc2NyaXB0b3Ioc3RyZWFtLCBwdWxsSW50b0Rlc2NyaXB0b3IpO1xuICAgICAgICB9XG4gICAgfVxufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRJblJlYWRhYmxlU3RhdGUoY29udHJvbGxlciwgYnl0ZXNXcml0dGVuLCBwdWxsSW50b0Rlc2NyaXB0b3IpIHtcbiAgICBpZiAocHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkICsgYnl0ZXNXcml0dGVuID4gcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVMZW5ndGgpIHtcbiAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ2J5dGVzV3JpdHRlbiBvdXQgb2YgcmFuZ2UnKTtcbiAgICB9XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckZpbGxIZWFkUHVsbEludG9EZXNjcmlwdG9yKGNvbnRyb2xsZXIsIGJ5dGVzV3JpdHRlbiwgcHVsbEludG9EZXNjcmlwdG9yKTtcbiAgICBpZiAocHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkIDwgcHVsbEludG9EZXNjcmlwdG9yLmVsZW1lbnRTaXplKSB7XG4gICAgICAgIC8vIFRPRE86IEZpZ3VyZSBvdXQgd2hldGhlciB3ZSBzaG91bGQgZGV0YWNoIHRoZSBidWZmZXIgb3Igbm90IGhlcmUuXG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclNoaWZ0UGVuZGluZ1B1bGxJbnRvKGNvbnRyb2xsZXIpO1xuICAgIGNvbnN0IHJlbWFpbmRlclNpemUgPSBwdWxsSW50b0Rlc2NyaXB0b3IuYnl0ZXNGaWxsZWQgJSBwdWxsSW50b0Rlc2NyaXB0b3IuZWxlbWVudFNpemU7XG4gICAgaWYgKHJlbWFpbmRlclNpemUgPiAwKSB7XG4gICAgICAgIGNvbnN0IGVuZCA9IHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlT2Zmc2V0ICsgcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkO1xuICAgICAgICBjb25zdCByZW1haW5kZXIgPSBwdWxsSW50b0Rlc2NyaXB0b3IuYnVmZmVyLnNsaWNlKGVuZCAtIHJlbWFpbmRlclNpemUsIGVuZCk7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFbnF1ZXVlQ2h1bmtUb1F1ZXVlKGNvbnRyb2xsZXIsIHJlbWFpbmRlciwgMCwgcmVtYWluZGVyLmJ5dGVMZW5ndGgpO1xuICAgIH1cbiAgICBwdWxsSW50b0Rlc2NyaXB0b3IuYnVmZmVyID0gVHJhbnNmZXJBcnJheUJ1ZmZlcihwdWxsSW50b0Rlc2NyaXB0b3IuYnVmZmVyKTtcbiAgICBwdWxsSW50b0Rlc2NyaXB0b3IuYnl0ZXNGaWxsZWQgLT0gcmVtYWluZGVyU2l6ZTtcbiAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ29tbWl0UHVsbEludG9EZXNjcmlwdG9yKGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW0sIHB1bGxJbnRvRGVzY3JpcHRvcik7XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclByb2Nlc3NQdWxsSW50b0Rlc2NyaXB0b3JzVXNpbmdRdWV1ZShjb250cm9sbGVyKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJSZXNwb25kSW50ZXJuYWwoY29udHJvbGxlciwgYnl0ZXNXcml0dGVuKSB7XG4gICAgY29uc3QgZmlyc3REZXNjcmlwdG9yID0gY29udHJvbGxlci5fcGVuZGluZ1B1bGxJbnRvcy5wZWVrKCk7XG4gICAgY29uc3Qgc3RhdGUgPSBjb250cm9sbGVyLl9jb250cm9sbGVkUmVhZGFibGVCeXRlU3RyZWFtLl9zdGF0ZTtcbiAgICBpZiAoc3RhdGUgPT09ICdjbG9zZWQnKSB7XG4gICAgICAgIGlmIChieXRlc1dyaXR0ZW4gIT09IDApIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ2J5dGVzV3JpdHRlbiBtdXN0IGJlIDAgd2hlbiBjYWxsaW5nIHJlc3BvbmQoKSBvbiBhIGNsb3NlZCBzdHJlYW0nKTtcbiAgICAgICAgfVxuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZEluQ2xvc2VkU3RhdGUoY29udHJvbGxlciwgZmlyc3REZXNjcmlwdG9yKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJSZXNwb25kSW5SZWFkYWJsZVN0YXRlKGNvbnRyb2xsZXIsIGJ5dGVzV3JpdHRlbiwgZmlyc3REZXNjcmlwdG9yKTtcbiAgICB9XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNhbGxQdWxsSWZOZWVkZWQoY29udHJvbGxlcik7XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyU2hpZnRQZW5kaW5nUHVsbEludG8oY29udHJvbGxlcikge1xuICAgIGNvbnN0IGRlc2NyaXB0b3IgPSBjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zLnNoaWZ0KCk7XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckludmFsaWRhdGVCWU9CUmVxdWVzdChjb250cm9sbGVyKTtcbiAgICByZXR1cm4gZGVzY3JpcHRvcjtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJTaG91bGRDYWxsUHVsbChjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbTtcbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSAhPT0gJ3JlYWRhYmxlJykge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmIChjb250cm9sbGVyLl9jbG9zZVJlcXVlc3RlZCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghY29udHJvbGxlci5fc3RhcnRlZCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmIChSZWFkYWJsZVN0cmVhbUhhc0RlZmF1bHRSZWFkZXIoc3RyZWFtKSAmJiBSZWFkYWJsZVN0cmVhbUdldE51bVJlYWRSZXF1ZXN0cyhzdHJlYW0pID4gMCkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgaWYgKFJlYWRhYmxlU3RyZWFtSGFzQllPQlJlYWRlcihzdHJlYW0pICYmIFJlYWRhYmxlU3RyZWFtR2V0TnVtUmVhZEludG9SZXF1ZXN0cyhzdHJlYW0pID4gMCkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgY29uc3QgZGVzaXJlZFNpemUgPSBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyR2V0RGVzaXJlZFNpemUoY29udHJvbGxlcik7XG4gICAgaWYgKGRlc2lyZWRTaXplID4gMCkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgcmV0dXJuIGZhbHNlO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyhjb250cm9sbGVyKSB7XG4gICAgY29udHJvbGxlci5fcHVsbEFsZ29yaXRobSA9IHVuZGVmaW5lZDtcbiAgICBjb250cm9sbGVyLl9jYW5jZWxBbGdvcml0aG0gPSB1bmRlZmluZWQ7XG59XG4vLyBBIGNsaWVudCBvZiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyIG1heSB1c2UgdGhlc2UgZnVuY3Rpb25zIGRpcmVjdGx5IHRvIGJ5cGFzcyBzdGF0ZSBjaGVjay5cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDbG9zZShjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbTtcbiAgICBpZiAoY29udHJvbGxlci5fY2xvc2VSZXF1ZXN0ZWQgfHwgc3RyZWFtLl9zdGF0ZSAhPT0gJ3JlYWRhYmxlJykge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZSA+IDApIHtcbiAgICAgICAgY29udHJvbGxlci5fY2xvc2VSZXF1ZXN0ZWQgPSB0cnVlO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zLmxlbmd0aCA+IDApIHtcbiAgICAgICAgY29uc3QgZmlyc3RQZW5kaW5nUHVsbEludG8gPSBjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zLnBlZWsoKTtcbiAgICAgICAgaWYgKGZpcnN0UGVuZGluZ1B1bGxJbnRvLmJ5dGVzRmlsbGVkID4gMCkge1xuICAgICAgICAgICAgY29uc3QgZSA9IG5ldyBUeXBlRXJyb3IoJ0luc3VmZmljaWVudCBieXRlcyB0byBmaWxsIGVsZW1lbnRzIGluIHRoZSBnaXZlbiBidWZmZXInKTtcbiAgICAgICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlKTtcbiAgICAgICAgICAgIHRocm93IGU7XG4gICAgICAgIH1cbiAgICB9XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyhjb250cm9sbGVyKTtcbiAgICBSZWFkYWJsZVN0cmVhbUNsb3NlKHN0cmVhbSk7XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyRW5xdWV1ZShjb250cm9sbGVyLCBjaHVuaykge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW07XG4gICAgaWYgKGNvbnRyb2xsZXIuX2Nsb3NlUmVxdWVzdGVkIHx8IHN0cmVhbS5fc3RhdGUgIT09ICdyZWFkYWJsZScpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBjb25zdCBidWZmZXIgPSBjaHVuay5idWZmZXI7XG4gICAgY29uc3QgYnl0ZU9mZnNldCA9IGNodW5rLmJ5dGVPZmZzZXQ7XG4gICAgY29uc3QgYnl0ZUxlbmd0aCA9IGNodW5rLmJ5dGVMZW5ndGg7XG4gICAgY29uc3QgdHJhbnNmZXJyZWRCdWZmZXIgPSBUcmFuc2ZlckFycmF5QnVmZmVyKGJ1ZmZlcik7XG4gICAgaWYgKFJlYWRhYmxlU3RyZWFtSGFzRGVmYXVsdFJlYWRlcihzdHJlYW0pKSB7XG4gICAgICAgIGlmIChSZWFkYWJsZVN0cmVhbUdldE51bVJlYWRSZXF1ZXN0cyhzdHJlYW0pID09PSAwKSB7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyRW5xdWV1ZUNodW5rVG9RdWV1ZShjb250cm9sbGVyLCB0cmFuc2ZlcnJlZEJ1ZmZlciwgYnl0ZU9mZnNldCwgYnl0ZUxlbmd0aCk7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICBjb25zdCB0cmFuc2ZlcnJlZFZpZXcgPSBuZXcgVWludDhBcnJheSh0cmFuc2ZlcnJlZEJ1ZmZlciwgYnl0ZU9mZnNldCwgYnl0ZUxlbmd0aCk7XG4gICAgICAgICAgICBSZWFkYWJsZVN0cmVhbUZ1bGZpbGxSZWFkUmVxdWVzdChzdHJlYW0sIHRyYW5zZmVycmVkVmlldywgZmFsc2UpO1xuICAgICAgICB9XG4gICAgfVxuICAgIGVsc2UgaWYgKFJlYWRhYmxlU3RyZWFtSGFzQllPQlJlYWRlcihzdHJlYW0pKSB7XG4gICAgICAgIC8vIFRPRE86IElkZWFsbHkgaW4gdGhpcyBicmFuY2ggZGV0YWNoaW5nIHNob3VsZCBoYXBwZW4gb25seSBpZiB0aGUgYnVmZmVyIGlzIG5vdCBjb25zdW1lZCBmdWxseS5cbiAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVucXVldWVDaHVua1RvUXVldWUoY29udHJvbGxlciwgdHJhbnNmZXJyZWRCdWZmZXIsIGJ5dGVPZmZzZXQsIGJ5dGVMZW5ndGgpO1xuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUHJvY2Vzc1B1bGxJbnRvRGVzY3JpcHRvcnNVc2luZ1F1ZXVlKGNvbnRyb2xsZXIpO1xuICAgIH1cbiAgICBlbHNlIHtcbiAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVucXVldWVDaHVua1RvUXVldWUoY29udHJvbGxlciwgdHJhbnNmZXJyZWRCdWZmZXIsIGJ5dGVPZmZzZXQsIGJ5dGVMZW5ndGgpO1xuICAgIH1cbiAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZChjb250cm9sbGVyKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbTtcbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSAhPT0gJ3JlYWRhYmxlJykge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDbGVhclBlbmRpbmdQdWxsSW50b3MoY29udHJvbGxlcik7XG4gICAgUmVzZXRRdWV1ZShjb250cm9sbGVyKTtcbiAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKGNvbnRyb2xsZXIpO1xuICAgIFJlYWRhYmxlU3RyZWFtRXJyb3Ioc3RyZWFtLCBlKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJHZXREZXNpcmVkU2l6ZShjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc3RhdGUgPSBjb250cm9sbGVyLl9jb250cm9sbGVkUmVhZGFibGVCeXRlU3RyZWFtLl9zdGF0ZTtcbiAgICBpZiAoc3RhdGUgPT09ICdlcnJvcmVkJykge1xuICAgICAgICByZXR1cm4gbnVsbDtcbiAgICB9XG4gICAgaWYgKHN0YXRlID09PSAnY2xvc2VkJykge1xuICAgICAgICByZXR1cm4gMDtcbiAgICB9XG4gICAgcmV0dXJuIGNvbnRyb2xsZXIuX3N0cmF0ZWd5SFdNIC0gY29udHJvbGxlci5fcXVldWVUb3RhbFNpemU7XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZChjb250cm9sbGVyLCBieXRlc1dyaXR0ZW4pIHtcbiAgICBieXRlc1dyaXR0ZW4gPSBOdW1iZXIoYnl0ZXNXcml0dGVuKTtcbiAgICBpZiAoIUlzRmluaXRlTm9uTmVnYXRpdmVOdW1iZXIoYnl0ZXNXcml0dGVuKSkge1xuICAgICAgICB0aHJvdyBuZXcgUmFuZ2VFcnJvcignYnl0ZXNXcml0dGVuIG11c3QgYmUgYSBmaW5pdGUnKTtcbiAgICB9XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRJbnRlcm5hbChjb250cm9sbGVyLCBieXRlc1dyaXR0ZW4pO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRXaXRoTmV3Vmlldyhjb250cm9sbGVyLCB2aWV3KSB7XG4gICAgY29uc3QgZmlyc3REZXNjcmlwdG9yID0gY29udHJvbGxlci5fcGVuZGluZ1B1bGxJbnRvcy5wZWVrKCk7XG4gICAgaWYgKGZpcnN0RGVzY3JpcHRvci5ieXRlT2Zmc2V0ICsgZmlyc3REZXNjcmlwdG9yLmJ5dGVzRmlsbGVkICE9PSB2aWV3LmJ5dGVPZmZzZXQpIHtcbiAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ1RoZSByZWdpb24gc3BlY2lmaWVkIGJ5IHZpZXcgZG9lcyBub3QgbWF0Y2ggYnlvYlJlcXVlc3QnKTtcbiAgICB9XG4gICAgaWYgKGZpcnN0RGVzY3JpcHRvci5ieXRlTGVuZ3RoICE9PSB2aWV3LmJ5dGVMZW5ndGgpIHtcbiAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ1RoZSBidWZmZXIgb2YgdmlldyBoYXMgZGlmZmVyZW50IGNhcGFjaXR5IHRoYW4gYnlvYlJlcXVlc3QnKTtcbiAgICB9XG4gICAgZmlyc3REZXNjcmlwdG9yLmJ1ZmZlciA9IHZpZXcuYnVmZmVyO1xuICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJSZXNwb25kSW50ZXJuYWwoY29udHJvbGxlciwgdmlldy5ieXRlTGVuZ3RoKTtcbn1cbmZ1bmN0aW9uIFNldFVwUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcihzdHJlYW0sIGNvbnRyb2xsZXIsIHN0YXJ0QWxnb3JpdGhtLCBwdWxsQWxnb3JpdGhtLCBjYW5jZWxBbGdvcml0aG0sIGhpZ2hXYXRlck1hcmssIGF1dG9BbGxvY2F0ZUNodW5rU2l6ZSkge1xuICAgIGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW0gPSBzdHJlYW07XG4gICAgY29udHJvbGxlci5fcHVsbEFnYWluID0gZmFsc2U7XG4gICAgY29udHJvbGxlci5fcHVsbGluZyA9IGZhbHNlO1xuICAgIGNvbnRyb2xsZXIuX2J5b2JSZXF1ZXN0ID0gbnVsbDtcbiAgICAvLyBOZWVkIHRvIHNldCB0aGUgc2xvdHMgc28gdGhhdCB0aGUgYXNzZXJ0IGRvZXNuJ3QgZmlyZS4gSW4gdGhlIHNwZWMgdGhlIHNsb3RzIGFscmVhZHkgZXhpc3QgaW1wbGljaXRseS5cbiAgICBjb250cm9sbGVyLl9xdWV1ZSA9IGNvbnRyb2xsZXIuX3F1ZXVlVG90YWxTaXplID0gdW5kZWZpbmVkO1xuICAgIFJlc2V0UXVldWUoY29udHJvbGxlcik7XG4gICAgY29udHJvbGxlci5fY2xvc2VSZXF1ZXN0ZWQgPSBmYWxzZTtcbiAgICBjb250cm9sbGVyLl9zdGFydGVkID0gZmFsc2U7XG4gICAgY29udHJvbGxlci5fc3RyYXRlZ3lIV00gPSBoaWdoV2F0ZXJNYXJrO1xuICAgIGNvbnRyb2xsZXIuX3B1bGxBbGdvcml0aG0gPSBwdWxsQWxnb3JpdGhtO1xuICAgIGNvbnRyb2xsZXIuX2NhbmNlbEFsZ29yaXRobSA9IGNhbmNlbEFsZ29yaXRobTtcbiAgICBjb250cm9sbGVyLl9hdXRvQWxsb2NhdGVDaHVua1NpemUgPSBhdXRvQWxsb2NhdGVDaHVua1NpemU7XG4gICAgY29udHJvbGxlci5fcGVuZGluZ1B1bGxJbnRvcyA9IG5ldyBTaW1wbGVRdWV1ZSgpO1xuICAgIHN0cmVhbS5fcmVhZGFibGVTdHJlYW1Db250cm9sbGVyID0gY29udHJvbGxlcjtcbiAgICBjb25zdCBzdGFydFJlc3VsdCA9IHN0YXJ0QWxnb3JpdGhtKCk7XG4gICAgdXBvblByb21pc2UocHJvbWlzZVJlc29sdmVkV2l0aChzdGFydFJlc3VsdCksICgpID0+IHtcbiAgICAgICAgY29udHJvbGxlci5fc3RhcnRlZCA9IHRydWU7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDYWxsUHVsbElmTmVlZGVkKGNvbnRyb2xsZXIpO1xuICAgIH0sIHIgPT4ge1xuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyRXJyb3IoY29udHJvbGxlciwgcik7XG4gICAgfSk7XG59XG5mdW5jdGlvbiBTZXRVcFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJGcm9tVW5kZXJseWluZ1NvdXJjZShzdHJlYW0sIHVuZGVybHlpbmdCeXRlU291cmNlLCBoaWdoV2F0ZXJNYXJrKSB7XG4gICAgY29uc3QgY29udHJvbGxlciA9IE9iamVjdC5jcmVhdGUoUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlci5wcm90b3R5cGUpO1xuICAgIGxldCBzdGFydEFsZ29yaXRobSA9ICgpID0+IHVuZGVmaW5lZDtcbiAgICBsZXQgcHVsbEFsZ29yaXRobSA9ICgpID0+IHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICBsZXQgY2FuY2VsQWxnb3JpdGhtID0gKCkgPT4gcHJvbWlzZVJlc29sdmVkV2l0aCh1bmRlZmluZWQpO1xuICAgIGlmICh1bmRlcmx5aW5nQnl0ZVNvdXJjZS5zdGFydCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHN0YXJ0QWxnb3JpdGhtID0gKCkgPT4gdW5kZXJseWluZ0J5dGVTb3VyY2Uuc3RhcnQoY29udHJvbGxlcik7XG4gICAgfVxuICAgIGlmICh1bmRlcmx5aW5nQnl0ZVNvdXJjZS5wdWxsICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcHVsbEFsZ29yaXRobSA9ICgpID0+IHVuZGVybHlpbmdCeXRlU291cmNlLnB1bGwoY29udHJvbGxlcik7XG4gICAgfVxuICAgIGlmICh1bmRlcmx5aW5nQnl0ZVNvdXJjZS5jYW5jZWwgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICBjYW5jZWxBbGdvcml0aG0gPSByZWFzb24gPT4gdW5kZXJseWluZ0J5dGVTb3VyY2UuY2FuY2VsKHJlYXNvbik7XG4gICAgfVxuICAgIGNvbnN0IGF1dG9BbGxvY2F0ZUNodW5rU2l6ZSA9IHVuZGVybHlpbmdCeXRlU291cmNlLmF1dG9BbGxvY2F0ZUNodW5rU2l6ZTtcbiAgICBTZXRVcFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIoc3RyZWFtLCBjb250cm9sbGVyLCBzdGFydEFsZ29yaXRobSwgcHVsbEFsZ29yaXRobSwgY2FuY2VsQWxnb3JpdGhtLCBoaWdoV2F0ZXJNYXJrLCBhdXRvQWxsb2NhdGVDaHVua1NpemUpO1xufVxuZnVuY3Rpb24gU2V0VXBSZWFkYWJsZVN0cmVhbUJZT0JSZXF1ZXN0KHJlcXVlc3QsIGNvbnRyb2xsZXIsIHZpZXcpIHtcbiAgICByZXF1ZXN0Ll9hc3NvY2lhdGVkUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlciA9IGNvbnRyb2xsZXI7XG4gICAgcmVxdWVzdC5fdmlldyA9IHZpZXc7XG59XG4vLyBIZWxwZXIgZnVuY3Rpb25zIGZvciB0aGUgUmVhZGFibGVTdHJlYW1CWU9CUmVxdWVzdC5cbmZ1bmN0aW9uIGJ5b2JSZXF1ZXN0QnJhbmRDaGVja0V4Y2VwdGlvbihuYW1lKSB7XG4gICAgcmV0dXJuIG5ldyBUeXBlRXJyb3IoYFJlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QucHJvdG90eXBlLiR7bmFtZX0gY2FuIG9ubHkgYmUgdXNlZCBvbiBhIFJlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3RgKTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyLlxuZnVuY3Rpb24gYnl0ZVN0cmVhbUNvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlci5wcm90b3R5cGUuJHtuYW1lfSBjYW4gb25seSBiZSB1c2VkIG9uIGEgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcmApO1xufVxuXG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgUmVhZGFibGVTdHJlYW0uXG5mdW5jdGlvbiBBY3F1aXJlUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHN0cmVhbSkge1xuICAgIHJldHVybiBuZXcgUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHN0cmVhbSk7XG59XG4vLyBSZWFkYWJsZVN0cmVhbSBBUEkgZXhwb3NlZCBmb3IgY29udHJvbGxlcnMuXG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbUFkZFJlYWRJbnRvUmVxdWVzdChzdHJlYW0sIHJlYWRJbnRvUmVxdWVzdCkge1xuICAgIHN0cmVhbS5fcmVhZGVyLl9yZWFkSW50b1JlcXVlc3RzLnB1c2gocmVhZEludG9SZXF1ZXN0KTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRJbnRvUmVxdWVzdChzdHJlYW0sIGNodW5rLCBkb25lKSB7XG4gICAgY29uc3QgcmVhZGVyID0gc3RyZWFtLl9yZWFkZXI7XG4gICAgY29uc3QgcmVhZEludG9SZXF1ZXN0ID0gcmVhZGVyLl9yZWFkSW50b1JlcXVlc3RzLnNoaWZ0KCk7XG4gICAgaWYgKGRvbmUpIHtcbiAgICAgICAgcmVhZEludG9SZXF1ZXN0Ll9jbG9zZVN0ZXBzKGNodW5rKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIHJlYWRJbnRvUmVxdWVzdC5fY2h1bmtTdGVwcyhjaHVuayk7XG4gICAgfVxufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1HZXROdW1SZWFkSW50b1JlcXVlc3RzKHN0cmVhbSkge1xuICAgIHJldHVybiBzdHJlYW0uX3JlYWRlci5fcmVhZEludG9SZXF1ZXN0cy5sZW5ndGg7XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbUhhc0JZT0JSZWFkZXIoc3RyZWFtKSB7XG4gICAgY29uc3QgcmVhZGVyID0gc3RyZWFtLl9yZWFkZXI7XG4gICAgaWYgKHJlYWRlciA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtQllPQlJlYWRlcihyZWFkZXIpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG4vKipcbiAqIEEgQllPQiByZWFkZXIgdmVuZGVkIGJ5IGEge0BsaW5rIFJlYWRhYmxlU3RyZWFtfS5cbiAqXG4gKiBAcHVibGljXG4gKi9cbmNsYXNzIFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlciB7XG4gICAgY29uc3RydWN0b3Ioc3RyZWFtKSB7XG4gICAgICAgIGFzc2VydFJlcXVpcmVkQXJndW1lbnQoc3RyZWFtLCAxLCAnUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyJyk7XG4gICAgICAgIGFzc2VydFJlYWRhYmxlU3RyZWFtKHN0cmVhbSwgJ0ZpcnN0IHBhcmFtZXRlcicpO1xuICAgICAgICBpZiAoSXNSZWFkYWJsZVN0cmVhbUxvY2tlZChzdHJlYW0pKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGlzIHN0cmVhbSBoYXMgYWxyZWFkeSBiZWVuIGxvY2tlZCBmb3IgZXhjbHVzaXZlIHJlYWRpbmcgYnkgYW5vdGhlciByZWFkZXInKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoIUlzUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcihzdHJlYW0uX3JlYWRhYmxlU3RyZWFtQ29udHJvbGxlcikpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCBjb25zdHJ1Y3QgYSBSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIgZm9yIGEgc3RyZWFtIG5vdCBjb25zdHJ1Y3RlZCB3aXRoIGEgYnl0ZSAnICtcbiAgICAgICAgICAgICAgICAnc291cmNlJyk7XG4gICAgICAgIH1cbiAgICAgICAgUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljSW5pdGlhbGl6ZSh0aGlzLCBzdHJlYW0pO1xuICAgICAgICB0aGlzLl9yZWFkSW50b1JlcXVlc3RzID0gbmV3IFNpbXBsZVF1ZXVlKCk7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFJldHVybnMgYSBwcm9taXNlIHRoYXQgd2lsbCBiZSBmdWxmaWxsZWQgd2hlbiB0aGUgc3RyZWFtIGJlY29tZXMgY2xvc2VkLCBvciByZWplY3RlZCBpZiB0aGUgc3RyZWFtIGV2ZXIgZXJyb3JzIG9yXG4gICAgICogdGhlIHJlYWRlcidzIGxvY2sgaXMgcmVsZWFzZWQgYmVmb3JlIHRoZSBzdHJlYW0gZmluaXNoZXMgY2xvc2luZy5cbiAgICAgKi9cbiAgICBnZXQgY2xvc2VkKCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChieW9iUmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbignY2xvc2VkJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9jbG9zZWRQcm9taXNlO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBJZiB0aGUgcmVhZGVyIGlzIGFjdGl2ZSwgYmVoYXZlcyB0aGUgc2FtZSBhcyB7QGxpbmsgUmVhZGFibGVTdHJlYW0uY2FuY2VsIHwgc3RyZWFtLmNhbmNlbChyZWFzb24pfS5cbiAgICAgKi9cbiAgICBjYW5jZWwocmVhc29uID0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIodGhpcykpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKGJ5b2JSZWFkZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdjYW5jZWwnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX293bmVyUmVhZGFibGVTdHJlYW0gPT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgocmVhZGVyTG9ja0V4Y2VwdGlvbignY2FuY2VsJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNDYW5jZWwodGhpcywgcmVhc29uKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogQXR0ZW1wdHMgdG8gcmVhZHMgYnl0ZXMgaW50byB2aWV3LCBhbmQgcmV0dXJucyBhIHByb21pc2UgcmVzb2x2ZWQgd2l0aCB0aGUgcmVzdWx0LlxuICAgICAqXG4gICAgICogSWYgcmVhZGluZyBhIGNodW5rIGNhdXNlcyB0aGUgcXVldWUgdG8gYmVjb21lIGVtcHR5LCBtb3JlIGRhdGEgd2lsbCBiZSBwdWxsZWQgZnJvbSB0aGUgdW5kZXJseWluZyBzb3VyY2UuXG4gICAgICovXG4gICAgcmVhZCh2aWV3KSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIodGhpcykpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKGJ5b2JSZWFkZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdyZWFkJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmICghQXJyYXlCdWZmZXIuaXNWaWV3KHZpZXcpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChuZXcgVHlwZUVycm9yKCd2aWV3IG11c3QgYmUgYW4gYXJyYXkgYnVmZmVyIHZpZXcnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHZpZXcuYnl0ZUxlbmd0aCA9PT0gMCkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgobmV3IFR5cGVFcnJvcigndmlldyBtdXN0IGhhdmUgbm9uLXplcm8gYnl0ZUxlbmd0aCcpKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAodmlldy5idWZmZXIuYnl0ZUxlbmd0aCA9PT0gMCkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgobmV3IFR5cGVFcnJvcihgdmlldydzIGJ1ZmZlciBtdXN0IGhhdmUgbm9uLXplcm8gYnl0ZUxlbmd0aGApKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAodGhpcy5fb3duZXJSZWFkYWJsZVN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChyZWFkZXJMb2NrRXhjZXB0aW9uKCdyZWFkIGZyb20nKSk7XG4gICAgICAgIH1cbiAgICAgICAgbGV0IHJlc29sdmVQcm9taXNlO1xuICAgICAgICBsZXQgcmVqZWN0UHJvbWlzZTtcbiAgICAgICAgY29uc3QgcHJvbWlzZSA9IG5ld1Byb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgICAgICAgICAgcmVzb2x2ZVByb21pc2UgPSByZXNvbHZlO1xuICAgICAgICAgICAgcmVqZWN0UHJvbWlzZSA9IHJlamVjdDtcbiAgICAgICAgfSk7XG4gICAgICAgIGNvbnN0IHJlYWRJbnRvUmVxdWVzdCA9IHtcbiAgICAgICAgICAgIF9jaHVua1N0ZXBzOiBjaHVuayA9PiByZXNvbHZlUHJvbWlzZSh7IHZhbHVlOiBjaHVuaywgZG9uZTogZmFsc2UgfSksXG4gICAgICAgICAgICBfY2xvc2VTdGVwczogY2h1bmsgPT4gcmVzb2x2ZVByb21pc2UoeyB2YWx1ZTogY2h1bmssIGRvbmU6IHRydWUgfSksXG4gICAgICAgICAgICBfZXJyb3JTdGVwczogZSA9PiByZWplY3RQcm9taXNlKGUpXG4gICAgICAgIH07XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlclJlYWQodGhpcywgdmlldywgcmVhZEludG9SZXF1ZXN0KTtcbiAgICAgICAgcmV0dXJuIHByb21pc2U7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFJlbGVhc2VzIHRoZSByZWFkZXIncyBsb2NrIG9uIHRoZSBjb3JyZXNwb25kaW5nIHN0cmVhbS4gQWZ0ZXIgdGhlIGxvY2sgaXMgcmVsZWFzZWQsIHRoZSByZWFkZXIgaXMgbm8gbG9uZ2VyIGFjdGl2ZS5cbiAgICAgKiBJZiB0aGUgYXNzb2NpYXRlZCBzdHJlYW0gaXMgZXJyb3JlZCB3aGVuIHRoZSBsb2NrIGlzIHJlbGVhc2VkLCB0aGUgcmVhZGVyIHdpbGwgYXBwZWFyIGVycm9yZWQgaW4gdGhlIHNhbWUgd2F5XG4gICAgICogZnJvbSBub3cgb247IG90aGVyd2lzZSwgdGhlIHJlYWRlciB3aWxsIGFwcGVhciBjbG9zZWQuXG4gICAgICpcbiAgICAgKiBBIHJlYWRlcidzIGxvY2sgY2Fubm90IGJlIHJlbGVhc2VkIHdoaWxlIGl0IHN0aWxsIGhhcyBhIHBlbmRpbmcgcmVhZCByZXF1ZXN0LCBpLmUuLCBpZiBhIHByb21pc2UgcmV0dXJuZWQgYnlcbiAgICAgKiB0aGUgcmVhZGVyJ3Mge0BsaW5rIFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlci5yZWFkIHwgcmVhZCgpfSBtZXRob2QgaGFzIG5vdCB5ZXQgYmVlbiBzZXR0bGVkLiBBdHRlbXB0aW5nIHRvXG4gICAgICogZG8gc28gd2lsbCB0aHJvdyBhIGBUeXBlRXJyb3JgIGFuZCBsZWF2ZSB0aGUgcmVhZGVyIGxvY2tlZCB0byB0aGUgc3RyZWFtLlxuICAgICAqL1xuICAgIHJlbGVhc2VMb2NrKCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBieW9iUmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbigncmVsZWFzZUxvY2snKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAodGhpcy5fb3duZXJSZWFkYWJsZVN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX3JlYWRJbnRvUmVxdWVzdHMubGVuZ3RoID4gMCkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignVHJpZWQgdG8gcmVsZWFzZSBhIHJlYWRlciBsb2NrIHdoZW4gdGhhdCByZWFkZXIgaGFzIHBlbmRpbmcgcmVhZCgpIGNhbGxzIHVuLXNldHRsZWQnKTtcbiAgICAgICAgfVxuICAgICAgICBSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNSZWxlYXNlKHRoaXMpO1xuICAgIH1cbn1cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlci5wcm90b3R5cGUsIHtcbiAgICBjYW5jZWw6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIHJlYWQ6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIHJlbGVhc2VMb2NrOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBjbG9zZWQ6IHsgZW51bWVyYWJsZTogdHJ1ZSB9XG59KTtcbmlmICh0eXBlb2YgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcgPT09ICdzeW1ib2wnKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlci5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgcmVhZGVycy5cbmZ1bmN0aW9uIElzUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfcmVhZEludG9SZXF1ZXN0cycpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXJSZWFkKHJlYWRlciwgdmlldywgcmVhZEludG9SZXF1ZXN0KSB7XG4gICAgY29uc3Qgc3RyZWFtID0gcmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtO1xuICAgIHN0cmVhbS5fZGlzdHVyYmVkID0gdHJ1ZTtcbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgIHJlYWRJbnRvUmVxdWVzdC5fZXJyb3JTdGVwcyhzdHJlYW0uX3N0b3JlZEVycm9yKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJQdWxsSW50byhzdHJlYW0uX3JlYWRhYmxlU3RyZWFtQ29udHJvbGxlciwgdmlldywgcmVhZEludG9SZXF1ZXN0KTtcbiAgICB9XG59XG4vLyBIZWxwZXIgZnVuY3Rpb25zIGZvciB0aGUgUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyLlxuZnVuY3Rpb24gYnlvYlJlYWRlckJyYW5kQ2hlY2tFeGNlcHRpb24obmFtZSkge1xuICAgIHJldHVybiBuZXcgVHlwZUVycm9yKGBSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIucHJvdG90eXBlLiR7bmFtZX0gY2FuIG9ubHkgYmUgdXNlZCBvbiBhIFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlcmApO1xufVxuXG5mdW5jdGlvbiBFeHRyYWN0SGlnaFdhdGVyTWFyayhzdHJhdGVneSwgZGVmYXVsdEhXTSkge1xuICAgIGNvbnN0IHsgaGlnaFdhdGVyTWFyayB9ID0gc3RyYXRlZ3k7XG4gICAgaWYgKGhpZ2hXYXRlck1hcmsgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm4gZGVmYXVsdEhXTTtcbiAgICB9XG4gICAgaWYgKE51bWJlcklzTmFOKGhpZ2hXYXRlck1hcmspIHx8IGhpZ2hXYXRlck1hcmsgPCAwKSB7XG4gICAgICAgIHRocm93IG5ldyBSYW5nZUVycm9yKCdJbnZhbGlkIGhpZ2hXYXRlck1hcmsnKTtcbiAgICB9XG4gICAgcmV0dXJuIGhpZ2hXYXRlck1hcms7XG59XG5mdW5jdGlvbiBFeHRyYWN0U2l6ZUFsZ29yaXRobShzdHJhdGVneSkge1xuICAgIGNvbnN0IHsgc2l6ZSB9ID0gc3RyYXRlZ3k7XG4gICAgaWYgKCFzaXplKSB7XG4gICAgICAgIHJldHVybiAoKSA9PiAxO1xuICAgIH1cbiAgICByZXR1cm4gc2l6ZTtcbn1cblxuZnVuY3Rpb24gY29udmVydFF1ZXVpbmdTdHJhdGVneShpbml0LCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RGljdGlvbmFyeShpbml0LCBjb250ZXh0KTtcbiAgICBjb25zdCBoaWdoV2F0ZXJNYXJrID0gaW5pdCA9PT0gbnVsbCB8fCBpbml0ID09PSB2b2lkIDAgPyB2b2lkIDAgOiBpbml0LmhpZ2hXYXRlck1hcms7XG4gICAgY29uc3Qgc2l6ZSA9IGluaXQgPT09IG51bGwgfHwgaW5pdCA9PT0gdm9pZCAwID8gdm9pZCAwIDogaW5pdC5zaXplO1xuICAgIHJldHVybiB7XG4gICAgICAgIGhpZ2hXYXRlck1hcms6IGhpZ2hXYXRlck1hcmsgPT09IHVuZGVmaW5lZCA/IHVuZGVmaW5lZCA6IGNvbnZlcnRVbnJlc3RyaWN0ZWREb3VibGUoaGlnaFdhdGVyTWFyayksXG4gICAgICAgIHNpemU6IHNpemUgPT09IHVuZGVmaW5lZCA/IHVuZGVmaW5lZCA6IGNvbnZlcnRRdWV1aW5nU3RyYXRlZ3lTaXplKHNpemUsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3NpemUnIHRoYXRgKVxuICAgIH07XG59XG5mdW5jdGlvbiBjb252ZXJ0UXVldWluZ1N0cmF0ZWd5U2l6ZShmbiwgY29udGV4dCkge1xuICAgIGFzc2VydEZ1bmN0aW9uKGZuLCBjb250ZXh0KTtcbiAgICByZXR1cm4gY2h1bmsgPT4gY29udmVydFVucmVzdHJpY3RlZERvdWJsZShmbihjaHVuaykpO1xufVxuXG5mdW5jdGlvbiBjb252ZXJ0VW5kZXJseWluZ1Npbmsob3JpZ2luYWwsIGNvbnRleHQpIHtcbiAgICBhc3NlcnREaWN0aW9uYXJ5KG9yaWdpbmFsLCBjb250ZXh0KTtcbiAgICBjb25zdCBhYm9ydCA9IG9yaWdpbmFsID09PSBudWxsIHx8IG9yaWdpbmFsID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcmlnaW5hbC5hYm9ydDtcbiAgICBjb25zdCBjbG9zZSA9IG9yaWdpbmFsID09PSBudWxsIHx8IG9yaWdpbmFsID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcmlnaW5hbC5jbG9zZTtcbiAgICBjb25zdCBzdGFydCA9IG9yaWdpbmFsID09PSBudWxsIHx8IG9yaWdpbmFsID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcmlnaW5hbC5zdGFydDtcbiAgICBjb25zdCB0eXBlID0gb3JpZ2luYWwgPT09IG51bGwgfHwgb3JpZ2luYWwgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9yaWdpbmFsLnR5cGU7XG4gICAgY29uc3Qgd3JpdGUgPSBvcmlnaW5hbCA9PT0gbnVsbCB8fCBvcmlnaW5hbCA9PT0gdm9pZCAwID8gdm9pZCAwIDogb3JpZ2luYWwud3JpdGU7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgYWJvcnQ6IGFib3J0ID09PSB1bmRlZmluZWQgP1xuICAgICAgICAgICAgdW5kZWZpbmVkIDpcbiAgICAgICAgICAgIGNvbnZlcnRVbmRlcmx5aW5nU2lua0Fib3J0Q2FsbGJhY2soYWJvcnQsIG9yaWdpbmFsLCBgJHtjb250ZXh0fSBoYXMgbWVtYmVyICdhYm9ydCcgdGhhdGApLFxuICAgICAgICBjbG9zZTogY2xvc2UgPT09IHVuZGVmaW5lZCA/XG4gICAgICAgICAgICB1bmRlZmluZWQgOlxuICAgICAgICAgICAgY29udmVydFVuZGVybHlpbmdTaW5rQ2xvc2VDYWxsYmFjayhjbG9zZSwgb3JpZ2luYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ2Nsb3NlJyB0aGF0YCksXG4gICAgICAgIHN0YXJ0OiBzdGFydCA9PT0gdW5kZWZpbmVkID9cbiAgICAgICAgICAgIHVuZGVmaW5lZCA6XG4gICAgICAgICAgICBjb252ZXJ0VW5kZXJseWluZ1NpbmtTdGFydENhbGxiYWNrKHN0YXJ0LCBvcmlnaW5hbCwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAnc3RhcnQnIHRoYXRgKSxcbiAgICAgICAgd3JpdGU6IHdyaXRlID09PSB1bmRlZmluZWQgP1xuICAgICAgICAgICAgdW5kZWZpbmVkIDpcbiAgICAgICAgICAgIGNvbnZlcnRVbmRlcmx5aW5nU2lua1dyaXRlQ2FsbGJhY2sod3JpdGUsIG9yaWdpbmFsLCBgJHtjb250ZXh0fSBoYXMgbWVtYmVyICd3cml0ZScgdGhhdGApLFxuICAgICAgICB0eXBlXG4gICAgfTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU2lua0Fib3J0Q2FsbGJhY2soZm4sIG9yaWdpbmFsLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RnVuY3Rpb24oZm4sIGNvbnRleHQpO1xuICAgIHJldHVybiAocmVhc29uKSA9PiBwcm9taXNlQ2FsbChmbiwgb3JpZ2luYWwsIFtyZWFzb25dKTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU2lua0Nsb3NlQ2FsbGJhY2soZm4sIG9yaWdpbmFsLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RnVuY3Rpb24oZm4sIGNvbnRleHQpO1xuICAgIHJldHVybiAoKSA9PiBwcm9taXNlQ2FsbChmbiwgb3JpZ2luYWwsIFtdKTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU2lua1N0YXJ0Q2FsbGJhY2soZm4sIG9yaWdpbmFsLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RnVuY3Rpb24oZm4sIGNvbnRleHQpO1xuICAgIHJldHVybiAoY29udHJvbGxlcikgPT4gcmVmbGVjdENhbGwoZm4sIG9yaWdpbmFsLCBbY29udHJvbGxlcl0pO1xufVxuZnVuY3Rpb24gY29udmVydFVuZGVybHlpbmdTaW5rV3JpdGVDYWxsYmFjayhmbiwgb3JpZ2luYWwsIGNvbnRleHQpIHtcbiAgICBhc3NlcnRGdW5jdGlvbihmbiwgY29udGV4dCk7XG4gICAgcmV0dXJuIChjaHVuaywgY29udHJvbGxlcikgPT4gcHJvbWlzZUNhbGwoZm4sIG9yaWdpbmFsLCBbY2h1bmssIGNvbnRyb2xsZXJdKTtcbn1cblxuZnVuY3Rpb24gYXNzZXJ0V3JpdGFibGVTdHJlYW0oeCwgY29udGV4dCkge1xuICAgIGlmICghSXNXcml0YWJsZVN0cmVhbSh4KSkge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGAke2NvbnRleHR9IGlzIG5vdCBhIFdyaXRhYmxlU3RyZWFtLmApO1xuICAgIH1cbn1cblxuLyoqXG4gKiBBIHdyaXRhYmxlIHN0cmVhbSByZXByZXNlbnRzIGEgZGVzdGluYXRpb24gZm9yIGRhdGEsIGludG8gd2hpY2ggeW91IGNhbiB3cml0ZS5cbiAqXG4gKiBAcHVibGljXG4gKi9cbmNsYXNzIFdyaXRhYmxlU3RyZWFtIHtcbiAgICBjb25zdHJ1Y3RvcihyYXdVbmRlcmx5aW5nU2luayA9IHt9LCByYXdTdHJhdGVneSA9IHt9KSB7XG4gICAgICAgIGlmIChyYXdVbmRlcmx5aW5nU2luayA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByYXdVbmRlcmx5aW5nU2luayA9IG51bGw7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICBhc3NlcnRPYmplY3QocmF3VW5kZXJseWluZ1NpbmssICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCBzdHJhdGVneSA9IGNvbnZlcnRRdWV1aW5nU3RyYXRlZ3kocmF3U3RyYXRlZ3ksICdTZWNvbmQgcGFyYW1ldGVyJyk7XG4gICAgICAgIGNvbnN0IHVuZGVybHlpbmdTaW5rID0gY29udmVydFVuZGVybHlpbmdTaW5rKHJhd1VuZGVybHlpbmdTaW5rLCAnRmlyc3QgcGFyYW1ldGVyJyk7XG4gICAgICAgIEluaXRpYWxpemVXcml0YWJsZVN0cmVhbSh0aGlzKTtcbiAgICAgICAgY29uc3QgdHlwZSA9IHVuZGVybHlpbmdTaW5rLnR5cGU7XG4gICAgICAgIGlmICh0eXBlICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBSYW5nZUVycm9yKCdJbnZhbGlkIHR5cGUgaXMgc3BlY2lmaWVkJyk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3Qgc2l6ZUFsZ29yaXRobSA9IEV4dHJhY3RTaXplQWxnb3JpdGhtKHN0cmF0ZWd5KTtcbiAgICAgICAgY29uc3QgaGlnaFdhdGVyTWFyayA9IEV4dHJhY3RIaWdoV2F0ZXJNYXJrKHN0cmF0ZWd5LCAxKTtcbiAgICAgICAgU2V0VXBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRnJvbVVuZGVybHlpbmdTaW5rKHRoaXMsIHVuZGVybHlpbmdTaW5rLCBoaWdoV2F0ZXJNYXJrLCBzaXplQWxnb3JpdGhtKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB3aGV0aGVyIG9yIG5vdCB0aGUgd3JpdGFibGUgc3RyZWFtIGlzIGxvY2tlZCB0byBhIHdyaXRlci5cbiAgICAgKi9cbiAgICBnZXQgbG9ja2VkKCkge1xuICAgICAgICBpZiAoIUlzV3JpdGFibGVTdHJlYW0odGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IHN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24oJ2xvY2tlZCcpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBJc1dyaXRhYmxlU3RyZWFtTG9ja2VkKHRoaXMpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBBYm9ydHMgdGhlIHN0cmVhbSwgc2lnbmFsaW5nIHRoYXQgdGhlIHByb2R1Y2VyIGNhbiBubyBsb25nZXIgc3VjY2Vzc2Z1bGx5IHdyaXRlIHRvIHRoZSBzdHJlYW0gYW5kIGl0IGlzIHRvIGJlXG4gICAgICogaW1tZWRpYXRlbHkgbW92ZWQgdG8gYW4gZXJyb3JlZCBzdGF0ZSwgd2l0aCBhbnkgcXVldWVkLXVwIHdyaXRlcyBkaXNjYXJkZWQuIFRoaXMgd2lsbCBhbHNvIGV4ZWN1dGUgYW55IGFib3J0XG4gICAgICogbWVjaGFuaXNtIG9mIHRoZSB1bmRlcmx5aW5nIHNpbmsuXG4gICAgICpcbiAgICAgKiBUaGUgcmV0dXJuZWQgcHJvbWlzZSB3aWxsIGZ1bGZpbGwgaWYgdGhlIHN0cmVhbSBzaHV0cyBkb3duIHN1Y2Nlc3NmdWxseSwgb3IgcmVqZWN0IGlmIHRoZSB1bmRlcmx5aW5nIHNpbmsgc2lnbmFsZWRcbiAgICAgKiB0aGF0IHRoZXJlIHdhcyBhbiBlcnJvciBkb2luZyBzby4gQWRkaXRpb25hbGx5LCBpdCB3aWxsIHJlamVjdCB3aXRoIGEgYFR5cGVFcnJvcmAgKHdpdGhvdXQgYXR0ZW1wdGluZyB0byBjYW5jZWxcbiAgICAgKiB0aGUgc3RyZWFtKSBpZiB0aGUgc3RyZWFtIGlzIGN1cnJlbnRseSBsb2NrZWQuXG4gICAgICovXG4gICAgYWJvcnQocmVhc29uID0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmICghSXNXcml0YWJsZVN0cmVhbSh0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbignYWJvcnQnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKElzV3JpdGFibGVTdHJlYW1Mb2NrZWQodGhpcykpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCBhYm9ydCBhIHN0cmVhbSB0aGF0IGFscmVhZHkgaGFzIGEgd3JpdGVyJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBXcml0YWJsZVN0cmVhbUFib3J0KHRoaXMsIHJlYXNvbik7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIENsb3NlcyB0aGUgc3RyZWFtLiBUaGUgdW5kZXJseWluZyBzaW5rIHdpbGwgZmluaXNoIHByb2Nlc3NpbmcgYW55IHByZXZpb3VzbHktd3JpdHRlbiBjaHVua3MsIGJlZm9yZSBpbnZva2luZyBpdHNcbiAgICAgKiBjbG9zZSBiZWhhdmlvci4gRHVyaW5nIHRoaXMgdGltZSBhbnkgZnVydGhlciBhdHRlbXB0cyB0byB3cml0ZSB3aWxsIGZhaWwgKHdpdGhvdXQgZXJyb3JpbmcgdGhlIHN0cmVhbSkuXG4gICAgICpcbiAgICAgKiBUaGUgbWV0aG9kIHJldHVybnMgYSBwcm9taXNlIHRoYXQgd2lsbCBmdWxmaWxsIGlmIGFsbCByZW1haW5pbmcgY2h1bmtzIGFyZSBzdWNjZXNzZnVsbHkgd3JpdHRlbiBhbmQgdGhlIHN0cmVhbVxuICAgICAqIHN1Y2Nlc3NmdWxseSBjbG9zZXMsIG9yIHJlamVjdHMgaWYgYW4gZXJyb3IgaXMgZW5jb3VudGVyZWQgZHVyaW5nIHRoaXMgcHJvY2Vzcy4gQWRkaXRpb25hbGx5LCBpdCB3aWxsIHJlamVjdCB3aXRoXG4gICAgICogYSBgVHlwZUVycm9yYCAod2l0aG91dCBhdHRlbXB0aW5nIHRvIGNhbmNlbCB0aGUgc3RyZWFtKSBpZiB0aGUgc3RyZWFtIGlzIGN1cnJlbnRseSBsb2NrZWQuXG4gICAgICovXG4gICAgY2xvc2UoKSB7XG4gICAgICAgIGlmICghSXNXcml0YWJsZVN0cmVhbSh0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbignY2xvc2UnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKElzV3JpdGFibGVTdHJlYW1Mb2NrZWQodGhpcykpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCBjbG9zZSBhIHN0cmVhbSB0aGF0IGFscmVhZHkgaGFzIGEgd3JpdGVyJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodCh0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgobmV3IFR5cGVFcnJvcignQ2Fubm90IGNsb3NlIGFuIGFscmVhZHktY2xvc2luZyBzdHJlYW0nKSk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIFdyaXRhYmxlU3RyZWFtQ2xvc2UodGhpcyk7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIENyZWF0ZXMgYSB7QGxpbmsgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyIHwgd3JpdGVyfSBhbmQgbG9ja3MgdGhlIHN0cmVhbSB0byB0aGUgbmV3IHdyaXRlci4gV2hpbGUgdGhlIHN0cmVhbVxuICAgICAqIGlzIGxvY2tlZCwgbm8gb3RoZXIgd3JpdGVyIGNhbiBiZSBhY3F1aXJlZCB1bnRpbCB0aGlzIG9uZSBpcyByZWxlYXNlZC5cbiAgICAgKlxuICAgICAqIFRoaXMgZnVuY3Rpb25hbGl0eSBpcyBlc3BlY2lhbGx5IHVzZWZ1bCBmb3IgY3JlYXRpbmcgYWJzdHJhY3Rpb25zIHRoYXQgZGVzaXJlIHRoZSBhYmlsaXR5IHRvIHdyaXRlIHRvIGEgc3RyZWFtXG4gICAgICogd2l0aG91dCBpbnRlcnJ1cHRpb24gb3IgaW50ZXJsZWF2aW5nLiBCeSBnZXR0aW5nIGEgd3JpdGVyIGZvciB0aGUgc3RyZWFtLCB5b3UgY2FuIGVuc3VyZSBub2JvZHkgZWxzZSBjYW4gd3JpdGUgYXRcbiAgICAgKiB0aGUgc2FtZSB0aW1lLCB3aGljaCB3b3VsZCBjYXVzZSB0aGUgcmVzdWx0aW5nIHdyaXR0ZW4gZGF0YSB0byBiZSB1bnByZWRpY3RhYmxlIGFuZCBwcm9iYWJseSB1c2VsZXNzLlxuICAgICAqL1xuICAgIGdldFdyaXRlcigpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uKCdnZXRXcml0ZXInKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gQWNxdWlyZVdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKTtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhXcml0YWJsZVN0cmVhbS5wcm90b3R5cGUsIHtcbiAgICBhYm9ydDogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgY2xvc2U6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGdldFdyaXRlcjogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgbG9ja2VkOiB7IGVudW1lcmFibGU6IHRydWUgfVxufSk7XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnID09PSAnc3ltYm9sJykge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShXcml0YWJsZVN0cmVhbS5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnV3JpdGFibGVTdHJlYW0nLFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICB9KTtcbn1cbi8vIEFic3RyYWN0IG9wZXJhdGlvbnMgZm9yIHRoZSBXcml0YWJsZVN0cmVhbS5cbmZ1bmN0aW9uIEFjcXVpcmVXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXIoc3RyZWFtKSB7XG4gICAgcmV0dXJuIG5ldyBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXIoc3RyZWFtKTtcbn1cbi8vIFRocm93cyBpZiBhbmQgb25seSBpZiBzdGFydEFsZ29yaXRobSB0aHJvd3MuXG5mdW5jdGlvbiBDcmVhdGVXcml0YWJsZVN0cmVhbShzdGFydEFsZ29yaXRobSwgd3JpdGVBbGdvcml0aG0sIGNsb3NlQWxnb3JpdGhtLCBhYm9ydEFsZ29yaXRobSwgaGlnaFdhdGVyTWFyayA9IDEsIHNpemVBbGdvcml0aG0gPSAoKSA9PiAxKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gT2JqZWN0LmNyZWF0ZShXcml0YWJsZVN0cmVhbS5wcm90b3R5cGUpO1xuICAgIEluaXRpYWxpemVXcml0YWJsZVN0cmVhbShzdHJlYW0pO1xuICAgIGNvbnN0IGNvbnRyb2xsZXIgPSBPYmplY3QuY3JlYXRlKFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIucHJvdG90eXBlKTtcbiAgICBTZXRVcFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIoc3RyZWFtLCBjb250cm9sbGVyLCBzdGFydEFsZ29yaXRobSwgd3JpdGVBbGdvcml0aG0sIGNsb3NlQWxnb3JpdGhtLCBhYm9ydEFsZ29yaXRobSwgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSk7XG4gICAgcmV0dXJuIHN0cmVhbTtcbn1cbmZ1bmN0aW9uIEluaXRpYWxpemVXcml0YWJsZVN0cmVhbShzdHJlYW0pIHtcbiAgICBzdHJlYW0uX3N0YXRlID0gJ3dyaXRhYmxlJztcbiAgICAvLyBUaGUgZXJyb3IgdGhhdCB3aWxsIGJlIHJlcG9ydGVkIGJ5IG5ldyBtZXRob2QgY2FsbHMgb25jZSB0aGUgc3RhdGUgYmVjb21lcyBlcnJvcmVkLiBPbmx5IHNldCB3aGVuIFtbc3RhdGVdXSBpc1xuICAgIC8vICdlcnJvcmluZycgb3IgJ2Vycm9yZWQnLiBNYXkgYmUgc2V0IHRvIGFuIHVuZGVmaW5lZCB2YWx1ZS5cbiAgICBzdHJlYW0uX3N0b3JlZEVycm9yID0gdW5kZWZpbmVkO1xuICAgIHN0cmVhbS5fd3JpdGVyID0gdW5kZWZpbmVkO1xuICAgIC8vIEluaXRpYWxpemUgdG8gdW5kZWZpbmVkIGZpcnN0IGJlY2F1c2UgdGhlIGNvbnN0cnVjdG9yIG9mIHRoZSBjb250cm9sbGVyIGNoZWNrcyB0aGlzXG4gICAgLy8gdmFyaWFibGUgdG8gdmFsaWRhdGUgdGhlIGNhbGxlci5cbiAgICBzdHJlYW0uX3dyaXRhYmxlU3RyZWFtQ29udHJvbGxlciA9IHVuZGVmaW5lZDtcbiAgICAvLyBUaGlzIHF1ZXVlIGlzIHBsYWNlZCBoZXJlIGluc3RlYWQgb2YgdGhlIHdyaXRlciBjbGFzcyBpbiBvcmRlciB0byBhbGxvdyBmb3IgcGFzc2luZyBhIHdyaXRlciB0byB0aGUgbmV4dCBkYXRhXG4gICAgLy8gcHJvZHVjZXIgd2l0aG91dCB3YWl0aW5nIGZvciB0aGUgcXVldWVkIHdyaXRlcyB0byBmaW5pc2guXG4gICAgc3RyZWFtLl93cml0ZVJlcXVlc3RzID0gbmV3IFNpbXBsZVF1ZXVlKCk7XG4gICAgLy8gV3JpdGUgcmVxdWVzdHMgYXJlIHJlbW92ZWQgZnJvbSBfd3JpdGVSZXF1ZXN0cyB3aGVuIHdyaXRlKCkgaXMgY2FsbGVkIG9uIHRoZSB1bmRlcmx5aW5nIHNpbmsuIFRoaXMgcHJldmVudHNcbiAgICAvLyB0aGVtIGZyb20gYmVpbmcgZXJyb25lb3VzbHkgcmVqZWN0ZWQgb24gZXJyb3IuIElmIGEgd3JpdGUoKSBjYWxsIGlzIGluLWZsaWdodCwgdGhlIHJlcXVlc3QgaXMgc3RvcmVkIGhlcmUuXG4gICAgc3RyZWFtLl9pbkZsaWdodFdyaXRlUmVxdWVzdCA9IHVuZGVmaW5lZDtcbiAgICAvLyBUaGUgcHJvbWlzZSB0aGF0IHdhcyByZXR1cm5lZCBmcm9tIHdyaXRlci5jbG9zZSgpLiBTdG9yZWQgaGVyZSBiZWNhdXNlIGl0IG1heSBiZSBmdWxmaWxsZWQgYWZ0ZXIgdGhlIHdyaXRlclxuICAgIC8vIGhhcyBiZWVuIGRldGFjaGVkLlxuICAgIHN0cmVhbS5fY2xvc2VSZXF1ZXN0ID0gdW5kZWZpbmVkO1xuICAgIC8vIENsb3NlIHJlcXVlc3QgaXMgcmVtb3ZlZCBmcm9tIF9jbG9zZVJlcXVlc3Qgd2hlbiBjbG9zZSgpIGlzIGNhbGxlZCBvbiB0aGUgdW5kZXJseWluZyBzaW5rLiBUaGlzIHByZXZlbnRzIGl0XG4gICAgLy8gZnJvbSBiZWluZyBlcnJvbmVvdXNseSByZWplY3RlZCBvbiBlcnJvci4gSWYgYSBjbG9zZSgpIGNhbGwgaXMgaW4tZmxpZ2h0LCB0aGUgcmVxdWVzdCBpcyBzdG9yZWQgaGVyZS5cbiAgICBzdHJlYW0uX2luRmxpZ2h0Q2xvc2VSZXF1ZXN0ID0gdW5kZWZpbmVkO1xuICAgIC8vIFRoZSBwcm9taXNlIHRoYXQgd2FzIHJldHVybmVkIGZyb20gd3JpdGVyLmFib3J0KCkuIFRoaXMgbWF5IGFsc28gYmUgZnVsZmlsbGVkIGFmdGVyIHRoZSB3cml0ZXIgaGFzIGRldGFjaGVkLlxuICAgIHN0cmVhbS5fcGVuZGluZ0Fib3J0UmVxdWVzdCA9IHVuZGVmaW5lZDtcbiAgICAvLyBUaGUgYmFja3ByZXNzdXJlIHNpZ25hbCBzZXQgYnkgdGhlIGNvbnRyb2xsZXIuXG4gICAgc3RyZWFtLl9iYWNrcHJlc3N1cmUgPSBmYWxzZTtcbn1cbmZ1bmN0aW9uIElzV3JpdGFibGVTdHJlYW0oeCkge1xuICAgIGlmICghdHlwZUlzT2JqZWN0KHgpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgaWYgKCFPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoeCwgJ193cml0YWJsZVN0cmVhbUNvbnRyb2xsZXInKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gSXNXcml0YWJsZVN0cmVhbUxvY2tlZChzdHJlYW0pIHtcbiAgICBpZiAoc3RyZWFtLl93cml0ZXIgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1BYm9ydChzdHJlYW0sIHJlYXNvbikge1xuICAgIGNvbnN0IHN0YXRlID0gc3RyZWFtLl9zdGF0ZTtcbiAgICBpZiAoc3RhdGUgPT09ICdjbG9zZWQnIHx8IHN0YXRlID09PSAnZXJyb3JlZCcpIHtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICB9XG4gICAgaWYgKHN0cmVhbS5fcGVuZGluZ0Fib3J0UmVxdWVzdCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybiBzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QuX3Byb21pc2U7XG4gICAgfVxuICAgIGxldCB3YXNBbHJlYWR5RXJyb3JpbmcgPSBmYWxzZTtcbiAgICBpZiAoc3RhdGUgPT09ICdlcnJvcmluZycpIHtcbiAgICAgICAgd2FzQWxyZWFkeUVycm9yaW5nID0gdHJ1ZTtcbiAgICAgICAgLy8gcmVhc29uIHdpbGwgbm90IGJlIHVzZWQsIHNvIGRvbid0IGtlZXAgYSByZWZlcmVuY2UgdG8gaXQuXG4gICAgICAgIHJlYXNvbiA9IHVuZGVmaW5lZDtcbiAgICB9XG4gICAgY29uc3QgcHJvbWlzZSA9IG5ld1Byb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgICAgICBzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QgPSB7XG4gICAgICAgICAgICBfcHJvbWlzZTogdW5kZWZpbmVkLFxuICAgICAgICAgICAgX3Jlc29sdmU6IHJlc29sdmUsXG4gICAgICAgICAgICBfcmVqZWN0OiByZWplY3QsXG4gICAgICAgICAgICBfcmVhc29uOiByZWFzb24sXG4gICAgICAgICAgICBfd2FzQWxyZWFkeUVycm9yaW5nOiB3YXNBbHJlYWR5RXJyb3JpbmdcbiAgICAgICAgfTtcbiAgICB9KTtcbiAgICBzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QuX3Byb21pc2UgPSBwcm9taXNlO1xuICAgIGlmICghd2FzQWxyZWFkeUVycm9yaW5nKSB7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtU3RhcnRFcnJvcmluZyhzdHJlYW0sIHJlYXNvbik7XG4gICAgfVxuICAgIHJldHVybiBwcm9taXNlO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1DbG9zZShzdHJlYW0pIHtcbiAgICBjb25zdCBzdGF0ZSA9IHN0cmVhbS5fc3RhdGU7XG4gICAgaWYgKHN0YXRlID09PSAnY2xvc2VkJyB8fCBzdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoYFRoZSBzdHJlYW0gKGluICR7c3RhdGV9IHN0YXRlKSBpcyBub3QgaW4gdGhlIHdyaXRhYmxlIHN0YXRlIGFuZCBjYW5ub3QgYmUgY2xvc2VkYCkpO1xuICAgIH1cbiAgICBjb25zdCBwcm9taXNlID0gbmV3UHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICAgIGNvbnN0IGNsb3NlUmVxdWVzdCA9IHtcbiAgICAgICAgICAgIF9yZXNvbHZlOiByZXNvbHZlLFxuICAgICAgICAgICAgX3JlamVjdDogcmVqZWN0XG4gICAgICAgIH07XG4gICAgICAgIHN0cmVhbS5fY2xvc2VSZXF1ZXN0ID0gY2xvc2VSZXF1ZXN0O1xuICAgIH0pO1xuICAgIGNvbnN0IHdyaXRlciA9IHN0cmVhbS5fd3JpdGVyO1xuICAgIGlmICh3cml0ZXIgIT09IHVuZGVmaW5lZCAmJiBzdHJlYW0uX2JhY2twcmVzc3VyZSAmJiBzdGF0ZSA9PT0gJ3dyaXRhYmxlJykge1xuICAgICAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVzb2x2ZSh3cml0ZXIpO1xuICAgIH1cbiAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xvc2Uoc3RyZWFtLl93cml0YWJsZVN0cmVhbUNvbnRyb2xsZXIpO1xuICAgIHJldHVybiBwcm9taXNlO1xufVxuLy8gV3JpdGFibGVTdHJlYW0gQVBJIGV4cG9zZWQgZm9yIGNvbnRyb2xsZXJzLlxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1BZGRXcml0ZVJlcXVlc3Qoc3RyZWFtKSB7XG4gICAgY29uc3QgcHJvbWlzZSA9IG5ld1Byb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgICAgICBjb25zdCB3cml0ZVJlcXVlc3QgPSB7XG4gICAgICAgICAgICBfcmVzb2x2ZTogcmVzb2x2ZSxcbiAgICAgICAgICAgIF9yZWplY3Q6IHJlamVjdFxuICAgICAgICB9O1xuICAgICAgICBzdHJlYW0uX3dyaXRlUmVxdWVzdHMucHVzaCh3cml0ZVJlcXVlc3QpO1xuICAgIH0pO1xuICAgIHJldHVybiBwcm9taXNlO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWFsV2l0aFJlamVjdGlvbihzdHJlYW0sIGVycm9yKSB7XG4gICAgY29uc3Qgc3RhdGUgPSBzdHJlYW0uX3N0YXRlO1xuICAgIGlmIChzdGF0ZSA9PT0gJ3dyaXRhYmxlJykge1xuICAgICAgICBXcml0YWJsZVN0cmVhbVN0YXJ0RXJyb3Jpbmcoc3RyZWFtLCBlcnJvcik7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgV3JpdGFibGVTdHJlYW1GaW5pc2hFcnJvcmluZyhzdHJlYW0pO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1TdGFydEVycm9yaW5nKHN0cmVhbSwgcmVhc29uKSB7XG4gICAgY29uc3QgY29udHJvbGxlciA9IHN0cmVhbS5fd3JpdGFibGVTdHJlYW1Db250cm9sbGVyO1xuICAgIHN0cmVhbS5fc3RhdGUgPSAnZXJyb3JpbmcnO1xuICAgIHN0cmVhbS5fc3RvcmVkRXJyb3IgPSByZWFzb247XG4gICAgY29uc3Qgd3JpdGVyID0gc3RyZWFtLl93cml0ZXI7XG4gICAgaWYgKHdyaXRlciAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckVuc3VyZVJlYWR5UHJvbWlzZVJlamVjdGVkKHdyaXRlciwgcmVhc29uKTtcbiAgICB9XG4gICAgaWYgKCFXcml0YWJsZVN0cmVhbUhhc09wZXJhdGlvbk1hcmtlZEluRmxpZ2h0KHN0cmVhbSkgJiYgY29udHJvbGxlci5fc3RhcnRlZCkge1xuICAgICAgICBXcml0YWJsZVN0cmVhbUZpbmlzaEVycm9yaW5nKHN0cmVhbSk7XG4gICAgfVxufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1GaW5pc2hFcnJvcmluZyhzdHJlYW0pIHtcbiAgICBzdHJlYW0uX3N0YXRlID0gJ2Vycm9yZWQnO1xuICAgIHN0cmVhbS5fd3JpdGFibGVTdHJlYW1Db250cm9sbGVyW0Vycm9yU3RlcHNdKCk7XG4gICAgY29uc3Qgc3RvcmVkRXJyb3IgPSBzdHJlYW0uX3N0b3JlZEVycm9yO1xuICAgIHN0cmVhbS5fd3JpdGVSZXF1ZXN0cy5mb3JFYWNoKHdyaXRlUmVxdWVzdCA9PiB7XG4gICAgICAgIHdyaXRlUmVxdWVzdC5fcmVqZWN0KHN0b3JlZEVycm9yKTtcbiAgICB9KTtcbiAgICBzdHJlYW0uX3dyaXRlUmVxdWVzdHMgPSBuZXcgU2ltcGxlUXVldWUoKTtcbiAgICBpZiAoc3RyZWFtLl9wZW5kaW5nQWJvcnRSZXF1ZXN0ID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgV3JpdGFibGVTdHJlYW1SZWplY3RDbG9zZUFuZENsb3NlZFByb21pc2VJZk5lZWRlZChzdHJlYW0pO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IGFib3J0UmVxdWVzdCA9IHN0cmVhbS5fcGVuZGluZ0Fib3J0UmVxdWVzdDtcbiAgICBzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QgPSB1bmRlZmluZWQ7XG4gICAgaWYgKGFib3J0UmVxdWVzdC5fd2FzQWxyZWFkeUVycm9yaW5nKSB7XG4gICAgICAgIGFib3J0UmVxdWVzdC5fcmVqZWN0KHN0b3JlZEVycm9yKTtcbiAgICAgICAgV3JpdGFibGVTdHJlYW1SZWplY3RDbG9zZUFuZENsb3NlZFByb21pc2VJZk5lZWRlZChzdHJlYW0pO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IHByb21pc2UgPSBzdHJlYW0uX3dyaXRhYmxlU3RyZWFtQ29udHJvbGxlcltBYm9ydFN0ZXBzXShhYm9ydFJlcXVlc3QuX3JlYXNvbik7XG4gICAgdXBvblByb21pc2UocHJvbWlzZSwgKCkgPT4ge1xuICAgICAgICBhYm9ydFJlcXVlc3QuX3Jlc29sdmUoKTtcbiAgICAgICAgV3JpdGFibGVTdHJlYW1SZWplY3RDbG9zZUFuZENsb3NlZFByb21pc2VJZk5lZWRlZChzdHJlYW0pO1xuICAgIH0sIChyZWFzb24pID0+IHtcbiAgICAgICAgYWJvcnRSZXF1ZXN0Ll9yZWplY3QocmVhc29uKTtcbiAgICAgICAgV3JpdGFibGVTdHJlYW1SZWplY3RDbG9zZUFuZENsb3NlZFByb21pc2VJZk5lZWRlZChzdHJlYW0pO1xuICAgIH0pO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1GaW5pc2hJbkZsaWdodFdyaXRlKHN0cmVhbSkge1xuICAgIHN0cmVhbS5faW5GbGlnaHRXcml0ZVJlcXVlc3QuX3Jlc29sdmUodW5kZWZpbmVkKTtcbiAgICBzdHJlYW0uX2luRmxpZ2h0V3JpdGVSZXF1ZXN0ID0gdW5kZWZpbmVkO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1GaW5pc2hJbkZsaWdodFdyaXRlV2l0aEVycm9yKHN0cmVhbSwgZXJyb3IpIHtcbiAgICBzdHJlYW0uX2luRmxpZ2h0V3JpdGVSZXF1ZXN0Ll9yZWplY3QoZXJyb3IpO1xuICAgIHN0cmVhbS5faW5GbGlnaHRXcml0ZVJlcXVlc3QgPSB1bmRlZmluZWQ7XG4gICAgV3JpdGFibGVTdHJlYW1EZWFsV2l0aFJlamVjdGlvbihzdHJlYW0sIGVycm9yKTtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRmluaXNoSW5GbGlnaHRDbG9zZShzdHJlYW0pIHtcbiAgICBzdHJlYW0uX2luRmxpZ2h0Q2xvc2VSZXF1ZXN0Ll9yZXNvbHZlKHVuZGVmaW5lZCk7XG4gICAgc3RyZWFtLl9pbkZsaWdodENsb3NlUmVxdWVzdCA9IHVuZGVmaW5lZDtcbiAgICBjb25zdCBzdGF0ZSA9IHN0cmVhbS5fc3RhdGU7XG4gICAgaWYgKHN0YXRlID09PSAnZXJyb3JpbmcnKSB7XG4gICAgICAgIC8vIFRoZSBlcnJvciB3YXMgdG9vIGxhdGUgdG8gZG8gYW55dGhpbmcsIHNvIGl0IGlzIGlnbm9yZWQuXG4gICAgICAgIHN0cmVhbS5fc3RvcmVkRXJyb3IgPSB1bmRlZmluZWQ7XG4gICAgICAgIGlmIChzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgc3RyZWFtLl9wZW5kaW5nQWJvcnRSZXF1ZXN0Ll9yZXNvbHZlKCk7XG4gICAgICAgICAgICBzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QgPSB1bmRlZmluZWQ7XG4gICAgICAgIH1cbiAgICB9XG4gICAgc3RyZWFtLl9zdGF0ZSA9ICdjbG9zZWQnO1xuICAgIGNvbnN0IHdyaXRlciA9IHN0cmVhbS5fd3JpdGVyO1xuICAgIGlmICh3cml0ZXIgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZVJlc29sdmUod3JpdGVyKTtcbiAgICB9XG59XG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbUZpbmlzaEluRmxpZ2h0Q2xvc2VXaXRoRXJyb3Ioc3RyZWFtLCBlcnJvcikge1xuICAgIHN0cmVhbS5faW5GbGlnaHRDbG9zZVJlcXVlc3QuX3JlamVjdChlcnJvcik7XG4gICAgc3RyZWFtLl9pbkZsaWdodENsb3NlUmVxdWVzdCA9IHVuZGVmaW5lZDtcbiAgICAvLyBOZXZlciBleGVjdXRlIHNpbmsgYWJvcnQoKSBhZnRlciBzaW5rIGNsb3NlKCkuXG4gICAgaWYgKHN0cmVhbS5fcGVuZGluZ0Fib3J0UmVxdWVzdCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHN0cmVhbS5fcGVuZGluZ0Fib3J0UmVxdWVzdC5fcmVqZWN0KGVycm9yKTtcbiAgICAgICAgc3RyZWFtLl9wZW5kaW5nQWJvcnRSZXF1ZXN0ID0gdW5kZWZpbmVkO1xuICAgIH1cbiAgICBXcml0YWJsZVN0cmVhbURlYWxXaXRoUmVqZWN0aW9uKHN0cmVhbSwgZXJyb3IpO1xufVxuLy8gVE9ETyhyaWNlYSk6IEZpeCBhbHBoYWJldGljYWwgb3JkZXIuXG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodChzdHJlYW0pIHtcbiAgICBpZiAoc3RyZWFtLl9jbG9zZVJlcXVlc3QgPT09IHVuZGVmaW5lZCAmJiBzdHJlYW0uX2luRmxpZ2h0Q2xvc2VSZXF1ZXN0ID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtSGFzT3BlcmF0aW9uTWFya2VkSW5GbGlnaHQoc3RyZWFtKSB7XG4gICAgaWYgKHN0cmVhbS5faW5GbGlnaHRXcml0ZVJlcXVlc3QgPT09IHVuZGVmaW5lZCAmJiBzdHJlYW0uX2luRmxpZ2h0Q2xvc2VSZXF1ZXN0ID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtTWFya0Nsb3NlUmVxdWVzdEluRmxpZ2h0KHN0cmVhbSkge1xuICAgIHN0cmVhbS5faW5GbGlnaHRDbG9zZVJlcXVlc3QgPSBzdHJlYW0uX2Nsb3NlUmVxdWVzdDtcbiAgICBzdHJlYW0uX2Nsb3NlUmVxdWVzdCA9IHVuZGVmaW5lZDtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtTWFya0ZpcnN0V3JpdGVSZXF1ZXN0SW5GbGlnaHQoc3RyZWFtKSB7XG4gICAgc3RyZWFtLl9pbkZsaWdodFdyaXRlUmVxdWVzdCA9IHN0cmVhbS5fd3JpdGVSZXF1ZXN0cy5zaGlmdCgpO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1SZWplY3RDbG9zZUFuZENsb3NlZFByb21pc2VJZk5lZWRlZChzdHJlYW0pIHtcbiAgICBpZiAoc3RyZWFtLl9jbG9zZVJlcXVlc3QgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICBzdHJlYW0uX2Nsb3NlUmVxdWVzdC5fcmVqZWN0KHN0cmVhbS5fc3RvcmVkRXJyb3IpO1xuICAgICAgICBzdHJlYW0uX2Nsb3NlUmVxdWVzdCA9IHVuZGVmaW5lZDtcbiAgICB9XG4gICAgY29uc3Qgd3JpdGVyID0gc3RyZWFtLl93cml0ZXI7XG4gICAgaWYgKHdyaXRlciAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVqZWN0KHdyaXRlciwgc3RyZWFtLl9zdG9yZWRFcnJvcik7XG4gICAgfVxufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1VcGRhdGVCYWNrcHJlc3N1cmUoc3RyZWFtLCBiYWNrcHJlc3N1cmUpIHtcbiAgICBjb25zdCB3cml0ZXIgPSBzdHJlYW0uX3dyaXRlcjtcbiAgICBpZiAod3JpdGVyICE9PSB1bmRlZmluZWQgJiYgYmFja3ByZXNzdXJlICE9PSBzdHJlYW0uX2JhY2twcmVzc3VyZSkge1xuICAgICAgICBpZiAoYmFja3ByZXNzdXJlKSB7XG4gICAgICAgICAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVzZXQod3JpdGVyKTtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VSZXNvbHZlKHdyaXRlcik7XG4gICAgICAgIH1cbiAgICB9XG4gICAgc3RyZWFtLl9iYWNrcHJlc3N1cmUgPSBiYWNrcHJlc3N1cmU7XG59XG4vKipcbiAqIEEgZGVmYXVsdCB3cml0ZXIgdmVuZGVkIGJ5IGEge0BsaW5rIFdyaXRhYmxlU3RyZWFtfS5cbiAqXG4gKiBAcHVibGljXG4gKi9cbmNsYXNzIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlciB7XG4gICAgY29uc3RydWN0b3Ioc3RyZWFtKSB7XG4gICAgICAgIGFzc2VydFJlcXVpcmVkQXJndW1lbnQoc3RyZWFtLCAxLCAnV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyJyk7XG4gICAgICAgIGFzc2VydFdyaXRhYmxlU3RyZWFtKHN0cmVhbSwgJ0ZpcnN0IHBhcmFtZXRlcicpO1xuICAgICAgICBpZiAoSXNXcml0YWJsZVN0cmVhbUxvY2tlZChzdHJlYW0pKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGlzIHN0cmVhbSBoYXMgYWxyZWFkeSBiZWVuIGxvY2tlZCBmb3IgZXhjbHVzaXZlIHdyaXRpbmcgYnkgYW5vdGhlciB3cml0ZXInKTtcbiAgICAgICAgfVxuICAgICAgICB0aGlzLl9vd25lcldyaXRhYmxlU3RyZWFtID0gc3RyZWFtO1xuICAgICAgICBzdHJlYW0uX3dyaXRlciA9IHRoaXM7XG4gICAgICAgIGNvbnN0IHN0YXRlID0gc3RyZWFtLl9zdGF0ZTtcbiAgICAgICAgaWYgKHN0YXRlID09PSAnd3JpdGFibGUnKSB7XG4gICAgICAgICAgICBpZiAoIVdyaXRhYmxlU3RyZWFtQ2xvc2VRdWV1ZWRPckluRmxpZ2h0KHN0cmVhbSkgJiYgc3RyZWFtLl9iYWNrcHJlc3N1cmUpIHtcbiAgICAgICAgICAgICAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlSW5pdGlhbGl6ZSh0aGlzKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VJbml0aWFsaXplQXNSZXNvbHZlZCh0aGlzKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZSh0aGlzKTtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIGlmIChzdGF0ZSA9PT0gJ2Vycm9yaW5nJykge1xuICAgICAgICAgICAgZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemVBc1JlamVjdGVkKHRoaXMsIHN0cmVhbS5fc3RvcmVkRXJyb3IpO1xuICAgICAgICAgICAgZGVmYXVsdFdyaXRlckNsb3NlZFByb21pc2VJbml0aWFsaXplKHRoaXMpO1xuICAgICAgICB9XG4gICAgICAgIGVsc2UgaWYgKHN0YXRlID09PSAnY2xvc2VkJykge1xuICAgICAgICAgICAgZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemVBc1Jlc29sdmVkKHRoaXMpO1xuICAgICAgICAgICAgZGVmYXVsdFdyaXRlckNsb3NlZFByb21pc2VJbml0aWFsaXplQXNSZXNvbHZlZCh0aGlzKTtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgIGNvbnN0IHN0b3JlZEVycm9yID0gc3RyZWFtLl9zdG9yZWRFcnJvcjtcbiAgICAgICAgICAgIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VJbml0aWFsaXplQXNSZWplY3RlZCh0aGlzLCBzdG9yZWRFcnJvcik7XG4gICAgICAgICAgICBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemVBc1JlamVjdGVkKHRoaXMsIHN0b3JlZEVycm9yKTtcbiAgICAgICAgfVxuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIGEgcHJvbWlzZSB0aGF0IHdpbGwgYmUgZnVsZmlsbGVkIHdoZW4gdGhlIHN0cmVhbSBiZWNvbWVzIGNsb3NlZCwgb3IgcmVqZWN0ZWQgaWYgdGhlIHN0cmVhbSBldmVyIGVycm9ycyBvclxuICAgICAqIHRoZSB3cml0ZXLigJlzIGxvY2sgaXMgcmVsZWFzZWQgYmVmb3JlIHRoZSBzdHJlYW0gZmluaXNoZXMgY2xvc2luZy5cbiAgICAgKi9cbiAgICBnZXQgY2xvc2VkKCkge1xuICAgICAgICBpZiAoIUlzV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChkZWZhdWx0V3JpdGVyQnJhbmRDaGVja0V4Y2VwdGlvbignY2xvc2VkJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9jbG9zZWRQcm9taXNlO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIHRoZSBkZXNpcmVkIHNpemUgdG8gZmlsbCB0aGUgc3RyZWFt4oCZcyBpbnRlcm5hbCBxdWV1ZS4gSXQgY2FuIGJlIG5lZ2F0aXZlLCBpZiB0aGUgcXVldWUgaXMgb3Zlci1mdWxsLlxuICAgICAqIEEgcHJvZHVjZXIgY2FuIHVzZSB0aGlzIGluZm9ybWF0aW9uIHRvIGRldGVybWluZSB0aGUgcmlnaHQgYW1vdW50IG9mIGRhdGEgdG8gd3JpdGUuXG4gICAgICpcbiAgICAgKiBJdCB3aWxsIGJlIGBudWxsYCBpZiB0aGUgc3RyZWFtIGNhbm5vdCBiZSBzdWNjZXNzZnVsbHkgd3JpdHRlbiB0byAoZHVlIHRvIGVpdGhlciBiZWluZyBlcnJvcmVkLCBvciBoYXZpbmcgYW4gYWJvcnRcbiAgICAgKiBxdWV1ZWQgdXApLiBJdCB3aWxsIHJldHVybiB6ZXJvIGlmIHRoZSBzdHJlYW0gaXMgY2xvc2VkLiBBbmQgdGhlIGdldHRlciB3aWxsIHRocm93IGFuIGV4Y2VwdGlvbiBpZiBpbnZva2VkIHdoZW5cbiAgICAgKiB0aGUgd3JpdGVy4oCZcyBsb2NrIGlzIHJlbGVhc2VkLlxuICAgICAqL1xuICAgIGdldCBkZXNpcmVkU2l6ZSgpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgZGVmYXVsdFdyaXRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Rlc2lyZWRTaXplJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX293bmVyV3JpdGFibGVTdHJlYW0gPT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgdGhyb3cgZGVmYXVsdFdyaXRlckxvY2tFeGNlcHRpb24oJ2Rlc2lyZWRTaXplJyk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckdldERlc2lyZWRTaXplKHRoaXMpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIGEgcHJvbWlzZSB0aGF0IHdpbGwgYmUgZnVsZmlsbGVkIHdoZW4gdGhlIGRlc2lyZWQgc2l6ZSB0byBmaWxsIHRoZSBzdHJlYW3igJlzIGludGVybmFsIHF1ZXVlIHRyYW5zaXRpb25zXG4gICAgICogZnJvbSBub24tcG9zaXRpdmUgdG8gcG9zaXRpdmUsIHNpZ25hbGluZyB0aGF0IGl0IGlzIG5vIGxvbmdlciBhcHBseWluZyBiYWNrcHJlc3N1cmUuIE9uY2UgdGhlIGRlc2lyZWQgc2l6ZSBkaXBzXG4gICAgICogYmFjayB0byB6ZXJvIG9yIGJlbG93LCB0aGUgZ2V0dGVyIHdpbGwgcmV0dXJuIGEgbmV3IHByb21pc2UgdGhhdCBzdGF5cyBwZW5kaW5nIHVudGlsIHRoZSBuZXh0IHRyYW5zaXRpb24uXG4gICAgICpcbiAgICAgKiBJZiB0aGUgc3RyZWFtIGJlY29tZXMgZXJyb3JlZCBvciBhYm9ydGVkLCBvciB0aGUgd3JpdGVy4oCZcyBsb2NrIGlzIHJlbGVhc2VkLCB0aGUgcmV0dXJuZWQgcHJvbWlzZSB3aWxsIGJlY29tZVxuICAgICAqIHJlamVjdGVkLlxuICAgICAqL1xuICAgIGdldCByZWFkeSgpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoZGVmYXVsdFdyaXRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ3JlYWR5JykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9yZWFkeVByb21pc2U7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIElmIHRoZSByZWFkZXIgaXMgYWN0aXZlLCBiZWhhdmVzIHRoZSBzYW1lIGFzIHtAbGluayBXcml0YWJsZVN0cmVhbS5hYm9ydCB8IHN0cmVhbS5hYm9ydChyZWFzb24pfS5cbiAgICAgKi9cbiAgICBhYm9ydChyZWFzb24gPSB1bmRlZmluZWQpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoZGVmYXVsdFdyaXRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Fib3J0JykpO1xuICAgICAgICB9XG4gICAgICAgIGlmICh0aGlzLl9vd25lcldyaXRhYmxlU3RyZWFtID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKGRlZmF1bHRXcml0ZXJMb2NrRXhjZXB0aW9uKCdhYm9ydCcpKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyQWJvcnQodGhpcywgcmVhc29uKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogSWYgdGhlIHJlYWRlciBpcyBhY3RpdmUsIGJlaGF2ZXMgdGhlIHNhbWUgYXMge0BsaW5rIFdyaXRhYmxlU3RyZWFtLmNsb3NlIHwgc3RyZWFtLmNsb3NlKCl9LlxuICAgICAqL1xuICAgIGNsb3NlKCkge1xuICAgICAgICBpZiAoIUlzV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChkZWZhdWx0V3JpdGVyQnJhbmRDaGVja0V4Y2VwdGlvbignY2xvc2UnKSk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3Qgc3RyZWFtID0gdGhpcy5fb3duZXJXcml0YWJsZVN0cmVhbTtcbiAgICAgICAgaWYgKHN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChkZWZhdWx0V3JpdGVyTG9ja0V4Y2VwdGlvbignY2xvc2UnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKFdyaXRhYmxlU3RyZWFtQ2xvc2VRdWV1ZWRPckluRmxpZ2h0KHN0cmVhbSkpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCBjbG9zZSBhbiBhbHJlYWR5LWNsb3Npbmcgc3RyZWFtJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJDbG9zZSh0aGlzKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmVsZWFzZXMgdGhlIHdyaXRlcuKAmXMgbG9jayBvbiB0aGUgY29ycmVzcG9uZGluZyBzdHJlYW0uIEFmdGVyIHRoZSBsb2NrIGlzIHJlbGVhc2VkLCB0aGUgd3JpdGVyIGlzIG5vIGxvbmdlciBhY3RpdmUuXG4gICAgICogSWYgdGhlIGFzc29jaWF0ZWQgc3RyZWFtIGlzIGVycm9yZWQgd2hlbiB0aGUgbG9jayBpcyByZWxlYXNlZCwgdGhlIHdyaXRlciB3aWxsIGFwcGVhciBlcnJvcmVkIGluIHRoZSBzYW1lIHdheSBmcm9tXG4gICAgICogbm93IG9uOyBvdGhlcndpc2UsIHRoZSB3cml0ZXIgd2lsbCBhcHBlYXIgY2xvc2VkLlxuICAgICAqXG4gICAgICogTm90ZSB0aGF0IHRoZSBsb2NrIGNhbiBzdGlsbCBiZSByZWxlYXNlZCBldmVuIGlmIHNvbWUgb25nb2luZyB3cml0ZXMgaGF2ZSBub3QgeWV0IGZpbmlzaGVkIChpLmUuIGV2ZW4gaWYgdGhlXG4gICAgICogcHJvbWlzZXMgcmV0dXJuZWQgZnJvbSBwcmV2aW91cyBjYWxscyB0byB7QGxpbmsgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyLndyaXRlIHwgd3JpdGUoKX0gaGF2ZSBub3QgeWV0IHNldHRsZWQpLlxuICAgICAqIEl04oCZcyBub3QgbmVjZXNzYXJ5IHRvIGhvbGQgdGhlIGxvY2sgb24gdGhlIHdyaXRlciBmb3IgdGhlIGR1cmF0aW9uIG9mIHRoZSB3cml0ZTsgdGhlIGxvY2sgaW5zdGVhZCBzaW1wbHkgcHJldmVudHNcbiAgICAgKiBvdGhlciBwcm9kdWNlcnMgZnJvbSB3cml0aW5nIGluIGFuIGludGVybGVhdmVkIG1hbm5lci5cbiAgICAgKi9cbiAgICByZWxlYXNlTG9jaygpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgZGVmYXVsdFdyaXRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ3JlbGVhc2VMb2NrJyk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3Qgc3RyZWFtID0gdGhpcy5fb3duZXJXcml0YWJsZVN0cmVhbTtcbiAgICAgICAgaWYgKHN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyUmVsZWFzZSh0aGlzKTtcbiAgICB9XG4gICAgd3JpdGUoY2h1bmsgPSB1bmRlZmluZWQpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoZGVmYXVsdFdyaXRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ3dyaXRlJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmICh0aGlzLl9vd25lcldyaXRhYmxlU3RyZWFtID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKGRlZmF1bHRXcml0ZXJMb2NrRXhjZXB0aW9uKCd3cml0ZSB0bycpKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyV3JpdGUodGhpcywgY2h1bmspO1xuICAgIH1cbn1cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlci5wcm90b3R5cGUsIHtcbiAgICBhYm9ydDogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgY2xvc2U6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIHJlbGVhc2VMb2NrOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICB3cml0ZTogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgY2xvc2VkOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBkZXNpcmVkU2l6ZTogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgcmVhZHk6IHsgZW51bWVyYWJsZTogdHJ1ZSB9XG59KTtcbmlmICh0eXBlb2YgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcgPT09ICdzeW1ib2wnKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlci5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyLlxuZnVuY3Rpb24gSXNXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXIoeCkge1xuICAgIGlmICghdHlwZUlzT2JqZWN0KHgpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgaWYgKCFPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoeCwgJ19vd25lcldyaXRhYmxlU3RyZWFtJykpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbi8vIEEgY2xpZW50IG9mIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlciBtYXkgdXNlIHRoZXNlIGZ1bmN0aW9ucyBkaXJlY3RseSB0byBieXBhc3Mgc3RhdGUgY2hlY2suXG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJBYm9ydCh3cml0ZXIsIHJlYXNvbikge1xuICAgIGNvbnN0IHN0cmVhbSA9IHdyaXRlci5fb3duZXJXcml0YWJsZVN0cmVhbTtcbiAgICByZXR1cm4gV3JpdGFibGVTdHJlYW1BYm9ydChzdHJlYW0sIHJlYXNvbik7XG59XG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJDbG9zZSh3cml0ZXIpIHtcbiAgICBjb25zdCBzdHJlYW0gPSB3cml0ZXIuX293bmVyV3JpdGFibGVTdHJlYW07XG4gICAgcmV0dXJuIFdyaXRhYmxlU3RyZWFtQ2xvc2Uoc3RyZWFtKTtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckNsb3NlV2l0aEVycm9yUHJvcGFnYXRpb24od3JpdGVyKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gd3JpdGVyLl9vd25lcldyaXRhYmxlU3RyZWFtO1xuICAgIGNvbnN0IHN0YXRlID0gc3RyZWFtLl9zdGF0ZTtcbiAgICBpZiAoV3JpdGFibGVTdHJlYW1DbG9zZVF1ZXVlZE9ySW5GbGlnaHQoc3RyZWFtKSB8fCBzdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICB9XG4gICAgaWYgKHN0YXRlID09PSAnZXJyb3JlZCcpIHtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoc3RyZWFtLl9zdG9yZWRFcnJvcik7XG4gICAgfVxuICAgIHJldHVybiBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJDbG9zZSh3cml0ZXIpO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyRW5zdXJlQ2xvc2VkUHJvbWlzZVJlamVjdGVkKHdyaXRlciwgZXJyb3IpIHtcbiAgICBpZiAod3JpdGVyLl9jbG9zZWRQcm9taXNlU3RhdGUgPT09ICdwZW5kaW5nJykge1xuICAgICAgICBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZVJlamVjdCh3cml0ZXIsIGVycm9yKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVzZXRUb1JlamVjdGVkKHdyaXRlciwgZXJyb3IpO1xuICAgIH1cbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckVuc3VyZVJlYWR5UHJvbWlzZVJlamVjdGVkKHdyaXRlciwgZXJyb3IpIHtcbiAgICBpZiAod3JpdGVyLl9yZWFkeVByb21pc2VTdGF0ZSA9PT0gJ3BlbmRpbmcnKSB7XG4gICAgICAgIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VSZWplY3Qod3JpdGVyLCBlcnJvcik7XG4gICAgfVxuICAgIGVsc2Uge1xuICAgICAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVzZXRUb1JlamVjdGVkKHdyaXRlciwgZXJyb3IpO1xuICAgIH1cbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckdldERlc2lyZWRTaXplKHdyaXRlcikge1xuICAgIGNvbnN0IHN0cmVhbSA9IHdyaXRlci5fb3duZXJXcml0YWJsZVN0cmVhbTtcbiAgICBjb25zdCBzdGF0ZSA9IHN0cmVhbS5fc3RhdGU7XG4gICAgaWYgKHN0YXRlID09PSAnZXJyb3JlZCcgfHwgc3RhdGUgPT09ICdlcnJvcmluZycpIHtcbiAgICAgICAgcmV0dXJuIG51bGw7XG4gICAgfVxuICAgIGlmIChzdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgcmV0dXJuIDA7XG4gICAgfVxuICAgIHJldHVybiBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0RGVzaXJlZFNpemUoc3RyZWFtLl93cml0YWJsZVN0cmVhbUNvbnRyb2xsZXIpO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyUmVsZWFzZSh3cml0ZXIpIHtcbiAgICBjb25zdCBzdHJlYW0gPSB3cml0ZXIuX293bmVyV3JpdGFibGVTdHJlYW07XG4gICAgY29uc3QgcmVsZWFzZWRFcnJvciA9IG5ldyBUeXBlRXJyb3IoYFdyaXRlciB3YXMgcmVsZWFzZWQgYW5kIGNhbiBubyBsb25nZXIgYmUgdXNlZCB0byBtb25pdG9yIHRoZSBzdHJlYW0ncyBjbG9zZWRuZXNzYCk7XG4gICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyRW5zdXJlUmVhZHlQcm9taXNlUmVqZWN0ZWQod3JpdGVyLCByZWxlYXNlZEVycm9yKTtcbiAgICAvLyBUaGUgc3RhdGUgdHJhbnNpdGlvbnMgdG8gXCJlcnJvcmVkXCIgYmVmb3JlIHRoZSBzaW5rIGFib3J0KCkgbWV0aG9kIHJ1bnMsIGJ1dCB0aGUgd3JpdGVyLmNsb3NlZCBwcm9taXNlIGlzIG5vdFxuICAgIC8vIHJlamVjdGVkIHVudGlsIGFmdGVyd2FyZHMuIFRoaXMgbWVhbnMgdGhhdCBzaW1wbHkgdGVzdGluZyBzdGF0ZSB3aWxsIG5vdCB3b3JrLlxuICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckVuc3VyZUNsb3NlZFByb21pc2VSZWplY3RlZCh3cml0ZXIsIHJlbGVhc2VkRXJyb3IpO1xuICAgIHN0cmVhbS5fd3JpdGVyID0gdW5kZWZpbmVkO1xuICAgIHdyaXRlci5fb3duZXJXcml0YWJsZVN0cmVhbSA9IHVuZGVmaW5lZDtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcldyaXRlKHdyaXRlciwgY2h1bmspIHtcbiAgICBjb25zdCBzdHJlYW0gPSB3cml0ZXIuX293bmVyV3JpdGFibGVTdHJlYW07XG4gICAgY29uc3QgY29udHJvbGxlciA9IHN0cmVhbS5fd3JpdGFibGVTdHJlYW1Db250cm9sbGVyO1xuICAgIGNvbnN0IGNodW5rU2l6ZSA9IFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXRDaHVua1NpemUoY29udHJvbGxlciwgY2h1bmspO1xuICAgIGlmIChzdHJlYW0gIT09IHdyaXRlci5fb3duZXJXcml0YWJsZVN0cmVhbSkge1xuICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChkZWZhdWx0V3JpdGVyTG9ja0V4Y2VwdGlvbignd3JpdGUgdG8nKSk7XG4gICAgfVxuICAgIGNvbnN0IHN0YXRlID0gc3RyZWFtLl9zdGF0ZTtcbiAgICBpZiAoc3RhdGUgPT09ICdlcnJvcmVkJykge1xuICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChzdHJlYW0uX3N0b3JlZEVycm9yKTtcbiAgICB9XG4gICAgaWYgKFdyaXRhYmxlU3RyZWFtQ2xvc2VRdWV1ZWRPckluRmxpZ2h0KHN0cmVhbSkgfHwgc3RhdGUgPT09ICdjbG9zZWQnKSB7XG4gICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoJ1RoZSBzdHJlYW0gaXMgY2xvc2luZyBvciBjbG9zZWQgYW5kIGNhbm5vdCBiZSB3cml0dGVuIHRvJykpO1xuICAgIH1cbiAgICBpZiAoc3RhdGUgPT09ICdlcnJvcmluZycpIHtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoc3RyZWFtLl9zdG9yZWRFcnJvcik7XG4gICAgfVxuICAgIGNvbnN0IHByb21pc2UgPSBXcml0YWJsZVN0cmVhbUFkZFdyaXRlUmVxdWVzdChzdHJlYW0pO1xuICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJXcml0ZShjb250cm9sbGVyLCBjaHVuaywgY2h1bmtTaXplKTtcbiAgICByZXR1cm4gcHJvbWlzZTtcbn1cbmNvbnN0IGNsb3NlU2VudGluZWwgPSB7fTtcbi8qKlxuICogQWxsb3dzIGNvbnRyb2wgb2YgYSB7QGxpbmsgV3JpdGFibGVTdHJlYW0gfCB3cml0YWJsZSBzdHJlYW19J3Mgc3RhdGUgYW5kIGludGVybmFsIHF1ZXVlLlxuICpcbiAqIEBwdWJsaWNcbiAqL1xuY2xhc3MgV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlciB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0lsbGVnYWwgY29uc3RydWN0b3InKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogQ2xvc2VzIHRoZSBjb250cm9sbGVkIHdyaXRhYmxlIHN0cmVhbSwgbWFraW5nIGFsbCBmdXR1cmUgaW50ZXJhY3Rpb25zIHdpdGggaXQgZmFpbCB3aXRoIHRoZSBnaXZlbiBlcnJvciBgZWAuXG4gICAgICpcbiAgICAgKiBUaGlzIG1ldGhvZCBpcyByYXJlbHkgdXNlZCwgc2luY2UgdXN1YWxseSBpdCBzdWZmaWNlcyB0byByZXR1cm4gYSByZWplY3RlZCBwcm9taXNlIGZyb20gb25lIG9mIHRoZSB1bmRlcmx5aW5nXG4gICAgICogc2luaydzIG1ldGhvZHMuIEhvd2V2ZXIsIGl0IGNhbiBiZSB1c2VmdWwgZm9yIHN1ZGRlbmx5IHNodXR0aW5nIGRvd24gYSBzdHJlYW0gaW4gcmVzcG9uc2UgdG8gYW4gZXZlbnQgb3V0c2lkZSB0aGVcbiAgICAgKiBub3JtYWwgbGlmZWN5Y2xlIG9mIGludGVyYWN0aW9ucyB3aXRoIHRoZSB1bmRlcmx5aW5nIHNpbmsuXG4gICAgICovXG4gICAgZXJyb3IoZSA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlci5wcm90b3R5cGUuZXJyb3IgY2FuIG9ubHkgYmUgdXNlZCBvbiBhIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXInKTtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCBzdGF0ZSA9IHRoaXMuX2NvbnRyb2xsZWRXcml0YWJsZVN0cmVhbS5fc3RhdGU7XG4gICAgICAgIGlmIChzdGF0ZSAhPT0gJ3dyaXRhYmxlJykge1xuICAgICAgICAgICAgLy8gVGhlIHN0cmVhbSBpcyBjbG9zZWQsIGVycm9yZWQgb3Igd2lsbCBiZSBzb29uLiBUaGUgc2luayBjYW4ndCBkbyBhbnl0aGluZyB1c2VmdWwgaWYgaXQgZ2V0cyBhbiBlcnJvciBoZXJlLCBzb1xuICAgICAgICAgICAgLy8ganVzdCB0cmVhdCBpdCBhcyBhIG5vLW9wLlxuICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcih0aGlzLCBlKTtcbiAgICB9XG4gICAgLyoqIEBpbnRlcm5hbCAqL1xuICAgIFtBYm9ydFN0ZXBzXShyZWFzb24pIHtcbiAgICAgICAgY29uc3QgcmVzdWx0ID0gdGhpcy5fYWJvcnRBbGdvcml0aG0ocmVhc29uKTtcbiAgICAgICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyh0aGlzKTtcbiAgICAgICAgcmV0dXJuIHJlc3VsdDtcbiAgICB9XG4gICAgLyoqIEBpbnRlcm5hbCAqL1xuICAgIFtFcnJvclN0ZXBzXSgpIHtcbiAgICAgICAgUmVzZXRRdWV1ZSh0aGlzKTtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyLnByb3RvdHlwZSwge1xuICAgIGVycm9yOiB7IGVudW1lcmFibGU6IHRydWUgfVxufSk7XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnID09PSAnc3ltYm9sJykge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyLnByb3RvdHlwZSwgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcsIHtcbiAgICAgICAgdmFsdWU6ICdXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGltcGxlbWVudGluZyBpbnRlcmZhY2UgcmVxdWlyZWQgYnkgdGhlIFdyaXRhYmxlU3RyZWFtLlxuZnVuY3Rpb24gSXNXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfY29udHJvbGxlZFdyaXRhYmxlU3RyZWFtJykpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbmZ1bmN0aW9uIFNldFVwV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcihzdHJlYW0sIGNvbnRyb2xsZXIsIHN0YXJ0QWxnb3JpdGhtLCB3cml0ZUFsZ29yaXRobSwgY2xvc2VBbGdvcml0aG0sIGFib3J0QWxnb3JpdGhtLCBoaWdoV2F0ZXJNYXJrLCBzaXplQWxnb3JpdGhtKSB7XG4gICAgY29udHJvbGxlci5fY29udHJvbGxlZFdyaXRhYmxlU3RyZWFtID0gc3RyZWFtO1xuICAgIHN0cmVhbS5fd3JpdGFibGVTdHJlYW1Db250cm9sbGVyID0gY29udHJvbGxlcjtcbiAgICAvLyBOZWVkIHRvIHNldCB0aGUgc2xvdHMgc28gdGhhdCB0aGUgYXNzZXJ0IGRvZXNuJ3QgZmlyZS4gSW4gdGhlIHNwZWMgdGhlIHNsb3RzIGFscmVhZHkgZXhpc3QgaW1wbGljaXRseS5cbiAgICBjb250cm9sbGVyLl9xdWV1ZSA9IHVuZGVmaW5lZDtcbiAgICBjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZSA9IHVuZGVmaW5lZDtcbiAgICBSZXNldFF1ZXVlKGNvbnRyb2xsZXIpO1xuICAgIGNvbnRyb2xsZXIuX3N0YXJ0ZWQgPSBmYWxzZTtcbiAgICBjb250cm9sbGVyLl9zdHJhdGVneVNpemVBbGdvcml0aG0gPSBzaXplQWxnb3JpdGhtO1xuICAgIGNvbnRyb2xsZXIuX3N0cmF0ZWd5SFdNID0gaGlnaFdhdGVyTWFyaztcbiAgICBjb250cm9sbGVyLl93cml0ZUFsZ29yaXRobSA9IHdyaXRlQWxnb3JpdGhtO1xuICAgIGNvbnRyb2xsZXIuX2Nsb3NlQWxnb3JpdGhtID0gY2xvc2VBbGdvcml0aG07XG4gICAgY29udHJvbGxlci5fYWJvcnRBbGdvcml0aG0gPSBhYm9ydEFsZ29yaXRobTtcbiAgICBjb25zdCBiYWNrcHJlc3N1cmUgPSBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0QmFja3ByZXNzdXJlKGNvbnRyb2xsZXIpO1xuICAgIFdyaXRhYmxlU3RyZWFtVXBkYXRlQmFja3ByZXNzdXJlKHN0cmVhbSwgYmFja3ByZXNzdXJlKTtcbiAgICBjb25zdCBzdGFydFJlc3VsdCA9IHN0YXJ0QWxnb3JpdGhtKCk7XG4gICAgY29uc3Qgc3RhcnRQcm9taXNlID0gcHJvbWlzZVJlc29sdmVkV2l0aChzdGFydFJlc3VsdCk7XG4gICAgdXBvblByb21pc2Uoc3RhcnRQcm9taXNlLCAoKSA9PiB7XG4gICAgICAgIGNvbnRyb2xsZXIuX3N0YXJ0ZWQgPSB0cnVlO1xuICAgICAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQWR2YW5jZVF1ZXVlSWZOZWVkZWQoY29udHJvbGxlcik7XG4gICAgfSwgciA9PiB7XG4gICAgICAgIGNvbnRyb2xsZXIuX3N0YXJ0ZWQgPSB0cnVlO1xuICAgICAgICBXcml0YWJsZVN0cmVhbURlYWxXaXRoUmVqZWN0aW9uKHN0cmVhbSwgcik7XG4gICAgfSk7XG59XG5mdW5jdGlvbiBTZXRVcFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJGcm9tVW5kZXJseWluZ1Npbmsoc3RyZWFtLCB1bmRlcmx5aW5nU2luaywgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSkge1xuICAgIGNvbnN0IGNvbnRyb2xsZXIgPSBPYmplY3QuY3JlYXRlKFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIucHJvdG90eXBlKTtcbiAgICBsZXQgc3RhcnRBbGdvcml0aG0gPSAoKSA9PiB1bmRlZmluZWQ7XG4gICAgbGV0IHdyaXRlQWxnb3JpdGhtID0gKCkgPT4gcHJvbWlzZVJlc29sdmVkV2l0aCh1bmRlZmluZWQpO1xuICAgIGxldCBjbG9zZUFsZ29yaXRobSA9ICgpID0+IHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICBsZXQgYWJvcnRBbGdvcml0aG0gPSAoKSA9PiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgaWYgKHVuZGVybHlpbmdTaW5rLnN0YXJ0ICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgc3RhcnRBbGdvcml0aG0gPSAoKSA9PiB1bmRlcmx5aW5nU2luay5zdGFydChjb250cm9sbGVyKTtcbiAgICB9XG4gICAgaWYgKHVuZGVybHlpbmdTaW5rLndyaXRlICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgd3JpdGVBbGdvcml0aG0gPSBjaHVuayA9PiB1bmRlcmx5aW5nU2luay53cml0ZShjaHVuaywgY29udHJvbGxlcik7XG4gICAgfVxuICAgIGlmICh1bmRlcmx5aW5nU2luay5jbG9zZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGNsb3NlQWxnb3JpdGhtID0gKCkgPT4gdW5kZXJseWluZ1NpbmsuY2xvc2UoKTtcbiAgICB9XG4gICAgaWYgKHVuZGVybHlpbmdTaW5rLmFib3J0ICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgYWJvcnRBbGdvcml0aG0gPSByZWFzb24gPT4gdW5kZXJseWluZ1NpbmsuYWJvcnQocmVhc29uKTtcbiAgICB9XG4gICAgU2V0VXBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHN0cmVhbSwgY29udHJvbGxlciwgc3RhcnRBbGdvcml0aG0sIHdyaXRlQWxnb3JpdGhtLCBjbG9zZUFsZ29yaXRobSwgYWJvcnRBbGdvcml0aG0sIGhpZ2hXYXRlck1hcmssIHNpemVBbGdvcml0aG0pO1xufVxuLy8gQ2xlYXJBbGdvcml0aG1zIG1heSBiZSBjYWxsZWQgdHdpY2UuIEVycm9yaW5nIHRoZSBzYW1lIHN0cmVhbSBpbiBtdWx0aXBsZSB3YXlzIHdpbGwgb2Z0ZW4gcmVzdWx0IGluIHJlZHVuZGFudCBjYWxscy5cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoY29udHJvbGxlcikge1xuICAgIGNvbnRyb2xsZXIuX3dyaXRlQWxnb3JpdGhtID0gdW5kZWZpbmVkO1xuICAgIGNvbnRyb2xsZXIuX2Nsb3NlQWxnb3JpdGhtID0gdW5kZWZpbmVkO1xuICAgIGNvbnRyb2xsZXIuX2Fib3J0QWxnb3JpdGhtID0gdW5kZWZpbmVkO1xuICAgIGNvbnRyb2xsZXIuX3N0cmF0ZWd5U2l6ZUFsZ29yaXRobSA9IHVuZGVmaW5lZDtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbG9zZShjb250cm9sbGVyKSB7XG4gICAgRW5xdWV1ZVZhbHVlV2l0aFNpemUoY29udHJvbGxlciwgY2xvc2VTZW50aW5lbCwgMCk7XG4gICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckFkdmFuY2VRdWV1ZUlmTmVlZGVkKGNvbnRyb2xsZXIpO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckdldENodW5rU2l6ZShjb250cm9sbGVyLCBjaHVuaykge1xuICAgIHRyeSB7XG4gICAgICAgIHJldHVybiBjb250cm9sbGVyLl9zdHJhdGVneVNpemVBbGdvcml0aG0oY2h1bmspO1xuICAgIH1cbiAgICBjYXRjaCAoY2h1bmtTaXplRSkge1xuICAgICAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3JJZk5lZWRlZChjb250cm9sbGVyLCBjaHVua1NpemVFKTtcbiAgICAgICAgcmV0dXJuIDE7XG4gICAgfVxufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckdldERlc2lyZWRTaXplKGNvbnRyb2xsZXIpIHtcbiAgICByZXR1cm4gY29udHJvbGxlci5fc3RyYXRlZ3lIV00gLSBjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZTtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJXcml0ZShjb250cm9sbGVyLCBjaHVuaywgY2h1bmtTaXplKSB7XG4gICAgdHJ5IHtcbiAgICAgICAgRW5xdWV1ZVZhbHVlV2l0aFNpemUoY29udHJvbGxlciwgY2h1bmssIGNodW5rU2l6ZSk7XG4gICAgfVxuICAgIGNhdGNoIChlbnF1ZXVlRSkge1xuICAgICAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3JJZk5lZWRlZChjb250cm9sbGVyLCBlbnF1ZXVlRSk7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFdyaXRhYmxlU3RyZWFtO1xuICAgIGlmICghV3JpdGFibGVTdHJlYW1DbG9zZVF1ZXVlZE9ySW5GbGlnaHQoc3RyZWFtKSAmJiBzdHJlYW0uX3N0YXRlID09PSAnd3JpdGFibGUnKSB7XG4gICAgICAgIGNvbnN0IGJhY2twcmVzc3VyZSA9IFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXRCYWNrcHJlc3N1cmUoY29udHJvbGxlcik7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtVXBkYXRlQmFja3ByZXNzdXJlKHN0cmVhbSwgYmFja3ByZXNzdXJlKTtcbiAgICB9XG4gICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckFkdmFuY2VRdWV1ZUlmTmVlZGVkKGNvbnRyb2xsZXIpO1xufVxuLy8gQWJzdHJhY3Qgb3BlcmF0aW9ucyBmb3IgdGhlIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIuXG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQWR2YW5jZVF1ZXVlSWZOZWVkZWQoY29udHJvbGxlcikge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRXcml0YWJsZVN0cmVhbTtcbiAgICBpZiAoIWNvbnRyb2xsZXIuX3N0YXJ0ZWQpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAoc3RyZWFtLl9pbkZsaWdodFdyaXRlUmVxdWVzdCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY29uc3Qgc3RhdGUgPSBzdHJlYW0uX3N0YXRlO1xuICAgIGlmIChzdGF0ZSA9PT0gJ2Vycm9yaW5nJykge1xuICAgICAgICBXcml0YWJsZVN0cmVhbUZpbmlzaEVycm9yaW5nKHN0cmVhbSk7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKGNvbnRyb2xsZXIuX3F1ZXVlLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IHZhbHVlID0gUGVla1F1ZXVlVmFsdWUoY29udHJvbGxlcik7XG4gICAgaWYgKHZhbHVlID09PSBjbG9zZVNlbnRpbmVsKSB7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJQcm9jZXNzQ2xvc2UoY29udHJvbGxlcik7XG4gICAgfVxuICAgIGVsc2Uge1xuICAgICAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyUHJvY2Vzc1dyaXRlKGNvbnRyb2xsZXIsIHZhbHVlKTtcbiAgICB9XG59XG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3JJZk5lZWRlZChjb250cm9sbGVyLCBlcnJvcikge1xuICAgIGlmIChjb250cm9sbGVyLl9jb250cm9sbGVkV3JpdGFibGVTdHJlYW0uX3N0YXRlID09PSAnd3JpdGFibGUnKSB7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlcnJvcik7XG4gICAgfVxufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlclByb2Nlc3NDbG9zZShjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFdyaXRhYmxlU3RyZWFtO1xuICAgIFdyaXRhYmxlU3RyZWFtTWFya0Nsb3NlUmVxdWVzdEluRmxpZ2h0KHN0cmVhbSk7XG4gICAgRGVxdWV1ZVZhbHVlKGNvbnRyb2xsZXIpO1xuICAgIGNvbnN0IHNpbmtDbG9zZVByb21pc2UgPSBjb250cm9sbGVyLl9jbG9zZUFsZ29yaXRobSgpO1xuICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoY29udHJvbGxlcik7XG4gICAgdXBvblByb21pc2Uoc2lua0Nsb3NlUHJvbWlzZSwgKCkgPT4ge1xuICAgICAgICBXcml0YWJsZVN0cmVhbUZpbmlzaEluRmxpZ2h0Q2xvc2Uoc3RyZWFtKTtcbiAgICB9LCByZWFzb24gPT4ge1xuICAgICAgICBXcml0YWJsZVN0cmVhbUZpbmlzaEluRmxpZ2h0Q2xvc2VXaXRoRXJyb3Ioc3RyZWFtLCByZWFzb24pO1xuICAgIH0pO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlclByb2Nlc3NXcml0ZShjb250cm9sbGVyLCBjaHVuaykge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRXcml0YWJsZVN0cmVhbTtcbiAgICBXcml0YWJsZVN0cmVhbU1hcmtGaXJzdFdyaXRlUmVxdWVzdEluRmxpZ2h0KHN0cmVhbSk7XG4gICAgY29uc3Qgc2lua1dyaXRlUHJvbWlzZSA9IGNvbnRyb2xsZXIuX3dyaXRlQWxnb3JpdGhtKGNodW5rKTtcbiAgICB1cG9uUHJvbWlzZShzaW5rV3JpdGVQcm9taXNlLCAoKSA9PiB7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRmluaXNoSW5GbGlnaHRXcml0ZShzdHJlYW0pO1xuICAgICAgICBjb25zdCBzdGF0ZSA9IHN0cmVhbS5fc3RhdGU7XG4gICAgICAgIERlcXVldWVWYWx1ZShjb250cm9sbGVyKTtcbiAgICAgICAgaWYgKCFXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodChzdHJlYW0pICYmIHN0YXRlID09PSAnd3JpdGFibGUnKSB7XG4gICAgICAgICAgICBjb25zdCBiYWNrcHJlc3N1cmUgPSBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0QmFja3ByZXNzdXJlKGNvbnRyb2xsZXIpO1xuICAgICAgICAgICAgV3JpdGFibGVTdHJlYW1VcGRhdGVCYWNrcHJlc3N1cmUoc3RyZWFtLCBiYWNrcHJlc3N1cmUpO1xuICAgICAgICB9XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJBZHZhbmNlUXVldWVJZk5lZWRlZChjb250cm9sbGVyKTtcbiAgICB9LCByZWFzb24gPT4ge1xuICAgICAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ3dyaXRhYmxlJykge1xuICAgICAgICAgICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyhjb250cm9sbGVyKTtcbiAgICAgICAgfVxuICAgICAgICBXcml0YWJsZVN0cmVhbUZpbmlzaEluRmxpZ2h0V3JpdGVXaXRoRXJyb3Ioc3RyZWFtLCByZWFzb24pO1xuICAgIH0pO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckdldEJhY2twcmVzc3VyZShjb250cm9sbGVyKSB7XG4gICAgY29uc3QgZGVzaXJlZFNpemUgPSBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0RGVzaXJlZFNpemUoY29udHJvbGxlcik7XG4gICAgcmV0dXJuIGRlc2lyZWRTaXplIDw9IDA7XG59XG4vLyBBIGNsaWVudCBvZiBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyIG1heSB1c2UgdGhlc2UgZnVuY3Rpb25zIGRpcmVjdGx5IHRvIGJ5cGFzcyBzdGF0ZSBjaGVjay5cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlcnJvcikge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRXcml0YWJsZVN0cmVhbTtcbiAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKGNvbnRyb2xsZXIpO1xuICAgIFdyaXRhYmxlU3RyZWFtU3RhcnRFcnJvcmluZyhzdHJlYW0sIGVycm9yKTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBXcml0YWJsZVN0cmVhbS5cbmZ1bmN0aW9uIHN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24obmFtZSkge1xuICAgIHJldHVybiBuZXcgVHlwZUVycm9yKGBXcml0YWJsZVN0cmVhbS5wcm90b3R5cGUuJHtuYW1lfSBjYW4gb25seSBiZSB1c2VkIG9uIGEgV3JpdGFibGVTdHJlYW1gKTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXIuXG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyQnJhbmRDaGVja0V4Y2VwdGlvbihuYW1lKSB7XG4gICAgcmV0dXJuIG5ldyBUeXBlRXJyb3IoYFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlci5wcm90b3R5cGUuJHtuYW1lfSBjYW4gb25seSBiZSB1c2VkIG9uIGEgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyYCk7XG59XG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyTG9ja0V4Y2VwdGlvbihuYW1lKSB7XG4gICAgcmV0dXJuIG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCAnICsgbmFtZSArICcgYSBzdHJlYW0gdXNpbmcgYSByZWxlYXNlZCB3cml0ZXInKTtcbn1cbmZ1bmN0aW9uIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZSh3cml0ZXIpIHtcbiAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2UgPSBuZXdQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgd3JpdGVyLl9jbG9zZWRQcm9taXNlX3Jlc29sdmUgPSByZXNvbHZlO1xuICAgICAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2VfcmVqZWN0ID0gcmVqZWN0O1xuICAgICAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2VTdGF0ZSA9ICdwZW5kaW5nJztcbiAgICB9KTtcbn1cbmZ1bmN0aW9uIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZUFzUmVqZWN0ZWQod3JpdGVyLCByZWFzb24pIHtcbiAgICBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemUod3JpdGVyKTtcbiAgICBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZVJlamVjdCh3cml0ZXIsIHJlYXNvbik7XG59XG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemVBc1Jlc29sdmVkKHdyaXRlcikge1xuICAgIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZSh3cml0ZXIpO1xuICAgIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVzb2x2ZSh3cml0ZXIpO1xufVxuZnVuY3Rpb24gZGVmYXVsdFdyaXRlckNsb3NlZFByb21pc2VSZWplY3Qod3JpdGVyLCByZWFzb24pIHtcbiAgICBpZiAod3JpdGVyLl9jbG9zZWRQcm9taXNlX3JlamVjdCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgc2V0UHJvbWlzZUlzSGFuZGxlZFRvVHJ1ZSh3cml0ZXIuX2Nsb3NlZFByb21pc2UpO1xuICAgIHdyaXRlci5fY2xvc2VkUHJvbWlzZV9yZWplY3QocmVhc29uKTtcbiAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2VfcmVzb2x2ZSA9IHVuZGVmaW5lZDtcbiAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2VfcmVqZWN0ID0gdW5kZWZpbmVkO1xuICAgIHdyaXRlci5fY2xvc2VkUHJvbWlzZVN0YXRlID0gJ3JlamVjdGVkJztcbn1cbmZ1bmN0aW9uIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVzZXRUb1JlamVjdGVkKHdyaXRlciwgcmVhc29uKSB7XG4gICAgZGVmYXVsdFdyaXRlckNsb3NlZFByb21pc2VJbml0aWFsaXplQXNSZWplY3RlZCh3cml0ZXIsIHJlYXNvbik7XG59XG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZVJlc29sdmUod3JpdGVyKSB7XG4gICAgaWYgKHdyaXRlci5fY2xvc2VkUHJvbWlzZV9yZXNvbHZlID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2VfcmVzb2x2ZSh1bmRlZmluZWQpO1xuICAgIHdyaXRlci5fY2xvc2VkUHJvbWlzZV9yZXNvbHZlID0gdW5kZWZpbmVkO1xuICAgIHdyaXRlci5fY2xvc2VkUHJvbWlzZV9yZWplY3QgPSB1bmRlZmluZWQ7XG4gICAgd3JpdGVyLl9jbG9zZWRQcm9taXNlU3RhdGUgPSAncmVzb2x2ZWQnO1xufVxuZnVuY3Rpb24gZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemUod3JpdGVyKSB7XG4gICAgd3JpdGVyLl9yZWFkeVByb21pc2UgPSBuZXdQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgd3JpdGVyLl9yZWFkeVByb21pc2VfcmVzb2x2ZSA9IHJlc29sdmU7XG4gICAgICAgIHdyaXRlci5fcmVhZHlQcm9taXNlX3JlamVjdCA9IHJlamVjdDtcbiAgICB9KTtcbiAgICB3cml0ZXIuX3JlYWR5UHJvbWlzZVN0YXRlID0gJ3BlbmRpbmcnO1xufVxuZnVuY3Rpb24gZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemVBc1JlamVjdGVkKHdyaXRlciwgcmVhc29uKSB7XG4gICAgZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemUod3JpdGVyKTtcbiAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVqZWN0KHdyaXRlciwgcmVhc29uKTtcbn1cbmZ1bmN0aW9uIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VJbml0aWFsaXplQXNSZXNvbHZlZCh3cml0ZXIpIHtcbiAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlSW5pdGlhbGl6ZSh3cml0ZXIpO1xuICAgIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VSZXNvbHZlKHdyaXRlcik7XG59XG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVqZWN0KHdyaXRlciwgcmVhc29uKSB7XG4gICAgaWYgKHdyaXRlci5fcmVhZHlQcm9taXNlX3JlamVjdCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgc2V0UHJvbWlzZUlzSGFuZGxlZFRvVHJ1ZSh3cml0ZXIuX3JlYWR5UHJvbWlzZSk7XG4gICAgd3JpdGVyLl9yZWFkeVByb21pc2VfcmVqZWN0KHJlYXNvbik7XG4gICAgd3JpdGVyLl9yZWFkeVByb21pc2VfcmVzb2x2ZSA9IHVuZGVmaW5lZDtcbiAgICB3cml0ZXIuX3JlYWR5UHJvbWlzZV9yZWplY3QgPSB1bmRlZmluZWQ7XG4gICAgd3JpdGVyLl9yZWFkeVByb21pc2VTdGF0ZSA9ICdyZWplY3RlZCc7XG59XG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVzZXQod3JpdGVyKSB7XG4gICAgZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemUod3JpdGVyKTtcbn1cbmZ1bmN0aW9uIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VSZXNldFRvUmVqZWN0ZWQod3JpdGVyLCByZWFzb24pIHtcbiAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlSW5pdGlhbGl6ZUFzUmVqZWN0ZWQod3JpdGVyLCByZWFzb24pO1xufVxuZnVuY3Rpb24gZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZVJlc29sdmUod3JpdGVyKSB7XG4gICAgaWYgKHdyaXRlci5fcmVhZHlQcm9taXNlX3Jlc29sdmUgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIHdyaXRlci5fcmVhZHlQcm9taXNlX3Jlc29sdmUodW5kZWZpbmVkKTtcbiAgICB3cml0ZXIuX3JlYWR5UHJvbWlzZV9yZXNvbHZlID0gdW5kZWZpbmVkO1xuICAgIHdyaXRlci5fcmVhZHlQcm9taXNlX3JlamVjdCA9IHVuZGVmaW5lZDtcbiAgICB3cml0ZXIuX3JlYWR5UHJvbWlzZVN0YXRlID0gJ2Z1bGZpbGxlZCc7XG59XG5cbmZ1bmN0aW9uIGlzQWJvcnRTaWduYWwodmFsdWUpIHtcbiAgICBpZiAodHlwZW9mIHZhbHVlICE9PSAnb2JqZWN0JyB8fCB2YWx1ZSA9PT0gbnVsbCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICAgIHJldHVybiB0eXBlb2YgdmFsdWUuYWJvcnRlZCA9PT0gJ2Jvb2xlYW4nO1xuICAgIH1cbiAgICBjYXRjaCAoX2EpIHtcbiAgICAgICAgLy8gQWJvcnRTaWduYWwucHJvdG90eXBlLmFib3J0ZWQgdGhyb3dzIGlmIGl0cyBicmFuZCBjaGVjayBmYWlsc1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxufVxuXG4vLy8gPHJlZmVyZW5jZSBsaWI9XCJkb21cIiAvPlxuY29uc3QgTmF0aXZlRE9NRXhjZXB0aW9uID0gdHlwZW9mIERPTUV4Y2VwdGlvbiAhPT0gJ3VuZGVmaW5lZCcgPyBET01FeGNlcHRpb24gOiB1bmRlZmluZWQ7XG5cbi8vLyA8cmVmZXJlbmNlIHR5cGVzPVwibm9kZVwiIC8+XG5mdW5jdGlvbiBpc0RPTUV4Y2VwdGlvbkNvbnN0cnVjdG9yKGN0b3IpIHtcbiAgICBpZiAoISh0eXBlb2YgY3RvciA9PT0gJ2Z1bmN0aW9uJyB8fCB0eXBlb2YgY3RvciA9PT0gJ29iamVjdCcpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgbmV3IGN0b3IoKTtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuICAgIGNhdGNoIChfYSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxufVxuZnVuY3Rpb24gY3JlYXRlRE9NRXhjZXB0aW9uUG9seWZpbGwoKSB7XG4gICAgY29uc3QgY3RvciA9IGZ1bmN0aW9uIERPTUV4Y2VwdGlvbihtZXNzYWdlLCBuYW1lKSB7XG4gICAgICAgIHRoaXMubWVzc2FnZSA9IG1lc3NhZ2UgfHwgJyc7XG4gICAgICAgIHRoaXMubmFtZSA9IG5hbWUgfHwgJ0Vycm9yJztcbiAgICAgICAgaWYgKEVycm9yLmNhcHR1cmVTdGFja1RyYWNlKSB7XG4gICAgICAgICAgICBFcnJvci5jYXB0dXJlU3RhY2tUcmFjZSh0aGlzLCB0aGlzLmNvbnN0cnVjdG9yKTtcbiAgICAgICAgfVxuICAgIH07XG4gICAgY3Rvci5wcm90b3R5cGUgPSBPYmplY3QuY3JlYXRlKEVycm9yLnByb3RvdHlwZSk7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KGN0b3IucHJvdG90eXBlLCAnY29uc3RydWN0b3InLCB7IHZhbHVlOiBjdG9yLCB3cml0YWJsZTogdHJ1ZSwgY29uZmlndXJhYmxlOiB0cnVlIH0pO1xuICAgIHJldHVybiBjdG9yO1xufVxuY29uc3QgRE9NRXhjZXB0aW9uJDEgPSBpc0RPTUV4Y2VwdGlvbkNvbnN0cnVjdG9yKE5hdGl2ZURPTUV4Y2VwdGlvbikgPyBOYXRpdmVET01FeGNlcHRpb24gOiBjcmVhdGVET01FeGNlcHRpb25Qb2x5ZmlsbCgpO1xuXG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbVBpcGVUbyhzb3VyY2UsIGRlc3QsIHByZXZlbnRDbG9zZSwgcHJldmVudEFib3J0LCBwcmV2ZW50Q2FuY2VsLCBzaWduYWwpIHtcbiAgICBjb25zdCByZWFkZXIgPSBBY3F1aXJlUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyKHNvdXJjZSk7XG4gICAgY29uc3Qgd3JpdGVyID0gQWNxdWlyZVdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcihkZXN0KTtcbiAgICBzb3VyY2UuX2Rpc3R1cmJlZCA9IHRydWU7XG4gICAgbGV0IHNodXR0aW5nRG93biA9IGZhbHNlO1xuICAgIC8vIFRoaXMgaXMgdXNlZCB0byBrZWVwIHRyYWNrIG9mIHRoZSBzcGVjJ3MgcmVxdWlyZW1lbnQgdGhhdCB3ZSB3YWl0IGZvciBvbmdvaW5nIHdyaXRlcyBkdXJpbmcgc2h1dGRvd24uXG4gICAgbGV0IGN1cnJlbnRXcml0ZSA9IHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICByZXR1cm4gbmV3UHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICAgIGxldCBhYm9ydEFsZ29yaXRobTtcbiAgICAgICAgaWYgKHNpZ25hbCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICBhYm9ydEFsZ29yaXRobSA9ICgpID0+IHtcbiAgICAgICAgICAgICAgICBjb25zdCBlcnJvciA9IG5ldyBET01FeGNlcHRpb24kMSgnQWJvcnRlZCcsICdBYm9ydEVycm9yJyk7XG4gICAgICAgICAgICAgICAgY29uc3QgYWN0aW9ucyA9IFtdO1xuICAgICAgICAgICAgICAgIGlmICghcHJldmVudEFib3J0KSB7XG4gICAgICAgICAgICAgICAgICAgIGFjdGlvbnMucHVzaCgoKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBpZiAoZGVzdC5fc3RhdGUgPT09ICd3cml0YWJsZScpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm4gV3JpdGFibGVTdHJlYW1BYm9ydChkZXN0LCBlcnJvcik7XG4gICAgICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlc29sdmVkV2l0aCh1bmRlZmluZWQpO1xuICAgICAgICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgaWYgKCFwcmV2ZW50Q2FuY2VsKSB7XG4gICAgICAgICAgICAgICAgICAgIGFjdGlvbnMucHVzaCgoKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBpZiAoc291cmNlLl9zdGF0ZSA9PT0gJ3JlYWRhYmxlJykge1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybiBSZWFkYWJsZVN0cmVhbUNhbmNlbChzb3VyY2UsIGVycm9yKTtcbiAgICAgICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICBzaHV0ZG93bldpdGhBY3Rpb24oKCkgPT4gUHJvbWlzZS5hbGwoYWN0aW9ucy5tYXAoYWN0aW9uID0+IGFjdGlvbigpKSksIHRydWUsIGVycm9yKTtcbiAgICAgICAgICAgIH07XG4gICAgICAgICAgICBpZiAoc2lnbmFsLmFib3J0ZWQpIHtcbiAgICAgICAgICAgICAgICBhYm9ydEFsZ29yaXRobSgpO1xuICAgICAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHNpZ25hbC5hZGRFdmVudExpc3RlbmVyKCdhYm9ydCcsIGFib3J0QWxnb3JpdGhtKTtcbiAgICAgICAgfVxuICAgICAgICAvLyBVc2luZyByZWFkZXIgYW5kIHdyaXRlciwgcmVhZCBhbGwgY2h1bmtzIGZyb20gdGhpcyBhbmQgd3JpdGUgdGhlbSB0byBkZXN0XG4gICAgICAgIC8vIC0gQmFja3ByZXNzdXJlIG11c3QgYmUgZW5mb3JjZWRcbiAgICAgICAgLy8gLSBTaHV0ZG93biBtdXN0IHN0b3AgYWxsIGFjdGl2aXR5XG4gICAgICAgIGZ1bmN0aW9uIHBpcGVMb29wKCkge1xuICAgICAgICAgICAgcmV0dXJuIG5ld1Byb21pc2UoKHJlc29sdmVMb29wLCByZWplY3RMb29wKSA9PiB7XG4gICAgICAgICAgICAgICAgZnVuY3Rpb24gbmV4dChkb25lKSB7XG4gICAgICAgICAgICAgICAgICAgIGlmIChkb25lKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICByZXNvbHZlTG9vcCgpO1xuICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgICAgICAgICAgLy8gVXNlIGBQZXJmb3JtUHJvbWlzZVRoZW5gIGluc3RlYWQgb2YgYHVwb25Qcm9taXNlYCB0byBhdm9pZFxuICAgICAgICAgICAgICAgICAgICAgICAgLy8gYWRkaW5nIHVubmVjZXNzYXJ5IGAuY2F0Y2gocmV0aHJvd0Fzc2VydGlvbkVycm9yUmVqZWN0aW9uKWAgaGFuZGxlcnNcbiAgICAgICAgICAgICAgICAgICAgICAgIFBlcmZvcm1Qcm9taXNlVGhlbihwaXBlU3RlcCgpLCBuZXh0LCByZWplY3RMb29wKTtcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICBuZXh0KGZhbHNlKTtcbiAgICAgICAgICAgIH0pO1xuICAgICAgICB9XG4gICAgICAgIGZ1bmN0aW9uIHBpcGVTdGVwKCkge1xuICAgICAgICAgICAgaWYgKHNodXR0aW5nRG93bikge1xuICAgICAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVzb2x2ZWRXaXRoKHRydWUpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmV0dXJuIFBlcmZvcm1Qcm9taXNlVGhlbih3cml0ZXIuX3JlYWR5UHJvbWlzZSwgKCkgPT4ge1xuICAgICAgICAgICAgICAgIHJldHVybiBuZXdQcm9taXNlKChyZXNvbHZlUmVhZCwgcmVqZWN0UmVhZCkgPT4ge1xuICAgICAgICAgICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXJSZWFkKHJlYWRlciwge1xuICAgICAgICAgICAgICAgICAgICAgICAgX2NodW5rU3RlcHM6IGNodW5rID0+IHtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBjdXJyZW50V3JpdGUgPSBQZXJmb3JtUHJvbWlzZVRoZW4oV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyV3JpdGUod3JpdGVyLCBjaHVuayksIHVuZGVmaW5lZCwgbm9vcCk7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVzb2x2ZVJlYWQoZmFsc2UpO1xuICAgICAgICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgICAgICAgIF9jbG9zZVN0ZXBzOiAoKSA9PiByZXNvbHZlUmVhZCh0cnVlKSxcbiAgICAgICAgICAgICAgICAgICAgICAgIF9lcnJvclN0ZXBzOiByZWplY3RSZWFkXG4gICAgICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgfSk7XG4gICAgICAgIH1cbiAgICAgICAgLy8gRXJyb3JzIG11c3QgYmUgcHJvcGFnYXRlZCBmb3J3YXJkXG4gICAgICAgIGlzT3JCZWNvbWVzRXJyb3JlZChzb3VyY2UsIHJlYWRlci5fY2xvc2VkUHJvbWlzZSwgc3RvcmVkRXJyb3IgPT4ge1xuICAgICAgICAgICAgaWYgKCFwcmV2ZW50QWJvcnQpIHtcbiAgICAgICAgICAgICAgICBzaHV0ZG93bldpdGhBY3Rpb24oKCkgPT4gV3JpdGFibGVTdHJlYW1BYm9ydChkZXN0LCBzdG9yZWRFcnJvciksIHRydWUsIHN0b3JlZEVycm9yKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIHNodXRkb3duKHRydWUsIHN0b3JlZEVycm9yKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfSk7XG4gICAgICAgIC8vIEVycm9ycyBtdXN0IGJlIHByb3BhZ2F0ZWQgYmFja3dhcmRcbiAgICAgICAgaXNPckJlY29tZXNFcnJvcmVkKGRlc3QsIHdyaXRlci5fY2xvc2VkUHJvbWlzZSwgc3RvcmVkRXJyb3IgPT4ge1xuICAgICAgICAgICAgaWYgKCFwcmV2ZW50Q2FuY2VsKSB7XG4gICAgICAgICAgICAgICAgc2h1dGRvd25XaXRoQWN0aW9uKCgpID0+IFJlYWRhYmxlU3RyZWFtQ2FuY2VsKHNvdXJjZSwgc3RvcmVkRXJyb3IpLCB0cnVlLCBzdG9yZWRFcnJvcik7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgICAgICBzaHV0ZG93bih0cnVlLCBzdG9yZWRFcnJvcik7XG4gICAgICAgICAgICB9XG4gICAgICAgIH0pO1xuICAgICAgICAvLyBDbG9zaW5nIG11c3QgYmUgcHJvcGFnYXRlZCBmb3J3YXJkXG4gICAgICAgIGlzT3JCZWNvbWVzQ2xvc2VkKHNvdXJjZSwgcmVhZGVyLl9jbG9zZWRQcm9taXNlLCAoKSA9PiB7XG4gICAgICAgICAgICBpZiAoIXByZXZlbnRDbG9zZSkge1xuICAgICAgICAgICAgICAgIHNodXRkb3duV2l0aEFjdGlvbigoKSA9PiBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJDbG9zZVdpdGhFcnJvclByb3BhZ2F0aW9uKHdyaXRlcikpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICAgICAgc2h1dGRvd24oKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfSk7XG4gICAgICAgIC8vIENsb3NpbmcgbXVzdCBiZSBwcm9wYWdhdGVkIGJhY2t3YXJkXG4gICAgICAgIGlmIChXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodChkZXN0KSB8fCBkZXN0Ll9zdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgICAgIGNvbnN0IGRlc3RDbG9zZWQgPSBuZXcgVHlwZUVycm9yKCd0aGUgZGVzdGluYXRpb24gd3JpdGFibGUgc3RyZWFtIGNsb3NlZCBiZWZvcmUgYWxsIGRhdGEgY291bGQgYmUgcGlwZWQgdG8gaXQnKTtcbiAgICAgICAgICAgIGlmICghcHJldmVudENhbmNlbCkge1xuICAgICAgICAgICAgICAgIHNodXRkb3duV2l0aEFjdGlvbigoKSA9PiBSZWFkYWJsZVN0cmVhbUNhbmNlbChzb3VyY2UsIGRlc3RDbG9zZWQpLCB0cnVlLCBkZXN0Q2xvc2VkKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIHNodXRkb3duKHRydWUsIGRlc3RDbG9zZWQpO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIHNldFByb21pc2VJc0hhbmRsZWRUb1RydWUocGlwZUxvb3AoKSk7XG4gICAgICAgIGZ1bmN0aW9uIHdhaXRGb3JXcml0ZXNUb0ZpbmlzaCgpIHtcbiAgICAgICAgICAgIC8vIEFub3RoZXIgd3JpdGUgbWF5IGhhdmUgc3RhcnRlZCB3aGlsZSB3ZSB3ZXJlIHdhaXRpbmcgb24gdGhpcyBjdXJyZW50V3JpdGUsIHNvIHdlIGhhdmUgdG8gYmUgc3VyZSB0byB3YWl0XG4gICAgICAgICAgICAvLyBmb3IgdGhhdCB0b28uXG4gICAgICAgICAgICBjb25zdCBvbGRDdXJyZW50V3JpdGUgPSBjdXJyZW50V3JpdGU7XG4gICAgICAgICAgICByZXR1cm4gUGVyZm9ybVByb21pc2VUaGVuKGN1cnJlbnRXcml0ZSwgKCkgPT4gb2xkQ3VycmVudFdyaXRlICE9PSBjdXJyZW50V3JpdGUgPyB3YWl0Rm9yV3JpdGVzVG9GaW5pc2goKSA6IHVuZGVmaW5lZCk7XG4gICAgICAgIH1cbiAgICAgICAgZnVuY3Rpb24gaXNPckJlY29tZXNFcnJvcmVkKHN0cmVhbSwgcHJvbWlzZSwgYWN0aW9uKSB7XG4gICAgICAgICAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgICAgICAgICAgYWN0aW9uKHN0cmVhbS5fc3RvcmVkRXJyb3IpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICAgICAgdXBvblJlamVjdGlvbihwcm9taXNlLCBhY3Rpb24pO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIGZ1bmN0aW9uIGlzT3JCZWNvbWVzQ2xvc2VkKHN0cmVhbSwgcHJvbWlzZSwgYWN0aW9uKSB7XG4gICAgICAgICAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgICAgICAgICBhY3Rpb24oKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIHVwb25GdWxmaWxsbWVudChwcm9taXNlLCBhY3Rpb24pO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIGZ1bmN0aW9uIHNodXRkb3duV2l0aEFjdGlvbihhY3Rpb24sIG9yaWdpbmFsSXNFcnJvciwgb3JpZ2luYWxFcnJvcikge1xuICAgICAgICAgICAgaWYgKHNodXR0aW5nRG93bikge1xuICAgICAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHNodXR0aW5nRG93biA9IHRydWU7XG4gICAgICAgICAgICBpZiAoZGVzdC5fc3RhdGUgPT09ICd3cml0YWJsZScgJiYgIVdyaXRhYmxlU3RyZWFtQ2xvc2VRdWV1ZWRPckluRmxpZ2h0KGRlc3QpKSB7XG4gICAgICAgICAgICAgICAgdXBvbkZ1bGZpbGxtZW50KHdhaXRGb3JXcml0ZXNUb0ZpbmlzaCgpLCBkb1RoZVJlc3QpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICAgICAgZG9UaGVSZXN0KCk7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBmdW5jdGlvbiBkb1RoZVJlc3QoKSB7XG4gICAgICAgICAgICAgICAgdXBvblByb21pc2UoYWN0aW9uKCksICgpID0+IGZpbmFsaXplKG9yaWdpbmFsSXNFcnJvciwgb3JpZ2luYWxFcnJvciksIG5ld0Vycm9yID0+IGZpbmFsaXplKHRydWUsIG5ld0Vycm9yKSk7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgZnVuY3Rpb24gc2h1dGRvd24oaXNFcnJvciwgZXJyb3IpIHtcbiAgICAgICAgICAgIGlmIChzaHV0dGluZ0Rvd24pIHtcbiAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBzaHV0dGluZ0Rvd24gPSB0cnVlO1xuICAgICAgICAgICAgaWYgKGRlc3QuX3N0YXRlID09PSAnd3JpdGFibGUnICYmICFXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodChkZXN0KSkge1xuICAgICAgICAgICAgICAgIHVwb25GdWxmaWxsbWVudCh3YWl0Rm9yV3JpdGVzVG9GaW5pc2goKSwgKCkgPT4gZmluYWxpemUoaXNFcnJvciwgZXJyb3IpKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIGZpbmFsaXplKGlzRXJyb3IsIGVycm9yKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgICBmdW5jdGlvbiBmaW5hbGl6ZShpc0Vycm9yLCBlcnJvcikge1xuICAgICAgICAgICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyUmVsZWFzZSh3cml0ZXIpO1xuICAgICAgICAgICAgUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljUmVsZWFzZShyZWFkZXIpO1xuICAgICAgICAgICAgaWYgKHNpZ25hbCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICAgICAgc2lnbmFsLnJlbW92ZUV2ZW50TGlzdGVuZXIoJ2Fib3J0JywgYWJvcnRBbGdvcml0aG0pO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgaWYgKGlzRXJyb3IpIHtcbiAgICAgICAgICAgICAgICByZWplY3QoZXJyb3IpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICAgICAgcmVzb2x2ZSh1bmRlZmluZWQpO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgfSk7XG59XG5cbi8qKlxuICogQWxsb3dzIGNvbnRyb2wgb2YgYSB7QGxpbmsgUmVhZGFibGVTdHJlYW0gfCByZWFkYWJsZSBzdHJlYW19J3Mgc3RhdGUgYW5kIGludGVybmFsIHF1ZXVlLlxuICpcbiAqIEBwdWJsaWNcbiAqL1xuY2xhc3MgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlciB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0lsbGVnYWwgY29uc3RydWN0b3InKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB0aGUgZGVzaXJlZCBzaXplIHRvIGZpbGwgdGhlIGNvbnRyb2xsZWQgc3RyZWFtJ3MgaW50ZXJuYWwgcXVldWUuIEl0IGNhbiBiZSBuZWdhdGl2ZSwgaWYgdGhlIHF1ZXVlIGlzXG4gICAgICogb3Zlci1mdWxsLiBBbiB1bmRlcmx5aW5nIHNvdXJjZSBvdWdodCB0byB1c2UgdGhpcyBpbmZvcm1hdGlvbiB0byBkZXRlcm1pbmUgd2hlbiBhbmQgaG93IHRvIGFwcGx5IGJhY2twcmVzc3VyZS5cbiAgICAgKi9cbiAgICBnZXQgZGVzaXJlZFNpemUoKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBkZWZhdWx0Q29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Rlc2lyZWRTaXplJyk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXREZXNpcmVkU2l6ZSh0aGlzKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogQ2xvc2VzIHRoZSBjb250cm9sbGVkIHJlYWRhYmxlIHN0cmVhbS4gQ29uc3VtZXJzIHdpbGwgc3RpbGwgYmUgYWJsZSB0byByZWFkIGFueSBwcmV2aW91c2x5LWVucXVldWVkIGNodW5rcyBmcm9tXG4gICAgICogdGhlIHN0cmVhbSwgYnV0IG9uY2UgdGhvc2UgYXJlIHJlYWQsIHRoZSBzdHJlYW0gd2lsbCBiZWNvbWUgY2xvc2VkLlxuICAgICAqL1xuICAgIGNsb3NlKCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgZGVmYXVsdENvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdjbG9zZScpO1xuICAgICAgICB9XG4gICAgICAgIGlmICghUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNhbkNsb3NlT3JFbnF1ZXVlKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGUgc3RyZWFtIGlzIG5vdCBpbiBhIHN0YXRlIHRoYXQgcGVybWl0cyBjbG9zZScpO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbG9zZSh0aGlzKTtcbiAgICB9XG4gICAgZW5xdWV1ZShjaHVuayA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgZGVmYXVsdENvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdlbnF1ZXVlJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKCFSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2FuQ2xvc2VPckVucXVldWUodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1RoZSBzdHJlYW0gaXMgbm90IGluIGEgc3RhdGUgdGhhdCBwZXJtaXRzIGVucXVldWUnKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVucXVldWUodGhpcywgY2h1bmspO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBFcnJvcnMgdGhlIGNvbnRyb2xsZWQgcmVhZGFibGUgc3RyZWFtLCBtYWtpbmcgYWxsIGZ1dHVyZSBpbnRlcmFjdGlvbnMgd2l0aCBpdCBmYWlsIHdpdGggdGhlIGdpdmVuIGVycm9yIGBlYC5cbiAgICAgKi9cbiAgICBlcnJvcihlID0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBkZWZhdWx0Q29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Vycm9yJyk7XG4gICAgICAgIH1cbiAgICAgICAgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVycm9yKHRoaXMsIGUpO1xuICAgIH1cbiAgICAvKiogQGludGVybmFsICovXG4gICAgW0NhbmNlbFN0ZXBzXShyZWFzb24pIHtcbiAgICAgICAgUmVzZXRRdWV1ZSh0aGlzKTtcbiAgICAgICAgY29uc3QgcmVzdWx0ID0gdGhpcy5fY2FuY2VsQWxnb3JpdGhtKHJlYXNvbik7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXModGhpcyk7XG4gICAgICAgIHJldHVybiByZXN1bHQ7XG4gICAgfVxuICAgIC8qKiBAaW50ZXJuYWwgKi9cbiAgICBbUHVsbFN0ZXBzXShyZWFkUmVxdWVzdCkge1xuICAgICAgICBjb25zdCBzdHJlYW0gPSB0aGlzLl9jb250cm9sbGVkUmVhZGFibGVTdHJlYW07XG4gICAgICAgIGlmICh0aGlzLl9xdWV1ZS5sZW5ndGggPiAwKSB7XG4gICAgICAgICAgICBjb25zdCBjaHVuayA9IERlcXVldWVWYWx1ZSh0aGlzKTtcbiAgICAgICAgICAgIGlmICh0aGlzLl9jbG9zZVJlcXVlc3RlZCAmJiB0aGlzLl9xdWV1ZS5sZW5ndGggPT09IDApIHtcbiAgICAgICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKHRoaXMpO1xuICAgICAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtQ2xvc2Uoc3RyZWFtKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYWxsUHVsbElmTmVlZGVkKHRoaXMpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmVhZFJlcXVlc3QuX2NodW5rU3RlcHMoY2h1bmspO1xuICAgICAgICB9XG4gICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgUmVhZGFibGVTdHJlYW1BZGRSZWFkUmVxdWVzdChzdHJlYW0sIHJlYWRSZXF1ZXN0KTtcbiAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYWxsUHVsbElmTmVlZGVkKHRoaXMpO1xuICAgICAgICB9XG4gICAgfVxufVxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlci5wcm90b3R5cGUsIHtcbiAgICBjbG9zZTogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgZW5xdWV1ZTogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgZXJyb3I6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGRlc2lyZWRTaXplOiB7IGVudW1lcmFibGU6IHRydWUgfVxufSk7XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnID09PSAnc3ltYm9sJykge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyLnByb3RvdHlwZSwgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcsIHtcbiAgICAgICAgdmFsdWU6ICdSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlci5cbmZ1bmN0aW9uIElzUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcih4KSB7XG4gICAgaWYgKCF0eXBlSXNPYmplY3QoeCkpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICBpZiAoIU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbCh4LCAnX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbScpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZChjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc2hvdWxkUHVsbCA9IFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJTaG91bGRDYWxsUHVsbChjb250cm9sbGVyKTtcbiAgICBpZiAoIXNob3VsZFB1bGwpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAoY29udHJvbGxlci5fcHVsbGluZykge1xuICAgICAgICBjb250cm9sbGVyLl9wdWxsQWdhaW4gPSB0cnVlO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnRyb2xsZXIuX3B1bGxpbmcgPSB0cnVlO1xuICAgIGNvbnN0IHB1bGxQcm9taXNlID0gY29udHJvbGxlci5fcHVsbEFsZ29yaXRobSgpO1xuICAgIHVwb25Qcm9taXNlKHB1bGxQcm9taXNlLCAoKSA9PiB7XG4gICAgICAgIGNvbnRyb2xsZXIuX3B1bGxpbmcgPSBmYWxzZTtcbiAgICAgICAgaWYgKGNvbnRyb2xsZXIuX3B1bGxBZ2Fpbikge1xuICAgICAgICAgICAgY29udHJvbGxlci5fcHVsbEFnYWluID0gZmFsc2U7XG4gICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZChjb250cm9sbGVyKTtcbiAgICAgICAgfVxuICAgIH0sIGUgPT4ge1xuICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3IoY29udHJvbGxlciwgZSk7XG4gICAgfSk7XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyU2hvdWxkQ2FsbFB1bGwoY29udHJvbGxlcikge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbTtcbiAgICBpZiAoIVJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYW5DbG9zZU9yRW5xdWV1ZShjb250cm9sbGVyKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghY29udHJvbGxlci5fc3RhcnRlZCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmIChJc1JlYWRhYmxlU3RyZWFtTG9ja2VkKHN0cmVhbSkgJiYgUmVhZGFibGVTdHJlYW1HZXROdW1SZWFkUmVxdWVzdHMoc3RyZWFtKSA+IDApIHtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuICAgIGNvbnN0IGRlc2lyZWRTaXplID0gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckdldERlc2lyZWRTaXplKGNvbnRyb2xsZXIpO1xuICAgIGlmIChkZXNpcmVkU2l6ZSA+IDApIHtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuICAgIHJldHVybiBmYWxzZTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoY29udHJvbGxlcikge1xuICAgIGNvbnRyb2xsZXIuX3B1bGxBbGdvcml0aG0gPSB1bmRlZmluZWQ7XG4gICAgY29udHJvbGxlci5fY2FuY2VsQWxnb3JpdGhtID0gdW5kZWZpbmVkO1xuICAgIGNvbnRyb2xsZXIuX3N0cmF0ZWd5U2l6ZUFsZ29yaXRobSA9IHVuZGVmaW5lZDtcbn1cbi8vIEEgY2xpZW50IG9mIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIgbWF5IHVzZSB0aGVzZSBmdW5jdGlvbnMgZGlyZWN0bHkgdG8gYnlwYXNzIHN0YXRlIGNoZWNrLlxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsb3NlKGNvbnRyb2xsZXIpIHtcbiAgICBpZiAoIVJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYW5DbG9zZU9yRW5xdWV1ZShjb250cm9sbGVyKSkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbTtcbiAgICBjb250cm9sbGVyLl9jbG9zZVJlcXVlc3RlZCA9IHRydWU7XG4gICAgaWYgKGNvbnRyb2xsZXIuX3F1ZXVlLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKGNvbnRyb2xsZXIpO1xuICAgICAgICBSZWFkYWJsZVN0cmVhbUNsb3NlKHN0cmVhbSk7XG4gICAgfVxufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVucXVldWUoY29udHJvbGxlciwgY2h1bmspIHtcbiAgICBpZiAoIVJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYW5DbG9zZU9yRW5xdWV1ZShjb250cm9sbGVyKSkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbTtcbiAgICBpZiAoSXNSZWFkYWJsZVN0cmVhbUxvY2tlZChzdHJlYW0pICYmIFJlYWRhYmxlU3RyZWFtR2V0TnVtUmVhZFJlcXVlc3RzKHN0cmVhbSkgPiAwKSB7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRSZXF1ZXN0KHN0cmVhbSwgY2h1bmssIGZhbHNlKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIGxldCBjaHVua1NpemU7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICBjaHVua1NpemUgPSBjb250cm9sbGVyLl9zdHJhdGVneVNpemVBbGdvcml0aG0oY2h1bmspO1xuICAgICAgICB9XG4gICAgICAgIGNhdGNoIChjaHVua1NpemVFKSB7XG4gICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3IoY29udHJvbGxlciwgY2h1bmtTaXplRSk7XG4gICAgICAgICAgICB0aHJvdyBjaHVua1NpemVFO1xuICAgICAgICB9XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICBFbnF1ZXVlVmFsdWVXaXRoU2l6ZShjb250cm9sbGVyLCBjaHVuaywgY2h1bmtTaXplKTtcbiAgICAgICAgfVxuICAgICAgICBjYXRjaCAoZW5xdWV1ZUUpIHtcbiAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlbnF1ZXVlRSk7XG4gICAgICAgICAgICB0aHJvdyBlbnF1ZXVlRTtcbiAgICAgICAgfVxuICAgIH1cbiAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZChjb250cm9sbGVyKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlU3RyZWFtO1xuICAgIGlmIChzdHJlYW0uX3N0YXRlICE9PSAncmVhZGFibGUnKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgUmVzZXRRdWV1ZShjb250cm9sbGVyKTtcbiAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKGNvbnRyb2xsZXIpO1xuICAgIFJlYWRhYmxlU3RyZWFtRXJyb3Ioc3RyZWFtLCBlKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXREZXNpcmVkU2l6ZShjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc3RhdGUgPSBjb250cm9sbGVyLl9jb250cm9sbGVkUmVhZGFibGVTdHJlYW0uX3N0YXRlO1xuICAgIGlmIChzdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgIHJldHVybiBudWxsO1xuICAgIH1cbiAgICBpZiAoc3RhdGUgPT09ICdjbG9zZWQnKSB7XG4gICAgICAgIHJldHVybiAwO1xuICAgIH1cbiAgICByZXR1cm4gY29udHJvbGxlci5fc3RyYXRlZ3lIV00gLSBjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZTtcbn1cbi8vIFRoaXMgaXMgdXNlZCBpbiB0aGUgaW1wbGVtZW50YXRpb24gb2YgVHJhbnNmb3JtU3RyZWFtLlxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckhhc0JhY2twcmVzc3VyZShjb250cm9sbGVyKSB7XG4gICAgaWYgKFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJTaG91bGRDYWxsUHVsbChjb250cm9sbGVyKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNhbkNsb3NlT3JFbnF1ZXVlKGNvbnRyb2xsZXIpIHtcbiAgICBjb25zdCBzdGF0ZSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbS5fc3RhdGU7XG4gICAgaWYgKCFjb250cm9sbGVyLl9jbG9zZVJlcXVlc3RlZCAmJiBzdGF0ZSA9PT0gJ3JlYWRhYmxlJykge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgcmV0dXJuIGZhbHNlO1xufVxuZnVuY3Rpb24gU2V0VXBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHN0cmVhbSwgY29udHJvbGxlciwgc3RhcnRBbGdvcml0aG0sIHB1bGxBbGdvcml0aG0sIGNhbmNlbEFsZ29yaXRobSwgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSkge1xuICAgIGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbSA9IHN0cmVhbTtcbiAgICBjb250cm9sbGVyLl9xdWV1ZSA9IHVuZGVmaW5lZDtcbiAgICBjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZSA9IHVuZGVmaW5lZDtcbiAgICBSZXNldFF1ZXVlKGNvbnRyb2xsZXIpO1xuICAgIGNvbnRyb2xsZXIuX3N0YXJ0ZWQgPSBmYWxzZTtcbiAgICBjb250cm9sbGVyLl9jbG9zZVJlcXVlc3RlZCA9IGZhbHNlO1xuICAgIGNvbnRyb2xsZXIuX3B1bGxBZ2FpbiA9IGZhbHNlO1xuICAgIGNvbnRyb2xsZXIuX3B1bGxpbmcgPSBmYWxzZTtcbiAgICBjb250cm9sbGVyLl9zdHJhdGVneVNpemVBbGdvcml0aG0gPSBzaXplQWxnb3JpdGhtO1xuICAgIGNvbnRyb2xsZXIuX3N0cmF0ZWd5SFdNID0gaGlnaFdhdGVyTWFyaztcbiAgICBjb250cm9sbGVyLl9wdWxsQWxnb3JpdGhtID0gcHVsbEFsZ29yaXRobTtcbiAgICBjb250cm9sbGVyLl9jYW5jZWxBbGdvcml0aG0gPSBjYW5jZWxBbGdvcml0aG07XG4gICAgc3RyZWFtLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIgPSBjb250cm9sbGVyO1xuICAgIGNvbnN0IHN0YXJ0UmVzdWx0ID0gc3RhcnRBbGdvcml0aG0oKTtcbiAgICB1cG9uUHJvbWlzZShwcm9taXNlUmVzb2x2ZWRXaXRoKHN0YXJ0UmVzdWx0KSwgKCkgPT4ge1xuICAgICAgICBjb250cm9sbGVyLl9zdGFydGVkID0gdHJ1ZTtcbiAgICAgICAgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNhbGxQdWxsSWZOZWVkZWQoY29udHJvbGxlcik7XG4gICAgfSwgciA9PiB7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCByKTtcbiAgICB9KTtcbn1cbmZ1bmN0aW9uIFNldFVwUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckZyb21VbmRlcmx5aW5nU291cmNlKHN0cmVhbSwgdW5kZXJseWluZ1NvdXJjZSwgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSkge1xuICAgIGNvbnN0IGNvbnRyb2xsZXIgPSBPYmplY3QuY3JlYXRlKFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIucHJvdG90eXBlKTtcbiAgICBsZXQgc3RhcnRBbGdvcml0aG0gPSAoKSA9PiB1bmRlZmluZWQ7XG4gICAgbGV0IHB1bGxBbGdvcml0aG0gPSAoKSA9PiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgbGV0IGNhbmNlbEFsZ29yaXRobSA9ICgpID0+IHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICBpZiAodW5kZXJseWluZ1NvdXJjZS5zdGFydCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHN0YXJ0QWxnb3JpdGhtID0gKCkgPT4gdW5kZXJseWluZ1NvdXJjZS5zdGFydChjb250cm9sbGVyKTtcbiAgICB9XG4gICAgaWYgKHVuZGVybHlpbmdTb3VyY2UucHVsbCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHB1bGxBbGdvcml0aG0gPSAoKSA9PiB1bmRlcmx5aW5nU291cmNlLnB1bGwoY29udHJvbGxlcik7XG4gICAgfVxuICAgIGlmICh1bmRlcmx5aW5nU291cmNlLmNhbmNlbCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGNhbmNlbEFsZ29yaXRobSA9IHJlYXNvbiA9PiB1bmRlcmx5aW5nU291cmNlLmNhbmNlbChyZWFzb24pO1xuICAgIH1cbiAgICBTZXRVcFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIoc3RyZWFtLCBjb250cm9sbGVyLCBzdGFydEFsZ29yaXRobSwgcHVsbEFsZ29yaXRobSwgY2FuY2VsQWxnb3JpdGhtLCBoaWdoV2F0ZXJNYXJrLCBzaXplQWxnb3JpdGhtKTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyLlxuZnVuY3Rpb24gZGVmYXVsdENvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlci5wcm90b3R5cGUuJHtuYW1lfSBjYW4gb25seSBiZSB1c2VkIG9uIGEgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcmApO1xufVxuXG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbVRlZShzdHJlYW0sIGNsb25lRm9yQnJhbmNoMikge1xuICAgIGNvbnN0IHJlYWRlciA9IEFjcXVpcmVSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXIoc3RyZWFtKTtcbiAgICBsZXQgcmVhZGluZyA9IGZhbHNlO1xuICAgIGxldCBjYW5jZWxlZDEgPSBmYWxzZTtcbiAgICBsZXQgY2FuY2VsZWQyID0gZmFsc2U7XG4gICAgbGV0IHJlYXNvbjE7XG4gICAgbGV0IHJlYXNvbjI7XG4gICAgbGV0IGJyYW5jaDE7XG4gICAgbGV0IGJyYW5jaDI7XG4gICAgbGV0IHJlc29sdmVDYW5jZWxQcm9taXNlO1xuICAgIGNvbnN0IGNhbmNlbFByb21pc2UgPSBuZXdQcm9taXNlKHJlc29sdmUgPT4ge1xuICAgICAgICByZXNvbHZlQ2FuY2VsUHJvbWlzZSA9IHJlc29sdmU7XG4gICAgfSk7XG4gICAgZnVuY3Rpb24gcHVsbEFsZ29yaXRobSgpIHtcbiAgICAgICAgaWYgKHJlYWRpbmcpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgICAgIH1cbiAgICAgICAgcmVhZGluZyA9IHRydWU7XG4gICAgICAgIGNvbnN0IHJlYWRSZXF1ZXN0ID0ge1xuICAgICAgICAgICAgX2NodW5rU3RlcHM6IHZhbHVlID0+IHtcbiAgICAgICAgICAgICAgICAvLyBUaGlzIG5lZWRzIHRvIGJlIGRlbGF5ZWQgYSBtaWNyb3Rhc2sgYmVjYXVzZSBpdCB0YWtlcyBhdCBsZWFzdCBhIG1pY3JvdGFzayB0byBkZXRlY3QgZXJyb3JzICh1c2luZ1xuICAgICAgICAgICAgICAgIC8vIHJlYWRlci5fY2xvc2VkUHJvbWlzZSBiZWxvdyksIGFuZCB3ZSB3YW50IGVycm9ycyBpbiBzdHJlYW0gdG8gZXJyb3IgYm90aCBicmFuY2hlcyBpbW1lZGlhdGVseS4gV2UgY2Fubm90IGxldFxuICAgICAgICAgICAgICAgIC8vIHN1Y2Nlc3NmdWwgc3luY2hyb25vdXNseS1hdmFpbGFibGUgcmVhZHMgZ2V0IGFoZWFkIG9mIGFzeW5jaHJvbm91c2x5LWF2YWlsYWJsZSBlcnJvcnMuXG4gICAgICAgICAgICAgICAgcXVldWVNaWNyb3Rhc2soKCkgPT4ge1xuICAgICAgICAgICAgICAgICAgICByZWFkaW5nID0gZmFsc2U7XG4gICAgICAgICAgICAgICAgICAgIGNvbnN0IHZhbHVlMSA9IHZhbHVlO1xuICAgICAgICAgICAgICAgICAgICBjb25zdCB2YWx1ZTIgPSB2YWx1ZTtcbiAgICAgICAgICAgICAgICAgICAgLy8gVGhlcmUgaXMgbm8gd2F5IHRvIGFjY2VzcyB0aGUgY2xvbmluZyBjb2RlIHJpZ2h0IG5vdyBpbiB0aGUgcmVmZXJlbmNlIGltcGxlbWVudGF0aW9uLlxuICAgICAgICAgICAgICAgICAgICAvLyBJZiB3ZSBhZGQgb25lIHRoZW4gd2UnbGwgbmVlZCBhbiBpbXBsZW1lbnRhdGlvbiBmb3Igc2VyaWFsaXphYmxlIG9iamVjdHMuXG4gICAgICAgICAgICAgICAgICAgIC8vIGlmICghY2FuY2VsZWQyICYmIGNsb25lRm9yQnJhbmNoMikge1xuICAgICAgICAgICAgICAgICAgICAvLyAgIHZhbHVlMiA9IFN0cnVjdHVyZWREZXNlcmlhbGl6ZShTdHJ1Y3R1cmVkU2VyaWFsaXplKHZhbHVlMikpO1xuICAgICAgICAgICAgICAgICAgICAvLyB9XG4gICAgICAgICAgICAgICAgICAgIGlmICghY2FuY2VsZWQxKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRW5xdWV1ZShicmFuY2gxLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIsIHZhbHVlMSk7XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgaWYgKCFjYW5jZWxlZDIpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlKGJyYW5jaDIuX3JlYWRhYmxlU3RyZWFtQ29udHJvbGxlciwgdmFsdWUyKTtcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgICByZXNvbHZlQ2FuY2VsUHJvbWlzZSh1bmRlZmluZWQpO1xuICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIF9jbG9zZVN0ZXBzOiAoKSA9PiB7XG4gICAgICAgICAgICAgICAgcmVhZGluZyA9IGZhbHNlO1xuICAgICAgICAgICAgICAgIGlmICghY2FuY2VsZWQxKSB7XG4gICAgICAgICAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbG9zZShicmFuY2gxLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIpO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICBpZiAoIWNhbmNlbGVkMikge1xuICAgICAgICAgICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xvc2UoYnJhbmNoMi5fcmVhZGFibGVTdHJlYW1Db250cm9sbGVyKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgX2Vycm9yU3RlcHM6ICgpID0+IHtcbiAgICAgICAgICAgICAgICByZWFkaW5nID0gZmFsc2U7XG4gICAgICAgICAgICB9XG4gICAgICAgIH07XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlclJlYWQocmVhZGVyLCByZWFkUmVxdWVzdCk7XG4gICAgICAgIHJldHVybiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgfVxuICAgIGZ1bmN0aW9uIGNhbmNlbDFBbGdvcml0aG0ocmVhc29uKSB7XG4gICAgICAgIGNhbmNlbGVkMSA9IHRydWU7XG4gICAgICAgIHJlYXNvbjEgPSByZWFzb247XG4gICAgICAgIGlmIChjYW5jZWxlZDIpIHtcbiAgICAgICAgICAgIGNvbnN0IGNvbXBvc2l0ZVJlYXNvbiA9IENyZWF0ZUFycmF5RnJvbUxpc3QoW3JlYXNvbjEsIHJlYXNvbjJdKTtcbiAgICAgICAgICAgIGNvbnN0IGNhbmNlbFJlc3VsdCA9IFJlYWRhYmxlU3RyZWFtQ2FuY2VsKHN0cmVhbSwgY29tcG9zaXRlUmVhc29uKTtcbiAgICAgICAgICAgIHJlc29sdmVDYW5jZWxQcm9taXNlKGNhbmNlbFJlc3VsdCk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGNhbmNlbFByb21pc2U7XG4gICAgfVxuICAgIGZ1bmN0aW9uIGNhbmNlbDJBbGdvcml0aG0ocmVhc29uKSB7XG4gICAgICAgIGNhbmNlbGVkMiA9IHRydWU7XG4gICAgICAgIHJlYXNvbjIgPSByZWFzb247XG4gICAgICAgIGlmIChjYW5jZWxlZDEpIHtcbiAgICAgICAgICAgIGNvbnN0IGNvbXBvc2l0ZVJlYXNvbiA9IENyZWF0ZUFycmF5RnJvbUxpc3QoW3JlYXNvbjEsIHJlYXNvbjJdKTtcbiAgICAgICAgICAgIGNvbnN0IGNhbmNlbFJlc3VsdCA9IFJlYWRhYmxlU3RyZWFtQ2FuY2VsKHN0cmVhbSwgY29tcG9zaXRlUmVhc29uKTtcbiAgICAgICAgICAgIHJlc29sdmVDYW5jZWxQcm9taXNlKGNhbmNlbFJlc3VsdCk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGNhbmNlbFByb21pc2U7XG4gICAgfVxuICAgIGZ1bmN0aW9uIHN0YXJ0QWxnb3JpdGhtKCkge1xuICAgICAgICAvLyBkbyBub3RoaW5nXG4gICAgfVxuICAgIGJyYW5jaDEgPSBDcmVhdGVSZWFkYWJsZVN0cmVhbShzdGFydEFsZ29yaXRobSwgcHVsbEFsZ29yaXRobSwgY2FuY2VsMUFsZ29yaXRobSk7XG4gICAgYnJhbmNoMiA9IENyZWF0ZVJlYWRhYmxlU3RyZWFtKHN0YXJ0QWxnb3JpdGhtLCBwdWxsQWxnb3JpdGhtLCBjYW5jZWwyQWxnb3JpdGhtKTtcbiAgICB1cG9uUmVqZWN0aW9uKHJlYWRlci5fY2xvc2VkUHJvbWlzZSwgKHIpID0+IHtcbiAgICAgICAgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVycm9yKGJyYW5jaDEuX3JlYWRhYmxlU3RyZWFtQ29udHJvbGxlciwgcik7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihicmFuY2gyLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIsIHIpO1xuICAgICAgICByZXNvbHZlQ2FuY2VsUHJvbWlzZSh1bmRlZmluZWQpO1xuICAgIH0pO1xuICAgIHJldHVybiBbYnJhbmNoMSwgYnJhbmNoMl07XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nRGVmYXVsdE9yQnl0ZVNvdXJjZShzb3VyY2UsIGNvbnRleHQpIHtcbiAgICBhc3NlcnREaWN0aW9uYXJ5KHNvdXJjZSwgY29udGV4dCk7XG4gICAgY29uc3Qgb3JpZ2luYWwgPSBzb3VyY2U7XG4gICAgY29uc3QgYXV0b0FsbG9jYXRlQ2h1bmtTaXplID0gb3JpZ2luYWwgPT09IG51bGwgfHwgb3JpZ2luYWwgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9yaWdpbmFsLmF1dG9BbGxvY2F0ZUNodW5rU2l6ZTtcbiAgICBjb25zdCBjYW5jZWwgPSBvcmlnaW5hbCA9PT0gbnVsbCB8fCBvcmlnaW5hbCA9PT0gdm9pZCAwID8gdm9pZCAwIDogb3JpZ2luYWwuY2FuY2VsO1xuICAgIGNvbnN0IHB1bGwgPSBvcmlnaW5hbCA9PT0gbnVsbCB8fCBvcmlnaW5hbCA9PT0gdm9pZCAwID8gdm9pZCAwIDogb3JpZ2luYWwucHVsbDtcbiAgICBjb25zdCBzdGFydCA9IG9yaWdpbmFsID09PSBudWxsIHx8IG9yaWdpbmFsID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcmlnaW5hbC5zdGFydDtcbiAgICBjb25zdCB0eXBlID0gb3JpZ2luYWwgPT09IG51bGwgfHwgb3JpZ2luYWwgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9yaWdpbmFsLnR5cGU7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgYXV0b0FsbG9jYXRlQ2h1bmtTaXplOiBhdXRvQWxsb2NhdGVDaHVua1NpemUgPT09IHVuZGVmaW5lZCA/XG4gICAgICAgICAgICB1bmRlZmluZWQgOlxuICAgICAgICAgICAgY29udmVydFVuc2lnbmVkTG9uZ0xvbmdXaXRoRW5mb3JjZVJhbmdlKGF1dG9BbGxvY2F0ZUNodW5rU2l6ZSwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAnYXV0b0FsbG9jYXRlQ2h1bmtTaXplJyB0aGF0YCksXG4gICAgICAgIGNhbmNlbDogY2FuY2VsID09PSB1bmRlZmluZWQgP1xuICAgICAgICAgICAgdW5kZWZpbmVkIDpcbiAgICAgICAgICAgIGNvbnZlcnRVbmRlcmx5aW5nU291cmNlQ2FuY2VsQ2FsbGJhY2soY2FuY2VsLCBvcmlnaW5hbCwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAnY2FuY2VsJyB0aGF0YCksXG4gICAgICAgIHB1bGw6IHB1bGwgPT09IHVuZGVmaW5lZCA/XG4gICAgICAgICAgICB1bmRlZmluZWQgOlxuICAgICAgICAgICAgY29udmVydFVuZGVybHlpbmdTb3VyY2VQdWxsQ2FsbGJhY2socHVsbCwgb3JpZ2luYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3B1bGwnIHRoYXRgKSxcbiAgICAgICAgc3RhcnQ6IHN0YXJ0ID09PSB1bmRlZmluZWQgP1xuICAgICAgICAgICAgdW5kZWZpbmVkIDpcbiAgICAgICAgICAgIGNvbnZlcnRVbmRlcmx5aW5nU291cmNlU3RhcnRDYWxsYmFjayhzdGFydCwgb3JpZ2luYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3N0YXJ0JyB0aGF0YCksXG4gICAgICAgIHR5cGU6IHR5cGUgPT09IHVuZGVmaW5lZCA/IHVuZGVmaW5lZCA6IGNvbnZlcnRSZWFkYWJsZVN0cmVhbVR5cGUodHlwZSwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAndHlwZScgdGhhdGApXG4gICAgfTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU291cmNlQ2FuY2VsQ2FsbGJhY2soZm4sIG9yaWdpbmFsLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RnVuY3Rpb24oZm4sIGNvbnRleHQpO1xuICAgIHJldHVybiAocmVhc29uKSA9PiBwcm9taXNlQ2FsbChmbiwgb3JpZ2luYWwsIFtyZWFzb25dKTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU291cmNlUHVsbENhbGxiYWNrKGZuLCBvcmlnaW5hbCwgY29udGV4dCkge1xuICAgIGFzc2VydEZ1bmN0aW9uKGZuLCBjb250ZXh0KTtcbiAgICByZXR1cm4gKGNvbnRyb2xsZXIpID0+IHByb21pc2VDYWxsKGZuLCBvcmlnaW5hbCwgW2NvbnRyb2xsZXJdKTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU291cmNlU3RhcnRDYWxsYmFjayhmbiwgb3JpZ2luYWwsIGNvbnRleHQpIHtcbiAgICBhc3NlcnRGdW5jdGlvbihmbiwgY29udGV4dCk7XG4gICAgcmV0dXJuIChjb250cm9sbGVyKSA9PiByZWZsZWN0Q2FsbChmbiwgb3JpZ2luYWwsIFtjb250cm9sbGVyXSk7XG59XG5mdW5jdGlvbiBjb252ZXJ0UmVhZGFibGVTdHJlYW1UeXBlKHR5cGUsIGNvbnRleHQpIHtcbiAgICB0eXBlID0gYCR7dHlwZX1gO1xuICAgIGlmICh0eXBlICE9PSAnYnl0ZXMnKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoYCR7Y29udGV4dH0gJyR7dHlwZX0nIGlzIG5vdCBhIHZhbGlkIGVudW1lcmF0aW9uIHZhbHVlIGZvciBSZWFkYWJsZVN0cmVhbVR5cGVgKTtcbiAgICB9XG4gICAgcmV0dXJuIHR5cGU7XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRSZWFkZXJPcHRpb25zKG9wdGlvbnMsIGNvbnRleHQpIHtcbiAgICBhc3NlcnREaWN0aW9uYXJ5KG9wdGlvbnMsIGNvbnRleHQpO1xuICAgIGNvbnN0IG1vZGUgPSBvcHRpb25zID09PSBudWxsIHx8IG9wdGlvbnMgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9wdGlvbnMubW9kZTtcbiAgICByZXR1cm4ge1xuICAgICAgICBtb2RlOiBtb2RlID09PSB1bmRlZmluZWQgPyB1bmRlZmluZWQgOiBjb252ZXJ0UmVhZGFibGVTdHJlYW1SZWFkZXJNb2RlKG1vZGUsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ21vZGUnIHRoYXRgKVxuICAgIH07XG59XG5mdW5jdGlvbiBjb252ZXJ0UmVhZGFibGVTdHJlYW1SZWFkZXJNb2RlKG1vZGUsIGNvbnRleHQpIHtcbiAgICBtb2RlID0gYCR7bW9kZX1gO1xuICAgIGlmIChtb2RlICE9PSAnYnlvYicpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSAnJHttb2RlfScgaXMgbm90IGEgdmFsaWQgZW51bWVyYXRpb24gdmFsdWUgZm9yIFJlYWRhYmxlU3RyZWFtUmVhZGVyTW9kZWApO1xuICAgIH1cbiAgICByZXR1cm4gbW9kZTtcbn1cblxuZnVuY3Rpb24gY29udmVydEl0ZXJhdG9yT3B0aW9ucyhvcHRpb25zLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RGljdGlvbmFyeShvcHRpb25zLCBjb250ZXh0KTtcbiAgICBjb25zdCBwcmV2ZW50Q2FuY2VsID0gb3B0aW9ucyA9PT0gbnVsbCB8fCBvcHRpb25zID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcHRpb25zLnByZXZlbnRDYW5jZWw7XG4gICAgcmV0dXJuIHsgcHJldmVudENhbmNlbDogQm9vbGVhbihwcmV2ZW50Q2FuY2VsKSB9O1xufVxuXG5mdW5jdGlvbiBjb252ZXJ0UGlwZU9wdGlvbnMob3B0aW9ucywgY29udGV4dCkge1xuICAgIGFzc2VydERpY3Rpb25hcnkob3B0aW9ucywgY29udGV4dCk7XG4gICAgY29uc3QgcHJldmVudEFib3J0ID0gb3B0aW9ucyA9PT0gbnVsbCB8fCBvcHRpb25zID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcHRpb25zLnByZXZlbnRBYm9ydDtcbiAgICBjb25zdCBwcmV2ZW50Q2FuY2VsID0gb3B0aW9ucyA9PT0gbnVsbCB8fCBvcHRpb25zID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcHRpb25zLnByZXZlbnRDYW5jZWw7XG4gICAgY29uc3QgcHJldmVudENsb3NlID0gb3B0aW9ucyA9PT0gbnVsbCB8fCBvcHRpb25zID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcHRpb25zLnByZXZlbnRDbG9zZTtcbiAgICBjb25zdCBzaWduYWwgPSBvcHRpb25zID09PSBudWxsIHx8IG9wdGlvbnMgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9wdGlvbnMuc2lnbmFsO1xuICAgIGlmIChzaWduYWwgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICBhc3NlcnRBYm9ydFNpZ25hbChzaWduYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3NpZ25hbCcgdGhhdGApO1xuICAgIH1cbiAgICByZXR1cm4ge1xuICAgICAgICBwcmV2ZW50QWJvcnQ6IEJvb2xlYW4ocHJldmVudEFib3J0KSxcbiAgICAgICAgcHJldmVudENhbmNlbDogQm9vbGVhbihwcmV2ZW50Q2FuY2VsKSxcbiAgICAgICAgcHJldmVudENsb3NlOiBCb29sZWFuKHByZXZlbnRDbG9zZSksXG4gICAgICAgIHNpZ25hbFxuICAgIH07XG59XG5mdW5jdGlvbiBhc3NlcnRBYm9ydFNpZ25hbChzaWduYWwsIGNvbnRleHQpIHtcbiAgICBpZiAoIWlzQWJvcnRTaWduYWwoc2lnbmFsKSkge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGAke2NvbnRleHR9IGlzIG5vdCBhbiBBYm9ydFNpZ25hbC5gKTtcbiAgICB9XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRSZWFkYWJsZVdyaXRhYmxlUGFpcihwYWlyLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RGljdGlvbmFyeShwYWlyLCBjb250ZXh0KTtcbiAgICBjb25zdCByZWFkYWJsZSA9IHBhaXIgPT09IG51bGwgfHwgcGFpciA9PT0gdm9pZCAwID8gdm9pZCAwIDogcGFpci5yZWFkYWJsZTtcbiAgICBhc3NlcnRSZXF1aXJlZEZpZWxkKHJlYWRhYmxlLCAncmVhZGFibGUnLCAnUmVhZGFibGVXcml0YWJsZVBhaXInKTtcbiAgICBhc3NlcnRSZWFkYWJsZVN0cmVhbShyZWFkYWJsZSwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAncmVhZGFibGUnIHRoYXRgKTtcbiAgICBjb25zdCB3cml0YWJsZSA9IHBhaXIgPT09IG51bGwgfHwgcGFpciA9PT0gdm9pZCAwID8gdm9pZCAwIDogcGFpci53cml0YWJsZTtcbiAgICBhc3NlcnRSZXF1aXJlZEZpZWxkKHdyaXRhYmxlLCAnd3JpdGFibGUnLCAnUmVhZGFibGVXcml0YWJsZVBhaXInKTtcbiAgICBhc3NlcnRXcml0YWJsZVN0cmVhbSh3cml0YWJsZSwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAnd3JpdGFibGUnIHRoYXRgKTtcbiAgICByZXR1cm4geyByZWFkYWJsZSwgd3JpdGFibGUgfTtcbn1cblxuLyoqXG4gKiBBIHJlYWRhYmxlIHN0cmVhbSByZXByZXNlbnRzIGEgc291cmNlIG9mIGRhdGEsIGZyb20gd2hpY2ggeW91IGNhbiByZWFkLlxuICpcbiAqIEBwdWJsaWNcbiAqL1xuY2xhc3MgUmVhZGFibGVTdHJlYW0ge1xuICAgIGNvbnN0cnVjdG9yKHJhd1VuZGVybHlpbmdTb3VyY2UgPSB7fSwgcmF3U3RyYXRlZ3kgPSB7fSkge1xuICAgICAgICBpZiAocmF3VW5kZXJseWluZ1NvdXJjZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByYXdVbmRlcmx5aW5nU291cmNlID0gbnVsbDtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgIGFzc2VydE9iamVjdChyYXdVbmRlcmx5aW5nU291cmNlLCAnRmlyc3QgcGFyYW1ldGVyJyk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3Qgc3RyYXRlZ3kgPSBjb252ZXJ0UXVldWluZ1N0cmF0ZWd5KHJhd1N0cmF0ZWd5LCAnU2Vjb25kIHBhcmFtZXRlcicpO1xuICAgICAgICBjb25zdCB1bmRlcmx5aW5nU291cmNlID0gY29udmVydFVuZGVybHlpbmdEZWZhdWx0T3JCeXRlU291cmNlKHJhd1VuZGVybHlpbmdTb3VyY2UsICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgSW5pdGlhbGl6ZVJlYWRhYmxlU3RyZWFtKHRoaXMpO1xuICAgICAgICBpZiAodW5kZXJseWluZ1NvdXJjZS50eXBlID09PSAnYnl0ZXMnKSB7XG4gICAgICAgICAgICBpZiAoc3RyYXRlZ3kuc2l6ZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ1RoZSBzdHJhdGVneSBmb3IgYSBieXRlIHN0cmVhbSBjYW5ub3QgaGF2ZSBhIHNpemUgZnVuY3Rpb24nKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGNvbnN0IGhpZ2hXYXRlck1hcmsgPSBFeHRyYWN0SGlnaFdhdGVyTWFyayhzdHJhdGVneSwgMCk7XG4gICAgICAgICAgICBTZXRVcFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJGcm9tVW5kZXJseWluZ1NvdXJjZSh0aGlzLCB1bmRlcmx5aW5nU291cmNlLCBoaWdoV2F0ZXJNYXJrKTtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgIGNvbnN0IHNpemVBbGdvcml0aG0gPSBFeHRyYWN0U2l6ZUFsZ29yaXRobShzdHJhdGVneSk7XG4gICAgICAgICAgICBjb25zdCBoaWdoV2F0ZXJNYXJrID0gRXh0cmFjdEhpZ2hXYXRlck1hcmsoc3RyYXRlZ3ksIDEpO1xuICAgICAgICAgICAgU2V0VXBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRnJvbVVuZGVybHlpbmdTb3VyY2UodGhpcywgdW5kZXJseWluZ1NvdXJjZSwgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSk7XG4gICAgICAgIH1cbiAgICB9XG4gICAgLyoqXG4gICAgICogV2hldGhlciBvciBub3QgdGhlIHJlYWRhYmxlIHN0cmVhbSBpcyBsb2NrZWQgdG8gYSB7QGxpbmsgUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyIHwgcmVhZGVyfS5cbiAgICAgKi9cbiAgICBnZXQgbG9ja2VkKCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW0odGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IHN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24kMSgnbG9ja2VkJyk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIElzUmVhZGFibGVTdHJlYW1Mb2NrZWQodGhpcyk7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIENhbmNlbHMgdGhlIHN0cmVhbSwgc2lnbmFsaW5nIGEgbG9zcyBvZiBpbnRlcmVzdCBpbiB0aGUgc3RyZWFtIGJ5IGEgY29uc3VtZXIuXG4gICAgICpcbiAgICAgKiBUaGUgc3VwcGxpZWQgYHJlYXNvbmAgYXJndW1lbnQgd2lsbCBiZSBnaXZlbiB0byB0aGUgdW5kZXJseWluZyBzb3VyY2UncyB7QGxpbmsgVW5kZXJseWluZ1NvdXJjZS5jYW5jZWwgfCBjYW5jZWwoKX1cbiAgICAgKiBtZXRob2QsIHdoaWNoIG1pZ2h0IG9yIG1pZ2h0IG5vdCB1c2UgaXQuXG4gICAgICovXG4gICAgY2FuY2VsKHJlYXNvbiA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW0odGhpcykpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKHN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24kMSgnY2FuY2VsJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChJc1JlYWRhYmxlU3RyZWFtTG9ja2VkKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FuY2VsIGEgc3RyZWFtIHRoYXQgYWxyZWFkeSBoYXMgYSByZWFkZXInKSk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIFJlYWRhYmxlU3RyZWFtQ2FuY2VsKHRoaXMsIHJlYXNvbik7XG4gICAgfVxuICAgIGdldFJlYWRlcihyYXdPcHRpb25zID0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbSh0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbiQxKCdnZXRSZWFkZXInKTtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCBvcHRpb25zID0gY29udmVydFJlYWRlck9wdGlvbnMocmF3T3B0aW9ucywgJ0ZpcnN0IHBhcmFtZXRlcicpO1xuICAgICAgICBpZiAob3B0aW9ucy5tb2RlID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJldHVybiBBY3F1aXJlUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyKHRoaXMpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBBY3F1aXJlUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHRoaXMpO1xuICAgIH1cbiAgICBwaXBlVGhyb3VnaChyYXdUcmFuc2Zvcm0sIHJhd09wdGlvbnMgPSB7fSkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW0odGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IHN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24kMSgncGlwZVRocm91Z2gnKTtcbiAgICAgICAgfVxuICAgICAgICBhc3NlcnRSZXF1aXJlZEFyZ3VtZW50KHJhd1RyYW5zZm9ybSwgMSwgJ3BpcGVUaHJvdWdoJyk7XG4gICAgICAgIGNvbnN0IHRyYW5zZm9ybSA9IGNvbnZlcnRSZWFkYWJsZVdyaXRhYmxlUGFpcihyYXdUcmFuc2Zvcm0sICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgY29uc3Qgb3B0aW9ucyA9IGNvbnZlcnRQaXBlT3B0aW9ucyhyYXdPcHRpb25zLCAnU2Vjb25kIHBhcmFtZXRlcicpO1xuICAgICAgICBpZiAoSXNSZWFkYWJsZVN0cmVhbUxvY2tlZCh0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLnBpcGVUaHJvdWdoIGNhbm5vdCBiZSB1c2VkIG9uIGEgbG9ja2VkIFJlYWRhYmxlU3RyZWFtJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKElzV3JpdGFibGVTdHJlYW1Mb2NrZWQodHJhbnNmb3JtLndyaXRhYmxlKSkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLnBpcGVUaHJvdWdoIGNhbm5vdCBiZSB1c2VkIG9uIGEgbG9ja2VkIFdyaXRhYmxlU3RyZWFtJyk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3QgcHJvbWlzZSA9IFJlYWRhYmxlU3RyZWFtUGlwZVRvKHRoaXMsIHRyYW5zZm9ybS53cml0YWJsZSwgb3B0aW9ucy5wcmV2ZW50Q2xvc2UsIG9wdGlvbnMucHJldmVudEFib3J0LCBvcHRpb25zLnByZXZlbnRDYW5jZWwsIG9wdGlvbnMuc2lnbmFsKTtcbiAgICAgICAgc2V0UHJvbWlzZUlzSGFuZGxlZFRvVHJ1ZShwcm9taXNlKTtcbiAgICAgICAgcmV0dXJuIHRyYW5zZm9ybS5yZWFkYWJsZTtcbiAgICB9XG4gICAgcGlwZVRvKGRlc3RpbmF0aW9uLCByYXdPcHRpb25zID0ge30pIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uJDEoJ3BpcGVUbycpKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoZGVzdGluYXRpb24gPT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoYFBhcmFtZXRlciAxIGlzIHJlcXVpcmVkIGluICdwaXBlVG8nLmApO1xuICAgICAgICB9XG4gICAgICAgIGlmICghSXNXcml0YWJsZVN0cmVhbShkZXN0aW5hdGlvbikpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoYFJlYWRhYmxlU3RyZWFtLnByb3RvdHlwZS5waXBlVG8ncyBmaXJzdCBhcmd1bWVudCBtdXN0IGJlIGEgV3JpdGFibGVTdHJlYW1gKSk7XG4gICAgICAgIH1cbiAgICAgICAgbGV0IG9wdGlvbnM7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICBvcHRpb25zID0gY29udmVydFBpcGVPcHRpb25zKHJhd09wdGlvbnMsICdTZWNvbmQgcGFyYW1ldGVyJyk7XG4gICAgICAgIH1cbiAgICAgICAgY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKGUpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChJc1JlYWRhYmxlU3RyZWFtTG9ja2VkKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChuZXcgVHlwZUVycm9yKCdSZWFkYWJsZVN0cmVhbS5wcm90b3R5cGUucGlwZVRvIGNhbm5vdCBiZSB1c2VkIG9uIGEgbG9ja2VkIFJlYWRhYmxlU3RyZWFtJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChJc1dyaXRhYmxlU3RyZWFtTG9ja2VkKGRlc3RpbmF0aW9uKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgobmV3IFR5cGVFcnJvcignUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLnBpcGVUbyBjYW5ub3QgYmUgdXNlZCBvbiBhIGxvY2tlZCBXcml0YWJsZVN0cmVhbScpKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gUmVhZGFibGVTdHJlYW1QaXBlVG8odGhpcywgZGVzdGluYXRpb24sIG9wdGlvbnMucHJldmVudENsb3NlLCBvcHRpb25zLnByZXZlbnRBYm9ydCwgb3B0aW9ucy5wcmV2ZW50Q2FuY2VsLCBvcHRpb25zLnNpZ25hbCk7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFRlZXMgdGhpcyByZWFkYWJsZSBzdHJlYW0sIHJldHVybmluZyBhIHR3by1lbGVtZW50IGFycmF5IGNvbnRhaW5pbmcgdGhlIHR3byByZXN1bHRpbmcgYnJhbmNoZXMgYXNcbiAgICAgKiBuZXcge0BsaW5rIFJlYWRhYmxlU3RyZWFtfSBpbnN0YW5jZXMuXG4gICAgICpcbiAgICAgKiBUZWVpbmcgYSBzdHJlYW0gd2lsbCBsb2NrIGl0LCBwcmV2ZW50aW5nIGFueSBvdGhlciBjb25zdW1lciBmcm9tIGFjcXVpcmluZyBhIHJlYWRlci5cbiAgICAgKiBUbyBjYW5jZWwgdGhlIHN0cmVhbSwgY2FuY2VsIGJvdGggb2YgdGhlIHJlc3VsdGluZyBicmFuY2hlczsgYSBjb21wb3NpdGUgY2FuY2VsbGF0aW9uIHJlYXNvbiB3aWxsIHRoZW4gYmVcbiAgICAgKiBwcm9wYWdhdGVkIHRvIHRoZSBzdHJlYW0ncyB1bmRlcmx5aW5nIHNvdXJjZS5cbiAgICAgKlxuICAgICAqIE5vdGUgdGhhdCB0aGUgY2h1bmtzIHNlZW4gaW4gZWFjaCBicmFuY2ggd2lsbCBiZSB0aGUgc2FtZSBvYmplY3QuIElmIHRoZSBjaHVua3MgYXJlIG5vdCBpbW11dGFibGUsXG4gICAgICogdGhpcyBjb3VsZCBhbGxvdyBpbnRlcmZlcmVuY2UgYmV0d2VlbiB0aGUgdHdvIGJyYW5jaGVzLlxuICAgICAqL1xuICAgIHRlZSgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uJDEoJ3RlZScpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IGJyYW5jaGVzID0gUmVhZGFibGVTdHJlYW1UZWUodGhpcyk7XG4gICAgICAgIHJldHVybiBDcmVhdGVBcnJheUZyb21MaXN0KGJyYW5jaGVzKTtcbiAgICB9XG4gICAgdmFsdWVzKHJhd09wdGlvbnMgPSB1bmRlZmluZWQpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uJDEoJ3ZhbHVlcycpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IG9wdGlvbnMgPSBjb252ZXJ0SXRlcmF0b3JPcHRpb25zKHJhd09wdGlvbnMsICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgcmV0dXJuIEFjcXVpcmVSZWFkYWJsZVN0cmVhbUFzeW5jSXRlcmF0b3IodGhpcywgb3B0aW9ucy5wcmV2ZW50Q2FuY2VsKTtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhSZWFkYWJsZVN0cmVhbS5wcm90b3R5cGUsIHtcbiAgICBjYW5jZWw6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGdldFJlYWRlcjogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgcGlwZVRocm91Z2g6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIHBpcGVUbzogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgdGVlOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICB2YWx1ZXM6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGxvY2tlZDogeyBlbnVtZXJhYmxlOiB0cnVlIH1cbn0pO1xuaWYgKHR5cGVvZiBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZyA9PT0gJ3N5bWJvbCcpIHtcbiAgICBPYmplY3QuZGVmaW5lUHJvcGVydHkoUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLCBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZywge1xuICAgICAgICB2YWx1ZTogJ1JlYWRhYmxlU3RyZWFtJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLmFzeW5jSXRlcmF0b3IgPT09ICdzeW1ib2wnKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFJlYWRhYmxlU3RyZWFtLnByb3RvdHlwZSwgU3ltYm9sUG9seWZpbGwuYXN5bmNJdGVyYXRvciwge1xuICAgICAgICB2YWx1ZTogUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLnZhbHVlcyxcbiAgICAgICAgd3JpdGFibGU6IHRydWUsXG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZVxuICAgIH0pO1xufVxuLy8gQWJzdHJhY3Qgb3BlcmF0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtLlxuLy8gVGhyb3dzIGlmIGFuZCBvbmx5IGlmIHN0YXJ0QWxnb3JpdGhtIHRocm93cy5cbmZ1bmN0aW9uIENyZWF0ZVJlYWRhYmxlU3RyZWFtKHN0YXJ0QWxnb3JpdGhtLCBwdWxsQWxnb3JpdGhtLCBjYW5jZWxBbGdvcml0aG0sIGhpZ2hXYXRlck1hcmsgPSAxLCBzaXplQWxnb3JpdGhtID0gKCkgPT4gMSkge1xuICAgIGNvbnN0IHN0cmVhbSA9IE9iamVjdC5jcmVhdGUoUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlKTtcbiAgICBJbml0aWFsaXplUmVhZGFibGVTdHJlYW0oc3RyZWFtKTtcbiAgICBjb25zdCBjb250cm9sbGVyID0gT2JqZWN0LmNyZWF0ZShSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyLnByb3RvdHlwZSk7XG4gICAgU2V0VXBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHN0cmVhbSwgY29udHJvbGxlciwgc3RhcnRBbGdvcml0aG0sIHB1bGxBbGdvcml0aG0sIGNhbmNlbEFsZ29yaXRobSwgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSk7XG4gICAgcmV0dXJuIHN0cmVhbTtcbn1cbmZ1bmN0aW9uIEluaXRpYWxpemVSZWFkYWJsZVN0cmVhbShzdHJlYW0pIHtcbiAgICBzdHJlYW0uX3N0YXRlID0gJ3JlYWRhYmxlJztcbiAgICBzdHJlYW0uX3JlYWRlciA9IHVuZGVmaW5lZDtcbiAgICBzdHJlYW0uX3N0b3JlZEVycm9yID0gdW5kZWZpbmVkO1xuICAgIHN0cmVhbS5fZGlzdHVyYmVkID0gZmFsc2U7XG59XG5mdW5jdGlvbiBJc1JlYWRhYmxlU3RyZWFtKHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfcmVhZGFibGVTdHJlYW1Db250cm9sbGVyJykpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbmZ1bmN0aW9uIElzUmVhZGFibGVTdHJlYW1Mb2NrZWQoc3RyZWFtKSB7XG4gICAgaWYgKHN0cmVhbS5fcmVhZGVyID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbi8vIFJlYWRhYmxlU3RyZWFtIEFQSSBleHBvc2VkIGZvciBjb250cm9sbGVycy5cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtQ2FuY2VsKHN0cmVhbSwgcmVhc29uKSB7XG4gICAgc3RyZWFtLl9kaXN0dXJiZWQgPSB0cnVlO1xuICAgIGlmIChzdHJlYW0uX3N0YXRlID09PSAnY2xvc2VkJykge1xuICAgICAgICByZXR1cm4gcHJvbWlzZVJlc29sdmVkV2l0aCh1bmRlZmluZWQpO1xuICAgIH1cbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKHN0cmVhbS5fc3RvcmVkRXJyb3IpO1xuICAgIH1cbiAgICBSZWFkYWJsZVN0cmVhbUNsb3NlKHN0cmVhbSk7XG4gICAgY29uc3Qgc291cmNlQ2FuY2VsUHJvbWlzZSA9IHN0cmVhbS5fcmVhZGFibGVTdHJlYW1Db250cm9sbGVyW0NhbmNlbFN0ZXBzXShyZWFzb24pO1xuICAgIHJldHVybiB0cmFuc2Zvcm1Qcm9taXNlV2l0aChzb3VyY2VDYW5jZWxQcm9taXNlLCBub29wKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtQ2xvc2Uoc3RyZWFtKSB7XG4gICAgc3RyZWFtLl9zdGF0ZSA9ICdjbG9zZWQnO1xuICAgIGNvbnN0IHJlYWRlciA9IHN0cmVhbS5fcmVhZGVyO1xuICAgIGlmIChyZWFkZXIgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcihyZWFkZXIpKSB7XG4gICAgICAgIHJlYWRlci5fcmVhZFJlcXVlc3RzLmZvckVhY2gocmVhZFJlcXVlc3QgPT4ge1xuICAgICAgICAgICAgcmVhZFJlcXVlc3QuX2Nsb3NlU3RlcHMoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIHJlYWRlci5fcmVhZFJlcXVlc3RzID0gbmV3IFNpbXBsZVF1ZXVlKCk7XG4gICAgfVxuICAgIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlUmVzb2x2ZShyZWFkZXIpO1xufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1FcnJvcihzdHJlYW0sIGUpIHtcbiAgICBzdHJlYW0uX3N0YXRlID0gJ2Vycm9yZWQnO1xuICAgIHN0cmVhbS5fc3RvcmVkRXJyb3IgPSBlO1xuICAgIGNvbnN0IHJlYWRlciA9IHN0cmVhbS5fcmVhZGVyO1xuICAgIGlmIChyZWFkZXIgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcihyZWFkZXIpKSB7XG4gICAgICAgIHJlYWRlci5fcmVhZFJlcXVlc3RzLmZvckVhY2gocmVhZFJlcXVlc3QgPT4ge1xuICAgICAgICAgICAgcmVhZFJlcXVlc3QuX2Vycm9yU3RlcHMoZSk7XG4gICAgICAgIH0pO1xuICAgICAgICByZWFkZXIuX3JlYWRSZXF1ZXN0cyA9IG5ldyBTaW1wbGVRdWV1ZSgpO1xuICAgIH1cbiAgICBlbHNlIHtcbiAgICAgICAgcmVhZGVyLl9yZWFkSW50b1JlcXVlc3RzLmZvckVhY2gocmVhZEludG9SZXF1ZXN0ID0+IHtcbiAgICAgICAgICAgIHJlYWRJbnRvUmVxdWVzdC5fZXJyb3JTdGVwcyhlKTtcbiAgICAgICAgfSk7XG4gICAgICAgIHJlYWRlci5fcmVhZEludG9SZXF1ZXN0cyA9IG5ldyBTaW1wbGVRdWV1ZSgpO1xuICAgIH1cbiAgICBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZVJlamVjdChyZWFkZXIsIGUpO1xufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtLlxuZnVuY3Rpb24gc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbiQxKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLiR7bmFtZX0gY2FuIG9ubHkgYmUgdXNlZCBvbiBhIFJlYWRhYmxlU3RyZWFtYCk7XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRRdWV1aW5nU3RyYXRlZ3lJbml0KGluaXQsIGNvbnRleHQpIHtcbiAgICBhc3NlcnREaWN0aW9uYXJ5KGluaXQsIGNvbnRleHQpO1xuICAgIGNvbnN0IGhpZ2hXYXRlck1hcmsgPSBpbml0ID09PSBudWxsIHx8IGluaXQgPT09IHZvaWQgMCA/IHZvaWQgMCA6IGluaXQuaGlnaFdhdGVyTWFyaztcbiAgICBhc3NlcnRSZXF1aXJlZEZpZWxkKGhpZ2hXYXRlck1hcmssICdoaWdoV2F0ZXJNYXJrJywgJ1F1ZXVpbmdTdHJhdGVneUluaXQnKTtcbiAgICByZXR1cm4ge1xuICAgICAgICBoaWdoV2F0ZXJNYXJrOiBjb252ZXJ0VW5yZXN0cmljdGVkRG91YmxlKGhpZ2hXYXRlck1hcmspXG4gICAgfTtcbn1cblxuY29uc3QgYnl0ZUxlbmd0aFNpemVGdW5jdGlvbiA9IGZ1bmN0aW9uIHNpemUoY2h1bmspIHtcbiAgICByZXR1cm4gY2h1bmsuYnl0ZUxlbmd0aDtcbn07XG4vKipcbiAqIEEgcXVldWluZyBzdHJhdGVneSB0aGF0IGNvdW50cyB0aGUgbnVtYmVyIG9mIGJ5dGVzIGluIGVhY2ggY2h1bmsuXG4gKlxuICogQHB1YmxpY1xuICovXG5jbGFzcyBCeXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5IHtcbiAgICBjb25zdHJ1Y3RvcihvcHRpb25zKSB7XG4gICAgICAgIGFzc2VydFJlcXVpcmVkQXJndW1lbnQob3B0aW9ucywgMSwgJ0J5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3knKTtcbiAgICAgICAgb3B0aW9ucyA9IGNvbnZlcnRRdWV1aW5nU3RyYXRlZ3lJbml0KG9wdGlvbnMsICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgdGhpcy5fYnl0ZUxlbmd0aFF1ZXVpbmdTdHJhdGVneUhpZ2hXYXRlck1hcmsgPSBvcHRpb25zLmhpZ2hXYXRlck1hcms7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFJldHVybnMgdGhlIGhpZ2ggd2F0ZXIgbWFyayBwcm92aWRlZCB0byB0aGUgY29uc3RydWN0b3IuXG4gICAgICovXG4gICAgZ2V0IGhpZ2hXYXRlck1hcmsoKSB7XG4gICAgICAgIGlmICghSXNCeXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5KHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBieXRlTGVuZ3RoQnJhbmRDaGVja0V4Y2VwdGlvbignaGlnaFdhdGVyTWFyaycpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9ieXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5SGlnaFdhdGVyTWFyaztcbiAgICB9XG4gICAgLyoqXG4gICAgICogTWVhc3VyZXMgdGhlIHNpemUgb2YgYGNodW5rYCBieSByZXR1cm5pbmcgdGhlIHZhbHVlIG9mIGl0cyBgYnl0ZUxlbmd0aGAgcHJvcGVydHkuXG4gICAgICovXG4gICAgZ2V0IHNpemUoKSB7XG4gICAgICAgIGlmICghSXNCeXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5KHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBieXRlTGVuZ3RoQnJhbmRDaGVja0V4Y2VwdGlvbignc2l6ZScpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBieXRlTGVuZ3RoU2l6ZUZ1bmN0aW9uO1xuICAgIH1cbn1cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKEJ5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3kucHJvdG90eXBlLCB7XG4gICAgaGlnaFdhdGVyTWFyazogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgc2l6ZTogeyBlbnVtZXJhYmxlOiB0cnVlIH1cbn0pO1xuaWYgKHR5cGVvZiBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZyA9PT0gJ3N5bWJvbCcpIHtcbiAgICBPYmplY3QuZGVmaW5lUHJvcGVydHkoQnl0ZUxlbmd0aFF1ZXVpbmdTdHJhdGVneS5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnQnl0ZUxlbmd0aFF1ZXVpbmdTdHJhdGVneScsXG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZVxuICAgIH0pO1xufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIEJ5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3kuXG5mdW5jdGlvbiBieXRlTGVuZ3RoQnJhbmRDaGVja0V4Y2VwdGlvbihuYW1lKSB7XG4gICAgcmV0dXJuIG5ldyBUeXBlRXJyb3IoYEJ5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3kucHJvdG90eXBlLiR7bmFtZX0gY2FuIG9ubHkgYmUgdXNlZCBvbiBhIEJ5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3lgKTtcbn1cbmZ1bmN0aW9uIElzQnl0ZUxlbmd0aFF1ZXVpbmdTdHJhdGVneSh4KSB7XG4gICAgaWYgKCF0eXBlSXNPYmplY3QoeCkpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICBpZiAoIU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbCh4LCAnX2J5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3lIaWdoV2F0ZXJNYXJrJykpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cblxuY29uc3QgY291bnRTaXplRnVuY3Rpb24gPSBmdW5jdGlvbiBzaXplKCkge1xuICAgIHJldHVybiAxO1xufTtcbi8qKlxuICogQSBxdWV1aW5nIHN0cmF0ZWd5IHRoYXQgY291bnRzIHRoZSBudW1iZXIgb2YgY2h1bmtzLlxuICpcbiAqIEBwdWJsaWNcbiAqL1xuY2xhc3MgQ291bnRRdWV1aW5nU3RyYXRlZ3kge1xuICAgIGNvbnN0cnVjdG9yKG9wdGlvbnMpIHtcbiAgICAgICAgYXNzZXJ0UmVxdWlyZWRBcmd1bWVudChvcHRpb25zLCAxLCAnQ291bnRRdWV1aW5nU3RyYXRlZ3knKTtcbiAgICAgICAgb3B0aW9ucyA9IGNvbnZlcnRRdWV1aW5nU3RyYXRlZ3lJbml0KG9wdGlvbnMsICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgdGhpcy5fY291bnRRdWV1aW5nU3RyYXRlZ3lIaWdoV2F0ZXJNYXJrID0gb3B0aW9ucy5oaWdoV2F0ZXJNYXJrO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIHRoZSBoaWdoIHdhdGVyIG1hcmsgcHJvdmlkZWQgdG8gdGhlIGNvbnN0cnVjdG9yLlxuICAgICAqL1xuICAgIGdldCBoaWdoV2F0ZXJNYXJrKCkge1xuICAgICAgICBpZiAoIUlzQ291bnRRdWV1aW5nU3RyYXRlZ3kodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGNvdW50QnJhbmRDaGVja0V4Y2VwdGlvbignaGlnaFdhdGVyTWFyaycpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9jb3VudFF1ZXVpbmdTdHJhdGVneUhpZ2hXYXRlck1hcms7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIE1lYXN1cmVzIHRoZSBzaXplIG9mIGBjaHVua2AgYnkgYWx3YXlzIHJldHVybmluZyAxLlxuICAgICAqIFRoaXMgZW5zdXJlcyB0aGF0IHRoZSB0b3RhbCBxdWV1ZSBzaXplIGlzIGEgY291bnQgb2YgdGhlIG51bWJlciBvZiBjaHVua3MgaW4gdGhlIHF1ZXVlLlxuICAgICAqL1xuICAgIGdldCBzaXplKCkge1xuICAgICAgICBpZiAoIUlzQ291bnRRdWV1aW5nU3RyYXRlZ3kodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGNvdW50QnJhbmRDaGVja0V4Y2VwdGlvbignc2l6ZScpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBjb3VudFNpemVGdW5jdGlvbjtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhDb3VudFF1ZXVpbmdTdHJhdGVneS5wcm90b3R5cGUsIHtcbiAgICBoaWdoV2F0ZXJNYXJrOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBzaXplOiB7IGVudW1lcmFibGU6IHRydWUgfVxufSk7XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnID09PSAnc3ltYm9sJykge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShDb3VudFF1ZXVpbmdTdHJhdGVneS5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnQ291bnRRdWV1aW5nU3RyYXRlZ3knLFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICB9KTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBDb3VudFF1ZXVpbmdTdHJhdGVneS5cbmZ1bmN0aW9uIGNvdW50QnJhbmRDaGVja0V4Y2VwdGlvbihuYW1lKSB7XG4gICAgcmV0dXJuIG5ldyBUeXBlRXJyb3IoYENvdW50UXVldWluZ1N0cmF0ZWd5LnByb3RvdHlwZS4ke25hbWV9IGNhbiBvbmx5IGJlIHVzZWQgb24gYSBDb3VudFF1ZXVpbmdTdHJhdGVneWApO1xufVxuZnVuY3Rpb24gSXNDb3VudFF1ZXVpbmdTdHJhdGVneSh4KSB7XG4gICAgaWYgKCF0eXBlSXNPYmplY3QoeCkpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICBpZiAoIU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbCh4LCAnX2NvdW50UXVldWluZ1N0cmF0ZWd5SGlnaFdhdGVyTWFyaycpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRUcmFuc2Zvcm1lcihvcmlnaW5hbCwgY29udGV4dCkge1xuICAgIGFzc2VydERpY3Rpb25hcnkob3JpZ2luYWwsIGNvbnRleHQpO1xuICAgIGNvbnN0IGZsdXNoID0gb3JpZ2luYWwgPT09IG51bGwgfHwgb3JpZ2luYWwgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9yaWdpbmFsLmZsdXNoO1xuICAgIGNvbnN0IHJlYWRhYmxlVHlwZSA9IG9yaWdpbmFsID09PSBudWxsIHx8IG9yaWdpbmFsID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcmlnaW5hbC5yZWFkYWJsZVR5cGU7XG4gICAgY29uc3Qgc3RhcnQgPSBvcmlnaW5hbCA9PT0gbnVsbCB8fCBvcmlnaW5hbCA9PT0gdm9pZCAwID8gdm9pZCAwIDogb3JpZ2luYWwuc3RhcnQ7XG4gICAgY29uc3QgdHJhbnNmb3JtID0gb3JpZ2luYWwgPT09IG51bGwgfHwgb3JpZ2luYWwgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9yaWdpbmFsLnRyYW5zZm9ybTtcbiAgICBjb25zdCB3cml0YWJsZVR5cGUgPSBvcmlnaW5hbCA9PT0gbnVsbCB8fCBvcmlnaW5hbCA9PT0gdm9pZCAwID8gdm9pZCAwIDogb3JpZ2luYWwud3JpdGFibGVUeXBlO1xuICAgIHJldHVybiB7XG4gICAgICAgIGZsdXNoOiBmbHVzaCA9PT0gdW5kZWZpbmVkID9cbiAgICAgICAgICAgIHVuZGVmaW5lZCA6XG4gICAgICAgICAgICBjb252ZXJ0VHJhbnNmb3JtZXJGbHVzaENhbGxiYWNrKGZsdXNoLCBvcmlnaW5hbCwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAnZmx1c2gnIHRoYXRgKSxcbiAgICAgICAgcmVhZGFibGVUeXBlLFxuICAgICAgICBzdGFydDogc3RhcnQgPT09IHVuZGVmaW5lZCA/XG4gICAgICAgICAgICB1bmRlZmluZWQgOlxuICAgICAgICAgICAgY29udmVydFRyYW5zZm9ybWVyU3RhcnRDYWxsYmFjayhzdGFydCwgb3JpZ2luYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3N0YXJ0JyB0aGF0YCksXG4gICAgICAgIHRyYW5zZm9ybTogdHJhbnNmb3JtID09PSB1bmRlZmluZWQgP1xuICAgICAgICAgICAgdW5kZWZpbmVkIDpcbiAgICAgICAgICAgIGNvbnZlcnRUcmFuc2Zvcm1lclRyYW5zZm9ybUNhbGxiYWNrKHRyYW5zZm9ybSwgb3JpZ2luYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3RyYW5zZm9ybScgdGhhdGApLFxuICAgICAgICB3cml0YWJsZVR5cGVcbiAgICB9O1xufVxuZnVuY3Rpb24gY29udmVydFRyYW5zZm9ybWVyRmx1c2hDYWxsYmFjayhmbiwgb3JpZ2luYWwsIGNvbnRleHQpIHtcbiAgICBhc3NlcnRGdW5jdGlvbihmbiwgY29udGV4dCk7XG4gICAgcmV0dXJuIChjb250cm9sbGVyKSA9PiBwcm9taXNlQ2FsbChmbiwgb3JpZ2luYWwsIFtjb250cm9sbGVyXSk7XG59XG5mdW5jdGlvbiBjb252ZXJ0VHJhbnNmb3JtZXJTdGFydENhbGxiYWNrKGZuLCBvcmlnaW5hbCwgY29udGV4dCkge1xuICAgIGFzc2VydEZ1bmN0aW9uKGZuLCBjb250ZXh0KTtcbiAgICByZXR1cm4gKGNvbnRyb2xsZXIpID0+IHJlZmxlY3RDYWxsKGZuLCBvcmlnaW5hbCwgW2NvbnRyb2xsZXJdKTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRUcmFuc2Zvcm1lclRyYW5zZm9ybUNhbGxiYWNrKGZuLCBvcmlnaW5hbCwgY29udGV4dCkge1xuICAgIGFzc2VydEZ1bmN0aW9uKGZuLCBjb250ZXh0KTtcbiAgICByZXR1cm4gKGNodW5rLCBjb250cm9sbGVyKSA9PiBwcm9taXNlQ2FsbChmbiwgb3JpZ2luYWwsIFtjaHVuaywgY29udHJvbGxlcl0pO1xufVxuXG4vLyBDbGFzcyBUcmFuc2Zvcm1TdHJlYW1cbi8qKlxuICogQSB0cmFuc2Zvcm0gc3RyZWFtIGNvbnNpc3RzIG9mIGEgcGFpciBvZiBzdHJlYW1zOiBhIHtAbGluayBXcml0YWJsZVN0cmVhbSB8IHdyaXRhYmxlIHN0cmVhbX0sXG4gKiBrbm93biBhcyBpdHMgd3JpdGFibGUgc2lkZSwgYW5kIGEge0BsaW5rIFJlYWRhYmxlU3RyZWFtIHwgcmVhZGFibGUgc3RyZWFtfSwga25vd24gYXMgaXRzIHJlYWRhYmxlIHNpZGUuXG4gKiBJbiBhIG1hbm5lciBzcGVjaWZpYyB0byB0aGUgdHJhbnNmb3JtIHN0cmVhbSBpbiBxdWVzdGlvbiwgd3JpdGVzIHRvIHRoZSB3cml0YWJsZSBzaWRlIHJlc3VsdCBpbiBuZXcgZGF0YSBiZWluZ1xuICogbWFkZSBhdmFpbGFibGUgZm9yIHJlYWRpbmcgZnJvbSB0aGUgcmVhZGFibGUgc2lkZS5cbiAqXG4gKiBAcHVibGljXG4gKi9cbmNsYXNzIFRyYW5zZm9ybVN0cmVhbSB7XG4gICAgY29uc3RydWN0b3IocmF3VHJhbnNmb3JtZXIgPSB7fSwgcmF3V3JpdGFibGVTdHJhdGVneSA9IHt9LCByYXdSZWFkYWJsZVN0cmF0ZWd5ID0ge30pIHtcbiAgICAgICAgaWYgKHJhd1RyYW5zZm9ybWVyID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJhd1RyYW5zZm9ybWVyID0gbnVsbDtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCB3cml0YWJsZVN0cmF0ZWd5ID0gY29udmVydFF1ZXVpbmdTdHJhdGVneShyYXdXcml0YWJsZVN0cmF0ZWd5LCAnU2Vjb25kIHBhcmFtZXRlcicpO1xuICAgICAgICBjb25zdCByZWFkYWJsZVN0cmF0ZWd5ID0gY29udmVydFF1ZXVpbmdTdHJhdGVneShyYXdSZWFkYWJsZVN0cmF0ZWd5LCAnVGhpcmQgcGFyYW1ldGVyJyk7XG4gICAgICAgIGNvbnN0IHRyYW5zZm9ybWVyID0gY29udmVydFRyYW5zZm9ybWVyKHJhd1RyYW5zZm9ybWVyLCAnRmlyc3QgcGFyYW1ldGVyJyk7XG4gICAgICAgIGlmICh0cmFuc2Zvcm1lci5yZWFkYWJsZVR5cGUgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ0ludmFsaWQgcmVhZGFibGVUeXBlIHNwZWNpZmllZCcpO1xuICAgICAgICB9XG4gICAgICAgIGlmICh0cmFuc2Zvcm1lci53cml0YWJsZVR5cGUgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ0ludmFsaWQgd3JpdGFibGVUeXBlIHNwZWNpZmllZCcpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IHJlYWRhYmxlSGlnaFdhdGVyTWFyayA9IEV4dHJhY3RIaWdoV2F0ZXJNYXJrKHJlYWRhYmxlU3RyYXRlZ3ksIDApO1xuICAgICAgICBjb25zdCByZWFkYWJsZVNpemVBbGdvcml0aG0gPSBFeHRyYWN0U2l6ZUFsZ29yaXRobShyZWFkYWJsZVN0cmF0ZWd5KTtcbiAgICAgICAgY29uc3Qgd3JpdGFibGVIaWdoV2F0ZXJNYXJrID0gRXh0cmFjdEhpZ2hXYXRlck1hcmsod3JpdGFibGVTdHJhdGVneSwgMSk7XG4gICAgICAgIGNvbnN0IHdyaXRhYmxlU2l6ZUFsZ29yaXRobSA9IEV4dHJhY3RTaXplQWxnb3JpdGhtKHdyaXRhYmxlU3RyYXRlZ3kpO1xuICAgICAgICBsZXQgc3RhcnRQcm9taXNlX3Jlc29sdmU7XG4gICAgICAgIGNvbnN0IHN0YXJ0UHJvbWlzZSA9IG5ld1Byb21pc2UocmVzb2x2ZSA9PiB7XG4gICAgICAgICAgICBzdGFydFByb21pc2VfcmVzb2x2ZSA9IHJlc29sdmU7XG4gICAgICAgIH0pO1xuICAgICAgICBJbml0aWFsaXplVHJhbnNmb3JtU3RyZWFtKHRoaXMsIHN0YXJ0UHJvbWlzZSwgd3JpdGFibGVIaWdoV2F0ZXJNYXJrLCB3cml0YWJsZVNpemVBbGdvcml0aG0sIHJlYWRhYmxlSGlnaFdhdGVyTWFyaywgcmVhZGFibGVTaXplQWxnb3JpdGhtKTtcbiAgICAgICAgU2V0VXBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlckZyb21UcmFuc2Zvcm1lcih0aGlzLCB0cmFuc2Zvcm1lcik7XG4gICAgICAgIGlmICh0cmFuc2Zvcm1lci5zdGFydCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICBzdGFydFByb21pc2VfcmVzb2x2ZSh0cmFuc2Zvcm1lci5zdGFydCh0aGlzLl90cmFuc2Zvcm1TdHJlYW1Db250cm9sbGVyKSk7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICBzdGFydFByb21pc2VfcmVzb2x2ZSh1bmRlZmluZWQpO1xuICAgICAgICB9XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFRoZSByZWFkYWJsZSBzaWRlIG9mIHRoZSB0cmFuc2Zvcm0gc3RyZWFtLlxuICAgICAqL1xuICAgIGdldCByZWFkYWJsZSgpIHtcbiAgICAgICAgaWYgKCFJc1RyYW5zZm9ybVN0cmVhbSh0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbiQyKCdyZWFkYWJsZScpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9yZWFkYWJsZTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogVGhlIHdyaXRhYmxlIHNpZGUgb2YgdGhlIHRyYW5zZm9ybSBzdHJlYW0uXG4gICAgICovXG4gICAgZ2V0IHdyaXRhYmxlKCkge1xuICAgICAgICBpZiAoIUlzVHJhbnNmb3JtU3RyZWFtKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uJDIoJ3dyaXRhYmxlJyk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIHRoaXMuX3dyaXRhYmxlO1xuICAgIH1cbn1cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKFRyYW5zZm9ybVN0cmVhbS5wcm90b3R5cGUsIHtcbiAgICByZWFkYWJsZTogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgd3JpdGFibGU6IHsgZW51bWVyYWJsZTogdHJ1ZSB9XG59KTtcbmlmICh0eXBlb2YgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcgPT09ICdzeW1ib2wnKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFRyYW5zZm9ybVN0cmVhbS5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnVHJhbnNmb3JtU3RyZWFtJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG5mdW5jdGlvbiBJbml0aWFsaXplVHJhbnNmb3JtU3RyZWFtKHN0cmVhbSwgc3RhcnRQcm9taXNlLCB3cml0YWJsZUhpZ2hXYXRlck1hcmssIHdyaXRhYmxlU2l6ZUFsZ29yaXRobSwgcmVhZGFibGVIaWdoV2F0ZXJNYXJrLCByZWFkYWJsZVNpemVBbGdvcml0aG0pIHtcbiAgICBmdW5jdGlvbiBzdGFydEFsZ29yaXRobSgpIHtcbiAgICAgICAgcmV0dXJuIHN0YXJ0UHJvbWlzZTtcbiAgICB9XG4gICAgZnVuY3Rpb24gd3JpdGVBbGdvcml0aG0oY2h1bmspIHtcbiAgICAgICAgcmV0dXJuIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRTaW5rV3JpdGVBbGdvcml0aG0oc3RyZWFtLCBjaHVuayk7XG4gICAgfVxuICAgIGZ1bmN0aW9uIGFib3J0QWxnb3JpdGhtKHJlYXNvbikge1xuICAgICAgICByZXR1cm4gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNpbmtBYm9ydEFsZ29yaXRobShzdHJlYW0sIHJlYXNvbik7XG4gICAgfVxuICAgIGZ1bmN0aW9uIGNsb3NlQWxnb3JpdGhtKCkge1xuICAgICAgICByZXR1cm4gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNpbmtDbG9zZUFsZ29yaXRobShzdHJlYW0pO1xuICAgIH1cbiAgICBzdHJlYW0uX3dyaXRhYmxlID0gQ3JlYXRlV3JpdGFibGVTdHJlYW0oc3RhcnRBbGdvcml0aG0sIHdyaXRlQWxnb3JpdGhtLCBjbG9zZUFsZ29yaXRobSwgYWJvcnRBbGdvcml0aG0sIHdyaXRhYmxlSGlnaFdhdGVyTWFyaywgd3JpdGFibGVTaXplQWxnb3JpdGhtKTtcbiAgICBmdW5jdGlvbiBwdWxsQWxnb3JpdGhtKCkge1xuICAgICAgICByZXR1cm4gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNvdXJjZVB1bGxBbGdvcml0aG0oc3RyZWFtKTtcbiAgICB9XG4gICAgZnVuY3Rpb24gY2FuY2VsQWxnb3JpdGhtKHJlYXNvbikge1xuICAgICAgICBUcmFuc2Zvcm1TdHJlYW1FcnJvcldyaXRhYmxlQW5kVW5ibG9ja1dyaXRlKHN0cmVhbSwgcmVhc29uKTtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICB9XG4gICAgc3RyZWFtLl9yZWFkYWJsZSA9IENyZWF0ZVJlYWRhYmxlU3RyZWFtKHN0YXJ0QWxnb3JpdGhtLCBwdWxsQWxnb3JpdGhtLCBjYW5jZWxBbGdvcml0aG0sIHJlYWRhYmxlSGlnaFdhdGVyTWFyaywgcmVhZGFibGVTaXplQWxnb3JpdGhtKTtcbiAgICAvLyBUaGUgW1tiYWNrcHJlc3N1cmVdXSBzbG90IGlzIHNldCB0byB1bmRlZmluZWQgc28gdGhhdCBpdCBjYW4gYmUgaW5pdGlhbGlzZWQgYnkgVHJhbnNmb3JtU3RyZWFtU2V0QmFja3ByZXNzdXJlLlxuICAgIHN0cmVhbS5fYmFja3ByZXNzdXJlID0gdW5kZWZpbmVkO1xuICAgIHN0cmVhbS5fYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZSA9IHVuZGVmaW5lZDtcbiAgICBzdHJlYW0uX2JhY2twcmVzc3VyZUNoYW5nZVByb21pc2VfcmVzb2x2ZSA9IHVuZGVmaW5lZDtcbiAgICBUcmFuc2Zvcm1TdHJlYW1TZXRCYWNrcHJlc3N1cmUoc3RyZWFtLCB0cnVlKTtcbiAgICBzdHJlYW0uX3RyYW5zZm9ybVN0cmVhbUNvbnRyb2xsZXIgPSB1bmRlZmluZWQ7XG59XG5mdW5jdGlvbiBJc1RyYW5zZm9ybVN0cmVhbSh4KSB7XG4gICAgaWYgKCF0eXBlSXNPYmplY3QoeCkpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICBpZiAoIU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbCh4LCAnX3RyYW5zZm9ybVN0cmVhbUNvbnRyb2xsZXInKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuLy8gVGhpcyBpcyBhIG5vLW9wIGlmIGJvdGggc2lkZXMgYXJlIGFscmVhZHkgZXJyb3JlZC5cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbUVycm9yKHN0cmVhbSwgZSkge1xuICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihzdHJlYW0uX3JlYWRhYmxlLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIsIGUpO1xuICAgIFRyYW5zZm9ybVN0cmVhbUVycm9yV3JpdGFibGVBbmRVbmJsb2NrV3JpdGUoc3RyZWFtLCBlKTtcbn1cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbUVycm9yV3JpdGFibGVBbmRVbmJsb2NrV3JpdGUoc3RyZWFtLCBlKSB7XG4gICAgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoc3RyZWFtLl90cmFuc2Zvcm1TdHJlYW1Db250cm9sbGVyKTtcbiAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3JJZk5lZWRlZChzdHJlYW0uX3dyaXRhYmxlLl93cml0YWJsZVN0cmVhbUNvbnRyb2xsZXIsIGUpO1xuICAgIGlmIChzdHJlYW0uX2JhY2twcmVzc3VyZSkge1xuICAgICAgICAvLyBQcmV0ZW5kIHRoYXQgcHVsbCgpIHdhcyBjYWxsZWQgdG8gcGVybWl0IGFueSBwZW5kaW5nIHdyaXRlKCkgY2FsbHMgdG8gY29tcGxldGUuIFRyYW5zZm9ybVN0cmVhbVNldEJhY2twcmVzc3VyZSgpXG4gICAgICAgIC8vIGNhbm5vdCBiZSBjYWxsZWQgZnJvbSBlbnF1ZXVlKCkgb3IgcHVsbCgpIG9uY2UgdGhlIFJlYWRhYmxlU3RyZWFtIGlzIGVycm9yZWQsIHNvIHRoaXMgd2lsbCB3aWxsIGJlIHRoZSBmaW5hbCB0aW1lXG4gICAgICAgIC8vIF9iYWNrcHJlc3N1cmUgaXMgc2V0LlxuICAgICAgICBUcmFuc2Zvcm1TdHJlYW1TZXRCYWNrcHJlc3N1cmUoc3RyZWFtLCBmYWxzZSk7XG4gICAgfVxufVxuZnVuY3Rpb24gVHJhbnNmb3JtU3RyZWFtU2V0QmFja3ByZXNzdXJlKHN0cmVhbSwgYmFja3ByZXNzdXJlKSB7XG4gICAgLy8gUGFzc2VzIGFsc28gd2hlbiBjYWxsZWQgZHVyaW5nIGNvbnN0cnVjdGlvbi5cbiAgICBpZiAoc3RyZWFtLl9iYWNrcHJlc3N1cmVDaGFuZ2VQcm9taXNlICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgc3RyZWFtLl9iYWNrcHJlc3N1cmVDaGFuZ2VQcm9taXNlX3Jlc29sdmUoKTtcbiAgICB9XG4gICAgc3RyZWFtLl9iYWNrcHJlc3N1cmVDaGFuZ2VQcm9taXNlID0gbmV3UHJvbWlzZShyZXNvbHZlID0+IHtcbiAgICAgICAgc3RyZWFtLl9iYWNrcHJlc3N1cmVDaGFuZ2VQcm9taXNlX3Jlc29sdmUgPSByZXNvbHZlO1xuICAgIH0pO1xuICAgIHN0cmVhbS5fYmFja3ByZXNzdXJlID0gYmFja3ByZXNzdXJlO1xufVxuLy8gQ2xhc3MgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJcbi8qKlxuICogQWxsb3dzIGNvbnRyb2wgb2YgdGhlIHtAbGluayBSZWFkYWJsZVN0cmVhbX0gYW5kIHtAbGluayBXcml0YWJsZVN0cmVhbX0gb2YgdGhlIGFzc29jaWF0ZWQge0BsaW5rIFRyYW5zZm9ybVN0cmVhbX0uXG4gKlxuICogQHB1YmxpY1xuICovXG5jbGFzcyBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlciB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0lsbGVnYWwgY29uc3RydWN0b3InKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB0aGUgZGVzaXJlZCBzaXplIHRvIGZpbGwgdGhlIHJlYWRhYmxlIHNpZGXigJlzIGludGVybmFsIHF1ZXVlLiBJdCBjYW4gYmUgbmVnYXRpdmUsIGlmIHRoZSBxdWV1ZSBpcyBvdmVyLWZ1bGwuXG4gICAgICovXG4gICAgZ2V0IGRlc2lyZWRTaXplKCkge1xuICAgICAgICBpZiAoIUlzVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGRlZmF1bHRDb250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbiQxKCdkZXNpcmVkU2l6ZScpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IHJlYWRhYmxlQ29udHJvbGxlciA9IHRoaXMuX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW0uX3JlYWRhYmxlLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXI7XG4gICAgICAgIHJldHVybiBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0RGVzaXJlZFNpemUocmVhZGFibGVDb250cm9sbGVyKTtcbiAgICB9XG4gICAgZW5xdWV1ZShjaHVuayA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGRlZmF1bHRDb250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbiQxKCdlbnF1ZXVlJyk7XG4gICAgICAgIH1cbiAgICAgICAgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlKHRoaXMsIGNodW5rKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogRXJyb3JzIGJvdGggdGhlIHJlYWRhYmxlIHNpZGUgYW5kIHRoZSB3cml0YWJsZSBzaWRlIG9mIHRoZSBjb250cm9sbGVkIHRyYW5zZm9ybSBzdHJlYW0sIG1ha2luZyBhbGwgZnV0dXJlXG4gICAgICogaW50ZXJhY3Rpb25zIHdpdGggaXQgZmFpbCB3aXRoIHRoZSBnaXZlbiBlcnJvciBgZWAuIEFueSBjaHVua3MgcXVldWVkIGZvciB0cmFuc2Zvcm1hdGlvbiB3aWxsIGJlIGRpc2NhcmRlZC5cbiAgICAgKi9cbiAgICBlcnJvcihyZWFzb24gPSB1bmRlZmluZWQpIHtcbiAgICAgICAgaWYgKCFJc1RyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBkZWZhdWx0Q29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24kMSgnZXJyb3InKTtcbiAgICAgICAgfVxuICAgICAgICBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlckVycm9yKHRoaXMsIHJlYXNvbik7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIENsb3NlcyB0aGUgcmVhZGFibGUgc2lkZSBhbmQgZXJyb3JzIHRoZSB3cml0YWJsZSBzaWRlIG9mIHRoZSBjb250cm9sbGVkIHRyYW5zZm9ybSBzdHJlYW0uIFRoaXMgaXMgdXNlZnVsIHdoZW4gdGhlXG4gICAgICogdHJhbnNmb3JtZXIgb25seSBuZWVkcyB0byBjb25zdW1lIGEgcG9ydGlvbiBvZiB0aGUgY2h1bmtzIHdyaXR0ZW4gdG8gdGhlIHdyaXRhYmxlIHNpZGUuXG4gICAgICovXG4gICAgdGVybWluYXRlKCkge1xuICAgICAgICBpZiAoIUlzVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGRlZmF1bHRDb250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbiQxKCd0ZXJtaW5hdGUnKTtcbiAgICAgICAgfVxuICAgICAgICBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlclRlcm1pbmF0ZSh0aGlzKTtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlci5wcm90b3R5cGUsIHtcbiAgICBlbnF1ZXVlOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBlcnJvcjogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgdGVybWluYXRlOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBkZXNpcmVkU2l6ZTogeyBlbnVtZXJhYmxlOiB0cnVlIH1cbn0pO1xuaWYgKHR5cGVvZiBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZyA9PT0gJ3N5bWJvbCcpIHtcbiAgICBPYmplY3QuZGVmaW5lUHJvcGVydHkoVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIucHJvdG90eXBlLCBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZywge1xuICAgICAgICB2YWx1ZTogJ1RyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBUcmFuc2Zvcm0gU3RyZWFtIERlZmF1bHQgQ29udHJvbGxlciBBYnN0cmFjdCBPcGVyYXRpb25zXG5mdW5jdGlvbiBJc1RyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfY29udHJvbGxlZFRyYW5zZm9ybVN0cmVhbScpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG5mdW5jdGlvbiBTZXRVcFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHN0cmVhbSwgY29udHJvbGxlciwgdHJhbnNmb3JtQWxnb3JpdGhtLCBmbHVzaEFsZ29yaXRobSkge1xuICAgIGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW0gPSBzdHJlYW07XG4gICAgc3RyZWFtLl90cmFuc2Zvcm1TdHJlYW1Db250cm9sbGVyID0gY29udHJvbGxlcjtcbiAgICBjb250cm9sbGVyLl90cmFuc2Zvcm1BbGdvcml0aG0gPSB0cmFuc2Zvcm1BbGdvcml0aG07XG4gICAgY29udHJvbGxlci5fZmx1c2hBbGdvcml0aG0gPSBmbHVzaEFsZ29yaXRobTtcbn1cbmZ1bmN0aW9uIFNldFVwVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJGcm9tVHJhbnNmb3JtZXIoc3RyZWFtLCB0cmFuc2Zvcm1lcikge1xuICAgIGNvbnN0IGNvbnRyb2xsZXIgPSBPYmplY3QuY3JlYXRlKFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyLnByb3RvdHlwZSk7XG4gICAgbGV0IHRyYW5zZm9ybUFsZ29yaXRobSA9IChjaHVuaykgPT4ge1xuICAgICAgICB0cnkge1xuICAgICAgICAgICAgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlKGNvbnRyb2xsZXIsIGNodW5rKTtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgICAgIH1cbiAgICAgICAgY2F0Y2ggKHRyYW5zZm9ybVJlc3VsdEUpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKHRyYW5zZm9ybVJlc3VsdEUpO1xuICAgICAgICB9XG4gICAgfTtcbiAgICBsZXQgZmx1c2hBbGdvcml0aG0gPSAoKSA9PiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgaWYgKHRyYW5zZm9ybWVyLnRyYW5zZm9ybSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHRyYW5zZm9ybUFsZ29yaXRobSA9IGNodW5rID0+IHRyYW5zZm9ybWVyLnRyYW5zZm9ybShjaHVuaywgY29udHJvbGxlcik7XG4gICAgfVxuICAgIGlmICh0cmFuc2Zvcm1lci5mbHVzaCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGZsdXNoQWxnb3JpdGhtID0gKCkgPT4gdHJhbnNmb3JtZXIuZmx1c2goY29udHJvbGxlcik7XG4gICAgfVxuICAgIFNldFVwVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIoc3RyZWFtLCBjb250cm9sbGVyLCB0cmFuc2Zvcm1BbGdvcml0aG0sIGZsdXNoQWxnb3JpdGhtKTtcbn1cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKGNvbnRyb2xsZXIpIHtcbiAgICBjb250cm9sbGVyLl90cmFuc2Zvcm1BbGdvcml0aG0gPSB1bmRlZmluZWQ7XG4gICAgY29udHJvbGxlci5fZmx1c2hBbGdvcml0aG0gPSB1bmRlZmluZWQ7XG59XG5mdW5jdGlvbiBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlckVucXVldWUoY29udHJvbGxlciwgY2h1bmspIHtcbiAgICBjb25zdCBzdHJlYW0gPSBjb250cm9sbGVyLl9jb250cm9sbGVkVHJhbnNmb3JtU3RyZWFtO1xuICAgIGNvbnN0IHJlYWRhYmxlQ29udHJvbGxlciA9IHN0cmVhbS5fcmVhZGFibGUuX3JlYWRhYmxlU3RyZWFtQ29udHJvbGxlcjtcbiAgICBpZiAoIVJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYW5DbG9zZU9yRW5xdWV1ZShyZWFkYWJsZUNvbnRyb2xsZXIpKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1JlYWRhYmxlIHNpZGUgaXMgbm90IGluIGEgc3RhdGUgdGhhdCBwZXJtaXRzIGVucXVldWUnKTtcbiAgICB9XG4gICAgLy8gV2UgdGhyb3R0bGUgdHJhbnNmb3JtIGludm9jYXRpb25zIGJhc2VkIG9uIHRoZSBiYWNrcHJlc3N1cmUgb2YgdGhlIFJlYWRhYmxlU3RyZWFtLCBidXQgd2Ugc3RpbGxcbiAgICAvLyBhY2NlcHQgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlKCkgY2FsbHMuXG4gICAgdHJ5IHtcbiAgICAgICAgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVucXVldWUocmVhZGFibGVDb250cm9sbGVyLCBjaHVuayk7XG4gICAgfVxuICAgIGNhdGNoIChlKSB7XG4gICAgICAgIC8vIFRoaXMgaGFwcGVucyB3aGVuIHJlYWRhYmxlU3RyYXRlZ3kuc2l6ZSgpIHRocm93cy5cbiAgICAgICAgVHJhbnNmb3JtU3RyZWFtRXJyb3JXcml0YWJsZUFuZFVuYmxvY2tXcml0ZShzdHJlYW0sIGUpO1xuICAgICAgICB0aHJvdyBzdHJlYW0uX3JlYWRhYmxlLl9zdG9yZWRFcnJvcjtcbiAgICB9XG4gICAgY29uc3QgYmFja3ByZXNzdXJlID0gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckhhc0JhY2twcmVzc3VyZShyZWFkYWJsZUNvbnRyb2xsZXIpO1xuICAgIGlmIChiYWNrcHJlc3N1cmUgIT09IHN0cmVhbS5fYmFja3ByZXNzdXJlKSB7XG4gICAgICAgIFRyYW5zZm9ybVN0cmVhbVNldEJhY2twcmVzc3VyZShzdHJlYW0sIHRydWUpO1xuICAgIH1cbn1cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3IoY29udHJvbGxlciwgZSkge1xuICAgIFRyYW5zZm9ybVN0cmVhbUVycm9yKGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW0sIGUpO1xufVxuZnVuY3Rpb24gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJQZXJmb3JtVHJhbnNmb3JtKGNvbnRyb2xsZXIsIGNodW5rKSB7XG4gICAgY29uc3QgdHJhbnNmb3JtUHJvbWlzZSA9IGNvbnRyb2xsZXIuX3RyYW5zZm9ybUFsZ29yaXRobShjaHVuayk7XG4gICAgcmV0dXJuIHRyYW5zZm9ybVByb21pc2VXaXRoKHRyYW5zZm9ybVByb21pc2UsIHVuZGVmaW5lZCwgciA9PiB7XG4gICAgICAgIFRyYW5zZm9ybVN0cmVhbUVycm9yKGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW0sIHIpO1xuICAgICAgICB0aHJvdyByO1xuICAgIH0pO1xufVxuZnVuY3Rpb24gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJUZXJtaW5hdGUoY29udHJvbGxlcikge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW07XG4gICAgY29uc3QgcmVhZGFibGVDb250cm9sbGVyID0gc3RyZWFtLl9yZWFkYWJsZS5fcmVhZGFibGVTdHJlYW1Db250cm9sbGVyO1xuICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbG9zZShyZWFkYWJsZUNvbnRyb2xsZXIpO1xuICAgIGNvbnN0IGVycm9yID0gbmV3IFR5cGVFcnJvcignVHJhbnNmb3JtU3RyZWFtIHRlcm1pbmF0ZWQnKTtcbiAgICBUcmFuc2Zvcm1TdHJlYW1FcnJvcldyaXRhYmxlQW5kVW5ibG9ja1dyaXRlKHN0cmVhbSwgZXJyb3IpO1xufVxuLy8gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNpbmsgQWxnb3JpdGhtc1xuZnVuY3Rpb24gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNpbmtXcml0ZUFsZ29yaXRobShzdHJlYW0sIGNodW5rKSB7XG4gICAgY29uc3QgY29udHJvbGxlciA9IHN0cmVhbS5fdHJhbnNmb3JtU3RyZWFtQ29udHJvbGxlcjtcbiAgICBpZiAoc3RyZWFtLl9iYWNrcHJlc3N1cmUpIHtcbiAgICAgICAgY29uc3QgYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZSA9IHN0cmVhbS5fYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZTtcbiAgICAgICAgcmV0dXJuIHRyYW5zZm9ybVByb21pc2VXaXRoKGJhY2twcmVzc3VyZUNoYW5nZVByb21pc2UsICgpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHdyaXRhYmxlID0gc3RyZWFtLl93cml0YWJsZTtcbiAgICAgICAgICAgIGNvbnN0IHN0YXRlID0gd3JpdGFibGUuX3N0YXRlO1xuICAgICAgICAgICAgaWYgKHN0YXRlID09PSAnZXJyb3JpbmcnKSB7XG4gICAgICAgICAgICAgICAgdGhyb3cgd3JpdGFibGUuX3N0b3JlZEVycm9yO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmV0dXJuIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyUGVyZm9ybVRyYW5zZm9ybShjb250cm9sbGVyLCBjaHVuayk7XG4gICAgICAgIH0pO1xuICAgIH1cbiAgICByZXR1cm4gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJQZXJmb3JtVHJhbnNmb3JtKGNvbnRyb2xsZXIsIGNodW5rKTtcbn1cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRTaW5rQWJvcnRBbGdvcml0aG0oc3RyZWFtLCByZWFzb24pIHtcbiAgICAvLyBhYm9ydCgpIGlzIG5vdCBjYWxsZWQgc3luY2hyb25vdXNseSwgc28gaXQgaXMgcG9zc2libGUgZm9yIGFib3J0KCkgdG8gYmUgY2FsbGVkIHdoZW4gdGhlIHN0cmVhbSBpcyBhbHJlYWR5XG4gICAgLy8gZXJyb3JlZC5cbiAgICBUcmFuc2Zvcm1TdHJlYW1FcnJvcihzdHJlYW0sIHJlYXNvbik7XG4gICAgcmV0dXJuIHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbn1cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRTaW5rQ2xvc2VBbGdvcml0aG0oc3RyZWFtKSB7XG4gICAgLy8gc3RyZWFtLl9yZWFkYWJsZSBjYW5ub3QgY2hhbmdlIGFmdGVyIGNvbnN0cnVjdGlvbiwgc28gY2FjaGluZyBpdCBhY3Jvc3MgYSBjYWxsIHRvIHVzZXIgY29kZSBpcyBzYWZlLlxuICAgIGNvbnN0IHJlYWRhYmxlID0gc3RyZWFtLl9yZWFkYWJsZTtcbiAgICBjb25zdCBjb250cm9sbGVyID0gc3RyZWFtLl90cmFuc2Zvcm1TdHJlYW1Db250cm9sbGVyO1xuICAgIGNvbnN0IGZsdXNoUHJvbWlzZSA9IGNvbnRyb2xsZXIuX2ZsdXNoQWxnb3JpdGhtKCk7XG4gICAgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoY29udHJvbGxlcik7XG4gICAgLy8gUmV0dXJuIGEgcHJvbWlzZSB0aGF0IGlzIGZ1bGZpbGxlZCB3aXRoIHVuZGVmaW5lZCBvbiBzdWNjZXNzLlxuICAgIHJldHVybiB0cmFuc2Zvcm1Qcm9taXNlV2l0aChmbHVzaFByb21pc2UsICgpID0+IHtcbiAgICAgICAgaWYgKHJlYWRhYmxlLl9zdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgICAgICB0aHJvdyByZWFkYWJsZS5fc3RvcmVkRXJyb3I7XG4gICAgICAgIH1cbiAgICAgICAgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsb3NlKHJlYWRhYmxlLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIpO1xuICAgIH0sIHIgPT4ge1xuICAgICAgICBUcmFuc2Zvcm1TdHJlYW1FcnJvcihzdHJlYW0sIHIpO1xuICAgICAgICB0aHJvdyByZWFkYWJsZS5fc3RvcmVkRXJyb3I7XG4gICAgfSk7XG59XG4vLyBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0U291cmNlIEFsZ29yaXRobXNcbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRTb3VyY2VQdWxsQWxnb3JpdGhtKHN0cmVhbSkge1xuICAgIC8vIEludmFyaWFudC4gRW5mb3JjZWQgYnkgdGhlIHByb21pc2VzIHJldHVybmVkIGJ5IHN0YXJ0KCkgYW5kIHB1bGwoKS5cbiAgICBUcmFuc2Zvcm1TdHJlYW1TZXRCYWNrcHJlc3N1cmUoc3RyZWFtLCBmYWxzZSk7XG4gICAgLy8gUHJldmVudCB0aGUgbmV4dCBwdWxsKCkgY2FsbCB1bnRpbCB0aGVyZSBpcyBiYWNrcHJlc3N1cmUuXG4gICAgcmV0dXJuIHN0cmVhbS5fYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlci5cbmZ1bmN0aW9uIGRlZmF1bHRDb250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbiQxKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIucHJvdG90eXBlLiR7bmFtZX0gY2FuIG9ubHkgYmUgdXNlZCBvbiBhIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyYCk7XG59XG4vLyBIZWxwZXIgZnVuY3Rpb25zIGZvciB0aGUgVHJhbnNmb3JtU3RyZWFtLlxuZnVuY3Rpb24gc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbiQyKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgVHJhbnNmb3JtU3RyZWFtLnByb3RvdHlwZS4ke25hbWV9IGNhbiBvbmx5IGJlIHVzZWQgb24gYSBUcmFuc2Zvcm1TdHJlYW1gKTtcbn1cblxuZXhwb3J0IHsgQnl0ZUxlbmd0aFF1ZXVpbmdTdHJhdGVneSwgQ291bnRRdWV1aW5nU3RyYXRlZ3ksIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIsIFJlYWRhYmxlU3RyZWFtLCBSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIsIFJlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QsIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIsIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlciwgVHJhbnNmb3JtU3RyZWFtLCBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlciwgV3JpdGFibGVTdHJlYW0sIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIsIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlciB9O1xuLy8jIHNvdXJjZU1hcHBpbmdVUkw9cG9ueWZpbGwuZXMyMDE4Lm1qcy5tYXBcbiIsIm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZShcImNyeXB0b1wiKTs7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwiaHR0cFwiKTs7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwiaHR0cHNcIik7OyIsIm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZShcInN0cmVhbVwiKTs7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwidXJsXCIpOzsiLCJtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJ1dGlsXCIpOzsiLCJtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJ6bGliXCIpOzsiLCIvLyBUaGUgbW9kdWxlIGNhY2hlXG52YXIgX193ZWJwYWNrX21vZHVsZV9jYWNoZV9fID0ge307XG5cbi8vIFRoZSByZXF1aXJlIGZ1bmN0aW9uXG5mdW5jdGlvbiBfX3dlYnBhY2tfcmVxdWlyZV9fKG1vZHVsZUlkKSB7XG5cdC8vIENoZWNrIGlmIG1vZHVsZSBpcyBpbiBjYWNoZVxuXHRpZihfX3dlYnBhY2tfbW9kdWxlX2NhY2hlX19bbW9kdWxlSWRdKSB7XG5cdFx0cmV0dXJuIF9fd2VicGFja19tb2R1bGVfY2FjaGVfX1ttb2R1bGVJZF0uZXhwb3J0cztcblx0fVxuXHQvLyBDcmVhdGUgYSBuZXcgbW9kdWxlIChhbmQgcHV0IGl0IGludG8gdGhlIGNhY2hlKVxuXHR2YXIgbW9kdWxlID0gX193ZWJwYWNrX21vZHVsZV9jYWNoZV9fW21vZHVsZUlkXSA9IHtcblx0XHQvLyBubyBtb2R1bGUuaWQgbmVlZGVkXG5cdFx0Ly8gbm8gbW9kdWxlLmxvYWRlZCBuZWVkZWRcblx0XHRleHBvcnRzOiB7fVxuXHR9O1xuXG5cdC8vIEV4ZWN1dGUgdGhlIG1vZHVsZSBmdW5jdGlvblxuXHRfX3dlYnBhY2tfbW9kdWxlc19fW21vZHVsZUlkXS5jYWxsKG1vZHVsZS5leHBvcnRzLCBtb2R1bGUsIG1vZHVsZS5leHBvcnRzLCBfX3dlYnBhY2tfcmVxdWlyZV9fKTtcblxuXHQvLyBSZXR1cm4gdGhlIGV4cG9ydHMgb2YgdGhlIG1vZHVsZVxuXHRyZXR1cm4gbW9kdWxlLmV4cG9ydHM7XG59XG5cbiIsIi8vIG1vZHVsZSBleHBvcnRzIG11c3QgYmUgcmV0dXJuZWQgZnJvbSBydW50aW1lIHNvIGVudHJ5IGlubGluaW5nIGlzIGRpc2FibGVkXG4vLyBzdGFydHVwXG4vLyBMb2FkIGVudHJ5IG1vZHVsZSBhbmQgcmV0dXJuIGV4cG9ydHNcbnJldHVybiBfX3dlYnBhY2tfcmVxdWlyZV9fKDk5MCk7XG4iLCIvLyBkZWZpbmUgZ2V0dGVyIGZ1bmN0aW9ucyBmb3IgaGFybW9ueSBleHBvcnRzXG5fX3dlYnBhY2tfcmVxdWlyZV9fLmQgPSAoZXhwb3J0cywgZGVmaW5pdGlvbikgPT4ge1xuXHRmb3IodmFyIGtleSBpbiBkZWZpbml0aW9uKSB7XG5cdFx0aWYoX193ZWJwYWNrX3JlcXVpcmVfXy5vKGRlZmluaXRpb24sIGtleSkgJiYgIV9fd2VicGFja19yZXF1aXJlX18ubyhleHBvcnRzLCBrZXkpKSB7XG5cdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywga2V5LCB7IGVudW1lcmFibGU6IHRydWUsIGdldDogZGVmaW5pdGlvbltrZXldIH0pO1xuXHRcdH1cblx0fVxufTsiLCJfX3dlYnBhY2tfcmVxdWlyZV9fLm8gPSAob2JqLCBwcm9wKSA9PiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqLCBwcm9wKSIsIi8vIGRlZmluZSBfX2VzTW9kdWxlIG9uIGV4cG9ydHNcbl9fd2VicGFja19yZXF1aXJlX18uciA9IChleHBvcnRzKSA9PiB7XG5cdGlmKHR5cGVvZiBTeW1ib2wgIT09ICd1bmRlZmluZWQnICYmIFN5bWJvbC50b1N0cmluZ1RhZykge1xuXHRcdE9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCBTeW1ib2wudG9TdHJpbmdUYWcsIHsgdmFsdWU6ICdNb2R1bGUnIH0pO1xuXHR9XG5cdE9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCAnX19lc01vZHVsZScsIHsgdmFsdWU6IHRydWUgfSk7XG59OyJdLCJzb3VyY2VSb290IjoiIn0= + +/***/ }), + +/***/ 622: +/***/ (function(module) { + +module.exports = require("path"); + +/***/ }), + +/***/ 669: +/***/ (function(module) { + +module.exports = require("util"); + +/***/ }), + +/***/ 747: +/***/ (function(module) { + +module.exports = require("fs"); + +/***/ }), + +/***/ 761: +/***/ (function(module) { + +module.exports = require("zlib"); + +/***/ }), + +/***/ 762: +/***/ (function(module) { + +// API +module.exports = abort; + +/** + * Aborts leftover active jobs + * + * @param {object} state - current state object + */ +function abort(state) +{ + Object.keys(state.jobs).forEach(clean.bind(state)); + + // reset leftover jobs + state.jobs = {}; +} + +/** + * Cleans up leftover job by invoking abort function for the provided job id + * + * @this state + * @param {string|number} key - job id to abort + */ +function clean(key) +{ + if (typeof this.jobs[key] == 'function') + { + this.jobs[key](); + } +} + + +/***/ }), + +/***/ 769: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; +/*! + * mime-types + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + + + +/** + * Module dependencies. + * @private + */ + +var db = __webpack_require__(128) +var extname = __webpack_require__(622).extname + +/** + * Module variables. + * @private + */ + +var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/ +var TEXT_TYPE_REGEXP = /^text\//i + +/** + * Module exports. + * @public + */ + +exports.charset = charset +exports.charsets = { lookup: charset } +exports.contentType = contentType +exports.extension = extension +exports.extensions = Object.create(null) +exports.lookup = lookup +exports.types = Object.create(null) + +// Populate the extensions/types maps +populateMaps(exports.extensions, exports.types) + +/** + * Get the default charset for a MIME type. + * + * @param {string} type + * @return {boolean|string} + */ + +function charset (type) { + if (!type || typeof type !== 'string') { + return false + } + + // TODO: use media-typer + var match = EXTRACT_TYPE_REGEXP.exec(type) + var mime = match && db[match[1].toLowerCase()] + + if (mime && mime.charset) { + return mime.charset + } + + // default text/* to utf-8 + if (match && TEXT_TYPE_REGEXP.test(match[1])) { + return 'UTF-8' + } + + return false +} + +/** + * Create a full Content-Type header given a MIME type or extension. + * + * @param {string} str + * @return {boolean|string} + */ + +function contentType (str) { + // TODO: should this even be in this module? + if (!str || typeof str !== 'string') { + return false + } + + var mime = str.indexOf('/') === -1 + ? exports.lookup(str) + : str + + if (!mime) { + return false + } + + // TODO: use content-type or other module + if (mime.indexOf('charset') === -1) { + var charset = exports.charset(mime) + if (charset) mime += '; charset=' + charset.toLowerCase() + } + + return mime +} + +/** + * Get the default extension for a MIME type. + * + * @param {string} type + * @return {boolean|string} + */ + +function extension (type) { + if (!type || typeof type !== 'string') { + return false + } + + // TODO: use media-typer + var match = EXTRACT_TYPE_REGEXP.exec(type) + + // get extensions + var exts = match && exports.extensions[match[1].toLowerCase()] + + if (!exts || !exts.length) { + return false + } + + return exts[0] +} + +/** + * Lookup the MIME type for a file path/extension. + * + * @param {string} path + * @return {boolean|string} + */ + +function lookup (path) { + if (!path || typeof path !== 'string') { + return false + } + + // get the extension ("ext" or ".ext" or full path) + var extension = extname('x.' + path) + .toLowerCase() + .substr(1) + + if (!extension) { + return false + } + + return exports.types[extension] || false +} + +/** + * Populate the extensions and types maps. + * @private + */ + +function populateMaps (extensions, types) { + // source preference (least -> most) + var preference = ['nginx', 'apache', undefined, 'iana'] + + Object.keys(db).forEach(function forEachMimeType (type) { + var mime = db[type] + var exts = mime.extensions + + if (!exts || !exts.length) { + return + } + + // mime -> extensions + extensions[type] = exts + + // extension -> mime + for (var i = 0; i < exts.length; i++) { + var extension = exts[i] + + if (types[extension]) { + var from = preference.indexOf(db[types[extension]].source) + var to = preference.indexOf(mime.source) + + if (types[extension] !== 'application/octet-stream' && + (from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) { + // skip the remapping + continue + } + } + + // set the extension -> mime + types[extension] = type + } + }) +} + + +/***/ }), + +/***/ 792: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var defer = __webpack_require__(154); + +// API +module.exports = async; + +/** + * Runs provided callback asynchronously + * even if callback itself is not + * + * @param {function} callback - callback to invoke + * @returns {function} - augmented callback + */ +function async(callback) +{ + var isAsync = false; + + // check if async happened + defer(function() { isAsync = true; }); + + return function async_callback(err, result) + { + if (isAsync) + { + callback(err, result); + } + else + { + defer(function nextTick_callback() + { + callback(err, result); + }); + } + }; +} + + +/***/ }), + +/***/ 835: +/***/ (function(module) { + +module.exports = require("url"); + +/***/ }) + +/******/ }); \ No newline at end of file diff --git a/.github/actions/send-email/index.js b/.github/actions/send-email/index.js new file mode 100644 index 0000000000..c898a55d8d --- /dev/null +++ b/.github/actions/send-email/index.js @@ -0,0 +1,75 @@ +/*! + * 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. + */ + +const core = require('@actions/core'); +const formData = require('form-data'); +const Mailgun = require('mailgun.js'); + +const mailgun = new Mailgun(formData); +const optionalFields = ['cc', 'text', 'html']; + +function loadConfig() { + return { + apiKey: core.getInput('api-key'), + domain: core.getInput('domain'), + to: core.getInput('to'), + from: core.getInput('from'), + cc: core.getInput('cc'), + subject: core.getInput('subject'), + text: core.getInput('text'), + html: core.getInput('html'), + } +} + +function validate(config) { + for (param in config) { + if (optionalFields.includes(param)) { + continue; + } + validateRequiredParameter(config[param], `'${param}'`); + } +} + +function validateRequiredParameter(value, name) { + if (!isNonEmptyString(value)) { + throw new Error(`${name} must be a non-empty string.`); + } +} + +function sendEmail(config) { + const mg = mailgun.client({ + username: 'api', + key: config.apiKey, + }); + + return mg.messages + .create(domain, config) + .then((resp) => { + core.setOutput('response', resp.message); + return; + }) + .catch((err) => { + core.setFailed(err.message); + }); +} + +function isNonEmptyString(value) { + return typeof value === 'string' && value !== ''; +} + +const config = loadConfig(); +validate(config); +sendEmail(config); diff --git a/.github/actions/send-email/package-lock.json b/.github/actions/send-email/package-lock.json new file mode 100644 index 0000000000..f75907db07 --- /dev/null +++ b/.github/actions/send-email/package-lock.json @@ -0,0 +1,173 @@ +{ + "name": "send-email", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@actions/core": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", + "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" + }, + "@zeit/ncc": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@zeit/ncc/-/ncc-0.21.1.tgz", + "integrity": "sha512-M9WzgquSOt2nsjRkYM9LRylBLmmlwNCwYbm3Up3PDEshfvdmIfqpFNSK8EJvR18NwZjGHE5z2avlDtYQx2JQnw==", + "dev": true + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "fetch-blob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.1.tgz", + "integrity": "sha512-Uf+gxPCe1hTOFXwkxYyckn8iUSk6CFXGy5VENZKifovUTZC9eUODWSBhOBS7zICGrAetKzdwLMr85KhIcePMAQ==" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "ky": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.25.1.tgz", + "integrity": "sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA==" + }, + "ky-universal": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/ky-universal/-/ky-universal-0.8.2.tgz", + "integrity": "sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ==", + "requires": { + "abort-controller": "^3.0.0", + "node-fetch": "3.0.0-beta.9" + } + }, + "mailgun.js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/mailgun.js/-/mailgun.js-3.3.0.tgz", + "integrity": "sha512-Ikcl9Lp18oXu8/ht6Ow3b2yRYBEa4S70YvquOM2O4FA4NDboa8btZIMEQRRFAmBEfEbWOPrl/Z6E8kL8vnyonQ==", + "requires": { + "bluebird": "^3.7.2", + "btoa": "^1.1.2", + "ky": "^0.25.1", + "ky-universal": "^0.8.2", + "url": "^0.11.0", + "url-join": "0.0.1", + "web-streams-polyfill": "^3.0.1", + "webpack-merge": "^5.4.0" + } + }, + "node-fetch": { + "version": "3.0.0-beta.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0-beta.9.tgz", + "integrity": "sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==", + "requires": { + "data-uri-to-buffer": "^3.0.1", + "fetch-blob": "^2.1.1" + }, + "dependencies": { + "data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" + } + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "requires": { + "kind-of": "^6.0.2" + } + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=" + }, + "web-streams-polyfill": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.0.2.tgz", + "integrity": "sha512-JTNkNbAKoSo8NKiqu2UUaqRFCDWWZaCOsXuJEsToWopikTA0YHKKUf91GNkS/SnD8JixOkJjVsiacNlrFnRECA==" + }, + "webpack-merge": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz", + "integrity": "sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==", + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==" + } + } +} diff --git a/.github/actions/send-email/package.json b/.github/actions/send-email/package.json new file mode 100644 index 0000000000..678a63992a --- /dev/null +++ b/.github/actions/send-email/package.json @@ -0,0 +1,23 @@ +{ + "name": "send-email", + "version": "1.0.0", + "description": "Send Emails from GitHub Actions workflows using Mailgun.", + "main": "index.js", + "scripts": { + "pack": "ncc build" + }, + "keywords": [ + "Firebase", + "Release", + "Automation" + ], + "author": "Firebase (https://firebase.google.com/)", + "license": "Apache-2.0", + "dependencies": { + "@actions/core": "^1.2.6", + "mailgun.js": "^3.3.0" + }, + "devDependencies": { + "@zeit/ncc": "^0.21.1" + } +} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 10221dac24..6dd267cc3b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -66,3 +66,33 @@ jobs: with: name: dist path: dist + + - name: Send email on failure + if: failure() + uses: ./.github/actions/send-email + with: + api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} + domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} + from: 'GitHub ' + to: ${{ secrets.FIREBASE_ADMIN_GITHUB_EMAIL }} + subject: '[${{github.repository}}] Nightly build failed!' + html: > + Nightly workflow failed on: ${{github.repository}} +

    Navigate to the + failed workflow. + continue-on-error: true + + - name: Send email on cancelled + if: cancelled() + uses: ./.github/actions/send-email + with: + api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} + domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} + from: 'GitHub ' + to: ${{ secrets.FIREBASE_ADMIN_GITHUB_EMAIL }} + subject: '[${{github.repository}}] Nightly build got cancelled!' + html: > + Nightly workflow cancelled on: ${{github.repository}} +

    Navigate to the + cancelled workflow. + continue-on-error: true diff --git a/test/integration/remote-config.spec.ts b/test/integration/remote-config.spec.ts index f8773c1b3c..eebab8a770 100644 --- a/test/integration/remote-config.spec.ts +++ b/test/integration/remote-config.spec.ts @@ -88,6 +88,12 @@ describe('admin.remoteConfig', () => { }).to.throw('Cannot set property etag of # which has only a getter'); }); + // A failing integration test to trigger the send email action. + // Remove this once the testing is complete. + it('A failing integration test to trigger nightly email notifications', () => { + expect('a').to.be.equal('b'); + }); + describe('validateTemplate', () => { it('should succeed with a vaild template', () => { // set parameters, groups, and conditions From e9e8a0395dbc1842b347f8bda7ebf274262d740d Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 1 Apr 2021 13:52:50 -0400 Subject: [PATCH 092/160] chore: Fix bug in send-email action code (#1214) * chore: Fix bug in send-email action code * Add run id and trigger on repo dispatch event --- .github/actions/send-email/dist/index.js | 9 ++++++++- .github/actions/send-email/index.js | 9 ++++++++- .github/workflows/nightly.yml | 15 +++++++++------ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/actions/send-email/dist/index.js b/.github/actions/send-email/dist/index.js index 22a6566c3f..d1817b6db8 100644 --- a/.github/actions/send-email/dist/index.js +++ b/.github/actions/send-email/dist/index.js @@ -276,7 +276,14 @@ function sendEmail(config) { }); return mg.messages - .create(domain, config) + .create(config.domain, { + from: config.from, + to: config.to, + cc: config.cc, + subject: config.subject, + text: config.text, + html: config.html, + }) .then((resp) => { core.setOutput('response', resp.message); return; diff --git a/.github/actions/send-email/index.js b/.github/actions/send-email/index.js index c898a55d8d..38be9f04fc 100644 --- a/.github/actions/send-email/index.js +++ b/.github/actions/send-email/index.js @@ -56,7 +56,14 @@ function sendEmail(config) { }); return mg.messages - .create(domain, config) + .create(config.domain, { + from: config.from, + to: config.to, + cc: config.cc, + subject: config.subject, + text: config.text, + html: config.html, + }) .then((resp) => { core.setOutput('response', resp.message); return; diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6dd267cc3b..3f440bcc1f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,8 +16,11 @@ name: Nightly Builds on: # Runs every day at 06:00 AM (PT) and 08:00 PM (PT) / 04:00 AM (UTC) and 02:00 PM (UTC) + # or on 'firebase_build' repository dispatch event. schedule: - cron: "0 4,14 * * *" + repository_dispatch: + types: [firebase_build] jobs: nightly: @@ -75,11 +78,11 @@ jobs: domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} from: 'GitHub ' to: ${{ secrets.FIREBASE_ADMIN_GITHUB_EMAIL }} - subject: '[${{github.repository}}] Nightly build failed!' + subject: 'Nightly build ${{github.run_id}} of ${{github.repository}} failed!' html: > - Nightly workflow failed on: ${{github.repository}} + Nightly workflow ${{github.run_id}} failed on: ${{github.repository}}

    Navigate to the - failed workflow. + failed workflow. continue-on-error: true - name: Send email on cancelled @@ -90,9 +93,9 @@ jobs: domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} from: 'GitHub ' to: ${{ secrets.FIREBASE_ADMIN_GITHUB_EMAIL }} - subject: '[${{github.repository}}] Nightly build got cancelled!' + subject: 'Nightly build ${{github.run_id}} of ${{github.repository}} cancelled!' html: > - Nightly workflow cancelled on: ${{github.repository}} + Nightly workflow ${{github.run_id}} cancelled on: ${{github.repository}}

    Navigate to the - cancelled workflow. + cancelled workflow. continue-on-error: true From 1cc82ae629257d4d2d49ac6a100f152aef1e0b32 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 1 Apr 2021 14:05:10 -0400 Subject: [PATCH 093/160] Change dispatch event name in nightly workflow (#1216) - Change dispatch event name to `firebase_nightly_build` --- .github/workflows/nightly.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3f440bcc1f..a36144816b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,11 +16,11 @@ name: Nightly Builds on: # Runs every day at 06:00 AM (PT) and 08:00 PM (PT) / 04:00 AM (UTC) and 02:00 PM (UTC) - # or on 'firebase_build' repository dispatch event. + # or on 'firebase_nightly_build' repository dispatch event. schedule: - cron: "0 4,14 * * *" repository_dispatch: - types: [firebase_build] + types: [firebase_nightly_build] jobs: nightly: From d961c3f705a8259762a796ac4f4d6a6dd0992eb1 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 1 Apr 2021 18:08:56 -0400 Subject: [PATCH 094/160] chore: Clean up nightly workflow trigger tests (#1212) - Remove failing integration test added to trigger the send email workflow in nightly builds. --- test/integration/remote-config.spec.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/integration/remote-config.spec.ts b/test/integration/remote-config.spec.ts index eebab8a770..f8773c1b3c 100644 --- a/test/integration/remote-config.spec.ts +++ b/test/integration/remote-config.spec.ts @@ -88,12 +88,6 @@ describe('admin.remoteConfig', () => { }).to.throw('Cannot set property etag of # which has only a getter'); }); - // A failing integration test to trigger the send email action. - // Remove this once the testing is complete. - it('A failing integration test to trigger nightly email notifications', () => { - expect('a').to.be.equal('b'); - }); - describe('validateTemplate', () => { it('should succeed with a vaild template', () => { // set parameters, groups, and conditions From da0f44f7e2d80a8a3f79bd34959663a01d768c0e Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Thu, 8 Apr 2021 14:15:08 -0500 Subject: [PATCH 095/160] Add support for FIREBASE_STORAGE_EMULATOR_HOST env var (#1175) * Add support for FIREBASE_STORAGE_EMULATOR_HOST env var * Fixes lint error * Add test for FIREBASE_STORAGE_EMULATOR_HOST support * Lint fix * Minor fixes to storage tests * Address review comments * Address review suggestion Co-authored-by: Samuel Bushi --- src/storage/storage.ts | 4 ++++ test/unit/storage/storage.spec.ts | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 4364658a8c..aabf9dd30b 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -48,6 +48,10 @@ export class Storage implements StorageInterface { }); } + if (!process.env.STORAGE_EMULATOR_HOST && process.env.FIREBASE_STORAGE_EMULATOR_HOST) { + process.env.STORAGE_EMULATOR_HOST = process.env.FIREBASE_STORAGE_EMULATOR_HOST; + } + let storage: typeof StorageClient; try { storage = require('@google-cloud/storage').Storage; diff --git a/test/unit/storage/storage.spec.ts b/test/unit/storage/storage.spec.ts index 997d44846e..69ea28b217 100644 --- a/test/unit/storage/storage.spec.ts +++ b/test/unit/storage/storage.spec.ts @@ -113,4 +113,24 @@ describe('Storage', () => { expect(storage.bucket('foo').name).to.equal('foo'); }); }); + + describe('Emulator mode', () => { + const EMULATOR_HOST = 'http://localhost:9199'; + + before(() => { + delete process.env.STORAGE_EMULATOR_HOST; + process.env.FIREBASE_STORAGE_EMULATOR_HOST = EMULATOR_HOST; + }); + + it('sets STORAGE_EMULATOR_HOST if FIREBASE_STORAGE_EMULATOR_HOST is set', () => { + new Storage(mockApp); + + expect(process.env.STORAGE_EMULATOR_HOST).to.equal(EMULATOR_HOST); + }); + + after(() => { + delete process.env.STORAGE_EMULATOR_HOST; + delete process.env.FIREBASE_STORAGE_EMULATOR_HOST; + }); + }) }); From 011c53043896587e2d41228b3c8fdc463faff56a Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Wed, 14 Apr 2021 11:23:20 -0700 Subject: [PATCH 096/160] Revert "Disable one flaky tests in emulator. (#1205)" (#1227) This reverts commit 19660d921d20732857bf54393a09e8b5bce15d63. --- test/integration/auth.spec.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 53174f2ff5..29060b4cdd 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -441,12 +441,8 @@ describe('admin.auth', () => { .then((listUsersResult) => { // Confirm expected number of users. expect(listUsersResult.users.length).to.equal(2); - // TODO(yuchenshi): Investigate on why this is flaky in emulator. - if (!authEmulatorHost) { - // Confirm next page token present. - expect(typeof listUsersResult.pageToken).to.equal('string'); - } - + // Confirm next page token present. + expect(typeof listUsersResult.pageToken).to.equal('string'); // Confirm each user's uid and the hashed passwords. expect(listUsersResult.users[0].uid).to.equal(uids[1]); From 58f60d680bc47641ef0216fbe8eff07afb030e5b Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Fri, 23 Apr 2021 10:44:19 -0700 Subject: [PATCH 097/160] fix(rtdb): Fixing a token refresh livelock in Cloud Functions (#1234) --- src/database/database-internal.ts | 21 +++++++++------------ src/firebase-app.ts | 6 ++++++ test/unit/database/database.spec.ts | 4 +--- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/database/database-internal.ts b/src/database/database-internal.ts index 66d8db883c..9ad9e5bb51 100644 --- a/src/database/database-internal.ts +++ b/src/database/database-internal.ts @@ -116,18 +116,15 @@ export class DatabaseService { // eslint-disable-next-line @typescript-eslint/no-unused-vars private onTokenChange(_: string): void { - this.appInternal.INTERNAL.getToken() - .then((token) => { - const delayMillis = token.expirationTime - TOKEN_REFRESH_THRESHOLD_MILLIS - Date.now(); - // If the new token is set to expire soon (unlikely), do nothing. Somebody will eventually - // notice and refresh the token, at which point this callback will fire again. - if (delayMillis > 0) { - this.scheduleTokenRefresh(delayMillis); - } - }) - .catch((err) => { - console.error('Unexpected error while attempting to schedule a token refresh:', err); - }); + const token = this.appInternal.INTERNAL.getCachedToken(); + if (token) { + const delayMillis = token.expirationTime - TOKEN_REFRESH_THRESHOLD_MILLIS - Date.now(); + // If the new token is set to expire soon (unlikely), do nothing. Somebody will eventually + // notice and refresh the token, at which point this callback will fire again. + if (delayMillis > 0) { + this.scheduleTokenRefresh(delayMillis); + } + } } private scheduleTokenRefresh(delayMillis: number): void { diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 88947bec4c..c9e9588d59 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -74,6 +74,10 @@ export class FirebaseAppInternals { return Promise.resolve(this.cachedToken_); } + public getCachedToken(): FirebaseAccessToken | null { + return this.cachedToken_ || null; + } + private refreshToken(): Promise { return Promise.resolve(this.credential_.getAccessToken()) .then((result) => { @@ -97,6 +101,8 @@ export class FirebaseAppInternals { if (!this.cachedToken_ || this.cachedToken_.accessToken !== token.accessToken || this.cachedToken_.expirationTime !== token.expirationTime) { + // Update the cache before firing listeners. Listeners may directly query the + // cached token state. this.cachedToken_ = token; this.tokenListeners_.forEach((listener) => { listener(token.accessToken); diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 739dcbccfc..763041262e 100644 --- a/test/unit/database/database.spec.ts +++ b/test/unit/database/database.spec.ts @@ -211,9 +211,7 @@ describe('Database', () => { }); }); - // Currently doesn't work as expected since onTokenChange() can force a token refresh - // by calling getToken(). Skipping for now. - xit('should not reschedule when the token is about to expire in 5 minutes', () => { + it('should not reschedule when the token is about to expire in 5 minutes', () => { database.getDatabase(); return mockApp.INTERNAL.getToken() .then((token1) => { From be4ebc61ed013315ffad954575f6223d44b5a80a Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Tue, 27 Apr 2021 16:53:29 -0400 Subject: [PATCH 098/160] [chore] Release 9.7.0 (#1240) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 240abe3cd7..28a3112ff7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.6.0", + "version": "9.7.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From c6e9ef7a9c84eb565e53eb813b8c2204933de52d Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Mon, 3 May 2021 14:14:31 -0700 Subject: [PATCH 099/160] fix: adds missing EMAIL_NOT_FOUND error code (#1246) Catch `EMAIL_NOT_FOUND` and translate to `auth/email-not-found` when `/accounts:sendOobCode` is called for password reset on a user that does not exist. Fixes https://github.com/firebase/firebase-admin-node/issues/1202 --- src/utils/error.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/error.ts b/src/utils/error.ts index 5809a294b6..adc1b852b8 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -374,6 +374,10 @@ export class AuthClientErrorCode { code: 'email-already-exists', message: 'The email address is already in use by another account.', }; + public static EMAIL_NOT_FOUND = { + code: 'email-not-found', + message: 'There is no user record corresponding to the provided email.', + }; public static FORBIDDEN_CLAIM = { code: 'reserved-claim', message: 'The specified developer claim is reserved and cannot be specified.', @@ -854,6 +858,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { DUPLICATE_MFA_ENROLLMENT_ID: 'SECOND_FACTOR_UID_ALREADY_EXISTS', // setAccountInfo email already exists. EMAIL_EXISTS: 'EMAIL_ALREADY_EXISTS', + // /accounts:sendOobCode for password reset when user is not found. + EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND', // Reserved claim name. FORBIDDEN_CLAIM: 'FORBIDDEN_CLAIM', // Invalid claims provided. From d8b769a306281ab2baf0a6fd16ec4aa58d474377 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 May 2021 11:23:40 -0700 Subject: [PATCH 100/160] build(deps-dev): bump lodash from 4.17.19 to 4.17.21 (#1255) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21) 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 f4875def9a..d455f9a489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.4.2", + "version": "9.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -5307,9 +5307,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash._basecopy": { From 43bfbd9a7c9a16825e30588bb1cb47c8ce2214bd Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 6 May 2021 11:35:25 -0700 Subject: [PATCH 101/160] chore: Upgraded RTDB and other @firebase dependencies (#1250) --- package-lock.json | 138 ++++++++++++++++++++++++++++------------------ package.json | 10 ++-- 2 files changed, 89 insertions(+), 59 deletions(-) diff --git a/package-lock.json b/package-lock.json index d455f9a489..0f5e8a1210 100644 --- a/package-lock.json +++ b/package-lock.json @@ -156,94 +156,115 @@ } }, "@firebase/app": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.13.tgz", - "integrity": "sha512-xGrJETzvCb89VYbGSHFHCW7O/y067HRxT7MGehUE1xMxdPVBDNayHnxEuKwzfGvXAjVmajXBKFlKxaCWpgSjCQ==", + "version": "0.6.21", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.21.tgz", + "integrity": "sha512-SpWXdy/U06gTOEofSjhcsFGUtYmZim7ty6U4eMUQH0ObtymeVdTiK4tJcohMT5XoihQw4CLS2YvDySwx3+BlWg==", "dev": true, "requires": { - "@firebase/app-types": "0.6.1", - "@firebase/component": "0.1.21", + "@firebase/app-types": "0.6.2", + "@firebase/component": "0.5.0", "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.4", + "@firebase/util": "1.1.0", "dom-storage": "2.1.0", - "tslib": "^1.11.1", + "tslib": "^2.1.0", "xmlhttprequest": "1.8.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } } }, "@firebase/app-types": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.1.tgz", - "integrity": "sha512-L/ZnJRAq7F++utfuoTKX4CLBG5YR7tFO3PLzG1/oXXKEezJ0kRL3CMRoueBEmTCzVb/6SIs2Qlaw++uDgi5Xyg==" + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.2.tgz", + "integrity": "sha512-2VXvq/K+n8XMdM4L2xy5bYp2ZXMawJXluUIDzUBvMthVR+lhxK4pfFiqr1mmDbv9ydXvEAuFsD+6DpcZuJcSSw==" }, "@firebase/auth": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.2.tgz", - "integrity": "sha512-68TlDL0yh3kF8PiCzI8m8RWd/bf/xCLUsdz1NZ2Dwea0sp6e2WAhu0sem1GfhwuEwL+Ns4jCdX7qbe/OQlkVEA==", + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.5.tgz", + "integrity": "sha512-Cgs/TlVot2QkbJyEphvKmu+2qxYlNN+Q2+29aqZwryrnn1eLwlC7nT89K6O91/744HJRtiThm02bMj2Wh61E3Q==", "dev": true, "requires": { - "@firebase/auth-types": "0.10.1" + "@firebase/auth-types": "0.10.3" } }, "@firebase/auth-interop-types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.5.tgz", - "integrity": "sha512-88h74TMQ6wXChPA6h9Q3E1Jg6TkTHep2+k63OWg3s0ozyGVMeY+TTOti7PFPzq5RhszQPQOoCi59es4MaRvgCw==" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", + "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==" }, "@firebase/auth-types": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.1.tgz", - "integrity": "sha512-/+gBHb1O9x/YlG7inXfxff/6X3BPZt4zgBv4kql6HEmdzNQCodIRlEYnI+/da+lN+dha7PjaFH7C7ewMmfV7rw==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.3.tgz", + "integrity": "sha512-zExrThRqyqGUbXOFrH/sowuh2rRtfKHp9SBVY2vOqKWdCX1Ztn682n9WLtlUDsiYVIbBcwautYWk2HyCGFv0OA==", "dev": true }, "@firebase/component": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.21.tgz", - "integrity": "sha512-kd5sVmCLB95EK81Pj+yDTea8pzN2qo/1yr0ua9yVi6UgMzm6zAeih73iVUkaat96MAHy26yosMufkvd3zC4IKg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.0.tgz", + "integrity": "sha512-v18csWtXb0ri+3m7wuGLY/UDgcb89vuMlZGQ//+7jEPLIQeLbylvZhol1uzW9WzoOpxMxOS2W5qyVGX36wZvEA==", "dev": true, "requires": { - "@firebase/util": "0.3.4", - "tslib": "^1.11.1" + "@firebase/util": "1.1.0", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } } }, "@firebase/database": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.8.1.tgz", - "integrity": "sha512-/1HhR4ejpqUaM9Cn3KSeNdQvdlehWIhdfTVWFxS73ZlLYf7ayk9jITwH10H3ZOIm5yNzxF67p/U7Z/0IPhgWaQ==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.10.0.tgz", + "integrity": "sha512-GsHvuES83Edtboij2h3txKg+yV/TD4b5Owc01SgXEQtvj1lulkHt4Ufmd9OZz1WreWQJMIqKpbVowIDHjlkZJQ==", "requires": { - "@firebase/auth-interop-types": "0.1.5", - "@firebase/component": "0.1.21", - "@firebase/database-types": "0.6.1", + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.0", + "@firebase/database-types": "0.7.2", "@firebase/logger": "0.2.6", - "@firebase/util": "0.3.4", + "@firebase/util": "1.1.0", "faye-websocket": "0.11.3", - "tslib": "^1.11.1" + "tslib": "^2.1.0" }, "dependencies": { "@firebase/component": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.21.tgz", - "integrity": "sha512-kd5sVmCLB95EK81Pj+yDTea8pzN2qo/1yr0ua9yVi6UgMzm6zAeih73iVUkaat96MAHy26yosMufkvd3zC4IKg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.0.tgz", + "integrity": "sha512-v18csWtXb0ri+3m7wuGLY/UDgcb89vuMlZGQ//+7jEPLIQeLbylvZhol1uzW9WzoOpxMxOS2W5qyVGX36wZvEA==", "requires": { - "@firebase/util": "0.3.4", - "tslib": "^1.11.1" + "@firebase/util": "1.1.0", + "tslib": "^2.1.0" } }, "@firebase/util": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.4.tgz", - "integrity": "sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.1.0.tgz", + "integrity": "sha512-lfuSASuPKNdfebuFR8rjFamMQUPH9iiZHcKS755Rkm/5gRT0qC7BMhCh3ZkHf7NVbplzIc/GhmX2jM+igDRCag==", "requires": { - "tslib": "^1.11.1" + "tslib": "^2.1.0" } + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" } } }, "@firebase/database-types": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.6.1.tgz", - "integrity": "sha512-JtL3FUbWG+bM59iYuphfx9WOu2Mzf0OZNaqWiQ7lJR8wBe7bS9rIm9jlBFtksB7xcya1lZSQPA/GAy2jIlMIkA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.7.2.tgz", + "integrity": "sha512-cdAd/dgwvC0r3oLEDUR+ULs1vBsEvy0b27nlzKhU6LQgm9fCDzgaH9nFGv8x+S9dly4B0egAXkONkVoWcOAisg==", "requires": { - "@firebase/app-types": "0.6.1" + "@firebase/app-types": "0.6.2" } }, "@firebase/logger": { @@ -252,12 +273,20 @@ "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" }, "@firebase/util": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.3.4.tgz", - "integrity": "sha512-VwjJUE2Vgr2UMfH63ZtIX9Hd7x+6gayi6RUXaTqEYxSbf/JmehLmAEYSuxS/NckfzAXWeGnKclvnXVibDgpjQQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.1.0.tgz", + "integrity": "sha512-lfuSASuPKNdfebuFR8rjFamMQUPH9iiZHcKS755Rkm/5gRT0qC7BMhCh3ZkHf7NVbplzIc/GhmX2jM+igDRCag==", "dev": true, "requires": { - "tslib": "^1.11.1" + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } } }, "@google-cloud/common": { @@ -4338,9 +4367,9 @@ } }, "http-parser-js": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", - "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==" + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==" }, "http-proxy-agent": { "version": "4.0.1", @@ -8848,7 +8877,8 @@ "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true }, "tsutils": { "version": "3.17.1", diff --git a/package.json b/package.json index 28a3112ff7..1d5132c8c1 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,8 @@ ], "types": "./lib/index.d.ts", "dependencies": { - "@firebase/database": "^0.8.1", - "@firebase/database-types": "^0.6.1", + "@firebase/database": "^0.10.0", + "@firebase/database-types": "^0.7.2", "@types/node": "^10.10.0", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", @@ -68,9 +68,9 @@ "@google-cloud/storage": "^5.3.0" }, "devDependencies": { - "@firebase/app": "^0.6.13", - "@firebase/auth": "^0.16.2", - "@firebase/auth-types": "^0.10.1", + "@firebase/app": "^0.6.21", + "@firebase/auth": "^0.16.5", + "@firebase/auth-types": "^0.10.3", "@microsoft/api-extractor": "^7.11.2", "@types/bcrypt": "^2.0.0", "@types/chai": "^4.0.0", From e65dbcfbf505c1a7ac4929a3770f170ce0c367bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 May 2021 12:14:28 -0700 Subject: [PATCH 102/160] build(deps): bump y18n from 3.2.1 to 3.2.2 (#1208) Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2. - [Release notes](https://github.com/yargs/y18n/releases) - [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md) - [Commits](https://github.com/yargs/y18n/commits) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f5e8a1210..2192ef23ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -568,12 +568,6 @@ "strip-ansi": "^6.0.0" } }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "optional": true - }, "yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", @@ -3955,6 +3949,12 @@ "yargs": "^7.1.0" } }, + "y18n": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "dev": true + }, "yargs": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", @@ -6129,12 +6129,6 @@ "strip-ansi": "^5.0.0" } }, - "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", - "dev": true - }, "yargs": { "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", @@ -6814,12 +6808,6 @@ "strip-ansi": "^5.0.0" } }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, "yargs": { "version": "13.3.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", @@ -9476,10 +9464,9 @@ "dev": true }, "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, "yallist": { "version": "3.1.1", @@ -9586,9 +9573,9 @@ } }, "y18n": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.4.tgz", - "integrity": "sha512-deLOfD+RvFgrpAmSZgfGdWYE+OKyHcVHaRQ7NphG/63scpRvTHHeQMAxGGvaLVGJ+HYVcCXlzcTK0ZehFf+eHQ==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yargs-parser": { From 3ae6a69b81197efcc29438142493d50dc8e6be88 Mon Sep 17 00:00:00 2001 From: Abe Haskins Date: Mon, 10 May 2021 12:23:23 -0500 Subject: [PATCH 103/160] Fix storage emulator env formatting (#1257) * Fix storage emulator env formatting * Repair test * Rename test * Dang tests 2 good 4 me * Fix test * Fix tests again --- src/storage/storage.ts | 11 ++++++++++- test/unit/storage/storage.spec.ts | 23 ++++++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/storage/storage.ts b/src/storage/storage.ts index aabf9dd30b..7bac81ff46 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -49,7 +49,16 @@ export class Storage implements StorageInterface { } if (!process.env.STORAGE_EMULATOR_HOST && process.env.FIREBASE_STORAGE_EMULATOR_HOST) { - process.env.STORAGE_EMULATOR_HOST = process.env.FIREBASE_STORAGE_EMULATOR_HOST; + const firebaseStorageEmulatorHost = process.env.FIREBASE_STORAGE_EMULATOR_HOST; + + if (firebaseStorageEmulatorHost.match(/https?:\/\//)) { + throw new FirebaseError({ + code: 'storage/invalid-emulator-host', + message: 'FIREBASE_STORAGE_EMULATOR_HOST should not contain a protocol (http or https).', + }); + } + + process.env.STORAGE_EMULATOR_HOST = `http://${process.env.FIREBASE_STORAGE_EMULATOR_HOST}`; } let storage: typeof StorageClient; diff --git a/test/unit/storage/storage.spec.ts b/test/unit/storage/storage.spec.ts index 69ea28b217..e7d2360ee9 100644 --- a/test/unit/storage/storage.spec.ts +++ b/test/unit/storage/storage.spec.ts @@ -114,18 +114,27 @@ describe('Storage', () => { }); }); - describe('Emulator mode', () => { - const EMULATOR_HOST = 'http://localhost:9199'; + describe.only('Emulator mode', () => { + const VALID_EMULATOR_HOST = 'localhost:9199'; + const INVALID_EMULATOR_HOST = 'https://localhost:9199'; - before(() => { + beforeEach(() => { delete process.env.STORAGE_EMULATOR_HOST; - process.env.FIREBASE_STORAGE_EMULATOR_HOST = EMULATOR_HOST; + delete process.env.FIREBASE_STORAGE_EMULATOR_HOST; }); it('sets STORAGE_EMULATOR_HOST if FIREBASE_STORAGE_EMULATOR_HOST is set', () => { - new Storage(mockApp); - - expect(process.env.STORAGE_EMULATOR_HOST).to.equal(EMULATOR_HOST); + process.env.FIREBASE_STORAGE_EMULATOR_HOST = VALID_EMULATOR_HOST; + + new Storage(mockApp) + expect(process.env.STORAGE_EMULATOR_HOST).to.equal(`http://${VALID_EMULATOR_HOST}`); + }); + + it('throws if FIREBASE_STORAGE_EMULATOR_HOST has a protocol', () => { + process.env.FIREBASE_STORAGE_EMULATOR_HOST = INVALID_EMULATOR_HOST; + + expect(() => new Storage(mockApp)).to.throw( + 'FIREBASE_STORAGE_EMULATOR_HOST should not contain a protocol'); }); after(() => { From 8267b56e77283fcb366d88b0d3f8a98eeb51ab2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 May 2021 10:45:17 -0700 Subject: [PATCH 104/160] build(deps): bump hosted-git-info from 2.8.8 to 2.8.9 (#1260) Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9. - [Release notes](https://github.com/npm/hosted-git-info/releases) - [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md) - [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Hiranya Jayathilaka --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2192ef23ef..ec28e32334 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4319,9 +4319,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "html-encoding-sniffer": { From b88f8919c67c252b31d6b4b455fd7ba9b6226131 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Mon, 10 May 2021 16:22:51 -0400 Subject: [PATCH 105/160] feat: Add abuse reduction support (#1264) - Add abuse reduction support APIs --- .github/scripts/run_integration_tests.sh | 2 + .github/workflows/nightly.yml | 1 + .github/workflows/release.yml | 1 + .gitignore | 1 + docgen/content-sources/node/toc.yaml | 12 + etc/firebase-admin.api.md | 33 ++ package-lock.json | 166 ++++++++- package.json | 1 + .../app-check-api-client-internal.ts | 228 ++++++++++++ src/app-check/app-check.ts | 86 +++++ src/app-check/index.ts | 164 +++++++++ src/app-check/token-generator.ts | 145 ++++++++ src/app-check/token-verifier.ts | 165 +++++++++ src/auth/auth.ts | 21 +- src/auth/token-generator.ts | 238 +++---------- src/firebase-app.ts | 13 + src/firebase-namespace-api.ts | 2 + src/firebase-namespace.d.ts | 1 + src/firebase-namespace.ts | 14 + src/utils/crypto-signer.ts | 250 +++++++++++++ src/utils/jwt.ts | 101 +++++- test/integration/app-check.spec.ts | 103 ++++++ test/resources/mock.jwks.json | 12 + test/resources/mocks.ts | 35 ++ .../app-check-api-client-internal.spec.ts | 238 +++++++++++++ test/unit/app-check/app-check.spec.ts | 195 +++++++++++ test/unit/app-check/token-generator.spec.ts | 261 ++++++++++++++ test/unit/app-check/token-verifier.spec.ts | 245 +++++++++++++ test/unit/auth/token-generator.spec.ts | 245 +++---------- test/unit/auth/token-verifier.spec.ts | 3 +- test/unit/firebase-app.spec.ts | 28 ++ test/unit/firebase-namespace.spec.ts | 41 +++ test/unit/firebase.spec.ts | 15 + test/unit/index.spec.ts | 7 + test/unit/utils/crypto-signer.spec.ts | 224 ++++++++++++ test/unit/utils/jwt.spec.ts | 327 ++++++++++++++---- 36 files changed, 3154 insertions(+), 470 deletions(-) create mode 100644 src/app-check/app-check-api-client-internal.ts create mode 100644 src/app-check/app-check.ts create mode 100644 src/app-check/index.ts create mode 100644 src/app-check/token-generator.ts create mode 100644 src/app-check/token-verifier.ts create mode 100644 src/utils/crypto-signer.ts create mode 100644 test/integration/app-check.spec.ts create mode 100644 test/resources/mock.jwks.json create mode 100644 test/unit/app-check/app-check-api-client-internal.spec.ts create mode 100644 test/unit/app-check/app-check.spec.ts create mode 100644 test/unit/app-check/token-generator.spec.ts create mode 100644 test/unit/app-check/token-verifier.spec.ts create mode 100644 test/unit/utils/crypto-signer.spec.ts diff --git a/.github/scripts/run_integration_tests.sh b/.github/scripts/run_integration_tests.sh index fd479df552..37dc7d1216 100755 --- a/.github/scripts/run_integration_tests.sh +++ b/.github/scripts/run_integration_tests.sh @@ -22,4 +22,6 @@ gpg --quiet --batch --yes --decrypt --passphrase="${FIREBASE_SERVICE_ACCT_KEY}" echo "${FIREBASE_API_KEY}" > test/resources/apikey.txt +echo "${FIREBASE_APP_ID}" > test/resources/appid.txt + npm run test:integration -- --updateRules --testMultiTenancy diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a36144816b..536827b1e5 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -55,6 +55,7 @@ jobs: env: FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }} FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} + FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }} - name: Package release artifacts run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4740665e9c..d2a2797765 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,6 +66,7 @@ jobs: env: FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }} FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} + FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }} - name: Package release artifacts run: | diff --git a/.gitignore b/.gitignore index 672f8c23a9..4c60db05ce 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ node_modules/ # Real key file should not be checked in test/resources/key.json test/resources/apikey.txt +test/resources/appid.txt # Release tarballs should not be checked in firebase-admin-*.tgz diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index 487d3fcc39..aaf1eb562d 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -19,6 +19,18 @@ toc: - title: "App" path: /docs/reference/admin/node/admin.app.App-1 +- title: "admin.appCheck" + path: /docs/reference/admin/node/admin.appCheck + section: + - title: "AppCheck" + path: /docs/reference/admin/node/admin.appCheck.AppCheck-1 + - title: "AppCheckToken" + path: /docs/reference/admin/node/admin.appCheck.AppCheckToken + - title: "DecodedAppCheckToken" + path: /docs/reference/admin/node/admin.appCheck.DecodedAppCheckToken + - title: "VerifyAppCheckTokenResponse" + path: /docs/reference/admin/node/admin.appCheck.VerifyAppCheckTokenResponse + - title: "admin.auth" path: /docs/reference/admin/node/admin.auth section: diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index eaf280aec8..39c413b7ec 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -15,6 +15,8 @@ export function app(name?: string): app.App; // @public (undocumented) export namespace app { export interface App { + // (undocumented) + appCheck(): appCheck.AppCheck; // (undocumented) auth(): auth.Auth; // (undocumented) @@ -41,6 +43,37 @@ export namespace app { } } +// @public +export function appCheck(app?: app.App): appCheck.AppCheck; + +// @public (undocumented) +export namespace appCheck { + export interface AppCheck { + // (undocumented) + app: app.App; + createToken(appId: string): Promise; + verifyToken(appCheckToken: string): Promise; + } + export interface AppCheckToken { + token: string; + ttlMillis: number; + } + export interface DecodedAppCheckToken { + // (undocumented) + [key: string]: any; + app_id: string; + aud: string[]; + exp: number; + iat: number; + iss: string; + sub: string; + } + export interface VerifyAppCheckTokenResponse { + appId: string; + token: appCheck.DecodedAppCheckToken; + } +} + // @public export interface AppOptions { credential?: credential.Credential; diff --git a/package-lock.json b/package-lock.json index ec28e32334..99e55cda11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -664,6 +664,11 @@ "integrity": "sha512-IpgPxHrNxZiMNUSXqR1l/gePKPkfAmIKoDRP9hp7OwjU29ZR8WCJsOJ8iBKgw0Qk+pFwR+8Y1cy8ImLY6e9m4A==", "dev": true }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -876,6 +881,15 @@ "integrity": "sha512-dIOxFfI0C+jz89g6lQ+TqhGgPQ0MxSnh/E4xuC0blhFtyW269+mPG5QeLgbdwst/LvdP8o1y0o/Gz5EHXLec/g==", "dev": true }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", @@ -903,15 +917,61 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "requires": { + "@types/node": "*" + } + }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", "dev": true }, + "@types/express": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.11.tgz", + "integrity": "sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-jwt": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", + "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", + "requires": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz", + "integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/express-unless": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz", + "integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==", + "requires": { + "@types/express": "*" + } + }, "@types/firebase-token-generator": { "version": "2.0.28", - "resolved": "http://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", + "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", "integrity": "sha1-Z1VIHZMk4mt6XItFXWgUg3aCw5Y=", "dev": true }, @@ -942,6 +1002,11 @@ "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", "optional": true }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -974,6 +1039,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.26.tgz", "integrity": "sha512-myMwkO2Cr82kirHY8uknNRHEVtn0wV3DTQfkrjx17jmkstDRZ24gNUdl8AHXVyVclTYI/bNjgTPTAWvWLqXqkw==" }, + "@types/qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, "@types/request": { "version": "2.48.5", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz", @@ -996,6 +1071,15 @@ "@types/request": "*" } }, + "@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "@types/sinon": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.4.tgz", @@ -1646,7 +1730,7 @@ }, "binaryextensions": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", "dev": true }, @@ -3349,7 +3433,7 @@ }, "firebase-token-generator": { "version": "2.0.0", - "resolved": "http://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", "integrity": "sha1-l2fXWewTq9yZuhFf1eqZ2Lk9EgY=", "dev": true }, @@ -3770,7 +3854,7 @@ }, "globby": { "version": "5.0.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", "dev": true, "requires": { @@ -5023,7 +5107,7 @@ }, "istextorbinary": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", "dev": true, "requires": { @@ -5037,6 +5121,14 @@ "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", "dev": true }, + "jose": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.4.tgz", + "integrity": "sha512-EArN9f6aq1LT/fIGGsfghOnNXn4noD+3dG5lL/ljY3LcRjw1u9w+4ahu/4ahsN6N0kRLyyW6zqdoYk7LNx3+YQ==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5225,6 +5317,18 @@ "safe-buffer": "^5.0.1" } }, + "jwks-rsa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.2.tgz", + "integrity": "sha512-oRnlZvmP21LxqEgEFiPycLn3jyw/QuynyaERe7GMxR4TlTg7BRGBgEyEN+rRN4xGHMekXur1RY/MSt8UJBiSgA==", + "requires": { + "@types/express-jwt": "0.0.42", + "debug": "^4.1.0", + "jose": "^2.0.2", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.2" + } + }, "jws": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", @@ -5304,6 +5408,11 @@ "resolve": "^1.1.7" } }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -5401,6 +5510,11 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "optional": true }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, "lodash.escape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", @@ -5598,6 +5712,31 @@ "yallist": "^3.0.2" } }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, "lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -7140,7 +7279,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -7302,7 +7441,7 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, @@ -7361,8 +7500,7 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { "version": "1.8.0", @@ -7846,7 +7984,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -8670,7 +8808,7 @@ }, "textextensions": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=", "dev": true }, @@ -9142,9 +9280,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "optional": true }, "v8-compile-cache": { diff --git a/package.json b/package.json index 1d5132c8c1..d384d64579 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@types/node": "^10.10.0", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^2.0.2", "node-forge": "^0.10.0" }, "optionalDependencies": { diff --git a/src/app-check/app-check-api-client-internal.ts b/src/app-check/app-check-api-client-internal.ts new file mode 100644 index 0000000000..8d25e23cf7 --- /dev/null +++ b/src/app-check/app-check-api-client-internal.ts @@ -0,0 +1,228 @@ +/*! + * @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 { appCheck } from './index'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, HttpResponse +} from '../utils/api-request'; +import { FirebaseApp } from '../firebase-app'; +import { PrefixedFirebaseError } from '../utils/error'; + +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; + +import AppCheckToken = appCheck.AppCheckToken; + +// 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_CONFIG_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}` +}; + +/** + * Class that facilitates sending requests to the Firebase App Check backend API. + * + * @internal + */ +export class AppCheckApiClient { + private readonly httpClient: HttpClient; + private projectId?: string; + + constructor(private readonly app: FirebaseApp) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'First argument passed to admin.appCheck() must be a valid Firebase app instance.'); + } + this.httpClient = new AuthorizedHttpClient(app); + } + + /** + * Exchange a signed custom token to App Check token + * + * @param customToken The custom token to be exchanged. + * @param appId The mobile App ID. + * @return A promise that fulfills with a `AppCheckToken`. + */ + public exchangeToken(customToken: string, appId: string): Promise { + if (!validator.isNonEmptyString(appId)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + '`appId` must be a non-empty string.'); + } + if (!validator.isNonEmptyString(customToken)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + '`customToken` must be a non-empty string.'); + } + return this.getUrl(appId) + .then((url) => { + const request: HttpRequestConfig = { + method: 'POST', + url, + headers: FIREBASE_APP_CHECK_CONFIG_HEADERS, + data: { customToken } + }; + return this.httpClient.send(request); + }) + .then((resp) => { + return this.toAppCheckToken(resp); + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + private getUrl(appId: string): Promise { + return this.getProjectId() + .then((projectId) => { + const urlParams = { + projectId, + appId, + }; + const baseUrl = utils.formatString(FIREBASE_APP_CHECK_V1_API_URL_FORMAT, urlParams); + return utils.formatString(baseUrl); + }); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseAppCheckError( + '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 toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + if (!response.isJson()) { + return new FirebaseAppCheckError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + const error: Error = (response.data as ErrorResponse).error || {}; + let code: AppCheckErrorCode = 'unknown-error'; + if (error.status && error.status in APP_CHECK_ERROR_CODE_MAPPING) { + code = APP_CHECK_ERROR_CODE_MAPPING[error.status]; + } + const message = error.message || `Unknown server error: ${response.text}`; + return new FirebaseAppCheckError(code, message); + } + + /** + * Creates an AppCheckToken from the API response. + * + * @param resp API response object. + * @return An AppCheckToken instance. + */ + private toAppCheckToken(resp: HttpResponse): AppCheckToken { + const token = resp.data.attestationToken; + // `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); + return { + token, + ttlMillis + } + } + + /** + * Converts a duration string with the suffix `s` to milliseconds. + * + * @param duration The duration as a string with the suffix "s" preceded by the + * number of seconds, with fractional seconds. For example, 3 seconds with 0 nanoseconds + * is expressed as "3s", while 3 seconds and 1 nanosecond is expressed as "3.000000001s", + * and 3 seconds and 1 microsecond is expressed as "3.000001s". + * + * @return The duration in milliseconds. + */ + private stringToMilliseconds(duration: string): number { + if (!validator.isNonEmptyString(duration) || !duration.endsWith('s')) { + throw new FirebaseAppCheckError( + 'invalid-argument', '`ttl` must be a valid duration string with the suffix `s`.'); + } + const seconds = duration.slice(0, -1); + return Math.floor(Number(seconds) * 1000); + } +} + +interface ErrorResponse { + error?: Error; +} + +interface Error { + code?: number; + message?: string; + status?: string; +} + +export const APP_CHECK_ERROR_CODE_MAPPING: { [key: string]: AppCheckErrorCode } = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL: 'internal-error', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + UNKNOWN: 'unknown-error', +}; + +export type AppCheckErrorCode = + 'aborted' + | 'invalid-argument' + | 'invalid-credential' + | 'internal-error' + | 'permission-denied' + | 'unauthenticated' + | 'not-found' + | 'app-check-token-expired' + | 'unknown-error'; + +/** + * Firebase App Check error code structure. This extends PrefixedFirebaseError. + * + * @param {AppCheckErrorCode} code The error code. + * @param {string} message The error message. + * @constructor + */ +export class FirebaseAppCheckError extends PrefixedFirebaseError { + constructor(code: AppCheckErrorCode, message: string) { + super('app-check', 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__ = FirebaseAppCheckError.prototype; + } +} diff --git a/src/app-check/app-check.ts b/src/app-check/app-check.ts new file mode 100644 index 0000000000..42d8391043 --- /dev/null +++ b/src/app-check/app-check.ts @@ -0,0 +1,86 @@ +/*! + * @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 { FirebaseApp } from '../firebase-app'; +import { appCheck } from './index'; +import { AppCheckApiClient } from './app-check-api-client-internal'; +import { + appCheckErrorFromCryptoSignerError, AppCheckTokenGenerator +} from './token-generator'; +import { AppCheckTokenVerifier } from './token-verifier'; +import { cryptoSignerFromApp } from '../utils/crypto-signer'; + +import AppCheckInterface = appCheck.AppCheck; +import AppCheckToken = appCheck.AppCheckToken; +import VerifyAppCheckTokenResponse = appCheck.VerifyAppCheckTokenResponse; + +/** + * AppCheck service bound to the provided app. + */ +export class AppCheck implements AppCheckInterface { + + private readonly client: AppCheckApiClient; + private readonly tokenGenerator: AppCheckTokenGenerator; + private readonly appCheckTokenVerifier: AppCheckTokenVerifier; + + /** + * @param app The app for this AppCheck service. + * @constructor + */ + constructor(readonly app: FirebaseApp) { + this.client = new AppCheckApiClient(app); + try { + this.tokenGenerator = new AppCheckTokenGenerator(cryptoSignerFromApp(app)); + } catch (err) { + throw appCheckErrorFromCryptoSignerError(err); + } + this.appCheckTokenVerifier = new AppCheckTokenVerifier(app); + } + + /** + * Creates a new {@link appCheck.AppCheckToken `AppCheckToken`} that can be sent + * back to a client. + * + * @param appId The app ID to use as the JWT app_id. + * + * @return A promise that fulfills with a `AppCheckToken`. + */ + public createToken(appId: string): Promise { + return this.tokenGenerator.createCustomToken(appId) + .then((customToken) => { + return this.client.exchangeToken(customToken, appId); + }); + } + + /** + * Veifies an App Check token. + * + * @param appCheckToken The App Check token to verify. + * + * @return A promise that fulfills with a `VerifyAppCheckTokenResponse` on successful + * verification. + */ + public verifyToken(appCheckToken: string): Promise { + return this.appCheckTokenVerifier.verifyToken(appCheckToken) + .then((decodedToken) => { + return { + appId: decodedToken.app_id, + token: decodedToken, + }; + }); + } +} diff --git a/src/app-check/index.ts b/src/app-check/index.ts new file mode 100644 index 0000000000..6552d9208d --- /dev/null +++ b/src/app-check/index.ts @@ -0,0 +1,164 @@ +/*! + * @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 '../firebase-namespace-api'; + +/** + * Gets the {@link appCheck.AppCheck `AppCheck`} service for the + * default app or a given app. + * + * You can call `admin.appCheck()` with no arguments to access the default + * app's {@link appCheck.AppCheck `AppCheck`} service or as + * `admin.appCheck(app)` to access the + * {@link appCheck.AppCheck `AppCheck`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the `AppCheck` service for the default app + * var defaultAppCheck = admin.appCheck(); + * ``` + * + * @example + * ```javascript + * // Get the `AppCheck` service for a given app + * var otherAppCheck = admin.appCheck(otherApp); + * ``` + * + * @param app Optional app for which to return the `AppCheck` service. + * If not provided, the default `AppCheck` service is returned. + * + * @return The default `AppCheck` service if no + * app is provided, or the `AppCheck` service associated with the provided + * app. + */ +export declare function appCheck(app?: app.App): appCheck.AppCheck; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace appCheck { + /** + * The Firebase `AppCheck` service interface. + */ + export interface AppCheck { + app: app.App; + + /** + * Creates a new {@link appCheck.AppCheckToken `AppCheckToken`} that can be sent + * back to a client. + * + * @param appId The App ID of the Firebase App the token belongs to. + * + * @return A promise that fulfills with a `AppCheckToken`. + */ + createToken(appId: string): Promise; + + /** + * Verifies a Firebase App Check token (JWT). If the token is valid, the promise is + * fulfilled with the token's decoded claims; otherwise, the promise is + * rejected. + * + * @param appCheckToken The App Check token to verify. + * + * @return A promise fulfilled with the + * token's decoded claims if the App Check token is valid; otherwise, a rejected + * promise. + */ + verifyToken(appCheckToken: string): Promise; + } + + /** + * Interface representing an App Check token. + */ + export interface AppCheckToken { + /** + * The Firebase App Check token. + */ + token: string; + + /** + * The time-to-live duration of the token in milliseconds. + */ + ttlMillis: number; + } + + /** + * Interface representing a decoded Firebase App Check token, returned from the + * {@link appCheck.AppCheck.verifyToken `verifyToken()`} method. + */ + export interface DecodedAppCheckToken { + /** + * The issuer identifier for the issuer of the response. + * + * This value is a URL with the format + * `https://firebaseappcheck.googleapis.com/`, where `` is the + * same project number specified in the [`aud`](#aud) property. + */ + iss: string; + + /** + * The Firebase App ID corresponding to the app the token belonged to. + * + * As a convenience, this value is copied over to the [`app_id`](#app_id) property. + */ + sub: string; + + /** + * The audience for which this token is intended. + * + * This value is a JSON array of two strings, the first is the project number of your + * Firebase project, and the second is the project ID of the same project. + */ + aud: string[]; + + /** + * The App Check token's expiration time, in seconds since the Unix epoch. That is, the + * time at which this App Check token expires and should no longer be considered valid. + */ + exp: number; + + /** + * The App Check token's issued-at time, in seconds since the Unix epoch. That is, the + * time at which this App Check token was issued and should start to be considered + * valid. + */ + iat: number; + + /** + * The App ID corresponding to the App the App Check token belonged to. + * + * This value is not actually one of the JWT token claims. It is added as a + * convenience, and is set as the value of the [`sub`](#sub) property. + */ + app_id: string; + [key: string]: any; + } + + /** + * Interface representing a verified App Check token response. + */ + export interface VerifyAppCheckTokenResponse { + /** + * The App ID corresponding to the App the App Check token belonged to. + */ + appId: string; + + /** + * The decoded Firebase App Check token. + */ + token: appCheck.DecodedAppCheckToken; + } +} diff --git a/src/app-check/token-generator.ts b/src/app-check/token-generator.ts new file mode 100644 index 0000000000..1b557438bb --- /dev/null +++ b/src/app-check/token-generator.ts @@ -0,0 +1,145 @@ +/*! + * @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 * as validator from '../utils/validator'; +import { toWebSafeBase64 } from '../utils'; + +import { CryptoSigner, CryptoSignerError, CryptoSignerErrorCode } from '../utils/crypto-signer'; +import { + FirebaseAppCheckError, + AppCheckErrorCode, + APP_CHECK_ERROR_CODE_MAPPING, +} from './app-check-api-client-internal'; +import { HttpError } from '../utils/api-request'; + +const ONE_HOUR_IN_SECONDS = 60 * 60; + +// Audience to use for Firebase App Check Custom tokens +const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1beta.TokenExchangeService'; + +/** + * Class for generating Firebase App Check tokens. + * + * @internal + */ +export class AppCheckTokenGenerator { + + private readonly signer: CryptoSigner; + + /** + * The AppCheckTokenGenerator class constructor. + * + * @param signer The CryptoSigner instance for this token generator. + * @constructor + */ + constructor(signer: CryptoSigner) { + if (!validator.isNonNullObject(signer)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'INTERNAL ASSERT: Must provide a CryptoSigner to use AppCheckTokenGenerator.'); + } + this.signer = signer; + } + + /** + * Creates a new custom token that can be exchanged to an App Check token. + * + * @param appId The Application ID to use for the generated token. + * + * @return A Promise fulfilled with a custom token signed with a service account key + * that can be exchanged to an App Check token. + */ + public createCustomToken(appId: string): Promise { + if (!validator.isNonEmptyString(appId)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + '`appId` must be a non-empty string.'); + } + return this.signer.getAccountId().then((account) => { + const header = { + alg: this.signer.algorithm, + typ: 'JWT', + }; + const iat = Math.floor(Date.now() / 1000); + const body = { + iss: account, + sub: account, + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: appId, + aud: FIREBASE_APP_CHECK_AUDIENCE, + exp: iat + ONE_HOUR_IN_SECONDS, + iat, + }; + const token = `${this.encodeSegment(header)}.${this.encodeSegment(body)}`; + return this.signer.sign(Buffer.from(token)) + .then((signature) => { + return `${token}.${this.encodeSegment(signature)}`; + }); + }).catch((err) => { + throw appCheckErrorFromCryptoSignerError(err); + }); + } + + private encodeSegment(segment: object | Buffer): string { + const buffer: Buffer = (segment instanceof Buffer) ? segment : Buffer.from(JSON.stringify(segment)); + return toWebSafeBase64(buffer).replace(/=+$/, ''); + } +} + +/** + * Creates a new FirebaseAppCheckError by extracting the error code, message and other relevant + * details from a CryptoSignerError. + * + * @param err The Error to convert into a FirebaseAppCheckError error + * @return A Firebase App Check error that can be returned to the user. + */ +export function appCheckErrorFromCryptoSignerError(err: Error): Error { + if (!(err instanceof CryptoSignerError)) { + return err; + } + if (err.code === CryptoSignerErrorCode.SERVER_ERROR && validator.isNonNullObject(err.cause)) { + const httpError = err.cause as HttpError + const errorResponse = httpError.response.data; + if (errorResponse?.error) { + const status = errorResponse.error.status; + const description = errorResponse.error.message || JSON.stringify(httpError.response); + + let code: AppCheckErrorCode = 'unknown-error'; + if (status && status in APP_CHECK_ERROR_CODE_MAPPING) { + code = APP_CHECK_ERROR_CODE_MAPPING[status]; + } + return new FirebaseAppCheckError(code, + `Error returned from server while siging a custom token: ${description}` + ); + } + return new FirebaseAppCheckError('internal-error', + 'Error returned from server: ' + JSON.stringify(errorResponse) + '.' + ); + } + return new FirebaseAppCheckError(mapToAppCheckErrorCode(err.code), err.message); +} + +function mapToAppCheckErrorCode(code: string): AppCheckErrorCode { + switch (code) { + case CryptoSignerErrorCode.INVALID_CREDENTIAL: + return 'invalid-credential'; + case CryptoSignerErrorCode.INVALID_ARGUMENT: + return 'invalid-argument'; + default: + return 'internal-error'; + } +} diff --git a/src/app-check/token-verifier.ts b/src/app-check/token-verifier.ts new file mode 100644 index 0000000000..318a1fd10b --- /dev/null +++ b/src/app-check/token-verifier.ts @@ -0,0 +1,165 @@ +/*! + * 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 { appCheck } from '.'; +import * as validator from '../utils/validator'; +import * as util from '../utils/index'; +import { FirebaseAppCheckError } from './app-check-api-client-internal'; +import { FirebaseApp } from '../firebase-app'; +import { + ALGORITHM_RS256, DecodedToken, decodeJwt, JwtError, + JwtErrorCode, PublicKeySignatureVerifier, SignatureVerifier +} from '../utils/jwt'; + +import DecodedAppCheckToken = appCheck.DecodedAppCheckToken; + +const APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/'; +const JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1beta/jwks'; + +/** + * Class for verifying Firebase App Check tokens. + * + * @internal + */ +export class AppCheckTokenVerifier { + private readonly signatureVerifier: SignatureVerifier; + + constructor(private readonly app: FirebaseApp) { + this.signatureVerifier = PublicKeySignatureVerifier.withJwksUrl(JWKS_URL); + } + + /** + * Verifies the format and signature of a Firebase App Check token. + * + * @param token The Firebase Auth JWT token to verify. + * @return A promise fulfilled with the decoded claims of the Firebase App Check token. + */ + public verifyToken(token: string): Promise { + if (!validator.isString(token)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'App check token must be a non-null string.', + ); + } + + return this.ensureProjectId() + .then((projectId) => { + return this.decodeAndVerify(token, projectId); + }) + .then((decoded) => { + const decodedAppCheckToken = decoded.payload as DecodedAppCheckToken; + // eslint-disable-next-line @typescript-eslint/camelcase + decodedAppCheckToken.app_id = decodedAppCheckToken.sub; + return decodedAppCheckToken; + }); + } + + private ensureProjectId(): Promise { + return util.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseAppCheckError( + 'invalid-credential', + 'Must initialize app with a cert credential or set your Firebase project ID as the ' + + 'GOOGLE_CLOUD_PROJECT environment variable to verify an App Check token.' + ); + } + return projectId; + }) + } + + private decodeAndVerify(token: string, projectId: string): Promise { + return this.safeDecode(token) + .then((decodedToken) => { + this.verifyContent(decodedToken, projectId); + return this.verifySignature(token) + .then(() => decodedToken); + }); + } + + private safeDecode(jwtToken: string): Promise { + return decodeJwt(jwtToken) + .catch(() => { + const errorMessage = 'Decoding App Check token failed. Make sure you passed ' + + 'the entire string JWT which represents the Firebase App Check token.'; + throw new FirebaseAppCheckError('invalid-argument', errorMessage); + }); + } + + /** + * Verifies the content of a Firebase App Check JWT. + * + * @param fullDecodedToken The decoded JWT. + * @param projectId The Firebase Project Id. + */ + private verifyContent(fullDecodedToken: DecodedToken, projectId: string | null): void { + const header = fullDecodedToken.header; + const payload = fullDecodedToken.payload; + + const projectIdMatchMessage = ' Make sure the App Check token comes from the same ' + + 'Firebase project as the service account used to authenticate this SDK.'; + const scopedProjectId = `projects/${projectId}`; + + let errorMessage: string | undefined; + if (header.alg !== ALGORITHM_RS256) { + errorMessage = 'The provided App Check token has incorrect algorithm. Expected "' + + ALGORITHM_RS256 + '" but got ' + '"' + header.alg + '".'; + } else if (!validator.isNonEmptyArray(payload.aud) || !payload.aud.includes(scopedProjectId)) { + errorMessage = 'The provided App Check token has incorrect "aud" (audience) claim. Expected "' + + scopedProjectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage; + } else if (typeof payload.iss !== 'string' || !payload.iss.startsWith(APP_CHECK_ISSUER)) { + errorMessage = 'The provided App Check token has incorrect "iss" (issuer) claim.'; + } else if (typeof payload.sub !== 'string') { + errorMessage = 'The provided App Check token has no "sub" (subject) claim.'; + } else if (payload.sub === '') { + errorMessage = 'The provided App Check token has an empty string "sub" (subject) claim.'; + } + if (errorMessage) { + throw new FirebaseAppCheckError('invalid-argument', errorMessage); + } + } + + private verifySignature(jwtToken: string): + Promise { + return this.signatureVerifier.verify(jwtToken) + .catch((error: JwtError) => { + throw this.mapJwtErrorToAppCheckError(error); + }); + } + + /** + * Maps JwtError to FirebaseAppCheckError + * + * @param error JwtError to be mapped. + * @returns FirebaseAppCheckError instance. + */ + private mapJwtErrorToAppCheckError(error: JwtError): FirebaseAppCheckError { + if (error.code === JwtErrorCode.TOKEN_EXPIRED) { + const errorMessage = 'The provided App Check token has expired. Get a fresh App Check token' + + ' from your client app and try again.' + return new FirebaseAppCheckError('app-check-token-expired', errorMessage); + } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) { + const errorMessage = 'The provided App Check token has invalid signature.'; + return new FirebaseAppCheckError('invalid-argument', errorMessage); + } else if (error.code === JwtErrorCode.NO_MATCHING_KID) { + const errorMessage = 'The provided App Check token has "kid" claim which does not ' + + 'correspond to a known public key. Most likely the provided App Check token ' + + 'is expired, so get a fresh token from your client app and try again.'; + return new FirebaseAppCheckError('invalid-argument', errorMessage); + } + return new FirebaseAppCheckError('invalid-argument', error.message); + } +} diff --git a/src/auth/auth.ts b/src/auth/auth.ts index aa5d7b11ef..48f46c345a 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -21,7 +21,7 @@ import { isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, } from './identifier'; import { FirebaseApp } from '../firebase-app'; -import { FirebaseTokenGenerator, EmulatedSigner, cryptoSignerFromApp } from './token-generator'; +import { FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from './token-generator'; import { AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, useEmulator, } from './auth-api-request'; @@ -36,6 +36,7 @@ import { SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from './auth-config'; import { TenantManager } from './tenant-manager'; +import { cryptoSignerFromApp } from '../utils/crypto-signer'; import UserIdentifier = auth.UserIdentifier; import CreateRequest = auth.CreateRequest; @@ -82,8 +83,7 @@ export class BaseAuth implements BaseAuthI if (tokenGenerator) { this.tokenGenerator = tokenGenerator; } else { - const cryptoSigner = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app); - this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner); + this.tokenGenerator = createFirebaseTokenGenerator(app); } this.sessionCookieVerifier = createSessionCookieVerifier(app); @@ -772,9 +772,8 @@ export class TenantAwareAuth * @constructor */ constructor(app: FirebaseApp, tenantId: string) { - const cryptoSigner = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app); - const tokenGenerator = new FirebaseTokenGenerator(cryptoSigner, tenantId); - super(app, new TenantAwareAuthRequestHandler(app, tenantId), tokenGenerator); + super(app, new TenantAwareAuthRequestHandler(app, tenantId), + createFirebaseTokenGenerator(app, tenantId)); utils.addReadonlyGetter(this, 'tenantId', tenantId); } @@ -887,3 +886,13 @@ export class Auth extends BaseAuth implements AuthInterface return this.tenantManager_; } } + +function createFirebaseTokenGenerator(app: FirebaseApp, + tenantId?: string): FirebaseTokenGenerator { + try { + const signer = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app); + return new FirebaseTokenGenerator(signer, tenantId); + } catch (err) { + throw handleCryptoSignerError(err); + } +} diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index a8a76c7b28..6c464ec5f2 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -15,17 +15,16 @@ * limitations under the License. */ -import { FirebaseApp } from '../firebase-app'; -import { ServiceAccountCredential } from '../credential/credential-internal'; -import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; -import { AuthorizedHttpClient, HttpError, HttpRequestConfig, HttpClient } from '../utils/api-request'; +import { + AuthClientErrorCode, ErrorInfo, FirebaseAuthError +} from '../utils/error'; +import { CryptoSigner, CryptoSignerError, CryptoSignerErrorCode } from '../utils/crypto-signer'; import * as validator from '../utils/validator'; import { toWebSafeBase64 } from '../utils'; import { Algorithm } from 'jsonwebtoken'; +import { HttpError } from '../utils/api-request'; - -const ALGORITHM_RS256: Algorithm = 'RS256' as const; const ALGORITHM_NONE: Algorithm = 'none' as const; const ONE_HOUR_IN_SECONDS = 60 * 60; @@ -39,32 +38,6 @@ export const BLACKLISTED_CLAIMS = [ // Audience to use for Firebase Auth Custom tokens const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; -/** - * CryptoSigner interface represents an object that can be used to sign JWTs. - */ -export interface CryptoSigner { - - /** - * The name of the signing algorithm. - */ - readonly algorithm: Algorithm; - - /** - * Cryptographically signs a buffer of data. - * - * @param {Buffer} buffer The data to be signed. - * @return {Promise} A promise that resolves with the raw bytes of a signature. - */ - sign(buffer: Buffer): Promise; - - /** - * Returns the ID of the service account used to sign tokens. - * - * @return {Promise} A promise that resolves with a service account ID. - */ - getAccountId(): Promise; -} - /** * Represents the header of a JWT. */ @@ -87,148 +60,6 @@ interface JWTBody { tenant_id?: string; } -/** - * A CryptoSigner implementation that uses an explicitly specified service account private key to - * sign data. Performs all operations locally, and does not make any RPC calls. - */ -export class ServiceAccountSigner implements CryptoSigner { - - algorithm = ALGORITHM_RS256; - - /** - * Creates a new CryptoSigner instance from the given service account credential. - * - * @param {ServiceAccountCredential} credential A service account credential. - */ - constructor(private readonly credential: ServiceAccountCredential) { - if (!credential) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, - 'INTERNAL ASSERT: Must provide a service account credential to initialize ServiceAccountSigner.', - ); - } - } - - /** - * @inheritDoc - */ - public sign(buffer: Buffer): Promise { - const crypto = require('crypto'); // eslint-disable-line @typescript-eslint/no-var-requires - const sign = crypto.createSign('RSA-SHA256'); - sign.update(buffer); - return Promise.resolve(sign.sign(this.credential.privateKey)); - } - - /** - * @inheritDoc - */ - public getAccountId(): Promise { - return Promise.resolve(this.credential.clientEmail); - } -} - -/** - * A CryptoSigner implementation that uses the remote IAM service to sign data. If initialized without - * a service account ID, attempts to discover a service account ID by consulting the local Metadata - * service. This will succeed in managed environments like Google Cloud Functions and App Engine. - * - * @see https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signBlob - * @see https://cloud.google.com/compute/docs/storing-retrieving-metadata - */ -export class IAMSigner implements CryptoSigner { - algorithm = ALGORITHM_RS256; - - private readonly httpClient: AuthorizedHttpClient; - private serviceAccountId?: string; - - constructor(httpClient: AuthorizedHttpClient, serviceAccountId?: string) { - if (!httpClient) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'INTERNAL ASSERT: Must provide a HTTP client to initialize IAMSigner.', - ); - } - if (typeof serviceAccountId !== 'undefined' && !validator.isNonEmptyString(serviceAccountId)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'INTERNAL ASSERT: Service account ID must be undefined or a non-empty string.', - ); - } - this.httpClient = httpClient; - this.serviceAccountId = serviceAccountId; - } - - /** - * @inheritDoc - */ - public sign(buffer: Buffer): Promise { - return this.getAccountId().then((serviceAccount) => { - const request: HttpRequestConfig = { - method: 'POST', - url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:signBlob`, - data: { payload: buffer.toString('base64') }, - }; - return this.httpClient.send(request); - }).then((response: any) => { - // Response from IAM is base64 encoded. Decode it into a buffer and return. - return Buffer.from(response.data.signedBlob, 'base64'); - }).catch((err) => { - if (err instanceof HttpError) { - const error = err.response.data; - if (validator.isNonNullObject(error) && error.error) { - const errorCode = error.error.status; - const description = 'Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens ' + - 'for more details on how to use and troubleshoot this feature.'; - const errorMsg = `${error.error.message}; ${description}`; - - throw FirebaseAuthError.fromServerError(errorCode, errorMsg, error); - } - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'Error returned from server: ' + error + '. Additionally, an ' + - 'internal error occurred while attempting to extract the ' + - 'errorcode from the error.', - ); - } - throw err; - }); - } - - /** - * @inheritDoc - */ - public getAccountId(): Promise { - if (validator.isNonEmptyString(this.serviceAccountId)) { - return Promise.resolve(this.serviceAccountId); - } - const request: HttpRequestConfig = { - method: 'GET', - url: 'http://metadata/computeMetadata/v1/instance/service-accounts/default/email', - headers: { - 'Metadata-Flavor': 'Google', - }, - }; - const client = new HttpClient(); - return client.send(request).then((response) => { - if (!response.text) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'HTTP Response missing payload', - ); - } - this.serviceAccountId = response.text; - return response.text; - }).catch((err) => { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, - 'Failed to determine service account. Make sure to initialize ' + - 'the SDK with a service account credential. Alternatively specify a service ' + - `account with iam.serviceAccounts.signBlob permission. Original error: ${err}`, - ); - }); - } -} - /** * A CryptoSigner implementation that is used when communicating with the Auth emulator. * It produces unsigned tokens. @@ -253,22 +84,6 @@ export class EmulatedSigner implements CryptoSigner { } } -/** - * Create a new CryptoSigner instance for the given app. If the app has been initialized with a service - * account credential, creates a ServiceAccountSigner. Otherwise creates an IAMSigner. - * - * @param {FirebaseApp} app A FirebaseApp instance. - * @return {CryptoSigner} A CryptoSigner instance. - */ -export function cryptoSignerFromApp(app: FirebaseApp): CryptoSigner { - const credential = app.options.credential; - if (credential instanceof ServiceAccountCredential) { - return new ServiceAccountSigner(credential); - } - - return new IAMSigner(new AuthorizedHttpClient(app), app.options.serviceAccountId); -} - /** * Class for generating different types of Firebase Auth tokens (JWTs). */ @@ -361,6 +176,8 @@ export class FirebaseTokenGenerator { return Promise.all([token, signPromise]); }).then(([token, signature]) => { return `${token}.${this.encodeSegment(signature)}`; + }).catch((err) => { + throw handleCryptoSignerError(err); }); } @@ -383,3 +200,44 @@ export class FirebaseTokenGenerator { } } +/** + * Creates a new FirebaseAuthError by extracting the error code, message and other relevant + * details from a CryptoSignerError. + * + * @param {Error} err The Error to convert into a FirebaseAuthError error + * @return {FirebaseAuthError} A Firebase Auth error that can be returned to the user. + */ +export function handleCryptoSignerError(err: Error): Error { + if (!(err instanceof CryptoSignerError)) { + return err; + } + if (err.code === CryptoSignerErrorCode.SERVER_ERROR && validator.isNonNullObject(err.cause)) { + const httpError = err.cause; + const errorResponse = (httpError as HttpError).response.data; + if (validator.isNonNullObject(errorResponse) && errorResponse.error) { + const errorCode = errorResponse.error.status; + const description = 'Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens ' + + 'for more details on how to use and troubleshoot this feature.'; + const errorMsg = `${errorResponse.error.message}; ${description}`; + + return FirebaseAuthError.fromServerError(errorCode, errorMsg, errorResponse); + } + return new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, + 'Error returned from server: ' + errorResponse + '. Additionally, an ' + + 'internal error occurred while attempting to extract the ' + + 'errorcode from the error.' + ); + } + return new FirebaseAuthError(mapToAuthClientErrorCode(err.code), err.message); +} + +function mapToAuthClientErrorCode(code: string): ErrorInfo { + switch (code) { + case CryptoSignerErrorCode.INVALID_CREDENTIAL: + return AuthClientErrorCode.INVALID_CREDENTIAL; + case CryptoSignerErrorCode.INVALID_ARGUMENT: + return AuthClientErrorCode.INVALID_ARGUMENT; + default: + return AuthClientErrorCode.INTERNAL_ERROR; + } +} diff --git a/src/firebase-app.ts b/src/firebase-app.ts index c9e9588d59..84b30a52c5 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -35,6 +35,7 @@ import { InstanceId } from './instance-id/instance-id'; import { ProjectManagement } from './project-management/project-management'; import { SecurityRules } from './security-rules/security-rules'; import { RemoteConfig } from './remote-config/remote-config'; +import { AppCheck } from './app-check/app-check'; import Credential = credential.Credential; import Database = database.Database; @@ -318,6 +319,18 @@ export class FirebaseApp implements app.App { }); } + /** + * Returns the AppCheck service instance associated with this app. + * + * @return The AppCheck service instance of this app. + */ + public appCheck(): AppCheck { + return this.ensureService_('appCheck', () => { + const appCheckService: typeof AppCheck = require('./app-check/app-check').AppCheck; + return new appCheckService(this); + }); + } + /** * Returns the name of the FirebaseApp instance. * diff --git a/src/firebase-namespace-api.ts b/src/firebase-namespace-api.ts index 36df1b778d..6507fa3a88 100644 --- a/src/firebase-namespace-api.ts +++ b/src/firebase-namespace-api.ts @@ -15,6 +15,7 @@ */ import { Agent } from 'http'; +import { appCheck } from './app-check/index'; import { auth } from './auth/index'; import { credential } from './credential/index'; import { database } from './database/index'; @@ -222,6 +223,7 @@ export namespace app { */ options: AppOptions; + appCheck(): appCheck.AppCheck; auth(): auth.Auth; database(url?: string): database.Database; firestore(): firestore.Firestore; diff --git a/src/firebase-namespace.d.ts b/src/firebase-namespace.d.ts index 3de06b1bbb..ab013c3cac 100644 --- a/src/firebase-namespace.d.ts +++ b/src/firebase-namespace.d.ts @@ -16,6 +16,7 @@ export * from './credential/index'; export * from './firebase-namespace-api'; +export * from './app-check/index'; export * from './auth/index'; export * from './database/index'; export * from './firestore/index'; diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 43b12a92c9..fa1a409b48 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -23,6 +23,7 @@ import { FirebaseApp } from './firebase-app'; import { cert, refreshToken, applicationDefault } from './credential/credential'; import { getApplicationDefault } from './credential/credential-internal'; +import { appCheck } from './app-check/index'; import { auth } from './auth/index'; import { database } from './database/index'; import { firestore } from './firestore/index'; @@ -38,6 +39,7 @@ import * as validator from './utils/validator'; import { getSdkVersion } from './utils/index'; import App = app.App; +import AppCheck = appCheck.AppCheck; import Auth = auth.Auth; import Database = database.Database; import Firestore = firestore.Firestore; @@ -357,6 +359,18 @@ export class FirebaseNamespace { return Object.assign(fn, { RemoteConfig: remoteConfig }); } + /** + * Gets the `AppCheck` service namespace. The returned namespace can be used to get the + * `AppCheck` service for the default app or an explicitly specified app. + */ + get appCheck(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).appCheck(); + }; + const appCheck = require('./app-check/app-check').AppCheck; + return Object.assign(fn, { AppCheck: appCheck }); + } + // TODO: Change the return types to app.App in the following methods. /** diff --git a/src/utils/crypto-signer.ts b/src/utils/crypto-signer.ts new file mode 100644 index 0000000000..e8bb8b79ae --- /dev/null +++ b/src/utils/crypto-signer.ts @@ -0,0 +1,250 @@ +/*! + * @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 { FirebaseApp } from '../firebase-app'; +import { ServiceAccountCredential } from '../credential/credential-internal'; +import { AuthorizedHttpClient, HttpRequestConfig, HttpClient, HttpError } from './api-request'; + +import { Algorithm } from 'jsonwebtoken'; +import { ErrorInfo } from '../utils/error'; +import * as validator from '../utils/validator'; + +const ALGORITHM_RS256: Algorithm = 'RS256' as const; + +/** + * CryptoSigner interface represents an object that can be used to sign JWTs. + */ +export interface CryptoSigner { + + /** + * The name of the signing algorithm. + */ + readonly algorithm: Algorithm; + + /** + * Cryptographically signs a buffer of data. + * + * @param {Buffer} buffer The data to be signed. + * @return {Promise} A promise that resolves with the raw bytes of a signature. + */ + sign(buffer: Buffer): Promise; + + /** + * Returns the ID of the service account used to sign tokens. + * + * @return {Promise} A promise that resolves with a service account ID. + */ + getAccountId(): Promise; +} + +/** + * A CryptoSigner implementation that uses an explicitly specified service account private key to + * sign data. Performs all operations locally, and does not make any RPC calls. + */ +export class ServiceAccountSigner implements CryptoSigner { + + algorithm = ALGORITHM_RS256; + + /** + * Creates a new CryptoSigner instance from the given service account credential. + * + * @param {ServiceAccountCredential} credential A service account credential. + */ + constructor(private readonly credential: ServiceAccountCredential) { + if (!credential) { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_CREDENTIAL, + message: 'INTERNAL ASSERT: Must provide a service account credential to initialize ServiceAccountSigner.', + }); + } + } + + /** + * @inheritDoc + */ + public sign(buffer: Buffer): Promise { + const crypto = require('crypto'); // eslint-disable-line @typescript-eslint/no-var-requires + const sign = crypto.createSign('RSA-SHA256'); + sign.update(buffer); + return Promise.resolve(sign.sign(this.credential.privateKey)); + } + + /** + * @inheritDoc + */ + public getAccountId(): Promise { + return Promise.resolve(this.credential.clientEmail); + } +} + +/** + * A CryptoSigner implementation that uses the remote IAM service to sign data. If initialized without + * a service account ID, attempts to discover a service account ID by consulting the local Metadata + * service. This will succeed in managed environments like Google Cloud Functions and App Engine. + * + * @see https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signBlob + * @see https://cloud.google.com/compute/docs/storing-retrieving-metadata + */ +export class IAMSigner implements CryptoSigner { + algorithm = ALGORITHM_RS256; + + private readonly httpClient: AuthorizedHttpClient; + private serviceAccountId?: string; + + constructor(httpClient: AuthorizedHttpClient, serviceAccountId?: string) { + if (!httpClient) { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_ARGUMENT, + message: 'INTERNAL ASSERT: Must provide a HTTP client to initialize IAMSigner.', + }); + } + if (typeof serviceAccountId !== 'undefined' && !validator.isNonEmptyString(serviceAccountId)) { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_ARGUMENT, + message: 'INTERNAL ASSERT: Service account ID must be undefined or a non-empty string.', + }); + } + this.httpClient = httpClient; + this.serviceAccountId = serviceAccountId; + } + + /** + * @inheritDoc + */ + public sign(buffer: Buffer): Promise { + return this.getAccountId().then((serviceAccount) => { + const request: HttpRequestConfig = { + method: 'POST', + url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:signBlob`, + data: { payload: buffer.toString('base64') }, + }; + return this.httpClient.send(request); + }).then((response: any) => { + // Response from IAM is base64 encoded. Decode it into a buffer and return. + return Buffer.from(response.data.signedBlob, 'base64'); + }).catch((err) => { + if (err instanceof HttpError) { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.SERVER_ERROR, + message: err.message, + cause: err + }); + } + throw err + }); + } + + /** + * @inheritDoc + */ + public getAccountId(): Promise { + if (validator.isNonEmptyString(this.serviceAccountId)) { + return Promise.resolve(this.serviceAccountId); + } + const request: HttpRequestConfig = { + method: 'GET', + url: 'http://metadata/computeMetadata/v1/instance/service-accounts/default/email', + headers: { + 'Metadata-Flavor': 'Google', + }, + }; + const client = new HttpClient(); + return client.send(request).then((response) => { + if (!response.text) { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.INTERNAL_ERROR, + message: 'HTTP Response missing payload', + }); + } + this.serviceAccountId = response.text; + return response.text; + }).catch((err) => { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_CREDENTIAL, + message: 'Failed to determine service account. Make sure to initialize ' + + 'the SDK with a service account credential. Alternatively specify a service ' + + `account with iam.serviceAccounts.signBlob permission. Original error: ${err}`, + }); + }); + } +} + +/** + * Creates a new CryptoSigner instance for the given app. If the app has been initialized with a + * service account credential, creates a ServiceAccountSigner. + * + * @param {FirebaseApp} app A FirebaseApp instance. + * @return {CryptoSigner} A CryptoSigner instance. + */ +export function cryptoSignerFromApp(app: FirebaseApp): CryptoSigner { + const credential = app.options.credential; + if (credential instanceof ServiceAccountCredential) { + return new ServiceAccountSigner(credential); + } + + return new IAMSigner(new AuthorizedHttpClient(app), app.options.serviceAccountId); +} + +/** + * Defines extended error info type. This includes a code, message string, and error data. + */ +export interface ExtendedErrorInfo extends ErrorInfo { + cause?: Error; +} + +/** + * CryptoSigner error code structure. + * + * @param {ErrorInfo} errorInfo The error information (code and message). + * @constructor + */ +export class CryptoSignerError extends Error { + constructor(private errorInfo: ExtendedErrorInfo) { + super(errorInfo.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__ = CryptoSignerError.prototype; + } + + /** @return {string} The error code. */ + public get code(): string { + return this.errorInfo.code; + } + + /** @return {string} The error message. */ + public get message(): string { + return this.errorInfo.message; + } + + /** @return {object} The error data. */ + public get cause(): Error | undefined { + return this.errorInfo.cause; + } +} + +/** + * Crypto Signer error codes and their default messages. + */ +export class CryptoSignerErrorCode { + public static INVALID_ARGUMENT = 'invalid-argument'; + public static INTERNAL_ERROR = 'internal-error'; + public static INVALID_CREDENTIAL = 'invalid-credential'; + public static SERVER_ERROR = 'server-error'; +} diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index d048567061..1fab2ff9fb 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -16,6 +16,7 @@ import * as validator from './validator'; import * as jwt from 'jsonwebtoken'; +import * as jwks from 'jwks-rsa'; import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; import { Agent } from 'http'; @@ -28,6 +29,9 @@ export const ALGORITHM_RS256: jwt.Algorithm = 'RS256' as const; const JWT_CALLBACK_ERROR_PREFIX = 'error in secret or public key callback: '; const NO_MATCHING_KID_ERROR_MESSAGE = 'no-matching-kid-error'; +const NO_KID_IN_HEADER_ERROR_MESSAGE = 'no-kid-in-header-error'; + +const ONE_DAY_IN_SECONDS = 24 * 3600; export type Dictionary = { [key: string]: any } @@ -44,6 +48,51 @@ interface KeyFetcher { fetchPublicKeys(): Promise<{ [key: string]: string }>; } +export class JwksFetcher implements KeyFetcher { + private publicKeys: { [key: string]: string }; + private publicKeysExpireAt = 0; + private client: jwks.JwksClient; + + constructor(jwksUrl: string) { + if (!validator.isURL(jwksUrl)) { + throw new Error('The provided JWKS URL is not a valid URL.'); + } + + this.client = jwks({ + jwksUri: jwksUrl, + cache: false, // disable jwks-rsa LRU cache as the keys are always cahced for 24 hours. + }); + } + + public fetchPublicKeys(): Promise<{ [key: string]: string }> { + if (this.shouldRefresh()) { + return this.refresh(); + } + return Promise.resolve(this.publicKeys); + } + + private shouldRefresh(): boolean { + return !this.publicKeys || this.publicKeysExpireAt <= Date.now(); + } + + private refresh(): Promise<{ [key: string]: string }> { + return this.client.getSigningKeys() + .then((signingKeys) => { + // reset expire at from previous set of keys. + this.publicKeysExpireAt = 0; + const newKeys = signingKeys.reduce((map: { [key: string]: string }, signingKey: jwks.SigningKey) => { + map[signingKey.kid] = signingKey.getPublicKey(); + return map; + }, {}); + this.publicKeysExpireAt = Date.now() + (ONE_DAY_IN_SECONDS * 1000); + this.publicKeys = newKeys; + return newKeys; + }).catch((err) => { + throw new Error(`Error fetching Json Web Keys: ${err.message}`); + }); + } +} + /** * Class to fetch public keys from a client certificates URL. */ @@ -141,13 +190,51 @@ export class PublicKeySignatureVerifier implements SignatureVerifier { return new PublicKeySignatureVerifier(new UrlKeyFetcher(clientCertUrl, httpAgent)); } + public static withJwksUrl(jwksUrl: string): PublicKeySignatureVerifier { + return new PublicKeySignatureVerifier(new JwksFetcher(jwksUrl)); + } + public verify(token: string): Promise { if (!validator.isString(token)) { return Promise.reject(new JwtError(JwtErrorCode.INVALID_ARGUMENT, 'The provided token must be a string.')); } - return verifyJwtSignature(token, getKeyCallback(this.keyFetcher), { algorithms: [ALGORITHM_RS256] }); + return verifyJwtSignature(token, getKeyCallback(this.keyFetcher), { algorithms: [ALGORITHM_RS256] }) + .catch((error: JwtError) => { + if (error.code === JwtErrorCode.NO_KID_IN_HEADER) { + // No kid in JWT header. Try with all the public keys. + return this.verifyWithoutKid(token); + } + throw error; + }); + } + + private verifyWithoutKid(token: string): Promise { + return this.keyFetcher.fetchPublicKeys() + .then(publicKeys => this.verifyWithAllKeys(token, publicKeys)); + } + + private verifyWithAllKeys(token: string, keys: { [key: string]: string }): Promise { + const promises: Promise[] = []; + Object.values(keys).forEach((key) => { + const result = verifyJwtSignature(token, key) + .then(() => true) + .catch((error) => { + if (error.code === JwtErrorCode.TOKEN_EXPIRED) { + throw error; + } + return false; + }) + promises.push(result); + }); + + return Promise.all(promises) + .then((result) => { + if (result.every((r) => r === false)) { + throw new JwtError(JwtErrorCode.INVALID_SIGNATURE, 'Invalid token signature.'); + } + }); } } @@ -169,6 +256,9 @@ export class EmulatorSignatureVerifier implements SignatureVerifier { */ function getKeyCallback(fetcher: KeyFetcher): jwt.GetPublicKeyOrSecret { return (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => { + if (!header.kid) { + callback(new Error(NO_KID_IN_HEADER_ERROR_MESSAGE)); + } const kid = header.kid || ''; fetcher.fetchPublicKeys().then((publicKeys) => { if (!Object.prototype.hasOwnProperty.call(publicKeys, kid)) { @@ -212,8 +302,12 @@ export function verifyJwtSignature(token: string, secretOrPublicKey: jwt.Secret } else if (error.name === 'JsonWebTokenError') { if (error.message && error.message.includes(JWT_CALLBACK_ERROR_PREFIX)) { const message = error.message.split(JWT_CALLBACK_ERROR_PREFIX).pop() || 'Error fetching public keys.'; - const code = (message === NO_MATCHING_KID_ERROR_MESSAGE) ? JwtErrorCode.NO_MATCHING_KID : - JwtErrorCode.KEY_FETCH_ERROR; + let code = JwtErrorCode.KEY_FETCH_ERROR; + if (message === NO_MATCHING_KID_ERROR_MESSAGE) { + code = JwtErrorCode.NO_MATCHING_KID; + } else if (message === NO_KID_IN_HEADER_ERROR_MESSAGE) { + code = JwtErrorCode.NO_KID_IN_HEADER; + } return reject(new JwtError(code, message)); } } @@ -271,5 +365,6 @@ export enum JwtErrorCode { TOKEN_EXPIRED = 'token-expired', INVALID_SIGNATURE = 'invalid-token', NO_MATCHING_KID = 'no-matching-kid-error', + NO_KID_IN_HEADER = 'no-kid-error', KEY_FETCH_ERROR = 'key-fetch-error', } diff --git a/test/integration/app-check.spec.ts b/test/integration/app-check.spec.ts new file mode 100644 index 0000000000..32386f32bc --- /dev/null +++ b/test/integration/app-check.spec.ts @@ -0,0 +1,103 @@ +/*! + * 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 * as _ from 'lodash'; +import * as admin from '../../lib/index'; +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import fs = require('fs'); +import path = require('path'); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const chalk = require('chalk'); + +chai.should(); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +let appId: string; + +describe('admin.appCheck', () => { + before(async () => { + try { + appId = fs.readFileSync(path.join(__dirname, '../resources/appid.txt')).toString().trim(); + } catch (error) { + console.log(chalk.yellow( + 'Unable to find an an App ID. Skipping tests that require a valid App ID.', + error, + )); + } + }); + + describe('createToken', () => { + it('should succeed with a vaild token', function() { + if (!appId) { + this.skip(); + } + return admin.appCheck().createToken(appId as string) + .then((token) => { + expect(token).to.have.keys(['token', 'ttlMillis']); + expect(token.token).to.be.a('string').and.to.not.be.empty; + expect(token.ttlMillis).to.be.a('number'); + }); + }); + + it('should propagate API errors', () => { + // rejects with invalid-argument when appId is incorrect + return admin.appCheck().createToken('incorrect-app-id') + .should.eventually.be.rejected.and.have.property('code', 'app-check/invalid-argument'); + }); + + const invalidAppIds = ['', null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidAppIds.forEach((invalidAppId) => { + it(`should throw given an invalid appId: ${JSON.stringify(invalidAppId)}`, () => { + expect(() => admin.appCheck().createToken(invalidAppId as any)) + .to.throw('appId` must be a non-empty string.'); + }); + }); + }); + + describe('verifyToken', () => { + let validToken: admin.appCheck.AppCheckToken; + + before(async () => { + if (!appId) { + return; + } + // obtain a valid app check token + validToken = await admin.appCheck().createToken(appId as string); + }); + + it('should succeed with a decoded verifed token response', function() { + if (!appId) { + this.skip(); + } + return admin.appCheck().verifyToken(validToken.token) + .then((verifedToken) => { + expect(verifedToken).to.have.keys(['token', 'appId']); + expect(verifedToken.token).to.have.keys(['iss', 'sub', 'aud', 'exp', 'iat', 'app_id']); + expect(verifedToken.token.app_id).to.be.a('string').and.equals(appId); + }); + }); + + it('should propagate API errors', () => { + // rejects with invalid-argument when the token is invalid + return admin.appCheck().verifyToken('invalid-token') + .should.eventually.be.rejected.and.have.property('code', 'app-check/invalid-argument'); + }); + }); +}); diff --git a/test/resources/mock.jwks.json b/test/resources/mock.jwks.json new file mode 100644 index 0000000000..08695991c3 --- /dev/null +++ b/test/resources/mock.jwks.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "FGQdnRlzAmKyKr6-Hg_kMQrBkj_H6i6ADnBQz4OI6BU", + "alg": "RS256", + "n": "rFYQyEdjj43mnpXwj-3WgAE01TSYe1-XFE9mxUDShysFwtVZOHFSMm6kl-B3Y_O8NcPt5osntLlH6KHvygExAE0tDmFYq8aKt7LQQF8rTv0rI6MP92ezyCEp4MPmAPFD_tY160XGrkqApuY2_-L8eEXdkRyH2H7lCYypFC0u3DIY25Vlq-ZDkxB2kGykGgb1zVazCDDViqV1p9hSltmm4el9AyF08FsMCpk_NvwKOY4pJ_sm99CDKxMhQBaT9lrIQt0B1VqTpEwlOoiFiyXASRXp9ZTeL4mrLPqSeozwPvspD81wbgecd62F640scKBr3ko73L8M8UWcwgd-moKCJw" + } + ] +} diff --git a/test/resources/mocks.ts b/test/resources/mocks.ts index 5512756039..152364163e 100644 --- a/test/resources/mocks.ts +++ b/test/resources/mocks.ts @@ -36,6 +36,8 @@ const ONE_HOUR_IN_SECONDS = 60 * 60; export const uid = 'someUid'; export const projectId = 'project_id'; +export const projectNumber = '12345678'; +export const appId = '12345678:app:ID'; export const developerClaims = { one: 'uno', two: 'dos', @@ -146,6 +148,10 @@ export const refreshToken = { type: 'refreshToken', }; +// Randomly generated JSON Web Key Sets that do not correspond to anything related to Firebase. +// eslint-disable-next-line @typescript-eslint/no-var-requires +export const jwksResponse = require('./mock.jwks.json'); + // eslint-disable-next-line @typescript-eslint/no-var-requires export const certificateObject = require('./mock.key.json'); @@ -178,6 +184,14 @@ export const x509CertPairs = [ /* eslint-enable max-len */ ]; +// Randomly generated key pairs that don't correspond to anything related to Firebase or GCP +export const jwksKeyPair = { + /* eslint-disable max-len */ + // The private key for this key pair is identical to the one used in ./mock.jwks.json + private: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArFYQyEdjj43mnpXwj+3WgAE01TSYe1+XFE9mxUDShysFwtVZ\nOHFSMm6kl+B3Y/O8NcPt5osntLlH6KHvygExAE0tDmFYq8aKt7LQQF8rTv0rI6MP\n92ezyCEp4MPmAPFD/tY160XGrkqApuY2/+L8eEXdkRyH2H7lCYypFC0u3DIY25Vl\nq+ZDkxB2kGykGgb1zVazCDDViqV1p9hSltmm4el9AyF08FsMCpk/NvwKOY4pJ/sm\n99CDKxMhQBaT9lrIQt0B1VqTpEwlOoiFiyXASRXp9ZTeL4mrLPqSeozwPvspD81w\nbgecd62F640scKBr3ko73L8M8UWcwgd+moKCJwIDAQABAoIBAEDPJQSMhE6KKL5e\n2NbntJDy4zGC1A0hh6llqtpnZETc0w/QN/tX8ndw0IklKwD1ukPl6OOYVVhLjVVZ\nANpQ1GKuo1ETHsuKoMQwhMyQfbL41m5SdkCuSRfsENmsEiUslkuRtzlBRlRpRDR/\nwxM8A4IflBFsT1IFdpC+yx8BVuwLc35iVnaGQpo/jhSDibt07j+FdOKEWkMGj+rL\nsHC6cpB2NMTBl9CIDLW/eq1amBOAGtsSKqoGJvaQY/mZf7SPkRjYIfIl2PWSaduT\nfmMrsYYFtHUKVOMYAD7P5RWNkS8oERucnXT3ouAECvip3Ew2JqlQc0FP7FS5CxH3\nWdfvLuECgYEA8Q7rJrDOdO867s7P/lXMklbAGnuNnAZJdAEXUMIaPJi7al97F119\n4DKBuF7c/dDf8CdiOvMzP8r/F8+FFx2D61xxkQNeuxo5Xjlt23OzW5EI2S6ABesZ\n/3sQWqvKCGuqN7WENYF3EiKyByQ22MYXk8CE7KZuO57Aj88t6TsaNhkCgYEAtwSs\nhbqKSCneC1bQ3wfSAF2kPYRrQEEa2VCLlX1Mz7zHufxksUWAnAbU8O3hIGnXjz6T\nqzivyJJhFSgNGeYpwV67GfXnibpr3OZ/yx2YXIQfp0daivj++kvEU7aNfM9rHZA9\nS3Gh7hKELdB9b0DkrX5GpLiZWA6NnJdrIRYbAj8CgYBCZSyJvJsxBA+EZTxOvk0Z\nZYGGCc/oUKb8p6xHVx8o35yHYQMjXWHlVaP7J03RLy3vFLnuqLvN71ixszviMQP7\n2LuDCJ2YBVIVzNWgY07cgqcgQrmKZ8YCY2AOyVBdX2JD8+AVaLJmMV49r1DYBj/K\nN3WlRPYJv+Ej+xmXKus+SQKBgHh/Zkthxxu+HQigL0M4teYxwSoTnj2e39uGsXBK\nICGCLIniiDVDCmswAFFkfV3G8frI+5a26t2Gqs6wIPgVVxaOlWeBROGkUNIPHMKR\niLgY8XJEg3OOfuoyql9niP5M3jyHtCOQ/Elv/YDgjUWLl0Q3KLHZLHUSl+AqvYj6\nMewnAoGBANgYzPZgP+wreI55BFR470blKh1mFz+YGa+53DCd7JdMH2pdp4hoh303\nXxpOSVlAuyv9SgTsZ7WjGO5UdhaBzVPKgN0OO6JQmQ5ZrOR8ZJ7VB73FiVHCEerj\n1m2zyFv6OT7vqdg+V1/SzxMEmXXFQv1g69k6nWGazne3IJlzrSpj\n-----END RSA PRIVATE KEY-----\n', + public: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArFYQyEdjj43mnpXwj+3W\ngAE01TSYe1+XFE9mxUDShysFwtVZOHFSMm6kl+B3Y/O8NcPt5osntLlH6KHvygEx\nAE0tDmFYq8aKt7LQQF8rTv0rI6MP92ezyCEp4MPmAPFD/tY160XGrkqApuY2/+L8\neEXdkRyH2H7lCYypFC0u3DIY25Vlq+ZDkxB2kGykGgb1zVazCDDViqV1p9hSltmm\n4el9AyF08FsMCpk/NvwKOY4pJ/sm99CDKxMhQBaT9lrIQt0B1VqTpEwlOoiFiyXA\nSRXp9ZTeL4mrLPqSeozwPvspD81wbgecd62F640scKBr3ko73L8M8UWcwgd+moKC\nJwIDAQAB\n-----END PUBLIC KEY-----\n', +}; + /** * Generates a mocked Firebase ID token. * @@ -227,6 +241,27 @@ export function generateSessionCookie(overrides?: object, expiresIn?: number): s return jwt.sign(developerClaims, certificateObject.private_key, options); } +/** + * Generates a mocked App Check token. + * + * @param {object} overrides Overrides for the generated token's attributes. + * @return {string} A mocked App Check token with any provided overrides included. + */ +export function generateAppCheckToken(overrides?: object): string { + const options = _.assign({ + audience: ['projects/' + projectNumber, 'projects/' + projectId], + expiresIn: ONE_HOUR_IN_SECONDS, + issuer: 'https://firebaseappcheck.googleapis.com/' + projectNumber, + subject: appId, + algorithm: ALGORITHM, + header: { + kid: jwksResponse.keys[0].kid, + }, + }, overrides); + + return jwt.sign(developerClaims, jwksKeyPair.private, options); +} + /** Mock socket emitter class. */ export class MockSocketEmitter extends events.EventEmitter { public setTimeout: (_: number) => void = () => undefined; 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 new file mode 100644 index 0000000000..fba1fba20d --- /dev/null +++ b/test/unit/app-check/app-check-api-client-internal.spec.ts @@ -0,0 +1,238 @@ +/*! + * @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 _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import { HttpClient } from '../../../src/utils/api-request'; +import * as utils from '../utils'; +import * as mocks from '../../resources/mocks'; +import { getSdkVersion } from '../../../src/utils'; + +import { FirebaseApp } from '../../../src/firebase-app'; +import { AppCheckApiClient, FirebaseAppCheckError } from '../../../src/app-check/app-check-api-client-internal'; +import { FirebaseAppError } from '../../../src/utils/error'; +import { deepCopy } from '../../../src/utils/deep-copy'; + +const expect = chai.expect; + +describe('AppCheckApiClient', () => { + + const ERROR_RESPONSE = { + error: { + code: 404, + message: 'Requested entity not found', + status: 'NOT_FOUND', + }, + }; + + const EXPECTED_HEADERS = { + 'Authorization': 'Bearer mock-token', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, + }; + + 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 APP_ID = '1:1234:android:1234'; + + const TEST_TOKEN_TO_EXCHANGE = 'signed-custom-token'; + + const TEST_RESPONSE = { + attestationToken: 'token', + ttl: '3s' + }; + + const mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + }; + + const clientWithoutProjectId = new AppCheckApiClient( + mocks.mockCredentialApp()); + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + let app: FirebaseApp; + let apiClient: AppCheckApiClient; + + beforeEach(() => { + app = mocks.appWithOptions(mockOptions); + apiClient = new AppCheckApiClient(app); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + return app.delete(); + }); + + describe('Constructor', () => { + it('should reject when the app is null', () => { + expect(() => new AppCheckApiClient(null as unknown as FirebaseApp)) + .to.throw('First argument passed to admin.appCheck() must be a valid Firebase app instance.'); + }); + }); + + describe('exchangeToken', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should throw given no appId', () => { + expect(() => { + (apiClient as any).exchangeToken(TEST_TOKEN_TO_EXCHANGE); + }).to.throw('appId` must be a non-empty string.'); + }); + + const invalidAppIds = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidAppIds.forEach((invalidAppId) => { + it('should throw given a non-string appId: ' + JSON.stringify(invalidAppId), () => { + expect(() => { + apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, invalidAppId as any); + }).to.throw('appId` must be a non-empty string.'); + }); + }); + + it('should throw given an empty string appId', () => { + expect(() => { + apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, ''); + }).to.throw('appId` must be a non-empty string.'); + }); + + it('should throw given no customToken', () => { + expect(() => { + (apiClient as any).exchangeToken(undefined, APP_ID); + }).to.throw('customToken` must be a non-empty string.'); + }); + + const invalidCustomTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidCustomTokens.forEach((invalidCustomToken) => { + it('should throw given a non-string customToken: ' + JSON.stringify(invalidCustomToken), () => { + expect(() => { + apiClient.exchangeToken(invalidCustomToken as any, APP_ID); + }).to.throw('customToken` must be a non-empty string.'); + }); + }); + + it('should throw given an empty string customToken', () => { + expect(() => { + apiClient.exchangeToken('', APP_ID); + }).to.throw('customToken` must be a non-empty string.'); + }); + + 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 FirebaseAppCheckError('not-found', 'Requested entity not found'); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .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 FirebaseAppCheckError('unknown-error', 'Unknown server error: {}'); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .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 FirebaseAppCheckError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .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.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + ['', 'abc', '3s2', 'sssa', '3.000000001', '3.2', null, NaN, true, [], {}, 100, 1.2, -200, -2.4] + .forEach((invalidDuration) => { + it(`should throw if the returned ttl duration is: ${invalidDuration}`, () => { + const response = deepCopy(TEST_RESPONSE); + (response as any).ttl = invalidDuration; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(response, 200)); + stubs.push(stub); + const expected = new FirebaseAppCheckError( + 'invalid-argument', '`ttl` must be a valid duration string with the suffix `s`.'); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + it('should resolve with the App Check token on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200)); + 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.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`, + headers: EXPECTED_HEADERS, + data: { customToken: TEST_TOKEN_TO_EXCHANGE } + }); + }); + }); + + new Map([['3s', 3000], ['4.1s', 4100], ['3.000000001s', 3000], ['3.000001s', 3000]]) + .forEach((ttlMillis, ttlString) => { // value, key, map + // 3 seconds with 0 nanoseconds expressed as "3s" + // 3 seconds and 1 nanosecond expressed as "3.000000001s" + // 3 seconds and 1 microsecond expressed as "3.000001s" + it(`should resolve with ttlMillis as ${ttlMillis} when ttl + from server is: ${ttlString}`, () => { + const response = deepCopy(TEST_RESPONSE); + (response as any).ttl = ttlString; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(response, 200)); + stubs.push(stub); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .then((resp) => { + expect(resp.token).to.deep.equal(response.attestationToken); + expect(resp.ttlMillis).to.deep.equal(ttlMillis); + }); + }); + }); + }); +}); diff --git a/test/unit/app-check/app-check.spec.ts b/test/unit/app-check/app-check.spec.ts new file mode 100644 index 0000000000..c30970bc13 --- /dev/null +++ b/test/unit/app-check/app-check.spec.ts @@ -0,0 +1,195 @@ +/*! + * @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 _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as mocks from '../../resources/mocks'; + +import { FirebaseApp } from '../../../src/firebase-app'; +import { AppCheck } from '../../../src/app-check/app-check'; +import { AppCheckApiClient, FirebaseAppCheckError } from '../../../src/app-check/app-check-api-client-internal'; +import { AppCheckTokenGenerator } from '../../../src/app-check/token-generator'; +import { HttpClient } from '../../../src/utils/api-request'; +import { ServiceAccountSigner } from '../../../src/utils/crypto-signer'; +import { AppCheckTokenVerifier } from '../../../src/app-check/token-verifier'; + +const expect = chai.expect; + +describe('AppCheck', () => { + + const INTERNAL_ERROR = new FirebaseAppCheckError('internal-error', 'message'); + const APP_ID = '1:1234:android:1234'; + const TEST_TOKEN_TO_EXCHANGE = 'signed-custom-token'; + + let appCheck: AppCheck; + + let mockApp: FirebaseApp; + let mockCredentialApp: FirebaseApp; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + before(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + appCheck = new AppCheck(mockApp); + }); + + after(() => { + return mockApp.delete(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + describe('Constructor', () => { + const invalidApps = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidApps.forEach((invalidApp) => { + it('should throw given invalid app: ' + JSON.stringify(invalidApp), () => { + expect(() => { + const appCheckAny: any = AppCheck; + return new appCheckAny(invalidApp); + }).to.throw( + 'First argument passed to admin.appCheck() must be a valid Firebase app ' + + 'instance.'); + }); + }); + + it('should throw given no app', () => { + expect(() => { + const appCheckAny: any = AppCheck; + return new appCheckAny(); + }).to.throw( + 'First argument passed to admin.appCheck() must be a valid Firebase app ' + + 'instance.'); + }); + + it('should reject when initialized without project ID', () => { + // Project ID not set in 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 appCheckWithoutProjectId = new AppCheck(mockCredentialApp); + const stub = sinon.stub(AppCheckTokenGenerator.prototype, 'createCustomToken') + .resolves(TEST_TOKEN_TO_EXCHANGE); + stubs.push(stub); + return appCheckWithoutProjectId.createToken(APP_ID) + .should.eventually.rejectedWith(noProjectId); + }); + + it('should reject when failed to contact the Metadata server', () => { + // Remove the Project ID to force a request to the Metadata server + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const appCheckWithoutProjectId = new AppCheck(mockCredentialApp); + const stub = sinon.stub(HttpClient.prototype, 'send') + .rejects(new Error('network error.')); + stubs.push(stub); + const expected = 'Failed to determine service account. Make sure to initialize the SDK ' + + 'with a service account credential. Alternatively specify a service account with ' + + 'iam.serviceAccounts.signBlob permission. Original error: ' + + 'Error: network error.'; + return appCheckWithoutProjectId.createToken(APP_ID) + .should.eventually.be.rejectedWith(expected); + }); + + it('should reject when failed to sign the token', () => { + const expected = 'sign error'; + const stub = sinon.stub(ServiceAccountSigner.prototype, 'sign') + .rejects(new Error(expected)); + stubs.push(stub); + return appCheck.createToken(APP_ID) + .should.eventually.be.rejectedWith(expected); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new AppCheck(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(appCheck.app).to.equal(mockApp); + }); + }); + + describe('createToken', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(AppCheckApiClient.prototype, 'exchangeToken') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return appCheck.createToken(APP_ID) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + it('should resolve with AppCheckToken on success', () => { + const response = { token: 'token', ttlMillis: 3000 }; + const stub = sinon + .stub(AppCheckApiClient.prototype, 'exchangeToken') + .resolves(response); + stubs.push(stub); + return appCheck.createToken(APP_ID) + .then((token) => { + expect(token.token).equals('token'); + expect(token.ttlMillis).equals(3000); + }); + }); + }); + + describe('verifyToken', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(AppCheckTokenVerifier.prototype, 'verifyToken') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return appCheck.verifyToken('token') + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + it('should resolve with VerifyAppCheckTokenResponse on success', () => { + const response = { + sub: 'app-id', + iss: 'https://firebaseappcheck.googleapis.com/123456', + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: 'app-id', + aud: ['123456', 'project-id'], + exp: 1617741496, + iat: 1516239022, + }; + const stub = sinon + .stub(AppCheckTokenVerifier.prototype, 'verifyToken') + .resolves(response); + stubs.push(stub); + return appCheck.verifyToken('token') + .then((tokenResponse) => { + expect(tokenResponse.appId).equals('app-id'); + expect(tokenResponse.token).equals(response); + }); + }); + }); +}); diff --git a/test/unit/app-check/token-generator.spec.ts b/test/unit/app-check/token-generator.spec.ts new file mode 100644 index 0000000000..66bb7cffba --- /dev/null +++ b/test/unit/app-check/token-generator.spec.ts @@ -0,0 +1,261 @@ +/*! + * @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 _ from 'lodash'; +import * as jwt from 'jsonwebtoken'; +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 { + appCheckErrorFromCryptoSignerError, + AppCheckTokenGenerator +} from '../../../src/app-check/token-generator'; +import { + CryptoSignerError, CryptoSignerErrorCode, ServiceAccountSigner +} from '../../../src/utils/crypto-signer'; +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; +import { FirebaseAppCheckError } from '../../../src/app-check/app-check-api-client-internal'; +import * as utils from '../utils'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +const ALGORITHM = 'RS256'; +const ONE_HOUR_IN_SECONDS = 60 * 60; +const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1beta.TokenExchangeService'; + +/** + * Verifies a token is signed with the private key corresponding to the provided public key. + * + * @param {string} token The token to verify. + * @param {string} publicKey The public key to use to verify the token. + * @return {Promise} A promise fulfilled with the decoded token if it is valid; otherwise, a rejected promise. + */ +function verifyToken(token: string, publicKey: string): Promise { + return new Promise((resolve, reject) => { + jwt.verify(token, publicKey, { + algorithms: [ALGORITHM], + }, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res as object); + } + }); + }); +} + +describe('AppCheckTokenGenerator', () => { + const cert = new ServiceAccountCredential(mocks.certificateObject); + const APP_ID = 'test-app-id'; + + let clock: sinon.SinonFakeTimers | undefined; + afterEach(() => { + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + describe('Constructor', () => { + it('should throw given no arguments', () => { + expect(() => { + // Need to overcome the type system to allow a call with no parameter + const anyFirebaseAppCheckTokenGenerator: any = AppCheckTokenGenerator; + return new anyFirebaseAppCheckTokenGenerator(); + }).to.throw('Must provide a CryptoSigner to use AppCheckTokenGenerator'); + }); + }); + + const invalidSigners: any[] = [null, NaN, 0, 1, true, false, '', 'a', [], _.noop]; + invalidSigners.forEach((invalidSigner) => { + it('should throw given invalid signer: ' + JSON.stringify(invalidSigner), () => { + expect(() => { + return new AppCheckTokenGenerator(invalidSigner as any); + }).to.throw('Must provide a CryptoSigner to use AppCheckTokenGenerator'); + }); + }); + + describe('createCustomToken()', () => { + const tokenGenerator = new AppCheckTokenGenerator(new ServiceAccountSigner(cert)); + + it('should throw given no appId', () => { + expect(() => { + (tokenGenerator as any).createCustomToken(); + }).to.throw(FirebaseAppCheckError).with.property('code', 'app-check/invalid-argument'); + }); + + const invalidAppIds = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidAppIds.forEach((invalidAppId) => { + it('should throw given a non-string appId: ' + JSON.stringify(invalidAppId), () => { + expect(() => { + tokenGenerator.createCustomToken(invalidAppId as any); + }).to.throw(FirebaseAppCheckError).with.property('code', 'app-check/invalid-argument'); + }); + }); + + it('should throw given an empty string appId', () => { + expect(() => { + tokenGenerator.createCustomToken(''); + }).to.throw(FirebaseAppCheckError).with.property('code', 'app-check/invalid-argument'); + }); + + it('should be fulfilled with a Firebase Custom JWT', () => { + return tokenGenerator.createCustomToken(APP_ID) + .should.eventually.be.a('string').and.not.be.empty; + }); + + it('should be fulfilled with a JWT with the correct decoded payload', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(APP_ID) + .then((token) => { + const decoded = jwt.decode(token); + const expected: { [key: string]: any } = { + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: APP_ID, + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_APP_CHECK_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + }; + + expect(decoded).to.deep.equal(expected); + }); + }); + + it('should be fulfilled with a JWT with the correct header', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(APP_ID) + .then((token) => { + const decoded: any = jwt.decode(token, { + complete: true, + }); + expect(decoded.header).to.deep.equal({ + alg: ALGORITHM, + typ: 'JWT', + }); + }); + }); + + it('should be fulfilled with a JWT which can be verified by the service account public key', () => { + return tokenGenerator.createCustomToken(APP_ID) + .then((token) => { + return verifyToken(token, mocks.keyPairs[0].public); + }); + }); + + it('should be fulfilled with a JWT which cannot be verified by a random public key', () => { + return tokenGenerator.createCustomToken(APP_ID) + .then((token) => { + return verifyToken(token, mocks.keyPairs[1].public) + .should.eventually.be.rejectedWith('invalid signature'); + }); + }); + + it('should be fulfilled with a JWT which expires after one hour', () => { + clock = sinon.useFakeTimers(1000); + + let token: string; + return tokenGenerator.createCustomToken(APP_ID) + .then((result) => { + token = result; + + clock!.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + + // Token should still be valid + return verifyToken(token, mocks.keyPairs[0].public); + }) + .then(() => { + clock!.tick(1); + + // Token should now be invalid + return verifyToken(token, mocks.keyPairs[0].public) + .should.eventually.be.rejectedWith('jwt expired'); + }); + }); + + describe('appCheckErrorFromCryptoSignerError', () => { + it('should convert CryptoSignerError to FirebaseAppCheckError', () => { + const cryptoError = new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_ARGUMENT, + message: 'test error.', + }); + const appCheckError = appCheckErrorFromCryptoSignerError(cryptoError); + expect(appCheckError).to.be.an.instanceof(FirebaseAppCheckError); + expect(appCheckError).to.have.property('code', 'app-check/invalid-argument'); + expect(appCheckError).to.have.property('message', 'test error.'); + }); + + it('should convert CryptoSignerError HttpError to FirebaseAppCheckError', () => { + const cryptoError = new CryptoSignerError({ + code: CryptoSignerErrorCode.SERVER_ERROR, + message: 'test error.', + cause: utils.errorFrom({ + error: { + message: 'server error.', + }, + }) + }); + const appCheckError = appCheckErrorFromCryptoSignerError(cryptoError); + expect(appCheckError).to.be.an.instanceof(FirebaseAppCheckError); + expect(appCheckError).to.have.property('code', 'app-check/unknown-error'); + expect(appCheckError).to.have.property('message', + 'Error returned from server while siging a custom token: server error.'); + }); + + it('should convert CryptoSignerError HttpError with no error.message to FirebaseAppCheckError', () => { + const cryptoError = new CryptoSignerError({ + code: CryptoSignerErrorCode.SERVER_ERROR, + message: 'test error.', + cause: utils.errorFrom({ + error: {}, + }) + }); + const appCheckError = appCheckErrorFromCryptoSignerError(cryptoError); + expect(appCheckError).to.be.an.instanceof(FirebaseAppCheckError); + expect(appCheckError).to.have.property('code', 'app-check/unknown-error'); + expect(appCheckError).to.have.property('message', + 'Error returned from server while siging a custom token: '+ + '{"status":500,"headers":{},"data":{"error":{}},"text":"{\\"error\\":{}}"}'); + }); + + it('should convert CryptoSignerError HttpError with no errorcode to FirebaseAppCheckError', () => { + const cryptoError = new CryptoSignerError({ + code: CryptoSignerErrorCode.SERVER_ERROR, + message: 'test error.', + cause: utils.errorFrom('server error.') + }); + const appCheckError = appCheckErrorFromCryptoSignerError(cryptoError); + expect(appCheckError).to.be.an.instanceof(FirebaseAppCheckError); + expect(appCheckError).to.have.property('code', 'app-check/internal-error'); + expect(appCheckError).to.have.property('message', + 'Error returned from server: null.'); + }); + }); + }); +}); diff --git a/test/unit/app-check/token-verifier.spec.ts b/test/unit/app-check/token-verifier.spec.ts new file mode 100644 index 0000000000..27d0be2fbf --- /dev/null +++ b/test/unit/app-check/token-verifier.spec.ts @@ -0,0 +1,245 @@ +/*! + * @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 _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as mocks from '../../resources/mocks'; +import * as nock from 'nock'; + +import { AppCheckTokenVerifier } from '../../../src/app-check/token-verifier'; +import { JwtError, JwtErrorCode, PublicKeySignatureVerifier } from '../../../src/utils/jwt'; + +const expect = chai.expect; + +const ONE_HOUR_IN_SECONDS = 60 * 60; + +describe('AppCheckTokenVerifier', () => { + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + let tokenVerifier: AppCheckTokenVerifier; + let clock: sinon.SinonFakeTimers | undefined; + + before(() => { + tokenVerifier = new AppCheckTokenVerifier(mocks.app()); + }); + + after(() => { + nock.cleanAll(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + describe('verifyJWT()', () => { + let mockedRequests: nock.Scope[] = []; + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); + mockedRequests = []; + + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should throw given no App Check token', () => { + expect(() => { + (tokenVerifier as any).verifyToken(); + }).to.throw('App check token must be a non-null string'); + }); + + const invalidTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidTokens.forEach((invalidToken) => { + it('should throw given a non-string App Check token: ' + JSON.stringify(invalidToken), () => { + expect(() => { + tokenVerifier.verifyToken(invalidToken as any); + }).to.throw('App check token must be a non-null string'); + }); + }); + + it('should throw given an empty string App Check token', () => { + return tokenVerifier.verifyToken('') + .should.eventually.be.rejectedWith('Decoding App Check token failed'); + }); + + it('should be rejected given an invalid App Check token', () => { + return tokenVerifier.verifyToken('invalid-token') + .should.eventually.be.rejectedWith('Decoding App Check token failed'); + }); + + it('should throw if the token verifier was initialized with no "project_id"', () => { + const tokenVerifierWithNoProjectId = new AppCheckTokenVerifier(mocks.mockCredentialApp()); + const expected = 'Must initialize app with a cert credential or set your Firebase project ID as ' + + 'the GOOGLE_CLOUD_PROJECT environment variable to verify an App Check token.'; + return tokenVerifierWithNoProjectId.verifyToken('app.check.token') + .should.eventually.be.rejectedWith(expected); + }); + + it('should be rejected given an App Check token with an incorrect algorithm', () => { + const mockAppCheckToken = mocks.generateAppCheckToken({ + algorithm: 'HS256', + }); + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has incorrect algorithm'); + }); + + const invalidAudiences = [ + 'incorrectAudience', [], [mocks.projectNumber, mocks.projectId], + ['projects/' + mocks.projectNumber, mocks.projectId] + ]; + invalidAudiences.forEach((invalidAudience) => { + it('should be rejected given an App Check token with an incorrect audience:' + + JSON.stringify(invalidAudience), () => { + const mockAppCheckToken = mocks.generateAppCheckToken({ + audience: invalidAudience, + }); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has incorrect "aud" (audience) claim'); + }); + }); + + it('should be rejected given an App Check token with an incorrect issuer', () => { + const mockAppCheckToken = mocks.generateAppCheckToken({ + issuer: 'incorrectIssuer', + }); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has incorrect "iss" (issuer) claim'); + }); + + it('should be rejected given an App Check token with an empty subject', () => { + const mockAppCheckToken = mocks.generateAppCheckToken({ + subject: '', + }); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has an empty string "sub" (subject) claim'); + }); + + it('should be rejected when the verifier throws no maching kid error', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.NO_MATCHING_KID, 'No matching key ID.')); + stubs.push(verifierStub); + + const mockAppCheckToken = mocks.generateAppCheckToken({ + header: { + kid: 'wrongkid', + }, + }); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has "kid" claim which does not ' + + 'correspond to a known public key'); + }); + + it('should be rejected when the verifier throws expired token error', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.TOKEN_EXPIRED, 'Expired token.')); + stubs.push(verifierStub); + + const mockAppCheckToken = mocks.generateAppCheckToken(); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has expired. ' + + 'Get a fresh App Check token from your client app and try again.') + .and.have.property('code', 'app-check/app-check-token-expired'); + }); + + it('should be rejected when the verifier throws invalid signature error.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.INVALID_SIGNATURE, 'invalid signature.')); + stubs.push(verifierStub); + + const mockAppCheckToken = mocks.generateAppCheckToken(); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has invalid signature'); + }); + + it('should be rejected when the verifier throws key fetch error.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.KEY_FETCH_ERROR, 'Error fetching Json Web Keys.')); + stubs.push(verifierStub); + + const mockAppCheckToken = mocks.generateAppCheckToken(); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('Error fetching Json Web Keys.'); + }); + + it('should be fulfilled when the kid is not present in the header (should try all the keys)', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + clock = sinon.useFakeTimers(1000); + + const mockAppCheckToken = mocks.generateAppCheckToken({ + header: {}, + }); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.fulfilled.and.deep.equal({ + one: 'uno', + two: 'dos', + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: ['projects/' + mocks.projectNumber, 'projects/' + mocks.projectId], + iss: 'https://firebaseappcheck.googleapis.com/' + mocks.projectNumber, + sub: mocks.appId, + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: mocks.appId, + }); + }); + + it('should be fulfilled with decoded claims given a valid App Check token', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + clock = sinon.useFakeTimers(1000); + + const mockAppCheckToken = mocks.generateAppCheckToken(); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.fulfilled.and.deep.equal({ + one: 'uno', + two: 'dos', + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: ['projects/' + mocks.projectNumber, 'projects/' + mocks.projectId], + iss: 'https://firebaseappcheck.googleapis.com/' + mocks.projectNumber, + sub: mocks.appId, + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: mocks.appId, + }); + }); + + }); +}); diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index c519c7a3ed..6a6d148b09 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -26,14 +26,13 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../../resources/mocks'; import { - BLACKLISTED_CLAIMS, FirebaseTokenGenerator, ServiceAccountSigner, IAMSigner, EmulatedSigner + BLACKLISTED_CLAIMS, FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from '../../../src/auth/token-generator'; +import { CryptoSignerError, CryptoSignerErrorCode, ServiceAccountSigner } from '../../../src/utils/crypto-signer'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; -import { AuthorizedHttpClient, HttpClient } from '../../../src/utils/api-request'; -import { FirebaseApp } from '../../../src/firebase-app'; -import * as utils from '../utils'; import { FirebaseAuthError } from '../../../src/utils/error'; +import * as utils from '../utils'; chai.should(); chai.use(sinonChai); @@ -66,195 +65,6 @@ function verifyToken(token: string, publicKey: string): Promise { }); } -describe('CryptoSigner', () => { - describe('ServiceAccountSigner', () => { - it('should throw given no arguments', () => { - expect(() => { - const anyServiceAccountSigner: any = ServiceAccountSigner; - return new anyServiceAccountSigner(); - }).to.throw('Must provide a service account credential to initialize ServiceAccountSigner'); - }); - - it('should not throw given a valid certificate', () => { - expect(() => { - return new ServiceAccountSigner(new ServiceAccountCredential(mocks.certificateObject)); - }).not.to.throw(); - }); - - it('should sign using the private_key in the certificate', () => { - const payload = Buffer.from('test'); - const cert = new ServiceAccountCredential(mocks.certificateObject); - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const crypto = require('crypto'); - const rsa = crypto.createSign('RSA-SHA256'); - rsa.update(payload); - const result = rsa.sign(cert.privateKey, 'base64'); - - const signer = new ServiceAccountSigner(cert); - return signer.sign(payload).then((signature) => { - expect(signature.toString('base64')).to.equal(result); - }); - }); - - it('should return the client_email from the certificate', () => { - const cert = new ServiceAccountCredential(mocks.certificateObject); - const signer = new ServiceAccountSigner(cert); - return signer.getAccountId().should.eventually.equal(cert.clientEmail); - }); - }); - - describe('IAMSigner', () => { - let mockApp: FirebaseApp; - let getTokenStub: sinon.SinonStub; - const mockAccessToken: string = utils.generateRandomAccessToken(); - - beforeEach(() => { - mockApp = mocks.app(); - getTokenStub = utils.stubGetAccessToken(mockAccessToken, mockApp); - return mockApp.INTERNAL.getToken(); - }); - - afterEach(() => { - getTokenStub.restore(); - return mockApp.delete(); - }); - - it('should throw given no arguments', () => { - expect(() => { - const anyIAMSigner: any = IAMSigner; - return new anyIAMSigner(); - }).to.throw('Must provide a HTTP client to initialize IAMSigner'); - }); - - describe('explicit service account ID', () => { - const response = { signedBlob: Buffer.from('testsignature').toString('base64') }; - const input = Buffer.from('input'); - const signRequest = { - method: 'POST', - url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-service-account:signBlob', - headers: { Authorization: `Bearer ${mockAccessToken}` }, - data: { payload: input.toString('base64') }, - }; - let stub: sinon.SinonStub; - - afterEach(() => { - stub.restore(); - }); - - it('should sign using the IAM service', () => { - const expectedResult = utils.responseFrom(response); - stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - const requestHandler = new AuthorizedHttpClient(mockApp); - const signer = new IAMSigner(requestHandler, 'test-service-account'); - return signer.sign(input).then((signature) => { - expect(signature.toString('base64')).to.equal(response.signedBlob); - expect(stub).to.have.been.calledOnce.and.calledWith(signRequest); - }); - }); - - it('should fail if the IAM service responds with an error', () => { - const expectedResult = utils.errorFrom({ - error: { - status: 'PROJECT_NOT_FOUND', - message: 'test reason', - }, - }); - stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); - const requestHandler = new AuthorizedHttpClient(mockApp); - const signer = new IAMSigner(requestHandler, 'test-service-account'); - return signer.sign(input).catch((err) => { - const message = 'test reason; Please refer to ' + - 'https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on ' + - 'how to use and troubleshoot this feature.'; - expect(err.message).to.equal(message); - expect(stub).to.have.been.calledOnce.and.calledWith(signRequest); - }); - }); - - it('should return the explicitly specified service account', () => { - const signer = new IAMSigner(new AuthorizedHttpClient(mockApp), 'test-service-account'); - return signer.getAccountId().should.eventually.equal('test-service-account'); - }); - }); - - describe('auto discovered service account', () => { - const input = Buffer.from('input'); - const response = { signedBlob: Buffer.from('testsignature').toString('base64') }; - const metadataRequest = { - method: 'GET', - url: 'http://metadata/computeMetadata/v1/instance/service-accounts/default/email', - headers: { 'Metadata-Flavor': 'Google' }, - }; - const signRequest = { - method: 'POST', - url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/discovered-service-account:signBlob', - headers: { Authorization: `Bearer ${mockAccessToken}` }, - data: { payload: input.toString('base64') }, - }; - let stub: sinon.SinonStub; - - afterEach(() => { - stub.restore(); - }); - - it('should sign using the IAM service', () => { - stub = sinon.stub(HttpClient.prototype, 'send'); - stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); - stub.onCall(1).resolves(utils.responseFrom(response)); - const requestHandler = new AuthorizedHttpClient(mockApp); - const signer = new IAMSigner(requestHandler); - return signer.sign(input).then((signature) => { - expect(signature.toString('base64')).to.equal(response.signedBlob); - expect(stub).to.have.been.calledTwice; - expect(stub.getCall(0).args[0]).to.deep.equal(metadataRequest); - expect(stub.getCall(1).args[0]).to.deep.equal(signRequest); - }); - }); - - it('should fail if the IAM service responds with an error', () => { - const expectedResult = { - error: { - status: 'PROJECT_NOT_FOUND', - message: 'test reason', - }, - }; - stub = sinon.stub(HttpClient.prototype, 'send'); - stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); - stub.onCall(1).rejects(utils.errorFrom(expectedResult)); - const requestHandler = new AuthorizedHttpClient(mockApp); - const signer = new IAMSigner(requestHandler); - return signer.sign(input).catch((err) => { - const message = 'test reason; Please refer to ' + - 'https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on ' + - 'how to use and troubleshoot this feature.'; - expect(err.message).to.equal(message); - expect(stub).to.have.been.calledTwice; - expect(stub.getCall(0).args[0]).to.deep.equal(metadataRequest); - expect(stub.getCall(1).args[0]).to.deep.equal(signRequest); - }); - }); - - it('should return the discovered service account', () => { - stub = sinon.stub(HttpClient.prototype, 'send'); - stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); - const signer = new IAMSigner(new AuthorizedHttpClient(mockApp)); - return signer.getAccountId().should.eventually.equal('discovered-service-account'); - }); - - it('should return the expected error when failed to contact the Metadata server', () => { - stub = sinon.stub(HttpClient.prototype, 'send'); - stub.onCall(0).rejects(utils.errorFrom('test error')); - const signer = new IAMSigner(new AuthorizedHttpClient(mockApp)); - const expected = 'Failed to determine service account. Make sure to initialize the SDK with ' + - 'a service account credential. Alternatively specify a service account with ' + - 'iam.serviceAccounts.signBlob permission.'; - return signer.getAccountId().should.eventually.be.rejectedWith(expected); - }); - }); - }); -}); - describe('FirebaseTokenGenerator', () => { const tenantId = 'tenantId1'; const cert = new ServiceAccountCredential(mocks.certificateObject); @@ -384,7 +194,7 @@ describe('FirebaseTokenGenerator', () => { BLACKLISTED_CLAIMS.forEach((blacklistedClaim) => { it('should throw given a developer claims object with a blacklisted claim: ' + blacklistedClaim, () => { - const blacklistedDeveloperClaims: {[key: string]: any} = _.clone(mocks.developerClaims); + const blacklistedDeveloperClaims: { [key: string]: any } = _.clone(mocks.developerClaims); blacklistedDeveloperClaims[blacklistedClaim] = true; expect(() => { tokenGenerator.createCustomToken(mocks.uid, blacklistedDeveloperClaims); @@ -415,7 +225,7 @@ describe('FirebaseTokenGenerator', () => { return tokenGenerator.createCustomToken(mocks.uid) .then((token) => { const decoded = jwt.decode(token); - const expected: {[key: string]: any} = { + const expected: { [key: string]: any } = { uid: mocks.uid, iat: 1, exp: ONE_HOUR_IN_SECONDS + 1, @@ -440,7 +250,7 @@ describe('FirebaseTokenGenerator', () => { .then((token) => { const decoded = jwt.decode(token); - const expected: {[key: string]: any} = { + const expected: { [key: string]: any } = { uid: mocks.uid, iat: 1, exp: ONE_HOUR_IN_SECONDS + 1, @@ -526,4 +336,47 @@ describe('FirebaseTokenGenerator', () => { }); }); }); + + describe('handleCryptoSignerError', () => { + it('should convert CryptoSignerError to FirebaseAuthError', () => { + const cryptoError = new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_ARGUMENT, + message: 'test error.', + }); + const authError = handleCryptoSignerError(cryptoError); + expect(authError).to.be.an.instanceof(FirebaseAuthError); + expect(authError).to.have.property('code', 'auth/argument-error'); + expect(authError).to.have.property('message', 'test error.'); + }); + + it('should convert CryptoSignerError HttpError to FirebaseAuthError', () => { + const cryptoError = new CryptoSignerError({ + code: CryptoSignerErrorCode.SERVER_ERROR, + message: 'test error.', + cause: utils.errorFrom({ + error: { + message: 'server error.', + }, + }) + }); + const authError = handleCryptoSignerError(cryptoError); + expect(authError).to.be.an.instanceof(FirebaseAuthError); + expect(authError).to.have.property('code', 'auth/internal-error'); + expect(authError).to.have.property('message', 'server error.; Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on how to use and troubleshoot this feature. Raw server response: "{"error":{"message":"server error."}}"'); + }); + + it('should convert CryptoSignerError HttpError with no errorcode to FirebaseAuthError', () => { + const cryptoError = new CryptoSignerError({ + code: CryptoSignerErrorCode.SERVER_ERROR, + message: 'test error.', + cause: utils.errorFrom('server error.') + }); + const authError = handleCryptoSignerError(cryptoError); + expect(authError).to.be.an.instanceof(FirebaseAuthError); + expect(authError).to.have.property('code', 'auth/internal-error'); + expect(authError).to.have.property('message', + 'Error returned from server: null. Additionally, an internal error occurred ' + + 'while attempting to extract the errorcode from the error.'); + }); + }); }); diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 1f7e3546f8..a8afe7167b 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -27,7 +27,8 @@ import { Agent } from 'http'; import LegacyFirebaseTokenGenerator = require('firebase-token-generator'); import * as mocks from '../../resources/mocks'; -import { FirebaseTokenGenerator, ServiceAccountSigner } from '../../../src/auth/token-generator'; +import { FirebaseTokenGenerator } from '../../../src/auth/token-generator'; +import { ServiceAccountSigner } from '../../../src/utils/crypto-signer'; import * as verifier from '../../../src/auth/token-verifier'; import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index 6edf73b7ef..0989e718d0 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -41,6 +41,7 @@ import { instanceId } from '../../src/instance-id/index'; import { projectManagement } from '../../src/project-management/index'; import { securityRules } from '../../src/security-rules/index'; import { remoteConfig } from '../../src/remote-config/index'; +import { appCheck } from '../../src/app-check/index'; import { FirebaseAppError, AppErrorCodes } from '../../src/utils/error'; import Auth = auth.Auth; @@ -53,6 +54,7 @@ import InstanceId = instanceId.InstanceId; import ProjectManagement = projectManagement.ProjectManagement; import SecurityRules = securityRules.SecurityRules; import RemoteConfig = remoteConfig.RemoteConfig; +import AppCheck = appCheck.AppCheck; chai.should(); chai.use(sinonChai); @@ -669,6 +671,32 @@ describe('FirebaseApp', () => { }); }); + describe('appCheck()', () => { + it('should throw if the app has already been deleted', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + return app.delete().then(() => { + expect(() => { + return app.appCheck(); + }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); + }); + }); + + it('should return the AppCheck client', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + const appCheck: AppCheck = app.appCheck(); + expect(appCheck).to.not.be.null; + }); + + it('should return a cached version of AppCheck on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + const service1: AppCheck = app.appCheck(); + const service2: AppCheck = app.appCheck(); + expect(service1).to.equal(service2); + }); + }); + describe('INTERNAL.getToken()', () => { it('throws a custom credential implementation which returns invalid access tokens', () => { diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 07e5a8ba78..e2efb84f4e 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -56,7 +56,9 @@ import { instanceId } from '../../src/instance-id/index'; import { projectManagement } from '../../src/project-management/index'; import { securityRules } from '../../src/security-rules/index'; import { remoteConfig } from '../../src/remote-config/index'; +import { appCheck } from '../../src/app-check/index'; +import { AppCheck as AppCheckImpl } from '../../src/app-check/app-check'; import { Auth as AuthImpl } from '../../src/auth/auth'; import { InstanceId as InstanceIdImpl } from '../../src/instance-id/instance-id'; import { MachineLearning as MachineLearningImpl } from '../../src/machine-learning/machine-learning'; @@ -67,6 +69,7 @@ import { SecurityRules as SecurityRulesImpl } from '../../src/security-rules/sec import { Storage as StorageImpl } from '../../src/storage/storage'; import App = app.App; +import AppCheck = appCheck.AppCheck; import Auth = auth.Auth; import Database = database.Database; import Firestore = firestore.Firestore; @@ -759,4 +762,42 @@ describe('FirebaseNamespace', () => { expect(service1).to.equal(service2); }); }); + + describe('#appCheck()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.appCheck(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.appCheck(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should return a valid namespace when the default app is initialized', () => { + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); + const fac: AppCheck = firebaseNamespace.appCheck(); + expect(fac.app).to.be.deep.equal(app); + }); + + it('should return a valid namespace when the named app is initialized', () => { + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const fac: AppCheck = firebaseNamespace.appCheck(app); + expect(fac.app).to.be.deep.equal(app); + }); + + it('should return a reference to AppCheck type', () => { + expect(firebaseNamespace.appCheck.AppCheck).to.be.deep.equal(AppCheckImpl); + }); + + it('should return a cached version of AppCheck on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const service1: AppCheck = firebaseNamespace.appCheck(); + const service2: AppCheck = firebaseNamespace.appCheck(); + expect(service1).to.equal(service2); + }); + }); }); diff --git a/test/unit/firebase.spec.ts b/test/unit/firebase.spec.ts index 3048121529..da1256a2c2 100644 --- a/test/unit/firebase.spec.ts +++ b/test/unit/firebase.spec.ts @@ -235,6 +235,21 @@ describe('Firebase', () => { }); }); + describe('#appCheck', () => { + it('should throw if the app has not been initialized', () => { + expect(() => { + return firebaseAdmin.appCheck(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should return the appCheck service', () => { + firebaseAdmin.initializeApp(mocks.appOptions); + expect(() => { + return firebaseAdmin.appCheck(); + }).not.to.throw(); + }); + }); + describe('#storage', () => { it('should throw if the app has not be initialized', () => { expect(() => { diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index e8c5a6d17d..48c87e24e4 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -26,6 +26,7 @@ import './utils/error.spec'; import './utils/validator.spec'; import './utils/api-request.spec'; import './utils/jwt.spec'; +import './utils/crypto-signer.spec'; // Auth import './auth/auth.spec'; @@ -76,3 +77,9 @@ import './security-rules/security-rules-api-client.spec'; // RemoteConfig import './remote-config/remote-config.spec'; import './remote-config/remote-config-api-client.spec'; + +// AppCheck +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'; diff --git a/test/unit/utils/crypto-signer.spec.ts b/test/unit/utils/crypto-signer.spec.ts new file mode 100644 index 0000000000..9a59fd10eb --- /dev/null +++ b/test/unit/utils/crypto-signer.spec.ts @@ -0,0 +1,224 @@ +/*! + * @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 sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { ServiceAccountSigner, IAMSigner, CryptoSignerError } from '../../../src/utils/crypto-signer'; + +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; +import { AuthorizedHttpClient, HttpClient } from '../../../src/utils/api-request'; +import { FirebaseApp } from '../../../src/firebase-app'; +import * as utils from '../utils'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('CryptoSigner', () => { + describe('ServiceAccountSigner', () => { + it('should throw given no arguments', () => { + expect(() => { + const anyServiceAccountSigner: any = ServiceAccountSigner; + return new anyServiceAccountSigner(); + }).to.throw('Must provide a service account credential to initialize ServiceAccountSigner'); + }); + + it('should not throw given a valid certificate', () => { + expect(() => { + return new ServiceAccountSigner(new ServiceAccountCredential(mocks.certificateObject)); + }).not.to.throw(); + }); + + it('should sign using the private_key in the certificate', () => { + const payload = Buffer.from('test'); + const cert = new ServiceAccountCredential(mocks.certificateObject); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const crypto = require('crypto'); + const rsa = crypto.createSign('RSA-SHA256'); + rsa.update(payload); + const result = rsa.sign(cert.privateKey, 'base64'); + + const signer = new ServiceAccountSigner(cert); + return signer.sign(payload).then((signature) => { + expect(signature.toString('base64')).to.equal(result); + }); + }); + + it('should return the client_email from the certificate', () => { + const cert = new ServiceAccountCredential(mocks.certificateObject); + const signer = new ServiceAccountSigner(cert); + return signer.getAccountId().should.eventually.equal(cert.clientEmail); + }); + }); + + describe('IAMSigner', () => { + let mockApp: FirebaseApp; + let getTokenStub: sinon.SinonStub; + const mockAccessToken: string = utils.generateRandomAccessToken(); + + beforeEach(() => { + mockApp = mocks.app(); + getTokenStub = utils.stubGetAccessToken(mockAccessToken, mockApp); + return mockApp.INTERNAL.getToken(); + }); + + afterEach(() => { + getTokenStub.restore(); + return mockApp.delete(); + }); + + it('should throw given no arguments', () => { + expect(() => { + const anyIAMSigner: any = IAMSigner; + return new anyIAMSigner(); + }).to.throw('Must provide a HTTP client to initialize IAMSigner'); + }); + + describe('explicit service account ID', () => { + const response = { signedBlob: Buffer.from('testsignature').toString('base64') }; + const input = Buffer.from('input'); + const signRequest = { + method: 'POST', + url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-service-account:signBlob', + headers: { Authorization: `Bearer ${mockAccessToken}` }, + data: { payload: input.toString('base64') }, + }; + let stub: sinon.SinonStub; + + afterEach(() => { + stub.restore(); + }); + + it('should sign using the IAM service', () => { + const expectedResult = utils.responseFrom(response); + stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + const requestHandler = new AuthorizedHttpClient(mockApp); + const signer = new IAMSigner(requestHandler, 'test-service-account'); + return signer.sign(input).then((signature) => { + expect(signature.toString('base64')).to.equal(response.signedBlob); + expect(stub).to.have.been.calledOnce.and.calledWith(signRequest); + }); + }); + + it('should fail if the IAM service responds with an error', () => { + const expectedResult = utils.errorFrom({ + error: { + status: 'PROJECT_NOT_FOUND', + message: 'test reason', + }, + }); + stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + const requestHandler = new AuthorizedHttpClient(mockApp); + const signer = new IAMSigner(requestHandler, 'test-service-account'); + return signer.sign(input).catch((err) => { + expect(err).to.be.instanceOf(CryptoSignerError); + expect(err.message).to.equal('Server responded with status 500.'); + expect(err.cause).to.deep.equal(expectedResult); + expect(stub).to.have.been.calledOnce.and.calledWith(signRequest); + }); + }); + + it('should return the explicitly specified service account', () => { + const signer = new IAMSigner(new AuthorizedHttpClient(mockApp), 'test-service-account'); + return signer.getAccountId().should.eventually.equal('test-service-account'); + }); + }); + + describe('auto discovered service account', () => { + const input = Buffer.from('input'); + const response = { signedBlob: Buffer.from('testsignature').toString('base64') }; + const metadataRequest = { + method: 'GET', + url: 'http://metadata/computeMetadata/v1/instance/service-accounts/default/email', + headers: { 'Metadata-Flavor': 'Google' }, + }; + const signRequest = { + method: 'POST', + url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/discovered-service-account:signBlob', + headers: { Authorization: `Bearer ${mockAccessToken}` }, + data: { payload: input.toString('base64') }, + }; + let stub: sinon.SinonStub; + + afterEach(() => { + stub.restore(); + }); + + it('should sign using the IAM service', () => { + stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); + stub.onCall(1).resolves(utils.responseFrom(response)); + const requestHandler = new AuthorizedHttpClient(mockApp); + const signer = new IAMSigner(requestHandler); + return signer.sign(input).then((signature) => { + expect(signature.toString('base64')).to.equal(response.signedBlob); + expect(stub).to.have.been.calledTwice; + expect(stub.getCall(0).args[0]).to.deep.equal(metadataRequest); + expect(stub.getCall(1).args[0]).to.deep.equal(signRequest); + }); + }); + + it('should fail if the IAM service responds with an error', () => { + const expectedResult = utils.errorFrom({ + error: { + status: 'PROJECT_NOT_FOUND', + message: 'test reason', + }, + }); + stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); + stub.onCall(1).rejects(expectedResult); + const requestHandler = new AuthorizedHttpClient(mockApp); + const signer = new IAMSigner(requestHandler); + return signer.sign(input).catch((err) => { + expect(err).to.be.instanceOf(CryptoSignerError); + expect(err.message).to.equal('Server responded with status 500.'); + expect(err.cause).to.deep.equal(expectedResult); + expect(stub).to.have.been.calledTwice; + expect(stub.getCall(0).args[0]).to.deep.equal(metadataRequest); + expect(stub.getCall(1).args[0]).to.deep.equal(signRequest); + }); + }); + + it('should return the discovered service account', () => { + stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); + const signer = new IAMSigner(new AuthorizedHttpClient(mockApp)); + return signer.getAccountId().should.eventually.equal('discovered-service-account'); + }); + + it('should return the expected error when failed to contact the Metadata server', () => { + stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).rejects(utils.errorFrom('test error')); + const signer = new IAMSigner(new AuthorizedHttpClient(mockApp)); + const expected = 'Failed to determine service account. Make sure to initialize the SDK with ' + + 'a service account credential. Alternatively specify a service account with ' + + 'iam.serviceAccounts.signBlob permission.'; + return signer.getAccountId().should.eventually.be.rejectedWith(expected); + }); + }); + }); +}); diff --git a/test/unit/utils/jwt.spec.ts b/test/unit/utils/jwt.spec.ts index 525608feef..775bdd63b9 100644 --- a/test/unit/utils/jwt.spec.ts +++ b/test/unit/utils/jwt.spec.ts @@ -23,16 +23,19 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as nock from 'nock'; 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 * as jwtUtil from '../../../src/utils/jwt'; +import { + ALGORITHM_RS256, DecodedToken, decodeJwt, EmulatorSignatureVerifier, JwksFetcher, + JwtErrorCode, PublicKeySignatureVerifier, UrlKeyFetcher, verifyJwtSignature +} from '../../../src/utils/jwt'; const expect = chai.expect; const ONE_HOUR_IN_SECONDS = 60 * 60; +const ONE_DAY_IN_SECONDS = 86400; const publicCertPath = '/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; +const jwksPath = '/v1alpha/jwks'; /** * Returns a mocked out success response from the URL containing the public keys for the Google certs. @@ -80,6 +83,43 @@ function mockFailedFetchPublicKeys(): nock.Scope { .replyWithError('message'); } +/** + * Returns a mocked out success JWKS response. + * + * @return {Object} A nock response object. + */ +function mockFetchJsonWebKeys(path: string = jwksPath): nock.Scope { + return nock('https://firebaseappcheck.googleapis.com') + .get(path) + .reply(200, mocks.jwksResponse); +} + +/** + * Returns a mocked out error response for JWKS. + * The status code is 200 but the response itself will contain an 'error' key. + * + * @return {Object} A nock response object. + */ +function mockFetchJsonWebKeysWithErrorResponse(): nock.Scope { + return nock('https://firebaseappcheck.googleapis.com') + .get(jwksPath) + .reply(200, { + error: 'message', + error_description: 'description', // eslint-disable-line @typescript-eslint/camelcase + }); +} + +/** + * Returns a mocked out failed JSON Web Keys response. + * The status code is non-200 and the response itself will fail. + * + * @return {Object} A nock response object. + */ +function mockFailedFetchJsonWebKeys(): nock.Scope { + return nock('https://firebaseappcheck.googleapis.com') + .get(jwksPath) + .replyWithError('message'); +} const TOKEN_PAYLOAD = { one: 'uno', @@ -91,7 +131,7 @@ const TOKEN_PAYLOAD = { sub: mocks.uid, }; -const DECODED_SIGNED_TOKEN: jwtUtil.DecodedToken = { +const DECODED_SIGNED_TOKEN: DecodedToken = { header: { alg: 'RS256', kid: 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd', @@ -100,7 +140,7 @@ const DECODED_SIGNED_TOKEN: jwtUtil.DecodedToken = { payload: TOKEN_PAYLOAD }; -const DECODED_UNSIGNED_TOKEN: jwtUtil.DecodedToken = { +const DECODED_UNSIGNED_TOKEN: DecodedToken = { header: { alg: 'none', typ: 'JWT', @@ -122,25 +162,25 @@ describe('decodeJwt', () => { }); it('should reject given no token', () => { - return (jwtUtil.decodeJwt as any)() + return (decodeJwt as any)() .should.eventually.be.rejectedWith('The provided token must be a string.'); }); const invalidIdTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; invalidIdTokens.forEach((invalidIdToken) => { it('should reject given a non-string token: ' + JSON.stringify(invalidIdToken), () => { - return jwtUtil.decodeJwt(invalidIdToken as any) + return decodeJwt(invalidIdToken as any) .should.eventually.be.rejectedWith('The provided token must be a string.'); }); }); it('should reject given an empty string token', () => { - return jwtUtil.decodeJwt('') + return decodeJwt('') .should.eventually.be.rejectedWith('Decoding token failed.'); }); it('should reject given an invalid token', () => { - return jwtUtil.decodeJwt('invalid-token') + return decodeJwt('invalid-token') .should.eventually.be.rejectedWith('Decoding token failed.'); }); @@ -149,7 +189,7 @@ describe('decodeJwt', () => { const mockIdToken = mocks.generateIdToken(); - return jwtUtil.decodeJwt(mockIdToken) + return decodeJwt(mockIdToken) .should.eventually.be.fulfilled.and.deep.equal(DECODED_SIGNED_TOKEN); }); @@ -161,7 +201,7 @@ describe('decodeJwt', () => { header: {} }); - return jwtUtil.decodeJwt(mockIdToken) + return decodeJwt(mockIdToken) .should.eventually.be.fulfilled.and.deep.equal(DECODED_UNSIGNED_TOKEN); }); }); @@ -178,28 +218,28 @@ describe('verifyJwtSignature', () => { }); it('should throw given no token', () => { - return (jwtUtil.verifyJwtSignature as any)() + return (verifyJwtSignature as any)() .should.eventually.be.rejectedWith('The provided token must be a string.'); }); const invalidIdTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; invalidIdTokens.forEach((invalidIdToken) => { it('should reject given a non-string token: ' + JSON.stringify(invalidIdToken), () => { - return jwtUtil.verifyJwtSignature(invalidIdToken as any, mocks.keyPairs[0].public) + return verifyJwtSignature(invalidIdToken as any, mocks.keyPairs[0].public) .should.eventually.be.rejectedWith('The provided token must be a string.'); }); }); it('should reject given an empty string token', () => { - return jwtUtil.verifyJwtSignature('', mocks.keyPairs[0].public) + return verifyJwtSignature('', mocks.keyPairs[0].public) .should.eventually.be.rejectedWith('jwt must be provided'); }); it('should be fulfilled given a valid signed token and public key', () => { const mockIdToken = mocks.generateIdToken(); - return jwtUtil.verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, - { algorithms: [jwtUtil.ALGORITHM_RS256] }) + return verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + { algorithms: [ALGORITHM_RS256] }) .should.eventually.be.fulfilled; }); @@ -209,7 +249,7 @@ describe('verifyJwtSignature', () => { header: {} }); - return jwtUtil.verifyJwtSignature(mockIdToken, '') + return verifyJwtSignature(mockIdToken, '') .should.eventually.be.fulfilled; }); @@ -217,18 +257,18 @@ describe('verifyJwtSignature', () => { const mockIdToken = mocks.generateIdToken(); const getKeyCallback = (_: any, callback: any): void => callback(null, mocks.keyPairs[0].public); - return jwtUtil.verifyJwtSignature(mockIdToken, getKeyCallback, - { algorithms: [jwtUtil.ALGORITHM_RS256] }) + return verifyJwtSignature(mockIdToken, getKeyCallback, + { algorithms: [ALGORITHM_RS256] }) .should.eventually.be.fulfilled; }); it('should be rejected when the given algorithm does not match the token', () => { const mockIdToken = mocks.generateIdToken(); - return jwtUtil.verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + return verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, { algorithms: ['RS384'] }) .should.eventually.be.rejectedWith('invalid algorithm') - .with.property('code', jwtUtil.JwtErrorCode.INVALID_SIGNATURE); + .with.property('code', JwtErrorCode.INVALID_SIGNATURE); }); it('should be rejected given an expired token', () => { @@ -237,18 +277,18 @@ describe('verifyJwtSignature', () => { clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); // token should still be valid - return jwtUtil.verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, - { algorithms: [jwtUtil.ALGORITHM_RS256] }) + return verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + { algorithms: [ALGORITHM_RS256] }) .then(() => { clock!.tick(1); // token should now be invalid - return jwtUtil.verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, - { algorithms: [jwtUtil.ALGORITHM_RS256] }) + return verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + { algorithms: [ALGORITHM_RS256] }) .should.eventually.be.rejectedWith( 'The provided token has expired. Get a fresh token from your client app and try again.' ) - .with.property('code', jwtUtil.JwtErrorCode.TOKEN_EXPIRED); + .with.property('code', JwtErrorCode.TOKEN_EXPIRED); }); }); @@ -257,10 +297,10 @@ describe('verifyJwtSignature', () => { const getKeyCallback = (_: any, callback: any): void => callback(new Error('key fetch failed.')); - return jwtUtil.verifyJwtSignature(mockIdToken, getKeyCallback, - { algorithms: [jwtUtil.ALGORITHM_RS256] }) + return verifyJwtSignature(mockIdToken, getKeyCallback, + { algorithms: [ALGORITHM_RS256] }) .should.eventually.be.rejectedWith('key fetch failed.') - .with.property('code', jwtUtil.JwtErrorCode.KEY_FETCH_ERROR); + .with.property('code', JwtErrorCode.KEY_FETCH_ERROR); }); it('should be rejected with correct no matching key id found error.', () => { @@ -268,43 +308,49 @@ describe('verifyJwtSignature', () => { const getKeyCallback = (_: any, callback: any): void => callback(new Error('no-matching-kid-error')); - return jwtUtil.verifyJwtSignature(mockIdToken, getKeyCallback, - { algorithms: [jwtUtil.ALGORITHM_RS256] }) + return verifyJwtSignature(mockIdToken, getKeyCallback, + { algorithms: [ALGORITHM_RS256] }) .should.eventually.be.rejectedWith('no-matching-kid-error') - .with.property('code', jwtUtil.JwtErrorCode.NO_MATCHING_KID); + .with.property('code', JwtErrorCode.NO_MATCHING_KID); }); it('should be rejected given a public key that does not match the token.', () => { const mockIdToken = mocks.generateIdToken(); - return jwtUtil.verifyJwtSignature(mockIdToken, mocks.keyPairs[1].public, - { algorithms: [jwtUtil.ALGORITHM_RS256] }) + return verifyJwtSignature(mockIdToken, mocks.keyPairs[1].public, + { algorithms: [ALGORITHM_RS256] }) .should.eventually.be.rejectedWith('invalid signature') - .with.property('code', jwtUtil.JwtErrorCode.INVALID_SIGNATURE); + .with.property('code', JwtErrorCode.INVALID_SIGNATURE); }); it('should be rejected given an invalid JWT.', () => { - return jwtUtil.verifyJwtSignature('invalid-token', mocks.keyPairs[0].public) + return verifyJwtSignature('invalid-token', mocks.keyPairs[0].public) .should.eventually.be.rejectedWith('jwt malformed') - .with.property('code', jwtUtil.JwtErrorCode.INVALID_SIGNATURE); + .with.property('code', JwtErrorCode.INVALID_SIGNATURE); }); }); describe('PublicKeySignatureVerifier', () => { let stubs: sinon.SinonStub[] = []; - const verifier = new jwtUtil.PublicKeySignatureVerifier( - new jwtUtil.UrlKeyFetcher('https://www.example.com/publicKeys')); + let clock: sinon.SinonFakeTimers | undefined; + const verifier = new PublicKeySignatureVerifier( + new UrlKeyFetcher('https://www.example.com/publicKeys')); afterEach(() => { _.forEach(stubs, (stub) => stub.restore()); stubs = []; + + if (clock) { + clock.restore(); + clock = undefined; + } }); describe('Constructor', () => { it('should not throw when valid key fetcher is provided', () => { expect(() => { - new jwtUtil.PublicKeySignatureVerifier( - new jwtUtil.UrlKeyFetcher('https://www.example.com/publicKeys')); + new PublicKeySignatureVerifier( + new UrlKeyFetcher('https://www.example.com/publicKeys')); }).not.to.throw(); }); @@ -312,17 +358,27 @@ describe('PublicKeySignatureVerifier', () => { invalidKeyFetchers.forEach((invalidKeyFetcher) => { it('should throw given an invalid key fetcher: ' + JSON.stringify(invalidKeyFetcher), () => { expect(() => { - new jwtUtil.PublicKeySignatureVerifier(invalidKeyFetchers as any); + new PublicKeySignatureVerifier(invalidKeyFetchers as any); }).to.throw('The provided key fetcher is not an object or null.'); }); }); }); describe('withCertificateUrl', () => { - it('should return a PublicKeySignatureVerifier instance when a valid cert url is provided', () => { - expect( - jwtUtil.PublicKeySignatureVerifier.withCertificateUrl('https://www.example.com/publicKeys') - ).to.be.an.instanceOf(jwtUtil.PublicKeySignatureVerifier); + it('should return a PublicKeySignatureVerifier instance with a UrlKeyFetcher when a ' + + 'valid cert url is provided', () => { + const verifier = PublicKeySignatureVerifier.withCertificateUrl('https://www.example.com/publicKeys'); + expect(verifier).to.be.an.instanceOf(PublicKeySignatureVerifier); + expect((verifier as any).keyFetcher).to.be.an.instanceOf(UrlKeyFetcher); + }); + }); + + describe('withJwksUrl', () => { + it('should return a PublicKeySignatureVerifier instance with a JwksFetcher when a ' + + 'valid jwks url is provided', () => { + const verifier = PublicKeySignatureVerifier.withJwksUrl('https://www.example.com/publicKeys'); + expect(verifier).to.be.an.instanceOf(PublicKeySignatureVerifier); + expect((verifier as any).keyFetcher).to.be.an.instanceOf(JwksFetcher); }); }); @@ -346,7 +402,7 @@ describe('PublicKeySignatureVerifier', () => { }); it('should be fullfilled given a valid token', () => { - const keyFetcherStub = sinon.stub(jwtUtil.UrlKeyFetcher.prototype, 'fetchPublicKeys') + const keyFetcherStub = sinon.stub(UrlKeyFetcher.prototype, 'fetchPublicKeys') .resolves(VALID_PUBLIC_KEYS_RESPONSE); stubs.push(keyFetcherStub); const mockIdToken = mocks.generateIdToken(); @@ -354,8 +410,41 @@ describe('PublicKeySignatureVerifier', () => { return verifier.verify(mockIdToken).should.eventually.be.fulfilled; }); + it('should be fullfilled given a valid token without a kid (should check against all the keys)', () => { + const keyFetcherStub = sinon.stub(UrlKeyFetcher.prototype, 'fetchPublicKeys') + .resolves({ 'kid-other': 'key-other', ...VALID_PUBLIC_KEYS_RESPONSE }); + stubs.push(keyFetcherStub); + const mockIdToken = mocks.generateIdToken({ + header: {} + }); + + return verifier.verify(mockIdToken).should.eventually.be.fulfilled; + }); + + it('should be rejected given an expired token without a kid (should check against all the keys)', () => { + const keyFetcherStub = sinon.stub(UrlKeyFetcher.prototype, 'fetchPublicKeys') + .resolves({ 'kid-other': 'key-other', ...VALID_PUBLIC_KEYS_RESPONSE }); + stubs.push(keyFetcherStub); + clock = sinon.useFakeTimers(1000); + const mockIdToken = mocks.generateIdToken({ + header: {} + }); + clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + + // token should still be valid + return verifier.verify(mockIdToken) + .then(() => { + clock!.tick(1); + + // token should now be invalid + return verifier.verify(mockIdToken).should.eventually.be.rejectedWith( + 'The provided token has expired. Get a fresh token from your client app and try again.') + .with.property('code', JwtErrorCode.TOKEN_EXPIRED); + }); + }); + it('should be rejected given a token with an incorrect algorithm', () => { - const keyFetcherStub = sinon.stub(jwtUtil.UrlKeyFetcher.prototype, 'fetchPublicKeys') + const keyFetcherStub = sinon.stub(UrlKeyFetcher.prototype, 'fetchPublicKeys') .resolves(VALID_PUBLIC_KEYS_RESPONSE); stubs.push(keyFetcherStub); const mockIdToken = mocks.generateIdToken({ @@ -364,36 +453,36 @@ describe('PublicKeySignatureVerifier', () => { return verifier.verify(mockIdToken).should.eventually.be .rejectedWith('invalid algorithm') - .with.property('code', jwtUtil.JwtErrorCode.INVALID_SIGNATURE); + .with.property('code', JwtErrorCode.INVALID_SIGNATURE); }); // tests to cover the private getKeyCallback function. it('should reject when no matching kid found', () => { - const keyFetcherStub = sinon.stub(jwtUtil.UrlKeyFetcher.prototype, 'fetchPublicKeys') + const keyFetcherStub = sinon.stub(UrlKeyFetcher.prototype, 'fetchPublicKeys') .resolves({ 'not-a-matching-key': 'public-key' }); stubs.push(keyFetcherStub); const mockIdToken = mocks.generateIdToken(); return verifier.verify(mockIdToken).should.eventually.be .rejectedWith('no-matching-kid-error') - .with.property('code', jwtUtil.JwtErrorCode.NO_MATCHING_KID); + .with.property('code', JwtErrorCode.NO_MATCHING_KID); }); it('should reject when an error occurs while fetching the keys', () => { - const keyFetcherStub = sinon.stub(jwtUtil.UrlKeyFetcher.prototype, 'fetchPublicKeys') + const keyFetcherStub = sinon.stub(UrlKeyFetcher.prototype, 'fetchPublicKeys') .rejects(new Error('Error fetching public keys.')); stubs.push(keyFetcherStub); const mockIdToken = mocks.generateIdToken(); return verifier.verify(mockIdToken).should.eventually.be .rejectedWith('Error fetching public keys.') - .with.property('code', jwtUtil.JwtErrorCode.KEY_FETCH_ERROR); + .with.property('code', JwtErrorCode.KEY_FETCH_ERROR); }); }); }); describe('EmulatorSignatureVerifier', () => { - const emulatorVerifier = new jwtUtil.EmulatorSignatureVerifier(); + const emulatorVerifier = new EmulatorSignatureVerifier(); describe('verify', () => { it('should be fullfilled given a valid unsigned (emulator) token', () => { @@ -415,12 +504,12 @@ describe('EmulatorSignatureVerifier', () => { describe('UrlKeyFetcher', () => { const agent = new https.Agent(); - let keyFetcher: jwtUtil.UrlKeyFetcher; + let keyFetcher: UrlKeyFetcher; let clock: sinon.SinonFakeTimers | undefined; let httpsSpy: sinon.SinonSpy; beforeEach(() => { - keyFetcher = new jwtUtil.UrlKeyFetcher( + keyFetcher = new UrlKeyFetcher( 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', agent); httpsSpy = sinon.spy(https, 'request'); @@ -441,7 +530,7 @@ describe('UrlKeyFetcher', () => { describe('Constructor', () => { it('should not throw when valid key parameters are provided', () => { expect(() => { - new jwtUtil.UrlKeyFetcher('https://www.example.com/publicKeys', agent); + new UrlKeyFetcher('https://www.example.com/publicKeys', agent); }).not.to.throw(); }); @@ -449,7 +538,7 @@ describe('UrlKeyFetcher', () => { invalidCertURLs.forEach((invalidCertUrl) => { it('should throw given a non-URL public cert: ' + JSON.stringify(invalidCertUrl), () => { expect(() => { - new jwtUtil.UrlKeyFetcher(invalidCertUrl as any, agent); + new UrlKeyFetcher(invalidCertUrl as any, agent); }).to.throw('The provided public client certificate URL is not a valid URL.'); }); }); @@ -465,7 +554,7 @@ describe('UrlKeyFetcher', () => { it('should use the given HTTP Agent', () => { const agent = new https.Agent(); - const urlKeyFetcher = new jwtUtil.UrlKeyFetcher('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', agent); + const urlKeyFetcher = new UrlKeyFetcher('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', agent); mockedRequests.push(mockFetchPublicKeys()); return urlKeyFetcher.fetchPublicKeys() @@ -478,7 +567,7 @@ describe('UrlKeyFetcher', () => { it('should not fetch the public keys until the first time fetchPublicKeys() is called', () => { mockedRequests.push(mockFetchPublicKeys()); - const urlKeyFetcher = new jwtUtil.UrlKeyFetcher('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', agent); + const urlKeyFetcher = new UrlKeyFetcher('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', agent); expect(https.request).not.to.have.been.called; return urlKeyFetcher.fetchPublicKeys() @@ -539,3 +628,121 @@ describe('UrlKeyFetcher', () => { }); }); }); + +describe('JwksFetcher', () => { + let keyFetcher: JwksFetcher; + let clock: sinon.SinonFakeTimers | undefined; + let httpsSpy: sinon.SinonSpy; + + beforeEach(() => { + keyFetcher = new JwksFetcher( + 'https://firebaseappcheck.googleapis.com/v1alpha/jwks' + ); + httpsSpy = sinon.spy(https, 'request'); + }); + + afterEach(() => { + if (clock) { + clock.restore(); + clock = undefined; + } + httpsSpy.restore(); + }); + + after(() => { + nock.cleanAll(); + }); + + describe('Constructor', () => { + it('should not throw when valid url is provided', () => { + expect(() => { + new JwksFetcher('https://www.example.com/publicKeys'); + }).not.to.throw(); + }); + + const invalidJwksURLs = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, 'file://invalid']; + invalidJwksURLs.forEach((invalidJwksURL) => { + it('should throw given a non-URL jwks endpoint: ' + JSON.stringify(invalidJwksURL), () => { + expect(() => { + new JwksFetcher(invalidJwksURL as any); + }).to.throw('The provided JWKS URL is not a valid URL.'); + }); + }); + }); + + describe('fetchPublicKeys', () => { + let mockedRequests: nock.Scope[] = []; + + afterEach(() => { + _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); + mockedRequests = []; + }); + + it('should not fetch the public keys until the first time fetchPublicKeys() is called', () => { + mockedRequests.push(mockFetchJsonWebKeys()); + + const jwksFetcher = new JwksFetcher('https://firebaseappcheck.googleapis.com/v1alpha/jwks'); + expect(https.request).not.to.have.been.called; + + return jwksFetcher.fetchPublicKeys() + .then((result) => { + expect(https.request).to.have.been.calledOnce; + expect(result).to.have.key(mocks.jwksResponse.keys[0].kid); + }); + }); + + it('should not re-fetch the public keys every time fetchPublicKeys() is called', () => { + mockedRequests.push(mockFetchJsonWebKeys()); + + return keyFetcher.fetchPublicKeys().then(() => { + expect(https.request).to.have.been.calledOnce; + return keyFetcher.fetchPublicKeys(); + }).then(() => expect(https.request).to.have.been.calledOnce); + }); + + it('should refresh the public keys after the previous set of keys expire', () => { + mockedRequests.push(mockFetchJsonWebKeys()); + mockedRequests.push(mockFetchJsonWebKeys()); + mockedRequests.push(mockFetchJsonWebKeys()); + + clock = sinon.useFakeTimers(1000); + + return keyFetcher.fetchPublicKeys().then(() => { + expect(https.request).to.have.been.calledOnce; + clock!.tick((ONE_DAY_IN_SECONDS - 1) * 1000); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + expect(https.request).to.have.been.calledOnce; + clock!.tick(ONE_DAY_IN_SECONDS * 1000); // 24 hours in milliseconds + return keyFetcher.fetchPublicKeys(); + }).then(() => { + // App check keys do not contain cache headers so we cache the keys for 24 hours. + // 24 hours has passed + expect(https.request).to.have.been.calledTwice; + clock!.tick((ONE_DAY_IN_SECONDS - 1) * 1000); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + expect(https.request).to.have.been.calledTwice; + clock!.tick(ONE_DAY_IN_SECONDS * 1000); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + // 48 hours have passed + expect(https.request).to.have.been.calledThrice; + }); + }); + + it('should be rejected if fetching the public keys fails', () => { + mockedRequests.push(mockFailedFetchJsonWebKeys()); + + return keyFetcher.fetchPublicKeys() + .should.eventually.be.rejectedWith('message'); + }); + + it('should be rejected if fetching the public keys returns a response with an error message', () => { + mockedRequests.push(mockFetchJsonWebKeysWithErrorResponse()); + + return keyFetcher.fetchPublicKeys() + .should.eventually.be.rejectedWith('Error fetching Json Web Keys'); + }); + }); +}); From ab034f5f4317d50ed49c62817286143a1f7d6ad9 Mon Sep 17 00:00:00 2001 From: Nikhil Agarwal <54072321+nikhilag@users.noreply.github.com> Date: Tue, 11 May 2021 03:14:55 +0530 Subject: [PATCH 106/160] Fix @types/node conflict with grpc and port type (#1258) Upgraded the @types/node dependency to v12.12.47 --- package-lock.json | 6 +++--- package.json | 2 +- src/utils/api-request.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99e55cda11..4dfd37a11e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1035,9 +1035,9 @@ } }, "@types/node": { - "version": "10.17.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.26.tgz", - "integrity": "sha512-myMwkO2Cr82kirHY8uknNRHEVtn0wV3DTQfkrjx17jmkstDRZ24gNUdl8AHXVyVclTYI/bNjgTPTAWvWLqXqkw==" + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz", + "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==" }, "@types/qs": { "version": "6.9.6", diff --git a/package.json b/package.json index d384d64579..950cf1e5a2 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "dependencies": { "@firebase/database": "^0.10.0", "@firebase/database-types": "^0.7.2", - "@types/node": "^10.10.0", + "@types/node": ">=12.12.47", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^2.0.2", diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index a41a2f9b48..5b1b153288 100644 --- a/src/utils/api-request.ts +++ b/src/utils/api-request.ts @@ -728,7 +728,7 @@ class HttpRequestConfigImpl implements HttpRequestConfig { public buildRequestOptions(): https.RequestOptions { const parsed = this.buildUrl(); const protocol = parsed.protocol; - let port: string | undefined = parsed.port; + let port: string | null = parsed.port; if (!port) { const isHttps = protocol === 'https:'; port = isHttps ? '443' : '80'; From e5dc9f85bbf8f785e762dd5b314db40059fd62cc Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Mon, 10 May 2021 18:53:29 -0400 Subject: [PATCH 107/160] [chore] Release 9.8.0 (#1266) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 950cf1e5a2..f64b553668 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.7.0", + "version": "9.8.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From e759958d5d5f48ed89c7477acbc47811ce305ecb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 May 2021 16:14:15 -0700 Subject: [PATCH 108/160] build(deps): bump handlebars from 4.7.6 to 4.7.7 (#1253) Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.7.6 to 4.7.7. - [Release notes](https://github.com/wycats/handlebars.js/releases) - [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md) - [Commits](https://github.com/wycats/handlebars.js/compare/v4.7.6...v4.7.7) 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 4dfd37a11e..c701b2bb4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.7.0", + "version": "9.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4245,9 +4245,9 @@ } }, "handlebars": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", - "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", "dev": true, "requires": { "minimist": "^1.2.5", From e7155eacddd59cb449991476227c3a38681176cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 May 2021 16:28:17 -0700 Subject: [PATCH 109/160] build(deps): bump jose from 2.0.4 to 2.0.5 (#1265) Bumps [jose](https://github.com/panva/jose) from 2.0.4 to 2.0.5. - [Release notes](https://github.com/panva/jose/releases) - [Changelog](https://github.com/panva/jose/blob/v2.0.5/CHANGELOG.md) - [Commits](https://github.com/panva/jose/compare/v2.0.4...v2.0.5) 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 c701b2bb4d..a1e2c7999d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5122,9 +5122,9 @@ "dev": true }, "jose": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.4.tgz", - "integrity": "sha512-EArN9f6aq1LT/fIGGsfghOnNXn4noD+3dG5lL/ljY3LcRjw1u9w+4ahu/4ahsN6N0kRLyyW6zqdoYk7LNx3+YQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", "requires": { "@panva/asn1.js": "^1.0.0" } From fd23ad010c2763525bb71c7dc1895cdf0084bfd3 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Tue, 18 May 2021 14:47:58 -0700 Subject: [PATCH 110/160] fix: Revert regression introduced in #1257 (#1277) --- test/unit/storage/storage.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/storage/storage.spec.ts b/test/unit/storage/storage.spec.ts index e7d2360ee9..e37bffd0e4 100644 --- a/test/unit/storage/storage.spec.ts +++ b/test/unit/storage/storage.spec.ts @@ -114,7 +114,7 @@ describe('Storage', () => { }); }); - describe.only('Emulator mode', () => { + describe('Emulator mode', () => { const VALID_EMULATOR_HOST = 'localhost:9199'; const INVALID_EMULATOR_HOST = 'https://localhost:9199'; @@ -136,7 +136,7 @@ describe('Storage', () => { expect(() => new Storage(mockApp)).to.throw( 'FIREBASE_STORAGE_EMULATOR_HOST should not contain a protocol'); }); - + after(() => { delete process.env.STORAGE_EMULATOR_HOST; delete process.env.FIREBASE_STORAGE_EMULATOR_HOST; From 3b48235bf49d51d2d0f40b298ddaa71ce7432a20 Mon Sep 17 00:00:00 2001 From: bojeil-google Date: Tue, 18 May 2021 16:00:43 -0700 Subject: [PATCH 111/160] fix(auth): make MFA uid optional for updateUser operations (#1278) * fix(auth): make MFA uid optional for updateUser operations MFA `uid` should be optional for `updateUser` operations. When not specified, the backend will provision a `uid` for the enrolled second factor. Fixes https://github.com/firebase/firebase-admin-node/issues/1276 --- src/auth/auth-api-request.ts | 9 ++++----- test/unit/auth/auth-api-request.spec.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index d3ecf73fc7..bd4b2a5ce8 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -257,9 +257,8 @@ class AuthHttpClient extends AuthorizedHttpClient { * an error is thrown. * * @param request The AuthFactorInfo request object. - * @param writeOperationType The write operation type. */ -function validateAuthFactorInfo(request: AuthFactorInfo, writeOperationType: WriteOperationType): void { +function validateAuthFactorInfo(request: AuthFactorInfo): void { const validKeys = { mfaEnrollmentId: true, displayName: true, @@ -275,8 +274,8 @@ function validateAuthFactorInfo(request: AuthFactorInfo, writeOperationType: Wri // No enrollment ID is available for signupNewUser. Use another identifier. const authFactorInfoIdentifier = request.mfaEnrollmentId || request.phoneInfo || JSON.stringify(request); - const uidRequired = writeOperationType !== WriteOperationType.Create; - if ((typeof request.mfaEnrollmentId !== 'undefined' || uidRequired) && + // Enrollment uid may or may not be specified for update operations. + if (typeof request.mfaEnrollmentId !== 'undefined' && !validator.isNonEmptyString(request.mfaEnrollmentId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_UID, @@ -573,7 +572,7 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ENROLLED_FACTORS); } enrollments.forEach((authFactorInfoEntry: AuthFactorInfo) => { - validateAuthFactorInfo(authFactorInfoEntry, writeOperationType); + validateAuthFactorInfo(authFactorInfoEntry); }); } } diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 2df01889dd..0ca9bd5a85 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -2071,6 +2071,11 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { phoneNumber: '+16505551000', factorId: 'phone', } as UpdateMultiFactorInfoRequest, + { + // No error should be thrown when no uid is specified. + phoneNumber: '+16505551234', + factorId: 'phone', + } as UpdateMultiFactorInfoRequest, ], }, }; @@ -2096,6 +2101,9 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { mfaEnrollmentId: 'enrolledSecondFactor2', phoneInfo: '+16505551000', }, + { + phoneInfo: '+16505551234', + }, ], }, }; From 3a2d2fa929be3ae81c140fe3f6630cf9884bafe1 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 20 May 2021 10:28:49 -0700 Subject: [PATCH 112/160] chore: Enabled dependabot (#1279) --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..aff82a102d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" From 6bf2aeeb1d1bbdd2e6f4056d904afe9974c2c549 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 20 May 2021 12:00:43 -0700 Subject: [PATCH 113/160] chore: Remove gulp-replace dependency (#1285) --- package-lock.json | 56 +++++------------------------------------------ package.json | 1 - 2 files changed, 6 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index a1e2c7999d..fd6431ecd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -971,7 +971,7 @@ }, "@types/firebase-token-generator": { "version": "2.0.28", - "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", + "resolved": "http://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", "integrity": "sha1-Z1VIHZMk4mt6XItFXWgUg3aCw5Y=", "dev": true }, @@ -1728,12 +1728,6 @@ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, - "binaryextensions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", - "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", - "dev": true - }, "bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -3433,7 +3427,7 @@ }, "firebase-token-generator": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", "integrity": "sha1-l2fXWewTq9yZuhFf1eqZ2Lk9EgY=", "dev": true }, @@ -3854,7 +3848,7 @@ }, "globby": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "resolved": "http://registry.npmjs.org/globby/-/globby-5.0.0.tgz", "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", "dev": true, "requires": { @@ -4096,17 +4090,6 @@ } } }, - "gulp-replace": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-0.5.4.tgz", - "integrity": "sha1-aaZ5FLvRPFYr/xT1BKQDeWqg2qk=", - "dev": true, - "requires": { - "istextorbinary": "1.0.2", - "readable-stream": "^2.0.1", - "replacestream": "^4.0.0" - } - }, "gulp-typescript": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", @@ -5105,16 +5088,6 @@ "html-escaper": "^2.0.0" } }, - "istextorbinary": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", - "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", - "dev": true, - "requires": { - "binaryextensions": "~1.0.0", - "textextensions": "~1.0.0" - } - }, "jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", @@ -7279,7 +7252,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -7441,7 +7414,7 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, @@ -7742,17 +7715,6 @@ "remove-trailing-separator": "^1.1.0" } }, - "replacestream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz", - "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.3", - "object-assign": "^4.0.1", - "readable-stream": "^2.0.2" - } - }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -7984,7 +7946,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -8806,12 +8768,6 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "textextensions": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", - "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=", - "dev": true - }, "thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", diff --git a/package.json b/package.json index f64b553668..fee6d72a09 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,6 @@ "gulp": "^4.0.2", "gulp-filter": "^6.0.0", "gulp-header": "^1.8.8", - "gulp-replace": "^0.5.4", "gulp-typescript": "^5.0.1", "http-message-parser": "^0.0.34", "jsdom": "^15.0.0", From 11f2fb11929a23be97688b70ff1acdd5bd73d477 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 May 2021 12:11:01 -0700 Subject: [PATCH 114/160] build(deps-dev): bump gulp-header from 1.8.12 to 2.0.9 (#1283) Bumps [gulp-header](https://github.com/tracker1/gulp-header) from 1.8.12 to 2.0.9. - [Release notes](https://github.com/tracker1/gulp-header/releases) - [Changelog](https://github.com/gulp-community/gulp-header/blob/master/changelog.md) - [Commits](https://github.com/tracker1/gulp-header/compare/v1.8.12...v2.0.9) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 17 ++++++++++++----- package.json | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd6431ecd8..b5ddc137cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4068,13 +4068,14 @@ } }, "gulp-header": { - "version": "1.8.12", - "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz", - "integrity": "sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-2.0.9.tgz", + "integrity": "sha512-LMGiBx+qH8giwrOuuZXSGvswcIUh0OiioNkUpLhNyvaC6/Ga8X6cfAeme2L5PqsbXMhL8o8b/OmVqIQdxprhcQ==", "dev": true, "requires": { - "concat-with-sourcemaps": "*", - "lodash.template": "^4.4.0", + "concat-with-sourcemaps": "^1.1.0", + "lodash.template": "^4.5.0", + "map-stream": "0.0.7", "through2": "^2.0.0" }, "dependencies": { @@ -5746,6 +5747,12 @@ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "dev": true }, + "map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=", + "dev": true + }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", diff --git a/package.json b/package.json index fee6d72a09..5d19d0cab5 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "firebase-token-generator": "^2.0.0", "gulp": "^4.0.2", "gulp-filter": "^6.0.0", - "gulp-header": "^1.8.8", + "gulp-header": "^2.0.9", "gulp-typescript": "^5.0.1", "http-message-parser": "^0.0.34", "jsdom": "^15.0.0", From df3b3986a5086fdcdf8270d21fc3805f31d593a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 May 2021 12:43:41 -0700 Subject: [PATCH 115/160] build(deps-dev): bump run-sequence from 1.2.2 to 2.2.1 (#1282) Bumps [run-sequence](https://github.com/OverZealous/run-sequence) from 1.2.2 to 2.2.1. - [Release notes](https://github.com/OverZealous/run-sequence/releases) - [Changelog](https://github.com/OverZealous/run-sequence/blob/master/CHANGELOG.md) - [Commits](https://github.com/OverZealous/run-sequence/compare/v1.2.2...v2.2.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 338 +++++++++++----------------------------------- package.json | 2 +- 2 files changed, 77 insertions(+), 263 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5ddc137cf..991b60bc98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1263,6 +1263,15 @@ "ansi-wrap": "^0.1.0" } }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-escapes": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", @@ -1289,6 +1298,15 @@ "ansi-wrap": "0.1.0" } }, + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "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", @@ -1414,12 +1432,6 @@ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", - "dev": true - }, "array-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", @@ -1710,12 +1722,6 @@ "tweetnacl": "^0.14.3" } }, - "beeper": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", - "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", - "dev": true - }, "bignumber.js": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", @@ -2423,12 +2429,6 @@ "integrity": "sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA==", "optional": true }, - "dateformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", - "dev": true - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -2645,41 +2645,6 @@ "is-obj": "^2.0.0" } }, - "duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", - "dev": true, - "requires": { - "readable-stream": "~1.1.9" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -4119,106 +4084,6 @@ } } }, - "gulp-util": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", - "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-uniq": "^1.0.2", - "beeper": "^1.0.0", - "chalk": "^1.0.0", - "dateformat": "^2.0.0", - "fancy-log": "^1.1.0", - "gulplog": "^1.0.0", - "has-gulplog": "^0.1.0", - "lodash._reescape": "^3.0.0", - "lodash._reevaluate": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.template": "^3.0.0", - "minimist": "^1.1.0", - "multipipe": "^0.1.2", - "object-assign": "^3.0.0", - "replace-ext": "0.0.1", - "through2": "^2.0.0", - "vinyl": "^0.5.0" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", - "dev": true - }, - "lodash.template": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", - "dev": true, - "requires": { - "lodash._basecopy": "^3.0.0", - "lodash._basetostring": "^3.0.0", - "lodash._basevalues": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0", - "lodash.keys": "^3.0.0", - "lodash.restparam": "^3.0.0", - "lodash.templatesettings": "^3.0.0" - } - }, - "lodash.templatesettings": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", - "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0" - } - }, - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", - "dev": true - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", - "dev": true, - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - } - } - } - }, "gulplog": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", @@ -4289,15 +4154,6 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "has-gulplog": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", - "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", - "dev": true, - "requires": { - "sparkles": "^1.0.0" - } - }, "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", @@ -5424,60 +5280,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basetostring": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", - "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", - "dev": true - }, - "lodash._basevalues": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", - "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "lodash._reescape": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", - "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", - "dev": true - }, - "lodash._reevaluate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", - "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", - "dev": true - }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", "dev": true }, - "lodash._root": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", - "dev": true - }, "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -5489,15 +5297,6 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, - "lodash.escape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", - "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", - "dev": true, - "requires": { - "lodash._root": "^3.0.0" - } - }, "lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -5515,18 +5314,6 @@ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, "lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -5558,28 +5345,11 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, "lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", - "dev": true - }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", @@ -6355,15 +6125,6 @@ } } }, - "multipipe": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", - "dev": true, - "requires": { - "duplexer2": "0.0.2" - } - }, "mute-stdout": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", @@ -7928,13 +7689,66 @@ "dev": true }, "run-sequence": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-1.2.2.tgz", - "integrity": "sha1-UJWgvr6YczsBQL0I3YDsAw3azes=", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-2.2.1.tgz", + "integrity": "sha512-qkzZnQWMZjcKbh3CNly2srtrkaO/2H/SI5f2eliMCapdRD3UhMrwjfOAZJAnZ2H8Ju4aBzFZkBGXUqFs9V0yxw==", "dev": true, "requires": { - "chalk": "*", - "gulp-util": "*" + "chalk": "^1.1.3", + "fancy-log": "^1.3.2", + "plugin-error": "^0.1.2" + }, + "dependencies": { + "arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" + } + }, + "arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", + "dev": true + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", + "dev": true, + "requires": { + "kind-of": "^1.1.0" + } + }, + "kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", + "dev": true + }, + "plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", + "dev": true, + "requires": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" + } + } } }, "rxjs": { diff --git a/package.json b/package.json index 5d19d0cab5..561f1bb8c0 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "nyc": "^14.1.0", "request": "^2.75.0", "request-promise": "^4.1.1", - "run-sequence": "^1.1.5", + "run-sequence": "^2.2.1", "sinon": "^9.0.0", "sinon-chai": "^3.0.0", "ts-node": "^9.0.0", From 89387c1970c3b4b5990a47dee77d147a9348246e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 10:04:25 -0700 Subject: [PATCH 116/160] build(deps-dev): bump sinon from 9.0.2 to 9.2.4 (#1289) Bumps [sinon](https://github.com/sinonjs/sinon) from 9.0.2 to 9.2.4. - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/master/CHANGELOG.md) - [Commits](https://github.com/sinonjs/sinon/compare/v9.0.2...v9.2.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 59 +++++++++++++++++------------------------------ 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 991b60bc98..76724dcb1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -813,9 +813,9 @@ } }, "@sinonjs/commons": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", - "integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", "dev": true, "requires": { "type-detect": "4.0.8" @@ -830,20 +830,10 @@ "@sinonjs/commons": "^1.7.0" } }, - "@sinonjs/formatio": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", - "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^5.0.2" - } - }, "@sinonjs/samsam": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.0.3.tgz", - "integrity": "sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", + "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", "dev": true, "requires": { "@sinonjs/commons": "^1.6.0", @@ -5131,9 +5121,9 @@ "dev": true }, "just-extend": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", - "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, "jwa": { @@ -6233,9 +6223,9 @@ "dev": true }, "nise": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", - "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", + "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0", @@ -7884,26 +7874,19 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "sinon": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.2.tgz", - "integrity": "sha512-0uF8Q/QHkizNUmbK3LRFqx5cpTttEVXudywY9Uwzy8bTfZUhljZ7ARzSxnRHWYWtVTeh4Cw+tTb3iU21FQVO9A==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", + "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", "dev": true, "requires": { - "@sinonjs/commons": "^1.7.2", + "@sinonjs/commons": "^1.8.1", "@sinonjs/fake-timers": "^6.0.1", - "@sinonjs/formatio": "^5.0.1", - "@sinonjs/samsam": "^5.0.3", + "@sinonjs/samsam": "^5.3.1", "diff": "^4.0.2", - "nise": "^4.0.1", + "nise": "^4.0.4", "supports-color": "^7.1.0" }, "dependencies": { - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7911,9 +7894,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" From a549a6c5a0d33e456826d2f507c30f4fa410986d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 10:18:09 -0700 Subject: [PATCH 117/160] build(deps-dev): bump nyc from 14.1.1 to 15.1.0 (#1290) Bumps [nyc](https://github.com/istanbuljs/nyc) from 14.1.1 to 15.1.0. - [Release notes](https://github.com/istanbuljs/nyc/releases) - [Changelog](https://github.com/istanbuljs/nyc/blob/master/CHANGELOG.md) - [Commits](https://github.com/istanbuljs/nyc/compare/v14.1.1...v15.1.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 1371 ++++++++++++++++++++++++++++----------------- package.json | 2 +- 2 files changed, 863 insertions(+), 510 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76724dcb1f..2d20ad73ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,45 +13,214 @@ "@babel/highlight": "^7.10.4" } }, + "@babel/compat-data": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.0.tgz", + "integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==", + "dev": true + }, + "@babel/core": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", + "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.3", + "@babel/helper-compilation-targets": "^7.13.16", + "@babel/helper-module-transforms": "^7.14.2", + "@babel/helpers": "^7.14.0", + "@babel/parser": "^7.14.3", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "@babel/generator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", - "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz", + "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==", "dev": true, "requires": { - "@babel/types": "^7.10.4", + "@babel/types": "^7.14.2", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" } }, + "@babel/helper-compilation-targets": { + "version": "7.13.16", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz", + "integrity": "sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.15", + "@babel/helper-validator-option": "^7.12.17", + "browserslist": "^4.14.5", + "semver": "^6.3.0" + } + }, "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz", + "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.14.2" } }, "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", + "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", + "dev": true, + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-module-imports": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", + "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", + "dev": true, + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-module-transforms": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz", + "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-replace-supers": "^7.13.12", + "@babel/helper-simple-access": "^7.13.12", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/helper-validator-identifier": "^7.14.0", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + } + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", + "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-replace-supers": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.3.tgz", + "integrity": "sha512-Rlh8qEWZSTfdz+tgNV/N4gz1a0TMNwCUcENhMjHTHKp3LseYH5Jha0NSlyTQWMnjbYcwFt+bqAMqSLHVXkQ6UA==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/helper-member-expression-to-functions": "^7.13.12", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2" + } + }, + "@babel/helper-simple-access": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", + "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", + "dev": true, + "requires": { + "@babel/types": "^7.13.12" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", - "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.13" } }, "@babel/helper-validator-identifier": { @@ -60,6 +229,23 @@ "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, + "@babel/helper-validator-option": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz", + "integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==", + "dev": true, + "requires": { + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.0", + "@babel/types": "^7.14.0" + } + }, "@babel/highlight": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", @@ -103,56 +289,174 @@ } }, "@babel/parser": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", - "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.3.tgz", + "integrity": "sha512-7MpZDIfI7sUC5zWo2+foJ50CSI5lcqDehZ0lVgIhSi4bFEk94fLAKlF3Q0nzSQQ+ca0lm+O6G9ztKVBeu8PMRQ==", "dev": true }, "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/traverse": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", - "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz", + "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.10.4", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4", + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.2", + "@babel/helper-function-name": "^7.14.2", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.14.2", + "@babel/types": "^7.14.2", "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" + "globals": "^11.1.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, "@babel/types": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", - "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz", + "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.14.0", "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + } } }, "@firebase/app": { @@ -609,6 +913,55 @@ "protobufjs": "^6.8.6" } }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, "@microsoft/api-extractor": { "version": "7.11.2", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.11.2.tgz", @@ -1232,6 +1585,16 @@ "debug": "4" } }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.12.3", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", @@ -1341,12 +1704,12 @@ } }, "append-transform": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", "dev": true, "requires": { - "default-require-extensions": "^2.0.0" + "default-require-extensions": "^3.0.0" } }, "aproba": { @@ -1791,6 +2154,19 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + } + }, "buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -1836,50 +2212,15 @@ } }, "caching-transform": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", - "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", "dev": true, "requires": { - "hasha": "^3.0.0", - "make-dir": "^2.0.0", - "package-hash": "^3.0.0", - "write-file-atomic": "^2.4.2" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - } + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" } }, "callsites": { @@ -1894,6 +2235,12 @@ "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", "dev": true }, + "caniuse-lite": { + "version": "1.0.30001228", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz", + "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==", + "dev": true + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -2037,6 +2384,12 @@ } } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -2140,6 +2493,12 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, "colors": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", @@ -2289,43 +2648,6 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "cp-file": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", - "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "make-dir": "^2.0.0", - "nested-error-stacks": "^2.0.0", - "pify": "^4.0.1", - "safe-buffer": "^5.0.1" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "cross-spawn": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", @@ -2477,18 +2799,18 @@ } }, "default-require-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", "dev": true, "requires": { - "strip-bom": "^3.0.0" + "strip-bom": "^4.0.0" }, "dependencies": { "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true } } @@ -2674,6 +2996,12 @@ "safe-buffer": "^5.0.1" } }, + "electron-to-chromium": { + "version": "1.3.736", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz", + "integrity": "sha512-DY8dA7gR51MSo66DqitEQoUMQ0Z+A2DSXFi7tK304bdTVqczCAfUuyQw6Wdg8hIoo5zIxkU1L24RQtUce1Ioig==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3311,38 +3639,14 @@ } }, "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", "dev": true, "requires": { "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" } }, "find-up": { @@ -3452,13 +3756,56 @@ } }, "foreground-child": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", - "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, "requires": { - "cross-spawn": "^4", - "signal-exit": "^3.0.0" + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "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" + } + } } }, "forever-agent": { @@ -3487,6 +3834,12 @@ "map-cache": "^0.2.2" } }, + "fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true + }, "fs-extra": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", @@ -3638,6 +3991,12 @@ "stream-events": "^1.0.4" } }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -3650,6 +4009,12 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, "get-prop": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/get-prop/-/get-prop-0.0.10.tgz", @@ -4195,20 +4560,13 @@ "optional": true }, "hasha": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", - "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", "dev": true, "requires": { - "is-stream": "^1.0.1" - }, - "dependencies": { - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - } + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" } }, "he": { @@ -4368,6 +4726,12 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4736,8 +5100,7 @@ "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "optional": true + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" }, "is-stream-ended": { "version": "0.1.4", @@ -4810,114 +5173,144 @@ "dev": true }, "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", "dev": true }, "istanbul-lib-hook": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", - "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", "dev": true, "requires": { - "append-transform": "^1.0.0" + "append-transform": "^2.0.0" } }, "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" } }, - "istanbul-lib-report": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", - "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", "dev": true, "requires": { - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "supports-color": "^6.1.0" + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" }, "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "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" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } } } }, "istanbul-lib-source-maps": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", - "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", "dev": true, "requires": { "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", + "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" }, "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4927,12 +5320,13 @@ } }, "istanbul-reports": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", - "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", "dev": true, "requires": { - "html-escaper": "^2.0.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" } }, "jju": { @@ -5050,6 +5444,15 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "json5": { + "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", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -5247,21 +5650,12 @@ } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } + "p-locate": "^4.1.0" } }, "lodash": { @@ -5481,7 +5875,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "optional": true, "requires": { "semver": "^6.0.0" } @@ -5569,23 +5962,6 @@ "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", "dev": true }, - "merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dev": true, - "requires": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -6204,12 +6580,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, - "nested-error-stacks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", - "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", - "dev": true - }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -6290,6 +6660,21 @@ } } }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "node-releases": { + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", + "dev": true + }, "node-version": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/node-version/-/node-version-1.2.0.tgz", @@ -6516,51 +6901,53 @@ "dev": true }, "nyc": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", - "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "caching-transform": "^3.0.2", - "convert-source-map": "^1.6.0", - "cp-file": "^6.2.0", - "find-cache-dir": "^2.1.0", - "find-up": "^3.0.0", - "foreground-child": "^1.5.6", - "glob": "^7.1.3", - "istanbul-lib-coverage": "^2.0.5", - "istanbul-lib-hook": "^2.0.7", - "istanbul-lib-instrument": "^3.3.0", - "istanbul-lib-report": "^2.0.8", - "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^2.2.4", - "js-yaml": "^3.13.1", - "make-dir": "^2.1.0", - "merge-source-map": "^1.1.0", - "resolve-from": "^4.0.0", - "rimraf": "^2.6.3", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", "signal-exit": "^3.0.2", - "spawn-wrap": "^1.4.2", - "test-exclude": "^5.2.3", - "uuid": "^3.3.2", - "yargs": "^13.2.2", - "yargs-parser": "^13.0.0" + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, "camelcase": { @@ -6570,29 +6957,39 @@ "dev": true }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, "get-caller-file": { @@ -6602,25 +6999,15 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, "require-main-filename": { @@ -6629,38 +7016,41 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", @@ -6668,38 +7058,39 @@ "dev": true }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^3.0.0", + "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" + "yargs-parser": "^18.1.2" } }, "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -6904,12 +7295,12 @@ } }, "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "requires": { - "p-limit": "^2.0.0" + "p-limit": "^2.2.0" }, "dependencies": { "p-limit": { @@ -6923,19 +7314,28 @@ } } }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "package-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", - "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", "dev": true, "requires": { "graceful-fs": "^4.1.15", - "hasha": "^3.0.0", + "hasha": "^5.0.0", "lodash.flattendeep": "^4.4.0", "release-zalgo": "^1.0.0" } @@ -7121,22 +7521,29 @@ } }, "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "requires": { - "find-up": "^3.0.0" + "find-up": "^4.0.0" }, "dependencies": { "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true } } }, @@ -7181,6 +7588,15 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -8116,17 +8532,37 @@ "dev": true }, "spawn-wrap": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.3.tgz", - "integrity": "sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", "dev": true, "requires": { - "foreground-child": "^1.5.6", - "mkdirp": "^0.5.0", - "os-homedir": "^1.0.1", - "rimraf": "^2.6.2", + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", "signal-exit": "^3.0.2", - "which": "^1.3.0" + "which": "^2.0.1" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "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" + } + } } }, "spdx-correct": { @@ -8474,96 +8910,14 @@ } }, "test-exclude": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", - "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "requires": { - "glob": "^7.1.3", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", - "dev": true, - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" } }, "text-table": { @@ -9317,7 +9671,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "optional": true, "requires": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", diff --git a/package.json b/package.json index 561f1bb8c0..d776317acc 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "mz": "^2.7.0", "nock": "^13.0.0", "npm-run-all": "^4.1.5", - "nyc": "^14.1.0", + "nyc": "^15.1.0", "request": "^2.75.0", "request-promise": "^4.1.1", "run-sequence": "^2.2.1", From d6dcf9e53579461c8b490e53f34c63908ba58c9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 11:46:10 -0700 Subject: [PATCH 118/160] build(deps-dev): bump chalk from 1.1.3 to 4.1.1 (#1288) Bumps [chalk](https://github.com/chalk/chalk) from 1.1.3 to 4.1.1. - [Release notes](https://github.com/chalk/chalk/releases) - [Commits](https://github.com/chalk/chalk/compare/v1.1.3...v4.1.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 85 ++++++++++++++++++++++++++++++++++++++--------- package.json | 2 +- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d20ad73ff..230bcefe4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1667,10 +1667,30 @@ "dev": true }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + }, + "dependencies": { + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } }, "ansi-wrap": { "version": "0.1.0", @@ -2271,16 +2291,13 @@ } }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", "dev": true, "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "chardet": { @@ -8105,6 +8122,12 @@ "plugin-error": "^0.1.2" }, "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, "arr-diff": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", @@ -8127,6 +8150,19 @@ "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", "dev": true }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, "extend-shallow": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", @@ -8154,6 +8190,12 @@ "arr-union": "^2.0.1", "extend-shallow": "^1.1.2" } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true } } }, @@ -8808,10 +8850,21 @@ "optional": true }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + } + } }, "sver-compat": { "version": "1.5.0", diff --git a/package.json b/package.json index d776317acc..7e64ac8ffa 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "bcrypt": "^5.0.0", "chai": "^4.2.0", "chai-as-promised": "^7.0.0", - "chalk": "^1.1.3", + "chalk": "^4.1.1", "child-process-promise": "^2.2.1", "del": "^2.2.1", "eslint": "^6.8.0", From 9d87537b4c2dcb407a2c216aba28a10925713bab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 May 2021 11:52:06 -0700 Subject: [PATCH 119/160] build(deps-dev): bump @microsoft/api-extractor from 7.11.2 to 7.15.2 (#1291) Bumps [@microsoft/api-extractor](https://github.com/microsoft/rushstack) from 7.11.2 to 7.15.2. - [Release notes](https://github.com/microsoft/rushstack/releases) - [Commits](https://github.com/microsoft/rushstack/compare/@microsoft/api-extractor_v7.11.2...@microsoft/api-extractor_v7.15.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 168 +++++++++++++++++++++++++++++++++------------- 1 file changed, 122 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 230bcefe4e..11c7ced866 100644 --- a/package-lock.json +++ b/package-lock.json @@ -963,29 +963,42 @@ "dev": true }, "@microsoft/api-extractor": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.11.2.tgz", - "integrity": "sha512-iZPv22j9K02cbwIDblOgF1MxZG+KWovp3CQpWCD6UC/+YYO4DfLxX5uZYVNzfgT4vU8fN0rugJmGm85rHX6Ouw==", + "version": "7.15.2", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.15.2.tgz", + "integrity": "sha512-/Y/n+QOc1vM6Vg3OAUByT/wXdZciE7jV3ay33+vxl3aKva5cNsuOauL14T7XQWUiLko3ilPwrcnFcEjzXpLsuA==", "dev": true, "requires": { - "@microsoft/api-extractor-model": "7.10.8", - "@microsoft/tsdoc": "0.12.19", - "@rushstack/node-core-library": "3.34.7", - "@rushstack/rig-package": "0.2.7", - "@rushstack/ts-command-line": "4.7.6", + "@microsoft/api-extractor-model": "7.13.2", + "@microsoft/tsdoc": "0.13.2", + "@microsoft/tsdoc-config": "~0.15.2", + "@rushstack/node-core-library": "3.38.0", + "@rushstack/rig-package": "0.2.12", + "@rushstack/ts-command-line": "4.7.10", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.17.0", "semver": "~7.3.0", "source-map": "~0.6.1", - "typescript": "~4.0.5" + "typescript": "~4.2.4" }, "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true + "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" + } }, "source-map": { "version": "0.6.1", @@ -994,29 +1007,72 @@ "dev": true }, "typescript": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz", - "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true } } }, "@microsoft/api-extractor-model": { - "version": "7.10.8", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.10.8.tgz", - "integrity": "sha512-9TfiCTPnkUeLaYywZeg9rYbVPX9Tj6AAkO6ThnjSE0tTPLjMcL3RiHkqn0BJ4+aGcl56APwo32zj5+kG+NqxYA==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.13.2.tgz", + "integrity": "sha512-gA9Q8q5TPM2YYk7rLinAv9KqcodrmRC13BVmNzLswjtFxpz13lRh0BmrqD01/sddGpGMIuWFYlfUM4VSWxnggA==", "dev": true, "requires": { - "@microsoft/tsdoc": "0.12.19", - "@rushstack/node-core-library": "3.34.7" + "@microsoft/tsdoc": "0.13.2", + "@microsoft/tsdoc-config": "~0.15.2", + "@rushstack/node-core-library": "3.38.0" } }, "@microsoft/tsdoc": { - "version": "0.12.19", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.19.tgz", - "integrity": "sha512-IpgPxHrNxZiMNUSXqR1l/gePKPkfAmIKoDRP9hp7OwjU29ZR8WCJsOJ8iBKgw0Qk+pFwR+8Y1cy8ImLY6e9m4A==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz", + "integrity": "sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg==", "dev": true }, + "@microsoft/tsdoc-config": { + "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.13.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + } + } + }, "@panva/asn1.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", @@ -1087,9 +1143,9 @@ "optional": true }, "@rushstack/node-core-library": { - "version": "3.34.7", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.34.7.tgz", - "integrity": "sha512-7FwJ0jmZsh7bDIZ1IqDNphY9Kc6aAi1D2K8jiq+da4flMyL84HNeq2KxvwFLzjLwu3eMr88X+oBpgxCTD5Y57Q==", + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.38.0.tgz", + "integrity": "sha512-cmvl0yQx8sSmbuXwiRYJi8TO+jpTtrLJQ8UmFHhKvgPVJAW8cV8dnpD1Xx/BvTGrJZ2XtRAIkAhBS9okBnap4w==", "dev": true, "requires": { "@types/node": "10.17.13", @@ -1120,31 +1176,42 @@ "universalify": "^0.1.0" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "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" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true } } }, "@rushstack/rig-package": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.2.7.tgz", - "integrity": "sha512-hI1L0IIzCHqH/uW64mKqEQ0/MANA/IklVId3jGpj1kt9RJcBdeNUIlzDtHl437LZRAuEA8CyotRHzG6YDgWlTw==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.2.12.tgz", + "integrity": "sha512-nbePcvF8hQwv0ql9aeQxcaMPK/h1OLAC00W7fWCRWIvD2MchZOE8jumIIr66HGrfG2X1sw++m/ZYI4D+BM5ovQ==", "dev": true, "requires": { - "@types/node": "10.17.13", "resolve": "~1.17.0", "strip-json-comments": "~3.1.1" }, "dependencies": { - "@types/node": { - "version": "10.17.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", - "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", - "dev": true - }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -1154,9 +1221,9 @@ } }, "@rushstack/ts-command-line": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.7.6.tgz", - "integrity": "sha512-falJVNfpJtsL3gJaY77JXXycfzhzB9VkKhqEfjRWD69/f6ezMUorPR6Nc90MnIaWgePTcdTJPZibxOQrNpu1Uw==", + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.7.10.tgz", + "integrity": "sha512-8t042g8eerypNOEcdpxwRA3uCmz0duMo21rG4Z2mdz7JxJeylDmzjlU3wDdef2t3P1Z61JCdZB6fbm1Mh0zi7w==", "dev": true, "requires": { "@types/argparse": "1.0.38", @@ -2532,9 +2599,9 @@ } }, "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "optional": true }, @@ -4950,6 +5017,15 @@ "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", "dev": true }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", From 7afaf6c87b9ee1994b6a17dc6ee45dea213981b9 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Mon, 24 May 2021 14:54:20 -0700 Subject: [PATCH 120/160] chore: Teporarily disabling sendToDeviceGroup integration test (#1292) --- test/integration/messaging.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/messaging.spec.ts b/test/integration/messaging.spec.ts index e67a81602b..d8703b5335 100644 --- a/test/integration/messaging.spec.ts +++ b/test/integration/messaging.spec.ts @@ -172,7 +172,7 @@ describe('admin.messaging', () => { }); }); - it('sendToDeviceGroup() returns a response with success count', () => { + xit('sendToDeviceGroup() returns a response with success count', () => { return admin.messaging().sendToDeviceGroup(notificationKey, payload, options) .then((response) => { expect(typeof response.successCount).to.equal('number'); From 1d2ff694ddbcab2effa46813def446aa26e58f67 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Tue, 25 May 2021 14:18:32 -0700 Subject: [PATCH 121/160] feat(auth): Added code flow support for OIDC flow. (#1220) * OIDC codeflow support * improve configs to simulate the real cases * update for changes in signiture * resolve comments * improve validator logic * remove unnecessary logic * add tests and fix errors * add auth-api-request rests --- etc/firebase-admin.api.md | 8 ++ src/auth/auth-config.ts | 81 +++++++++++++++ src/auth/index.ts | 40 ++++++++ src/utils/error.ts | 8 ++ test/integration/auth.spec.ts | 76 ++++++++++---- test/unit/auth/auth-api-request.spec.ts | 71 +++++++++++++ test/unit/auth/auth-config.spec.ts | 129 ++++++++++++++++++++++++ 7 files changed, 396 insertions(+), 17 deletions(-) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index 39c413b7ec..1b9e7aa407 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -241,15 +241,23 @@ export namespace auth { export interface MultiFactorUpdateSettings { enrolledFactors: UpdateMultiFactorInfoRequest[] | null; } + export interface OAuthResponseType { + code?: boolean; + idToken?: boolean; + } export interface OIDCAuthProviderConfig extends AuthProviderConfig { clientId: string; + clientSecret?: string; issuer: string; + responseType?: OAuthResponseType; } export interface OIDCUpdateAuthProviderRequest { clientId?: string; + clientSecret?: string; displayName?: string; enabled?: boolean; issuer?: string; + responseType?: OAuthResponseType; } export interface PhoneIdentifier { // (undocumented) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 26db94126e..059d866574 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -24,6 +24,7 @@ import MultiFactorConfigState = auth.MultiFactorConfigState; import AuthFactorType = auth.AuthFactorType; import EmailSignInProviderConfig = auth.EmailSignInProviderConfig; import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; +import OAuthResponseType = auth.OAuthResponseType; import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; /** A maximum of 10 test phone number / code pairs can be configured. */ @@ -75,6 +76,8 @@ export interface OIDCConfigServerRequest { issuer?: string; displayName?: string; enabled?: boolean; + clientSecret?: string; + responseType?: OAuthResponseType; [key: string]: any; } @@ -87,6 +90,8 @@ export interface OIDCConfigServerResponse { issuer?: string; displayName?: string; enabled?: boolean; + clientSecret?: string; + responseType?: OAuthResponseType; } /** The server side email configuration request interface. */ @@ -650,6 +655,8 @@ export class OIDCConfig implements OIDCAuthProviderConfig { public readonly providerId: string; public readonly issuer: string; public readonly clientId: string; + public readonly clientSecret?: string; + public readonly responseType: OAuthResponseType; /** * Converts a client side request to a OIDCConfigServerRequest which is the format @@ -676,6 +683,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig { request.displayName = options.displayName; request.issuer = options.issuer; request.clientId = options.clientId; + if (typeof options.clientSecret !== 'undefined') { + request.clientSecret = options.clientSecret; + } + if (typeof options.responseType !== 'undefined') { + request.responseType = options.responseType; + } return request; } @@ -715,6 +728,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig { providerId: true, clientId: true, issuer: true, + clientSecret: true, + responseType: true, + }; + const validResponseTypes = { + idToken: true, + code: true, }; if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( @@ -773,6 +792,59 @@ export class OIDCConfig implements OIDCAuthProviderConfig { '"OIDCAuthProviderConfig.displayName" must be a valid string.', ); } + if (typeof options.clientSecret !== 'undefined' && + !validator.isNonEmptyString(options.clientSecret)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"OIDCAuthProviderConfig.clientSecret" must be a valid string.', + ); + } + if (validator.isNonNullObject(options.responseType) && typeof options.responseType !== 'undefined') { + Object.keys(options.responseType).forEach((key) => { + if (!(key in validResponseTypes)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid OAuthResponseType parameter.`, + ); + } + }); + + const idToken = options.responseType.idToken; + if (typeof idToken !== 'undefined' && !validator.isBoolean(idToken)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"OIDCAuthProviderConfig.responseType.idToken" must be a boolean.', + ); + } + + const code = options.responseType.code; + if (typeof code !== 'undefined') { + if (!validator.isBoolean(code)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"OIDCAuthProviderConfig.responseType.code" must be a boolean.', + ); + } + + // If code flow is enabled, client secret must be provided. + if (code && typeof options.clientSecret === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.MISSING_OAUTH_CLIENT_SECRET, + 'The OAuth configuration client secret is required to enable OIDC code flow.', + ); + } + } + + const allKeys = Object.keys(options.responseType).length; + const enabledCount = Object.values(options.responseType).filter(Boolean).length; + // Only one of OAuth response types can be set to true. + if (allKeys > 1 && enabledCount != 1) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_OAUTH_RESPONSETYPE, + 'Only exactly one OAuth responseType should be set to true.', + ); + } + } } /** @@ -806,6 +878,13 @@ export class OIDCConfig implements OIDCAuthProviderConfig { // When enabled is undefined, it takes its default value of false. this.enabled = !!response.enabled; this.displayName = response.displayName; + + if (typeof response.clientSecret !== 'undefined') { + this.clientSecret = response.clientSecret; + } + if (typeof response.responseType !== 'undefined') { + this.responseType = response.responseType; + } } /** @return {OIDCAuthProviderConfig} The plain object representation of the OIDCConfig. */ @@ -816,6 +895,8 @@ export class OIDCConfig implements OIDCAuthProviderConfig { providerId: this.providerId, issuer: this.issuer, clientId: this.clientId, + clientSecret: deepCopy(this.clientSecret), + responseType: deepCopy(this.responseType), }; } } diff --git a/src/auth/index.ts b/src/auth/index.ts index b1bc47f725..83fb622d67 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1289,6 +1289,25 @@ export namespace auth { callbackURL?: string; } + /** + * The interface representing OIDC provider's response object for OAuth + * authorization flow. + * We need either of them to be true, there are two cases: + * If set code to true, then we are doing code flow. + * If set idToken to true, then we are doing idToken flow. + */ + export interface OAuthResponseType { + /** + * Whether ID token is returned from IdP's authorization endpoint. + */ + idToken?: boolean; + + /** + * Whether authorization code is returned from IdP's authorization endpoint. + */ + code?: boolean; + } + /** * The [OIDC](https://openid.net/specs/openid-connect-core-1_0-final.html) Auth * provider configuration interface. An OIDC provider can be created via @@ -1321,6 +1340,16 @@ export namespace auth { * [spec](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation). */ issuer: string; + + /** + * The OIDC provider's client secret to enable OIDC code flow. + */ + clientSecret?: string; + + /** + * The OIDC provider's response object for OAuth authorization flow. + */ + responseType?: OAuthResponseType; } /** @@ -1403,6 +1432,17 @@ export namespace auth { * configuration's value is not modified. */ issuer?: string; + + /** + * The OIDC provider's client secret to enable OIDC code flow. + * If not provided, the existing configuration's value is not modified. + */ + clientSecret?: string; + + /** + * The OIDC provider's response object for OAuth authorization flow. + */ + responseType?: OAuthResponseType; } /** diff --git a/src/utils/error.ts b/src/utils/error.ts index adc1b852b8..caa781e8f3 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -525,6 +525,10 @@ export class AuthClientErrorCode { code: 'invalid-provider-uid', message: 'The providerUid must be a valid provider uid string.', }; + public static INVALID_OAUTH_RESPONSETYPE = { + code: 'invalid-oauth-responsetype', + message: 'Only exactly one OAuth responseType should be set to true.', + }; public static INVALID_SESSION_COOKIE_DURATION = { code: 'invalid-session-cookie-duration', message: 'The session cookie duration must be a valid number in milliseconds ' + @@ -597,6 +601,10 @@ export class AuthClientErrorCode { code: 'missing-oauth-client-id', message: 'The OAuth/OIDC configuration client ID must not be empty.', }; + public static MISSING_OAUTH_CLIENT_SECRET = { + code: 'missing-oauth-client-secret', + message: 'The OAuth configuration client secret is required to enable OIDC code flow.', + }; public static MISSING_PROVIDER_ID = { code: 'missing-provider-id', message: 'A valid provider ID must be provided in the request.', diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 29060b4cdd..075eacdca7 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -1334,12 +1334,21 @@ describe('admin.auth', () => { enabled: true, issuer: 'https://oidc.com/issuer1', clientId: 'CLIENT_ID1', + responseType: { + idToken: true, + code: false, + }, }; const modifiedConfigOptions = { displayName: 'OIDC_DISPLAY_NAME3', enabled: false, issuer: 'https://oidc.com/issuer3', clientId: 'CLIENT_ID3', + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + }, }; before(function() { @@ -1633,6 +1642,9 @@ describe('admin.auth', () => { enabled: true, issuer: 'https://oidc.com/issuer1', clientId: 'CLIENT_ID1', + responseType: { + idToken: true, + }, }; const authProviderConfig2 = { providerId: randomOidcProviderId(), @@ -1640,6 +1652,10 @@ describe('admin.auth', () => { enabled: true, issuer: 'https://oidc.com/issuer2', clientId: 'CLIENT_ID2', + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, }; const removeTempConfigs = (): Promise => { @@ -1706,39 +1722,65 @@ describe('admin.auth', () => { }); }); - it('updateProviderConfig() successfully overwrites an OIDC config', () => { + it('updateProviderConfig() successfully partially modifies an OIDC config', () => { + const deltaChanges = { + displayName: 'OIDC_DISPLAY_NAME3', + enabled: false, + issuer: 'https://oidc.com/issuer3', + clientId: 'CLIENT_ID3', + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + }, + }; + // Only above fields should be modified. const modifiedConfigOptions = { + providerId: authProviderConfig1.providerId, displayName: 'OIDC_DISPLAY_NAME3', enabled: false, issuer: 'https://oidc.com/issuer3', clientId: 'CLIENT_ID3', + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, }; - return admin.auth().updateProviderConfig(authProviderConfig1.providerId, modifiedConfigOptions) + return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges) .then((config) => { - const modifiedConfig = deepExtend( - { providerId: authProviderConfig1.providerId }, modifiedConfigOptions); - assertDeepEqualUnordered(modifiedConfig, config); + assertDeepEqualUnordered(modifiedConfigOptions, config); }); }); - it('updateProviderConfig() successfully partially modifies an OIDC config', () => { + it('updateProviderConfig() with invalid oauth response type should be rejected', () => { const deltaChanges = { displayName: 'OIDC_DISPLAY_NAME4', + enabled: false, issuer: 'https://oidc.com/issuer4', + clientId: 'CLIENT_ID4', + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: false, + }, }; - // Only above fields should be modified. - const modifiedConfigOptions = { - displayName: 'OIDC_DISPLAY_NAME4', + return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges). + should.eventually.be.rejected.and.have.property('code', 'auth/invalid-oauth-responsetype'); + }); + + it('updateProviderConfig() code flow with no client secret should be rejected', () => { + const deltaChanges = { + displayName: 'OIDC_DISPLAY_NAME5', enabled: false, - issuer: 'https://oidc.com/issuer4', - clientId: 'CLIENT_ID3', + issuer: 'https://oidc.com/issuer5', + clientId: 'CLIENT_ID5', + responseType: { + idToken: false, + code: true, + }, }; - return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges) - .then((config) => { - const modifiedConfig = deepExtend( - { providerId: authProviderConfig1.providerId }, modifiedConfigOptions); - assertDeepEqualUnordered(modifiedConfig, config); - }); + return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges). + should.eventually.be.rejected.and.have.property('code', 'auth/missing-oauth-client-secret'); }); it('deleteProviderConfig() successfully deletes an existing OIDC config', () => { diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 0ca9bd5a85..373982a6f6 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -3505,6 +3505,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const providerId = 'oidc.provider'; const path = handler.path('v2', `/oauthIdpConfigs?oauthIdpConfigId=${providerId}`, 'project_id'); const expectedHttpMethod = 'POST'; + const clientSecret = 'CLIENT_SECRET'; + const responseType = { code: true }; const configOptions = { providerId, displayName: 'OIDC_DISPLAY_NAME', @@ -3521,6 +3523,26 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedResult = utils.responseFrom(deepExtend({ name: `projects/project1/oauthIdpConfigs/${providerId}`, }, expectedRequest)); + const expectedCodeFlowOptions = { + providerId, + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + clientSecret, + responseType, + }; + const expectedCodeFlowRequest = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + clientSecret, + responseType, + }; + const expectedCodeFlowResult = utils.responseFrom(deepExtend({ + name: `projects/project1/oauthIdpConfigs/${providerId}`, + }, expectedCodeFlowRequest)); it('should be fulfilled given valid parameters', () => { const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); @@ -3535,6 +3557,19 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + it('should be fulfilled given valid parameters for OIDC code flow', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedCodeFlowResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.createOAuthIdpConfig(expectedCodeFlowOptions) + .then((response) => { + expect(response).to.deep.equal(expectedCodeFlowResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedCodeFlowRequest)); + }); + }); + it('should be rejected given invalid parameters', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_CONFIG, @@ -3597,6 +3632,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const providerId = 'oidc.provider'; const path = handler.path('v2', `/oauthIdpConfigs/${providerId}`, 'project_id'); const expectedHttpMethod = 'PATCH'; + const clientSecret = 'CLIENT_SECRET'; + const responseType = { code: true }; const configOptions = { displayName: 'OIDC_DISPLAY_NAME', enabled: true, @@ -3620,6 +3657,26 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { clientId: 'NEW_CLIENT_ID', issuer: 'https://oidc.com/issuer2', })); + const expectedCodeFlowOptions = { + providerId, + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + clientSecret, + responseType, + }; + const expectedCodeFlowRequest = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + clientSecret, + responseType, + }; + const expectedCodeFlowResult = utils.responseFrom(deepExtend({ + name: `projects/project1/oauthIdpConfigs/${providerId}`, + }, expectedCodeFlowRequest)); it('should be fulfilled given full parameters', () => { const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; @@ -3635,6 +3692,20 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + it('should be fulfilled given full parameters for OIDC code flow', () => { + const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId,clientSecret,responseType.code'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedCodeFlowResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, expectedCodeFlowOptions) + .then((response) => { + expect(response).to.deep.equal(expectedCodeFlowResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedCodeFlowRequest)); + }); + }); + it('should be fulfilled given partial parameters', () => { const expectedPath = path + '?updateMask=enabled,clientId'; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts index ad81c5e62c..d9e6ce4abc 100644 --- a/test/unit/auth/auth-config.spec.ts +++ b/test/unit/auth/auth-config.spec.ts @@ -727,6 +727,11 @@ describe('OIDCConfig', () => { issuer: 'https://oidc.com/issuer', displayName: 'oidcProviderName', enabled: true, + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + }, }; const serverResponse: OIDCConfigServerResponse = { name: 'projects/project_id/oauthIdpConfigs/oidc.provider', @@ -734,6 +739,10 @@ describe('OIDCConfig', () => { issuer: 'https://oidc.com/issuer', displayName: 'oidcProviderName', enabled: true, + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, }; const clientRequest: OIDCAuthProviderConfig = { providerId: 'oidc.provider', @@ -741,6 +750,11 @@ describe('OIDCConfig', () => { issuer: 'https://oidc.com/issuer', displayName: 'oidcProviderName', enabled: true, + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + }, }; const config = new OIDCConfig(serverResponse); @@ -769,6 +783,21 @@ describe('OIDCConfig', () => { expect(config.enabled).to.be.true; }); + it('should set readonly property clientSecret', () => { + expect(config.clientSecret).to.equal('CLIENT_SECRET'); + }); + + it('should set readonly property expected responseType', () => { + expect(config.responseType).to.deep.equal({ code: true }); + }); + + it('should not throw on no responseType and clientSecret', () => { + const testResponse = deepCopy(serverResponse); + delete testResponse.clientSecret; + delete testResponse.responseType; + expect(() => new OIDCConfig(testResponse)).not.to.throw(); + }); + it('should throw on missing issuer', () => { const invalidResponse = deepCopy(serverResponse); delete invalidResponse.issuer; @@ -831,6 +860,10 @@ describe('OIDCConfig', () => { providerId: 'oidc.provider', issuer: 'https://oidc.com/issuer', clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, }); }); }); @@ -844,12 +877,22 @@ describe('OIDCConfig', () => { const updateRequest: OIDCUpdateAuthProviderRequest = { clientId: 'CLIENT_ID', displayName: 'OIDC_PROVIDER_DISPLAY_NAME', + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + } }; const updateServerRequest: OIDCConfigServerRequest = { clientId: 'CLIENT_ID', displayName: 'OIDC_PROVIDER_DISPLAY_NAME', issuer: undefined, enabled: undefined, + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + } }; expect(OIDCConfig.buildServerRequest(updateRequest, true)).to.deep.equal(updateServerRequest); }); @@ -892,6 +935,62 @@ describe('OIDCConfig', () => { expect(() => OIDCConfig.validate(partialRequest, true)).not.to.throw(); }); + it('should throw on OAuth responseType contains invalid parameters', () => { + const invalidRequest = deepCopy(clientRequest) as any; + invalidRequest.responseType.unknownField = true; + expect(() => OIDCConfig.validate(invalidRequest, true)) + .to.throw('"unknownField" is not a valid OAuthResponseType parameter.'); + }); + + it('should not throw when exactly one OAuth responseType is true', () => { + const validRequest = deepCopy(clientRequest) as any; + validRequest.responseType.code = false; + validRequest.responseType.idToken = true; + expect(() => OIDCConfig.validate(validRequest, true)).not.to.throw(); + }); + + it('should not throw when only idToken responseType is set to true', () => { + const validRequest = deepCopy(clientRequest) as any; + validRequest.responseType = { idToken: true }; + expect(() => OIDCConfig.validate(validRequest, true)).not.to.throw(); + }); + + it('should not throw when only code responseType is set to true', () => { + const validRequest = deepCopy(clientRequest) as any; + const validResponseType = { code: true }; + validRequest.responseType = validResponseType; + expect(() => OIDCConfig.validate(validRequest, true)).not.to.throw(); + }); + + it('should throw on two OAuth responseTypes set to true', () => { + const invalidRequest = deepCopy(clientRequest) as any; + invalidRequest.responseType.idToken = true; + invalidRequest.responseType.code = true; + expect(() => OIDCConfig.validate(invalidRequest, true)) + .to.throw('Only exactly one OAuth responseType should be set to true.'); + }); + + it('should throw on no OAuth responseType set to true', () => { + const invalidRequest = deepCopy(clientRequest) as any; + invalidRequest.responseType.idToken = false; + invalidRequest.responseType.code = false; + expect(() => OIDCConfig.validate(invalidRequest, true)) + .to.throw('Only exactly one OAuth responseType should be set to true.'); + }); + + it('should not throw when responseType is empty', () => { + const testRequest = deepCopy(clientRequest) as any; + testRequest.responseType = {}; + expect(() => OIDCConfig.validate(testRequest, true)).not.to.throw(); + }); + + it('should throw on no client secret when OAuth responseType code flow set to true', () => { + const invalidRequest = deepCopy(clientRequest) as any; + delete invalidRequest.clientSecret; + expect(() => OIDCConfig.validate(invalidRequest, true)) + .to.throw('The OAuth configuration client secret is required to enable OIDC code flow.'); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on non-null OIDCAuthProviderConfig object:' + JSON.stringify(request), () => { @@ -957,5 +1056,35 @@ describe('OIDCConfig', () => { .to.throw('"OIDCAuthProviderConfig.displayName" must be a valid string.'); }); }); + + const invalidClientSecrets = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidClientSecrets.forEach((invalidClientSecret) => { + it('should throw on invalid clientSecret:' + JSON.stringify(invalidClientSecret), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.clientSecret = invalidClientSecret; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.clientSecret" must be a valid string.'); + }); + }); + + const invalidOAuthResponseIdTokenBooleans = [null, NaN, 0, 1, 'invalid', '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidOAuthResponseIdTokenBooleans.forEach((invalidOAuthResponseIdTokenBoolean) => { + it('should throw on invalid responseType.idToken:' + JSON.stringify(invalidOAuthResponseIdTokenBoolean), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.responseType.idToken = invalidOAuthResponseIdTokenBoolean; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.responseType.idToken" must be a boolean.'); + }); + }); + + const invalidOAuthResponseCodeBooleans = [null, NaN, 0, 1, 'invalid', '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidOAuthResponseCodeBooleans.forEach((invalidOAuthResponseCodeBoolean) => { + it('should throw on invalid responseType.code:' + JSON.stringify(invalidOAuthResponseCodeBoolean), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.responseType.code = invalidOAuthResponseCodeBoolean; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.responseType.code" must be a boolean.'); + }); + }); }); }); From 03c66d822c23462cd3479f944f405da67cbf444e Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 26 May 2021 16:14:08 -0400 Subject: [PATCH 122/160] Update supported Node version to 10.13.0v (#1300) --- CONTRIBUTING.md | 2 +- README.md | 2 +- package-lock.json | 94 ++++++++++++----------------------------------- package.json | 2 +- 4 files changed, 27 insertions(+), 73 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ac6a71cb1..a3ad29f277 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,7 +87,7 @@ information on using pull requests. ### Prerequisites -1. Node.js 10.10.0 or higher. +1. Node.js 10.13.0 or higher. 2. NPM 5 or higher (NPM 6 recommended). 3. Google Cloud SDK ([`gcloud`](https://cloud.google.com/sdk/downloads) utility) diff --git a/README.md b/README.md index 4e9b7df5e0..59a22770ff 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ requests, code review feedback, and also pull requests. ## Supported Environments -We support Node.js 10.10.0 and higher. +We support Node.js 10.13.0 and higher. Please also note that the Admin SDK should only be used in server-side/back-end environments controlled by the app developer. diff --git a/package-lock.json b/package-lock.json index 11c7ced866..a83343390f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -665,8 +665,7 @@ "@google-cloud/promisify": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", - "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", - "optional": true + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==" }, "@google-cloud/storage": { "version": "5.3.0", @@ -730,8 +729,7 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "optional": true + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" }, "ansi-styles": { "version": "4.3.0", @@ -793,8 +791,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "optional": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "locate-path": { "version": "5.0.0", @@ -839,7 +836,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "optional": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -850,7 +846,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "optional": true, "requires": { "ansi-regex": "^5.0.0" } @@ -1081,32 +1076,27 @@ "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", - "optional": true + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" }, "@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "optional": true + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" }, "@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "optional": true + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" }, "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", - "optional": true + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -1115,32 +1105,27 @@ "@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", - "optional": true + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", - "optional": true + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", - "optional": true + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", - "optional": true + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", - "optional": true + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "@rushstack/node-core-library": { "version": "3.38.0", @@ -1409,8 +1394,7 @@ "@types/long": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", - "optional": true + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/mime": { "version": "1.3.2", @@ -1602,7 +1586,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "optional": true, "requires": { "event-target-shim": "^5.0.0" } @@ -1647,7 +1630,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", - "optional": true, "requires": { "debug": "4" } @@ -2165,8 +2147,7 @@ "bignumber.js": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", - "optional": true + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" }, "binary-extensions": { "version": "1.13.1", @@ -3444,8 +3425,7 @@ "event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "optional": true + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, "expand-brackets": { "version": "2.1.4", @@ -3663,8 +3643,7 @@ "fast-text-encoding": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", - "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", - "optional": true + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" }, "faye-websocket": { "version": "0.11.3", @@ -4041,7 +4020,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.0.3.tgz", "integrity": "sha512-PkzQludeIFhd535/yucALT/Wxyj/y2zLyrMwPcJmnLHDugmV49NvAi/vb+VUq/eWztATZCNcb8ue+ywPG+oLuw==", - "optional": true, "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", @@ -4054,7 +4032,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.1.0.tgz", "integrity": "sha512-r57SV28+olVsflPlKyVig3Muo/VDlcsObMtvDGOEtEJXj+DDE8bEl0coIkXh//hbkSDTvo+f5lbihZOndYXQQQ==", - "optional": true, "requires": { "gaxios": "^3.0.0", "json-bigint": "^0.3.0" @@ -4285,7 +4262,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.0.3.tgz", "integrity": "sha512-2Np6ojPmaJGXHSMsBhtTQEKfSMdLc8hefoihv7N2cwEr8E5bq39fhoat6TcXHwa0XoGO5Guh9sp3nxHFPmibMw==", - "optional": true, "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", @@ -4345,7 +4321,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.1.tgz", "integrity": "sha512-VlQgtozgNVVVcYTXS36eQz4PXPt9gIPqLOhHN0QiV6W6h4qSCNVKPtKC5INtJsaHHF2r7+nOIa26MJeJMTaZEQ==", - "optional": true, "requires": { "node-forge": "^0.9.0" }, @@ -4353,8 +4328,7 @@ "node-forge": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.2.tgz", - "integrity": "sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==", - "optional": true + "integrity": "sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==" } } }, @@ -4373,7 +4347,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.1.tgz", "integrity": "sha512-33w4FNDkUcyIOq/TqyC+drnKdI4PdXmWp9lZzssyEQKuvu9ZFN3KttaSnDKo52U3E51oujVGop93mKxmqO8HHg==", - "optional": true, "requires": { "gaxios": "^3.0.0", "google-p12-pem": "^3.0.0", @@ -4753,7 +4726,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "optional": true, "requires": { "agent-base": "6", "debug": "4" @@ -5502,7 +5474,6 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz", "integrity": "sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ==", - "optional": true, "requires": { "bignumber.js": "^9.0.0" } @@ -5626,7 +5597,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "optional": true, "requires": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -5649,7 +5619,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "optional": true, "requires": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" @@ -5766,8 +5735,7 @@ "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", - "optional": true + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" }, "lodash.clonedeep": { "version": "4.5.0", @@ -5921,14 +5889,12 @@ "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "optional": true + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "optional": true, "requires": { "yallist": "^3.0.2" } @@ -6079,8 +6045,7 @@ "mime": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", - "optional": true + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" }, "mime-db": { "version": "1.44.0", @@ -6719,8 +6684,7 @@ "node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "optional": true + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, "node-forge": { "version": "0.10.0", @@ -7712,7 +7676,6 @@ "version": "6.10.1", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", - "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -7732,8 +7695,7 @@ "@types/node": { "version": "13.13.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.30.tgz", - "integrity": "sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA==", - "optional": true + "integrity": "sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA==" } } }, @@ -7752,7 +7714,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7762,7 +7723,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "optional": true, "requires": { "duplexify": "^4.1.1", "inherits": "^2.0.3", @@ -7773,7 +7733,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", - "optional": true, "requires": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", @@ -7785,7 +7744,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -8790,7 +8748,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "optional": true, "requires": { "stubs": "^3.0.0" } @@ -8922,8 +8879,7 @@ "stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", - "optional": true + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" }, "supports-color": { "version": "7.2.0", @@ -9309,7 +9265,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "optional": true, "requires": { "is-typedarray": "^1.0.0" } @@ -9816,8 +9771,7 @@ "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "optional": true + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index 7e64ac8ffa..5628206dd1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "homepage": "https://firebase.google.com/", "engines": { - "node": ">=10.10.0" + "node": ">=10.13.0" }, "scripts": { "build": "gulp build", From f914f928da7d72fc49b286d5bf6576cd1cb5a780 Mon Sep 17 00:00:00 2001 From: Xin Li Date: Wed, 26 May 2021 13:57:11 -0700 Subject: [PATCH 123/160] Fixed integration test failure of skipped tests (#1299) * Fix integration test failure of skipped testss * Trigger integration tests --- test/integration/auth.spec.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 075eacdca7..55b78fa232 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -1336,10 +1336,9 @@ describe('admin.auth', () => { clientId: 'CLIENT_ID1', responseType: { idToken: true, - code: false, }, }; - const modifiedConfigOptions = { + const deltaChanges = { displayName: 'OIDC_DISPLAY_NAME3', enabled: false, issuer: 'https://oidc.com/issuer3', @@ -1350,6 +1349,17 @@ describe('admin.auth', () => { code: true, }, }; + const modifiedConfigOptions = { + providerId: authProviderConfig.providerId, + displayName: 'OIDC_DISPLAY_NAME3', + enabled: false, + issuer: 'https://oidc.com/issuer3', + clientId: 'CLIENT_ID3', + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, + }; before(function() { if (!createdTenantId) { @@ -1378,12 +1388,10 @@ describe('admin.auth', () => { .then((config) => { assertDeepEqualUnordered(authProviderConfig, config); return tenantAwareAuth.updateProviderConfig( - authProviderConfig.providerId, modifiedConfigOptions); + authProviderConfig.providerId, deltaChanges); }) .then((config) => { - const modifiedConfig = deepExtend( - { providerId: authProviderConfig.providerId }, modifiedConfigOptions); - assertDeepEqualUnordered(modifiedConfig, config); + assertDeepEqualUnordered(modifiedConfigOptions, config); return tenantAwareAuth.deleteProviderConfig(authProviderConfig.providerId); }) .then(() => { From c52949612608c50bca574da5435efbff60102372 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 26 May 2021 17:48:45 -0400 Subject: [PATCH 124/160] [chore] Release 9.9.0 (#1302) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5628206dd1..0b981dfe5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.8.0", + "version": "9.9.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 2ffc177c1d8c2b3a4cc5162089e8b80cb2b61e9d Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 27 May 2021 14:27:19 -0400 Subject: [PATCH 125/160] Update OIDC reference docs (#1305) --- src/auth/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/auth/index.ts b/src/auth/index.ts index 83fb622d67..d0ab8415ab 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1292,9 +1292,11 @@ export namespace auth { /** * The interface representing OIDC provider's response object for OAuth * authorization flow. - * We need either of them to be true, there are two cases: - * If set code to true, then we are doing code flow. - * If set idToken to true, then we are doing idToken flow. + * One of the following settings is required: + *
      + *
    • Set code to true for the code flow.
    • + *
    • Set idToken to true for the ID token flow.
    • + *
    */ export interface OAuthResponseType { /** From f4a9a166d2e4c34eb37dd8280961f39f4e466d08 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 27 May 2021 14:44:40 -0400 Subject: [PATCH 126/160] Add OAuthResponseType to ToC (#1303) --- docgen/content-sources/node/toc.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index aaf1eb562d..bc19ced9d6 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -64,6 +64,8 @@ toc: path: /docs/reference/admin/node/admin.auth.MultiFactorSettings - title: "MultiFactorUpdateSettings" path: /docs/reference/admin/node/admin.auth.MultiFactorUpdateSettings + - title: "OAuthResponseType" + path: /docs/reference/admin/node/admin.auth.OAuthResponseType - title: "OIDCAuthProviderConfig" path: /docs/reference/admin/node/admin.auth.OIDCAuthProviderConfig - title: "OIDCUpdateAuthProviderRequest" From 29271ad4586f7ba297ef6909d08264895f542d73 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 27 May 2021 14:35:59 -0700 Subject: [PATCH 127/160] fix(auth): Better type hierarchies for Auth API (#1294) * fix(auth): Better type heirarchies for Auth API * fix: Moved factorId back to the base types * fix: Updated API report * fix: Fixed a grammar error in comment * fix: Update to comment text --- etc/firebase-admin.api.md | 35 +++++++++-------- src/auth/auth-api-request.ts | 13 +++++-- src/auth/index.ts | 50 +++++++++++++++++-------- test/unit/auth/auth-api-request.spec.ts | 6 --- 4 files changed, 62 insertions(+), 42 deletions(-) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index 1b9e7aa407..b16b7127cb 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -113,11 +113,7 @@ export namespace auth { tenantManager(): TenantManager; } export type AuthFactorType = 'phone'; - export interface AuthProviderConfig { - displayName?: string; - enabled: boolean; - providerId: string; - } + export type AuthProviderConfig = SAMLAuthProviderConfig | OIDCAuthProviderConfig; export interface AuthProviderConfigFilter { maxResults?: number; pageToken?: string; @@ -151,11 +147,23 @@ export namespace auth { verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; verifySessionCookie(sessionCookie: string, checkForRevocation?: boolean): Promise; } - export interface CreateMultiFactorInfoRequest { + export interface BaseAuthProviderConfig { + displayName?: string; + enabled: boolean; + providerId: string; + } + export interface BaseCreateMultiFactorInfoRequest { + displayName?: string; + factorId: string; + } + export interface BaseUpdateMultiFactorInfoRequest { displayName?: string; + enrollmentTime?: string; factorId: string; + uid?: string; } - export interface CreatePhoneMultiFactorInfoRequest extends CreateMultiFactorInfoRequest { + export type CreateMultiFactorInfoRequest = CreatePhoneMultiFactorInfoRequest; + export interface CreatePhoneMultiFactorInfoRequest extends BaseCreateMultiFactorInfoRequest { phoneNumber: string; } export interface CreateRequest extends UpdateRequest { @@ -245,7 +253,7 @@ export namespace auth { code?: boolean; idToken?: boolean; } - export interface OIDCAuthProviderConfig extends AuthProviderConfig { + export interface OIDCAuthProviderConfig extends BaseAuthProviderConfig { clientId: string; clientSecret?: string; issuer: string; @@ -272,7 +280,7 @@ export namespace auth { // (undocumented) providerUid: string; } - export interface SAMLAuthProviderConfig extends AuthProviderConfig { + export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig { callbackURL?: string; idpEntityId: string; rpEntityId: string; @@ -323,13 +331,8 @@ export namespace auth { } // (undocumented) export type UpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; - export interface UpdateMultiFactorInfoRequest { - displayName?: string; - enrollmentTime?: string; - factorId: string; - uid?: string; - } - export interface UpdatePhoneMultiFactorInfoRequest extends UpdateMultiFactorInfoRequest { + export type UpdateMultiFactorInfoRequest = UpdatePhoneMultiFactorInfoRequest; + export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactorInfoRequest { phoneNumber: string; } export interface UpdateRequest { diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index bd4b2a5ce8..e7f429f811 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -1480,7 +1480,12 @@ export abstract class AbstractAuthRequestHandler { } // Build the signupNewUser request. - const request: any = deepCopy(properties); + type SignUpNewUserRequest = CreateRequest & { + photoUrl?: string | null; + localId?: string; + mfaInfo?: AuthFactorInfo[]; + }; + const request: SignUpNewUserRequest = deepCopy(properties); // Rewrite photoURL to photoUrl. if (typeof request.photoURL !== 'undefined') { request.photoUrl = request.photoURL; @@ -1496,14 +1501,14 @@ export abstract class AbstractAuthRequestHandler { if (validator.isNonEmptyArray(request.multiFactor.enrolledFactors)) { const mfaInfo: AuthFactorInfo[] = []; try { - request.multiFactor.enrolledFactors.forEach((multiFactorInfo: any) => { + request.multiFactor.enrolledFactors.forEach((multiFactorInfo) => { // Enrollment time and uid are not allowed for signupNewUser endpoint. // They will automatically be provisioned server side. - if (multiFactorInfo.enrollmentTime) { + if ('enrollmentTime' in multiFactorInfo) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, '"enrollmentTime" is not supported when adding second factors via "createUser()"'); - } else if (multiFactorInfo.uid) { + } else if ('uid' in multiFactorInfo) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, '"uid" is not supported when adding second factors via "createUser()"'); diff --git a/src/auth/index.ts b/src/auth/index.ts index d0ab8415ab..6193336beb 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -112,7 +112,7 @@ export namespace auth { } /** - * Interface representing the common properties of a user enrolled second factor. + * Interface representing the common properties of a user-enrolled second factor. */ export interface MultiFactorInfo { @@ -143,7 +143,7 @@ export namespace auth { } /** - * Interface representing a phone specific user enrolled second factor. + * Interface representing a phone specific user-enrolled second factor. */ export interface PhoneMultiFactorInfo extends MultiFactorInfo { @@ -336,10 +336,10 @@ export namespace auth { } /** - * Interface representing common properties of a user enrolled second factor + * Interface representing common properties of a user-enrolled second factor * for an `UpdateRequest`. */ - export interface UpdateMultiFactorInfoRequest { + export interface BaseUpdateMultiFactorInfoRequest { /** * The ID of the enrolled second factor. This ID is unique to the user. When not provided, @@ -364,10 +364,10 @@ export namespace auth { } /** - * Interface representing a phone specific user enrolled second factor + * Interface representing a phone specific user-enrolled second factor * for an `UpdateRequest`. */ - export interface UpdatePhoneMultiFactorInfoRequest extends UpdateMultiFactorInfoRequest { + export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactorInfoRequest { /** * The phone number associated with a phone second factor. @@ -375,6 +375,12 @@ export namespace auth { phoneNumber: string; } + /** + * Type representing the properties of a user-enrolled second factor + * for an `UpdateRequest`. + */ + export type UpdateMultiFactorInfoRequest = | UpdatePhoneMultiFactorInfoRequest; + /** * Interface representing the properties to update on the provided user. */ @@ -443,10 +449,10 @@ export namespace auth { } /** - * Interface representing base properties of a user enrolled second factor for a + * Interface representing base properties of a user-enrolled second factor for a * `CreateRequest`. */ - export interface CreateMultiFactorInfoRequest { + export interface BaseCreateMultiFactorInfoRequest { /** * The optional display name for an enrolled second factor. @@ -460,10 +466,10 @@ export namespace auth { } /** - * Interface representing a phone specific user enrolled second factor for a + * Interface representing a phone specific user-enrolled second factor for a * `CreateRequest`. */ - export interface CreatePhoneMultiFactorInfoRequest extends CreateMultiFactorInfoRequest { + export interface CreatePhoneMultiFactorInfoRequest extends BaseCreateMultiFactorInfoRequest { /** * The phone number associated with a phone second factor. @@ -471,6 +477,12 @@ export namespace auth { phoneNumber: string; } + /** + * Type representing the properties of a user-enrolled second factor + * for a `CreateRequest`. + */ + export type CreateMultiFactorInfoRequest = | CreatePhoneMultiFactorInfoRequest; + /** * Interface representing the properties to set on a new user record to be * created. @@ -1221,7 +1233,7 @@ export namespace auth { /** * The base Auth provider configuration interface. */ - export interface AuthProviderConfig { + export interface BaseAuthProviderConfig { /** * The provider ID defined by the developer. @@ -1249,7 +1261,7 @@ export namespace auth { * Auth provider configuration interface. A SAML provider can be created via * {@link auth.Auth.createProviderConfig `createProviderConfig()`}. */ - export interface SAMLAuthProviderConfig extends AuthProviderConfig { + export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig { /** * The SAML IdP entity identifier. @@ -1301,7 +1313,7 @@ export namespace auth { export interface OAuthResponseType { /** * Whether ID token is returned from IdP's authorization endpoint. - */ + */ idToken?: boolean; /** @@ -1315,7 +1327,7 @@ export namespace auth { * provider configuration interface. An OIDC provider can be created via * {@link auth.Auth.createProviderConfig `createProviderConfig()`}. */ - export interface OIDCAuthProviderConfig extends AuthProviderConfig { + export interface OIDCAuthProviderConfig extends BaseAuthProviderConfig { /** * This is the required client ID used to confirm the audience of an OIDC @@ -1347,13 +1359,19 @@ export namespace auth { * The OIDC provider's client secret to enable OIDC code flow. */ clientSecret?: string; - + /** * The OIDC provider's response object for OAuth authorization flow. */ responseType?: OAuthResponseType; } + /** + * The Auth provider configuration type. + * {@link auth.Auth.createProviderConfig `createProviderConfig()`}. + */ + export type AuthProviderConfig = SAMLAuthProviderConfig | OIDCAuthProviderConfig; + /** * The request interface for updating a SAML Auth provider. This is used * when updating a SAML provider's configuration via @@ -1440,7 +1458,7 @@ export namespace auth { * If not provided, the existing configuration's value is not modified. */ clientSecret?: string; - + /** * The OIDC provider's response object for OAuth authorization flow. */ diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 373982a6f6..29ccf6eb4d 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -1425,12 +1425,6 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { factorId: 'phone', enrollmentTime: new Date().toUTCString(), }, - { - uid: 'mfaUid2', - phoneNumber: '+16505550002', - displayName: 'Personal phone number', - factorId: 'phone', - }, ], }, customClaims: { admin: true }, From 5d5cea27cda1a4ce42bac04bba367dc477039b2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jun 2021 10:03:27 -0700 Subject: [PATCH 128/160] build(deps-dev): bump nock from 13.0.5 to 13.0.11 (#1311) Bumps [nock](https://github.com/nock/nock) from 13.0.5 to 13.0.11. - [Release notes](https://github.com/nock/nock/releases) - [Changelog](https://github.com/nock/nock/blob/main/CHANGELOG.md) - [Commits](https://github.com/nock/nock/compare/v13.0.5...v13.0.11) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 101 +++++++++++++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index a83343390f..fbe7d2dc9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.8.0", + "version": "9.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -665,7 +665,8 @@ "@google-cloud/promisify": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", - "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==" + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", + "optional": true }, "@google-cloud/storage": { "version": "5.3.0", @@ -729,7 +730,8 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "optional": true }, "ansi-styles": { "version": "4.3.0", @@ -791,7 +793,8 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true }, "locate-path": { "version": "5.0.0", @@ -836,6 +839,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "optional": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -846,6 +850,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "optional": true, "requires": { "ansi-regex": "^5.0.0" } @@ -1076,27 +1081,32 @@ "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "optional": true }, "@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true }, "@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true }, "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "optional": true }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -1105,27 +1115,32 @@ "@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "optional": true }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "optional": true }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "optional": true }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "optional": true }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "optional": true }, "@rushstack/node-core-library": { "version": "3.38.0", @@ -1394,7 +1409,8 @@ "@types/long": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "optional": true }, "@types/mime": { "version": "1.3.2", @@ -1586,6 +1602,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, "requires": { "event-target-shim": "^5.0.0" } @@ -1630,6 +1647,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "optional": true, "requires": { "debug": "4" } @@ -2147,7 +2165,8 @@ "bignumber.js": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "optional": true }, "binary-extensions": { "version": "1.13.1", @@ -3425,7 +3444,8 @@ "event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true }, "expand-brackets": { "version": "2.1.4", @@ -3643,7 +3663,8 @@ "fast-text-encoding": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", - "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "optional": true }, "faye-websocket": { "version": "0.11.3", @@ -4020,6 +4041,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.0.3.tgz", "integrity": "sha512-PkzQludeIFhd535/yucALT/Wxyj/y2zLyrMwPcJmnLHDugmV49NvAi/vb+VUq/eWztATZCNcb8ue+ywPG+oLuw==", + "optional": true, "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", @@ -4032,6 +4054,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.1.0.tgz", "integrity": "sha512-r57SV28+olVsflPlKyVig3Muo/VDlcsObMtvDGOEtEJXj+DDE8bEl0coIkXh//hbkSDTvo+f5lbihZOndYXQQQ==", + "optional": true, "requires": { "gaxios": "^3.0.0", "json-bigint": "^0.3.0" @@ -4262,6 +4285,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.0.3.tgz", "integrity": "sha512-2Np6ojPmaJGXHSMsBhtTQEKfSMdLc8hefoihv7N2cwEr8E5bq39fhoat6TcXHwa0XoGO5Guh9sp3nxHFPmibMw==", + "optional": true, "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", @@ -4321,6 +4345,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.1.tgz", "integrity": "sha512-VlQgtozgNVVVcYTXS36eQz4PXPt9gIPqLOhHN0QiV6W6h4qSCNVKPtKC5INtJsaHHF2r7+nOIa26MJeJMTaZEQ==", + "optional": true, "requires": { "node-forge": "^0.9.0" }, @@ -4328,7 +4353,8 @@ "node-forge": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.2.tgz", - "integrity": "sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==" + "integrity": "sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==", + "optional": true } } }, @@ -4347,6 +4373,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.1.tgz", "integrity": "sha512-33w4FNDkUcyIOq/TqyC+drnKdI4PdXmWp9lZzssyEQKuvu9ZFN3KttaSnDKo52U3E51oujVGop93mKxmqO8HHg==", + "optional": true, "requires": { "gaxios": "^3.0.0", "google-p12-pem": "^3.0.0", @@ -4726,6 +4753,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "optional": true, "requires": { "agent-base": "6", "debug": "4" @@ -5474,6 +5502,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz", "integrity": "sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ==", + "optional": true, "requires": { "bignumber.js": "^9.0.0" } @@ -5597,6 +5626,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, "requires": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -5619,6 +5649,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, "requires": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" @@ -5735,7 +5766,8 @@ "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "optional": true }, "lodash.clonedeep": { "version": "4.5.0", @@ -5889,12 +5921,14 @@ "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "optional": true, "requires": { "yallist": "^3.0.2" } @@ -6045,7 +6079,8 @@ "mime": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "optional": true }, "mime-db": { "version": "1.44.0", @@ -6664,9 +6699,9 @@ } }, "nock": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.5.tgz", - "integrity": "sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg==", + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.11.tgz", + "integrity": "sha512-sKZltNkkWblkqqPAsjYW0bm3s9DcHRPiMOyKO/PkfJ+ANHZ2+LA2PLe22r4lLrKgXaiSaDQwW3qGsJFtIpQIeQ==", "dev": true, "requires": { "debug": "^4.1.0", @@ -6684,7 +6719,8 @@ "node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "optional": true }, "node-forge": { "version": "0.10.0", @@ -7676,6 +7712,7 @@ "version": "6.10.1", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", + "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -7695,7 +7732,8 @@ "@types/node": { "version": "13.13.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.30.tgz", - "integrity": "sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA==" + "integrity": "sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA==", + "optional": true } } }, @@ -7714,6 +7752,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7723,6 +7762,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "optional": true, "requires": { "duplexify": "^4.1.1", "inherits": "^2.0.3", @@ -7733,6 +7773,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "optional": true, "requires": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", @@ -7744,6 +7785,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -8748,6 +8790,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, "requires": { "stubs": "^3.0.0" } @@ -8879,7 +8922,8 @@ "stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "optional": true }, "supports-color": { "version": "7.2.0", @@ -9771,7 +9815,8 @@ "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "optional": true }, "xml-name-validator": { "version": "3.0.0", From 09e82c046224adc5ff62c96f75aba4ba8f1d581f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jun 2021 10:18:36 -0700 Subject: [PATCH 129/160] build(deps): bump ws from 7.3.1 to 7.4.6 (#1309) Bumps [ws](https://github.com/websockets/ws) from 7.3.1 to 7.4.6. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/7.3.1...7.4.6) 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 fbe7d2dc9a..c3396276e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9807,9 +9807,9 @@ } }, "ws": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", "dev": true }, "xdg-basedir": { From 312cdbb4397973f034f5e9ae246f4163bbe7fbe7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jun 2021 10:24:30 -0700 Subject: [PATCH 130/160] build(deps-dev): bump del from 2.2.2 to 6.0.0 (#1310) Bumps [del](https://github.com/sindresorhus/del) from 2.2.2 to 6.0.0. - [Release notes](https://github.com/sindresorhus/del/releases) - [Commits](https://github.com/sindresorhus/del/compare/v2.2.2...v6.0.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 266 +++++++++++++++++++++++++++++++++++----------- package.json | 2 +- 2 files changed, 207 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3396276e3..e8419efd36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1073,6 +1073,32 @@ } } }, + "@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" + } + }, "@panva/asn1.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", @@ -1945,18 +1971,9 @@ } }, "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, "array-unique": { @@ -2956,18 +2973,39 @@ } }, "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", "dev": true, "requires": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "dependencies": { + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "delayed-stream": { @@ -3008,6 +3046,23 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + }, + "dependencies": { + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3648,6 +3703,73 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "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.1", + "picomatch": "^2.2.3" + }, + "dependencies": { + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + } + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3666,6 +3788,15 @@ "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", "optional": true }, + "fastq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "faye-websocket": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", @@ -4251,23 +4382,23 @@ } }, "globby": { - "version": "5.0.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "version": "11.0.3", + "resolved": "http://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", "dev": true, "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" }, "dependencies": { - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true } } @@ -5134,28 +5265,16 @@ "optional": true }, "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", "dev": true }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true }, "is-plain-obj": { "version": "2.1.0", @@ -6055,6 +6174,12 @@ "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -7507,12 +7632,6 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -7806,6 +7925,12 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -8172,6 +8297,12 @@ "through2": "^3.0.1" } }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -8187,6 +8318,15 @@ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "dev": true }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "run-sequence": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-2.2.1.tgz", @@ -8444,6 +8584,12 @@ "integrity": "sha512-IifbusYiQBpUxxFJkR3wTU68xzBN0+bxCScEaKMjBvAQERg6FnTTc1F17rseLb1tjmkJ23730AXpFI0c47FgAg==", "dev": true }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", diff --git a/package.json b/package.json index 0b981dfe5c..478e0fe0c8 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "chai-as-promised": "^7.0.0", "chalk": "^4.1.1", "child-process-promise": "^2.2.1", - "del": "^2.2.1", + "del": "^6.0.0", "eslint": "^6.8.0", "firebase-token-generator": "^2.0.0", "gulp": "^4.0.2", From 278bfc9785e87292cd02817036330a3f3d87c8a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jun 2021 10:40:48 -0700 Subject: [PATCH 131/160] build(deps-dev): bump @types/jsonwebtoken from 8.5.0 to 8.5.1 (#1315) Bumps [@types/jsonwebtoken](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jsonwebtoken) from 8.5.0 to 8.5.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jsonwebtoken) --- updated-dependencies: - dependency-name: "@types/jsonwebtoken" 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 e8419efd36..27852ad0be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1418,9 +1418,9 @@ "dev": true }, "@types/jsonwebtoken": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz", - "integrity": "sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw==", "dev": true, "requires": { "@types/node": "*" From 2256f031adc2e28fa9899b5681171608f0473ac7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jun 2021 10:50:10 -0700 Subject: [PATCH 132/160] build(deps-dev): bump nock from 13.0.11 to 13.1.0 (#1313) Bumps [nock](https://github.com/nock/nock) from 13.0.11 to 13.1.0. - [Release notes](https://github.com/nock/nock/releases) - [Changelog](https://github.com/nock/nock/blob/main/CHANGELOG.md) - [Commits](https://github.com/nock/nock/compare/v13.0.11...v13.1.0) --- updated-dependencies: - dependency-name: nock 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 27852ad0be..1f621c03e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6824,9 +6824,9 @@ } }, "nock": { - "version": "13.0.11", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.11.tgz", - "integrity": "sha512-sKZltNkkWblkqqPAsjYW0bm3s9DcHRPiMOyKO/PkfJ+ANHZ2+LA2PLe22r4lLrKgXaiSaDQwW3qGsJFtIpQIeQ==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.1.0.tgz", + "integrity": "sha512-3N3DUY8XYrxxzWazQ+nSBpiaJ3q6gcpNh4gXovC/QBxrsvNp4tq+wsLHF6mJ3nrn3lPLn7KCJqKxy/9aD+0fdw==", "dev": true, "requires": { "debug": "^4.1.0", From 2e6e5d330f5a85c0179e9203848f8b336e11a04f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jun 2021 12:27:53 -0700 Subject: [PATCH 133/160] build(deps-dev): bump @types/sinon-chai from 3.2.4 to 3.2.5 (#1316) Bumps [@types/sinon-chai](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sinon-chai) from 3.2.4 to 3.2.5. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sinon-chai) --- updated-dependencies: - dependency-name: "@types/sinon-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 1f621c03e7..f153eee1ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1526,9 +1526,9 @@ } }, "@types/sinon-chai": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.4.tgz", - "integrity": "sha512-xq5KOWNg70PRC7dnR2VOxgYQ6paumW+4pTZP+6uTSdhpYsAUEeeT5bw6rRHHQrZ4KyR+M5ojOR+lje6TGSpUxA==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.5.tgz", + "integrity": "sha512-bKQqIpew7mmIGNRlxW6Zli/QVyc3zikpGzCa797B/tRnD9OtHvZ/ts8sYXV+Ilj9u3QRaUEM8xrjgd1gwm1BpQ==", "dev": true, "requires": { "@types/chai": "*", From 8548bb3765dc8e31bc3de8a065ba9f6e4825f942 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jun 2021 10:01:17 -0700 Subject: [PATCH 134/160] build(deps-dev): bump bcrypt from 5.0.0 to 5.0.1 (#1324) Bumps [bcrypt](https://github.com/kelektiv/node.bcrypt.js) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/kelektiv/node.bcrypt.js/releases) - [Changelog](https://github.com/kelektiv/node.bcrypt.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/kelektiv/node.bcrypt.js/compare/v5.0.0...v5.0.1) --- updated-dependencies: - dependency-name: bcrypt 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 | 291 ++++++++++++++++++---------------------------- 1 file changed, 116 insertions(+), 175 deletions(-) diff --git a/package-lock.json b/package-lock.json index f153eee1ec..24064ee240 100644 --- a/package-lock.json +++ b/package-lock.json @@ -962,6 +962,49 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, + "@mapbox/node-pre-gyp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", + "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", + "dev": true, + "requires": { + "detect-libc": "^1.0.3", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.1", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "rimraf": "^3.0.2", + "semver": "^7.3.4", + "tar": "^6.1.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.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" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "@microsoft/api-extractor": { "version": "7.15.2", "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.15.2.tgz", @@ -2161,13 +2204,13 @@ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "bcrypt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.0.tgz", - "integrity": "sha512-jB0yCBl4W/kVHM2whjfyqnxTmOHkCX4kHEa5nYKSoGeYe8YrjTYTc87/6bwt1g8cmV0QrbhKriETg9jWtcREhg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", + "integrity": "sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==", "dev": true, "requires": { - "node-addon-api": "^3.0.0", - "node-pre-gyp": "0.15.0" + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^3.1.0" } }, "bcrypt-pbkdf": { @@ -2457,9 +2500,9 @@ } }, "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true }, "class-utils": { @@ -2870,12 +2913,6 @@ "type-detect": "^4.0.0" } }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -4094,12 +4131,12 @@ } }, "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, "requires": { - "minipass": "^2.6.0" + "minipass": "^3.0.0" } }, "fs-mkdirp-stream": { @@ -4884,7 +4921,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "optional": true, "requires": { "agent-base": "6", "debug": "4" @@ -4911,15 +4947,6 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -6241,22 +6268,38 @@ "dev": true }, "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", "dev": true, "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } } }, "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, "requires": { - "minipass": "^2.9.0" + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } } }, "mixin-deep": { @@ -6770,28 +6813,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "needle": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz", - "integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==", - "dev": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -6836,48 +6857,21 @@ } }, "node-addon-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz", - "integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true }, "node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "optional": true + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, - "node-pre-gyp": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz", - "integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==", - "dev": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.3", - "needle": "^2.5.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -6900,13 +6894,12 @@ "dev": true }, "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "dev": true, "requires": { - "abbrev": "1", - "osenv": "^0.1.4" + "abbrev": "1" } }, "normalize-package-data": { @@ -6947,32 +6940,6 @@ "once": "^1.3.2" } }, - "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "dev": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "dev": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, "npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -7473,12 +7440,6 @@ "readable-stream": "^2.0.1" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -7494,16 +7455,6 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "p-limit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", @@ -7940,18 +7891,6 @@ "safe-buffer": "^5.1.0" } }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -8304,9 +8243,9 @@ "dev": true }, "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -8444,12 +8383,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, "saxes": { "version": "3.1.11", "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", @@ -9059,12 +8992,6 @@ "is-utf8": "^0.2.0" } }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, "stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", @@ -9157,18 +9084,31 @@ } }, "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", + "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", "dev": true, "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } } }, "teeny-request": { @@ -9996,7 +9936,8 @@ "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "optional": true }, "yargs": { "version": "16.1.0", From eb04602acea5022c43ff3c8ad9fffbf91f02072c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jun 2021 10:08:38 -0700 Subject: [PATCH 135/160] build(deps): bump @google-cloud/firestore from 4.5.0 to 4.12.2 (#1325) Bumps [@google-cloud/firestore](https://github.com/googleapis/nodejs-firestore) from 4.5.0 to 4.12.2. - [Release notes](https://github.com/googleapis/nodejs-firestore/releases) - [Changelog](https://github.com/googleapis/nodejs-firestore/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/nodejs-firestore/compare/v4.5.0...v4.12.2) --- updated-dependencies: - dependency-name: "@google-cloud/firestore" 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 | 344 ++++++++++++++++++++++------------------------ 1 file changed, 161 insertions(+), 183 deletions(-) diff --git a/package-lock.json b/package-lock.json index 24064ee240..51138b75eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -636,14 +636,15 @@ } }, "@google-cloud/firestore": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.5.0.tgz", - "integrity": "sha512-sExt4E+TlBqyv4l/av6kBZ4YGS99Cc3P5UvLRNj9z41mT9ekPGhIzptbj4K6O7h0KCyDIDOiJdi4gUPH9lip4g==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.12.2.tgz", + "integrity": "sha512-5rurTAJXQ0SANEf8K9eA2JAB5zAh+pu4tGRnkZx5gBWQLZXdBFdtepS+irvKuSXw1KbeAQOuRANSc/nguys6SQ==", "optional": true, "requires": { "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", - "google-gax": "^2.2.0" + "google-gax": "^2.12.0", + "protobufjs": "^6.8.6" } }, "@google-cloud/paginator": { @@ -697,91 +698,42 @@ } }, "@grpc/grpc-js": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.8.tgz", - "integrity": "sha512-64hg5rmEm6F/NvlWERhHmmgxbWU8nD2TMWE+9TvG7/WcOrFT3fzg/Uu631pXRFwmJ4aWO/kp9vVSlr8FUjBDLA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.2.tgz", + "integrity": "sha512-UXepkOKCATJrhHGsxt+CGfpZy9zUn1q9mop5kfcXq1fBkTePxVNPOdnISlCbJFlCtld+pSLGyZCzr9/zVprFKA==", "optional": true, "requires": { - "@grpc/proto-loader": "^0.6.0-pre14", - "@types/node": "^12.12.47", - "google-auth-library": "^6.0.0", - "semver": "^6.2.0" + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.2.tgz", + "integrity": "sha512-q2Qle60Ht2OQBCp9S5hv1JbI4uBBq6/mqSevFNK3ZEgRDBCAkWqZPUhD/K9gXOHrHKluliHiVq2L9sw1mVyAIg==", + "optional": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.10.0", + "yargs": "^16.1.1" }, "dependencies": { - "@grpc/proto-loader": { - "version": "0.6.0-pre9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.0-pre9.tgz", - "integrity": "sha512-oM+LjpEjNzW5pNJjt4/hq1HYayNeQT+eGrOPABJnYHv7TyNPDNzkQ76rDYZF86X5swJOa4EujEMzQ9iiTdPgww==", - "optional": true, - "requires": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.9.0", - "yargs": "^15.3.1" - } - }, - "@types/node": { - "version": "12.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.3.tgz", - "integrity": "sha512-8Jduo8wvvwDzEVJCOvS/G6sgilOLvvhn1eMmK3TW8/T217O7u1jdrK6ImKLv80tVryaPSVeKu6sjDEiFjd4/eg==", - "optional": true - }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "optional": true }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "optional": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "optional": true - }, "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "optional": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "optional": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "optional": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "optional": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "wrap-ansi": "^7.0.0" } }, "get-caller-file": { @@ -796,49 +748,10 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "optional": true }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "optional": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "optional": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "optional": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "optional": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "optional": true - }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "optional": true, "requires": { "emoji-regex": "^8.0.0", @@ -855,16 +768,10 @@ "ansi-regex": "^5.0.0" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "optional": true - }, "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "optional": true, "requires": { "ansi-styles": "^4.0.0", @@ -872,47 +779,35 @@ "strip-ansi": "^6.0.0" } }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true + }, "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "optional": true, "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } }, "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "optional": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "optional": true } } }, - "@grpc/proto-loader": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz", - "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==", - "optional": true, - "requires": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - } - }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1806,7 +1701,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" }, @@ -1815,7 +1709,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -1823,8 +1716,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, @@ -2896,7 +2788,8 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true }, "decode-uri-component": { "version": "0.2.0", @@ -3289,8 +3182,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-string-regexp": { "version": "1.0.5", @@ -4467,20 +4359,22 @@ } }, "google-gax": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.9.1.tgz", - "integrity": "sha512-KQ7HiMTB/PAzKv3OU00x6tC1H7MHvSxQfon5BSyW5o+lkMgRA8xoqvlxZCBC1dlW1azOPGF8vScy8QgFmhaQ9Q==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.14.1.tgz", + "integrity": "sha512-I5RDEN7MEptrCxeHX3ht7nKFGfyjgYX4hQKI9eVMBohMzVbFSwWUndo0CcKXu8es7NhB4gt2XYLm1AHkXhtHpA==", "optional": true, "requires": { - "@grpc/grpc-js": "~1.1.1", - "@grpc/proto-loader": "^0.5.1", + "@grpc/grpc-js": "~1.3.0", + "@grpc/proto-loader": "^0.6.1", "@types/long": "^4.0.0", "abort-controller": "^3.0.0", "duplexify": "^4.0.0", - "google-auth-library": "^6.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^7.0.2", "is-stream-ended": "^0.1.4", "node-fetch": "^2.6.1", - "protobufjs": "^6.9.0", + "object-hash": "^2.1.1", + "protobufjs": "^6.10.2", "retry-request": "^4.0.0" }, "dependencies": { @@ -4496,6 +4390,84 @@ "stream-shift": "^1.0.0" } }, + "gaxios": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.0.tgz", + "integrity": "sha512-pHplNbslpwCLMyII/lHPWFQbJWOX0B3R1hwBEOvzYi1GmdKZruuEHK4N9V6f7tf1EaPYyF80mui1+344p6SmLg==", + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", + "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", + "optional": true, + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.1.1.tgz", + "integrity": "sha512-+Q1linq/To3DYLyPz4UTEkQ0v5EOXadMM/S+taLV3W9611hq9zqg8kgGApqbTQnggtwdO9yU1y2YT7+83wdTRg==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", + "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", + "optional": true, + "requires": { + "node-forge": "^0.10.0" + } + }, + "gtoken": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.2.1.tgz", + "integrity": "sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==", + "optional": true, + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0" + } + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "optional": true, + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -4506,6 +4478,12 @@ "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true } } }, @@ -7327,6 +7305,12 @@ } } }, + "object-hash": { + "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": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", @@ -7779,9 +7763,9 @@ "dev": true }, "protobufjs": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz", - "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", @@ -7795,16 +7779,8 @@ "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/long": "^4.0.1", - "@types/node": "^13.7.0", + "@types/node": ">=13.7.0", "long": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "13.13.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.30.tgz", - "integrity": "sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA==", - "optional": true - } } }, "pseudomap": { @@ -8418,7 +8394,8 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true }, "set-value": { "version": "2.0.1", @@ -9931,7 +9908,8 @@ "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true }, "yallist": { "version": "3.1.1", From 3c78a7365700947c9493ca5e4701e7155bb03651 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jun 2021 10:16:13 -0700 Subject: [PATCH 136/160] build(deps-dev): bump @types/mocha from 2.2.48 to 8.2.2 (#1323) Bumps [@types/mocha](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mocha) from 2.2.48 to 8.2.2. - [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-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51138b75eb..972c8cd674 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1394,9 +1394,9 @@ "dev": true }, "@types/mocha": { - "version": "2.2.48", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", - "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.2.tgz", + "integrity": "sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw==", "dev": true }, "@types/nock": { diff --git a/package.json b/package.json index 478e0fe0c8..04aedf765b 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@types/jsonwebtoken": "^8.5.0", "@types/lodash": "^4.14.104", "@types/minimist": "^1.2.0", - "@types/mocha": "^2.2.48", + "@types/mocha": "^8.2.2", "@types/nock": "^9.1.0", "@types/request": "^2.47.0", "@types/request-promise": "^4.1.41", From 2a4de46364cffc7d277d843df38ff65c970bee59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 06:58:33 -0700 Subject: [PATCH 137/160] build(deps-dev): bump @firebase/app from 0.6.21 to 0.6.26 (#1329) Bumps [@firebase/app](https://github.com/firebase/firebase-js-sdk/tree/HEAD/packages/app) from 0.6.21 to 0.6.26. - [Release notes](https://github.com/firebase/firebase-js-sdk/releases) - [Changelog](https://github.com/firebase/firebase-js-sdk/blob/@firebase/app@0.6.26/packages/app/CHANGELOG.md) - [Commits](https://github.com/firebase/firebase-js-sdk/commits/@firebase/app@0.6.26/packages/app) --- updated-dependencies: - dependency-name: "@firebase/app" 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 | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 972c8cd674..7a3b7c1358 100644 --- a/package-lock.json +++ b/package-lock.json @@ -460,13 +460,13 @@ } }, "@firebase/app": { - "version": "0.6.21", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.21.tgz", - "integrity": "sha512-SpWXdy/U06gTOEofSjhcsFGUtYmZim7ty6U4eMUQH0ObtymeVdTiK4tJcohMT5XoihQw4CLS2YvDySwx3+BlWg==", + "version": "0.6.26", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.26.tgz", + "integrity": "sha512-y4tpb+uiYLQC5+/AHBtIGZMaTjJ2BHQEsXmPqxyhfVFDzWMcXFsc//RVxA/0OejajhJR6GeqDcIS3m47mUD+Aw==", "dev": true, "requires": { "@firebase/app-types": "0.6.2", - "@firebase/component": "0.5.0", + "@firebase/component": "0.5.2", "@firebase/logger": "0.2.6", "@firebase/util": "1.1.0", "dom-storage": "2.1.0", @@ -475,9 +475,9 @@ }, "dependencies": { "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "dev": true } } @@ -508,9 +508,9 @@ "dev": true }, "@firebase/component": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.0.tgz", - "integrity": "sha512-v18csWtXb0ri+3m7wuGLY/UDgcb89vuMlZGQ//+7jEPLIQeLbylvZhol1uzW9WzoOpxMxOS2W5qyVGX36wZvEA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.2.tgz", + "integrity": "sha512-QT+o6VaBCz/k8wmC/DErU9dQK2QeIoHtkBkryZVTSRkrvulglEWNIpbPp86UbuqZZd1wwzoh6m7BL6JbdEp9SQ==", "dev": true, "requires": { "@firebase/util": "1.1.0", @@ -518,9 +518,9 @@ }, "dependencies": { "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "dev": true } } @@ -586,9 +586,9 @@ }, "dependencies": { "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "dev": true } } From 24d3b91a163ab8d0c6adb12c89e449b4c898b153 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 07:09:20 -0700 Subject: [PATCH 138/160] build(deps): bump @firebase/database from 0.10.0 to 0.10.4 (#1328) Bumps [@firebase/database](https://github.com/firebase/firebase-js-sdk/tree/HEAD/packages/database) from 0.10.0 to 0.10.4. - [Release notes](https://github.com/firebase/firebase-js-sdk/releases) - [Changelog](https://github.com/firebase/firebase-js-sdk/blob/master/packages/database/CHANGELOG.md) - [Commits](https://github.com/firebase/firebase-js-sdk/commits/@firebase/database@0.10.4/packages/database) --- updated-dependencies: - dependency-name: "@firebase/database" 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 | 39 +++++++++------------------------------ 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a3b7c1358..c5e365b466 100644 --- a/package-lock.json +++ b/package-lock.json @@ -511,7 +511,6 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.2.tgz", "integrity": "sha512-QT+o6VaBCz/k8wmC/DErU9dQK2QeIoHtkBkryZVTSRkrvulglEWNIpbPp86UbuqZZd1wwzoh6m7BL6JbdEp9SQ==", - "dev": true, "requires": { "@firebase/util": "1.1.0", "tslib": "^2.1.0" @@ -520,18 +519,17 @@ "tslib": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", - "dev": true + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" } } }, "@firebase/database": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.10.0.tgz", - "integrity": "sha512-GsHvuES83Edtboij2h3txKg+yV/TD4b5Owc01SgXEQtvj1lulkHt4Ufmd9OZz1WreWQJMIqKpbVowIDHjlkZJQ==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.10.4.tgz", + "integrity": "sha512-Mi6fJGzv9JH+GoYhgzSQAxsUhanW4jU6lqe/9kTyxNxHd+asphoJXJcKDs97uxRaowmSzu5LSAkGlWe63vJ7wA==", "requires": { "@firebase/auth-interop-types": "0.1.6", - "@firebase/component": "0.5.0", + "@firebase/component": "0.5.2", "@firebase/database-types": "0.7.2", "@firebase/logger": "0.2.6", "@firebase/util": "1.1.0", @@ -539,27 +537,10 @@ "tslib": "^2.1.0" }, "dependencies": { - "@firebase/component": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.0.tgz", - "integrity": "sha512-v18csWtXb0ri+3m7wuGLY/UDgcb89vuMlZGQ//+7jEPLIQeLbylvZhol1uzW9WzoOpxMxOS2W5qyVGX36wZvEA==", - "requires": { - "@firebase/util": "1.1.0", - "tslib": "^2.1.0" - } - }, - "@firebase/util": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.1.0.tgz", - "integrity": "sha512-lfuSASuPKNdfebuFR8rjFamMQUPH9iiZHcKS755Rkm/5gRT0qC7BMhCh3ZkHf7NVbplzIc/GhmX2jM+igDRCag==", - "requires": { - "tslib": "^2.1.0" - } - }, "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" } } }, @@ -580,7 +561,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.1.0.tgz", "integrity": "sha512-lfuSASuPKNdfebuFR8rjFamMQUPH9iiZHcKS755Rkm/5gRT0qC7BMhCh3ZkHf7NVbplzIc/GhmX2jM+igDRCag==", - "dev": true, "requires": { "tslib": "^2.1.0" }, @@ -588,8 +568,7 @@ "tslib": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", - "dev": true + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" } } }, From f09bd6428483754bfbe4dcfe78b053fbc56d6635 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jun 2021 07:33:30 -0700 Subject: [PATCH 139/160] build(deps-dev): bump request-promise from 4.2.5 to 4.2.6 (#1331) Bumps [request-promise](https://github.com/request/request-promise) from 4.2.5 to 4.2.6. - [Release notes](https://github.com/request/request-promise/releases) - [Commits](https://github.com/request/request-promise/compare/v4.2.5...v4.2.6) --- updated-dependencies: - dependency-name: request-promise 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 | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index c5e365b466..be2d0e85b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8059,17 +8059,26 @@ } }, "request-promise": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.5.tgz", - "integrity": "sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", "dev": true, "requires": { "bluebird": "^3.5.0", - "request-promise-core": "1.1.3", + "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" }, "dependencies": { + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "requires": { + "lodash": "^4.17.19" + } + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", From 9539a50d4f02437e820f73a461146b11d8fda219 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 17:08:00 -0700 Subject: [PATCH 140/160] build(deps-dev): bump gulp-filter from 6.0.0 to 7.0.0 (#1334) Bumps [gulp-filter](https://github.com/sindresorhus/gulp-filter) from 6.0.0 to 7.0.0. - [Release notes](https://github.com/sindresorhus/gulp-filter/releases) - [Commits](https://github.com/sindresorhus/gulp-filter/compare/v6.0.0...v7.0.0) --- updated-dependencies: - dependency-name: gulp-filter 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 | 43 ++++++++++++++++++------------------------- package.json | 2 +- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index be2d0e85b5..4658e77f30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1361,9 +1361,9 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==", "dev": true }, "@types/minimist": { @@ -1812,6 +1812,12 @@ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, + "array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true + }, "array-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", @@ -4586,14 +4592,15 @@ } }, "gulp-filter": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-6.0.0.tgz", - "integrity": "sha512-veQFW93kf6jBdWdF/RxMEIlDK2mkjHyPftM381DID2C9ImTVngwYpyyThxm4/EpgcNOT37BLefzMOjEKbyYg0Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-7.0.0.tgz", + "integrity": "sha512-ZGWtJo0j1mHfP77tVuhyqem4MRA5NfNRjoVe6VAkLGeQQ/QGo2VsFwp7zfPTGDsd1rwzBmoDHhxpE6f5B3Zuaw==", "dev": true, "requires": { - "multimatch": "^4.0.0", + "multimatch": "^5.0.0", "plugin-error": "^1.0.1", - "streamfilter": "^3.0.0" + "streamfilter": "^3.0.0", + "to-absolute-glob": "^2.0.2" } }, "gulp-header": { @@ -6683,9 +6690,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "multimatch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz", - "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", "dev": true, "requires": { "@types/minimatch": "^3.0.3", @@ -6693,20 +6700,6 @@ "array-union": "^2.1.0", "arrify": "^2.0.1", "minimatch": "^3.0.4" - }, - "dependencies": { - "array-differ": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - } } }, "mute-stdout": { diff --git a/package.json b/package.json index 04aedf765b..11b2c6ce63 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "eslint": "^6.8.0", "firebase-token-generator": "^2.0.0", "gulp": "^4.0.2", - "gulp-filter": "^6.0.0", + "gulp-filter": "^7.0.0", "gulp-header": "^2.0.9", "gulp-typescript": "^5.0.1", "http-message-parser": "^0.0.34", From 9b0d7ef581f19b618ac8b66dfec319e0ed7be87e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jun 2021 17:32:20 -0700 Subject: [PATCH 141/160] build(deps-dev): bump @types/minimist from 1.2.0 to 1.2.1 (#1336) Bumps [@types/minimist](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/minimist) from 1.2.0 to 1.2.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/minimist) --- updated-dependencies: - dependency-name: "@types/minimist" 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 4658e77f30..d10b61f0f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1367,9 +1367,9 @@ "dev": true }, "@types/minimist": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", + "version": "1.2.1", + "resolved": "http://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz", + "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, "@types/mocha": { From 9872b9bcaf9af6e71f404caf10114155149da80d Mon Sep 17 00:00:00 2001 From: NothingEverHappens Date: Tue, 22 Jun 2021 14:45:32 -0400 Subject: [PATCH 142/160] fix(docs): replace all global.html -> admin.html (#1341) Co-authored-by: Hiranya Jayathilaka --- docgen/generate-docs.js | 1 + 1 file changed, 1 insertion(+) diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js index 12f93706b7..619526dff8 100644 --- a/docgen/generate-docs.js +++ b/docgen/generate-docs.js @@ -117,6 +117,7 @@ function fixLinks(file) { return fs.readFile(file, 'utf8').then(data => { const flattenedLinks = data .replace(/\.\.\//g, '') + .replace(/globals\.html/g, 'admin.html') .replace(/(modules|interfaces|classes|enums)\//g, ''); let caseFixedLinks = flattenedLinks; for (const lower in lowerToUpperLookup) { From 0f9a7defd8873e630bb27374f4f3d29c90af59aa Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 23 Jun 2021 12:04:23 -0700 Subject: [PATCH 143/160] feat(fis): Adding the admin.installations() API for deleting Firebase installation IDs (#1187) * feat(fis): Added admin.installations() API * fix: Marked IID APIs deprecated * fix: Deprecated App.instanceId() method; Added more unit and integration tests * fix: Throwing FirebaseInstallationsError from constructor * fix: Some docs updates * fix: Minor update to API doc comment * fix: Added Installations class to API ref toc * fix: Minor doc updates --- docgen/content-sources/node/toc.yaml | 6 + etc/firebase-admin.api.md | 16 ++ src/firebase-app.ts | 15 ++ src/firebase-namespace-api.ts | 3 + src/firebase-namespace.d.ts | 1 + src/firebase-namespace.ts | 14 ++ src/installations/index.ts | 85 ++++++++ .../installations-request-handler.ts} | 42 ++-- src/installations/installations.ts | 68 +++++++ src/instance-id/index.ts | 22 +- src/instance-id/instance-id.ts | 26 ++- src/utils/error.ts | 34 +++- test/integration/installations.spec.ts | 31 +++ test/integration/instance-id.spec.ts | 2 +- test/unit/firebase-app.spec.ts | 28 +++ test/unit/firebase-namespace.spec.ts | 43 ++++ test/unit/index.spec.ts | 5 +- .../installations-request-handler.spec.ts} | 38 ++-- test/unit/installations/installations.spec.ts | 190 ++++++++++++++++++ test/unit/instance-id/instance-id.spec.ts | 23 ++- 20 files changed, 617 insertions(+), 75 deletions(-) create mode 100644 src/installations/index.ts rename src/{instance-id/instance-id-request-internal.ts => installations/installations-request-handler.ts} (71%) create mode 100644 src/installations/installations.ts create mode 100644 test/integration/installations.spec.ts rename test/unit/{instance-id/instance-id-request.spec.ts => installations/installations-request-handler.spec.ts} (72%) create mode 100644 test/unit/installations/installations.spec.ts diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index bc19ced9d6..fa5808c5f7 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -144,6 +144,12 @@ toc: - title: "admin.firestore" path: /docs/reference/admin/node/admin.firestore +- title: "admin.installations" + path: /docs/reference/admin/node/admin.installations + section: + - title: "Installations" + path: /docs/reference/admin/node/admin.installations.Installations-1 + - title: "admin.instanceId" path: /docs/reference/admin/node/admin.instanceId section: diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index b16b7127cb..6017b0c72a 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -25,6 +25,8 @@ export namespace app { // (undocumented) firestore(): firestore.Firestore; // (undocumented) + installations(): installations.Installations; + // @deprecated (undocumented) instanceId(): instanceId.InstanceId; // (undocumented) machineLearning(): machineLearning.MachineLearning; @@ -542,13 +544,27 @@ export interface GoogleOAuthAccessToken { export function initializeApp(options?: AppOptions, name?: string): app.App; // @public +export function installations(app?: app.App): installations.Installations; + +// @public (undocumented) +export namespace installations { + export interface Installations { + // (undocumented) + app: app.App; + deleteInstallation(fid: string): Promise; + } +} + +// @public @deprecated export function instanceId(app?: app.App): instanceId.InstanceId; // @public (undocumented) export namespace instanceId { + // @deprecated export interface InstanceId { // (undocumented) app: app.App; + // @deprecated deleteInstanceId(instanceId: string): Promise; } } diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 84b30a52c5..29afbb75d2 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -31,6 +31,7 @@ import { database } from './database/index'; import { DatabaseService } from './database/database-internal'; import { Firestore } from '@google-cloud/firestore'; import { FirestoreService } from './firestore/firestore-internal'; +import { Installations } from './installations/installations'; import { InstanceId } from './instance-id/instance-id'; import { ProjectManagement } from './project-management/project-management'; import { SecurityRules } from './security-rules/security-rules'; @@ -256,9 +257,23 @@ export class FirebaseApp implements app.App { return service.client; } + /** + * Returns the `Installations` service instance associated with this app. + * + * @return The `Installations` service instance of this app. + */ + public installations(): Installations { + return this.ensureService_('installations', () => { + const fisService: typeof Installations = require('./installations/installations').Installations; + return new fisService(this); + }); + } + /** * Returns the InstanceId service instance associated with this app. * + * This API is deprecated. Use the `installations()` API instead. + * * @return The InstanceId service instance of this app. */ public instanceId(): InstanceId { diff --git a/src/firebase-namespace-api.ts b/src/firebase-namespace-api.ts index 6507fa3a88..509a2d1d18 100644 --- a/src/firebase-namespace-api.ts +++ b/src/firebase-namespace-api.ts @@ -20,6 +20,7 @@ import { auth } from './auth/index'; import { credential } from './credential/index'; import { database } from './database/index'; import { firestore } from './firestore/index'; +import { installations } from './installations/index'; import { instanceId } from './instance-id/index'; import { machineLearning } from './machine-learning/index'; import { messaging } from './messaging/index'; @@ -227,6 +228,8 @@ export namespace app { auth(): auth.Auth; database(url?: string): database.Database; firestore(): firestore.Firestore; + installations(): installations.Installations; + /** @deprecated */ instanceId(): instanceId.InstanceId; machineLearning(): machineLearning.MachineLearning; messaging(): messaging.Messaging; diff --git a/src/firebase-namespace.d.ts b/src/firebase-namespace.d.ts index ab013c3cac..bff64ccdf3 100644 --- a/src/firebase-namespace.d.ts +++ b/src/firebase-namespace.d.ts @@ -20,6 +20,7 @@ export * from './app-check/index'; export * from './auth/index'; export * from './database/index'; export * from './firestore/index'; +export * from './installations/index'; export * from './instance-id/index'; export * from './machine-learning/index'; export * from './messaging/index'; diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index fa1a409b48..311807301a 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -27,6 +27,7 @@ import { appCheck } from './app-check/index'; import { auth } from './auth/index'; import { database } from './database/index'; import { firestore } from './firestore/index'; +import { installations } from './installations/index'; import { instanceId } from './instance-id/index'; import { machineLearning } from './machine-learning/index'; import { messaging } from './messaging/index'; @@ -43,6 +44,7 @@ import AppCheck = appCheck.AppCheck; import Auth = auth.Auth; import Database = database.Database; import Firestore = firestore.Firestore; +import Installations = installations.Installations; import InstanceId = instanceId.InstanceId; import MachineLearning = machineLearning.MachineLearning; import Messaging = messaging.Messaging; @@ -311,6 +313,18 @@ export class FirebaseNamespace { return Object.assign(fn, { MachineLearning: machineLearning }); } + /** + * Gets the `Installations` service namespace. The returned namespace can be used to get the + * `Installations` service for the default app or an explicitly specified app. + */ + get installations(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).installations(); + }; + const installations = require('./installations/installations').Installations; + return Object.assign(fn, { Installations: installations }); + } + /** * Gets the `InstanceId` service namespace. The returned namespace can be used to get the * `Instance` service for the default app or an explicitly specified app. diff --git a/src/installations/index.ts b/src/installations/index.ts new file mode 100644 index 0000000000..373b904c9c --- /dev/null +++ b/src/installations/index.ts @@ -0,0 +1,85 @@ +/*! + * 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 '../firebase-namespace-api'; + +/** + * Gets the {@link installations.Installations `Installations`} service for the + * default app or a given app. + * + * `admin.installations()` can be called with no arguments to access the default + * app's {@link installations.Installations `Installations`} service or as + * `admin.installations(app)` to access the + * {@link installations.Installations `Installations`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the Installations service for the default app + * var defaultInstallations = admin.installations(); + * ``` + * + * @example + * ```javascript + * // Get the Installations service for a given app + * var otherInstallations = admin.installations(otherApp); + *``` + * + * @param app Optional app whose `Installations` service to + * return. If not provided, the default `Installations` service is + * returned. + * + * @return The default `Installations` service if + * no app is provided or the `Installations` service associated with the + * provided app. + */ +export declare function installations(app?: app.App): installations.Installations; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace installations { + /** + * Gets the {@link Installations `Installations`} service for the + * current app. + * + * @example + * ```javascript + * var installations = app.installations(); + * // The above is shorthand for: + * // var installations = admin.installations(app); + * ``` + * + * @return The `Installations` service for the + * current app. + */ + export interface Installations { + app: app.App; + + /** + * Deletes the specified installation ID and the associated data from Firebase. + * + * Note that Google Analytics for Firebase uses its own form of Instance ID to + * keep track of analytics data. Therefore deleting a Firebase installation ID does + * not delete Analytics data. See + * [Delete a Firebase installation](/docs/projects/manage-installations#delete-installation) + * for more information. + * + * @param fid The Firebase installation ID to be deleted. + * + * @return A promise fulfilled when the installation ID is deleted. + */ + deleteInstallation(fid: string): Promise; + } +} diff --git a/src/instance-id/instance-id-request-internal.ts b/src/installations/installations-request-handler.ts similarity index 71% rename from src/instance-id/instance-id-request-internal.ts rename to src/installations/installations-request-handler.ts index e0d404d602..299fd3136e 100644 --- a/src/instance-id/instance-id-request-internal.ts +++ b/src/installations/installations-request-handler.ts @@ -1,6 +1,6 @@ /*! * @license - * Copyright 2017 Google Inc. + * 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. @@ -16,7 +16,7 @@ */ import { FirebaseApp } from '../firebase-app'; -import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; +import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error'; import { ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError, } from '../utils/api-request'; @@ -33,10 +33,10 @@ const FIREBASE_IID_TIMEOUT = 10000; /** HTTP error codes raised by the backend server. */ const ERROR_CODES: {[key: number]: string} = { - 400: 'Malformed instance ID argument.', + 400: 'Malformed installation ID argument.', 401: 'Request not authorized.', - 403: 'Project does not match instance ID or the client does not have sufficient privileges.', - 404: 'Failed to find the instance ID.', + 403: 'Project does not match installation ID or the client does not have sufficient privileges.', + 404: 'Failed to find the installation ID.', 409: 'Already deleted.', 429: 'Request throttled out by the backend server.', 500: 'Internal server error.', @@ -44,9 +44,9 @@ const ERROR_CODES: {[key: number]: string} = { }; /** - * Class that provides mechanism to send requests to the Firebase Instance ID backend endpoints. + * Class that provides mechanism to send requests to the FIS backend endpoints. */ -export class FirebaseInstanceIdRequestHandler { +export class FirebaseInstallationsRequestHandler { private readonly host: string = FIREBASE_IID_HOST; private readonly timeout: number = FIREBASE_IID_TIMEOUT; @@ -54,7 +54,7 @@ export class FirebaseInstanceIdRequestHandler { private path: string; /** - * @param {FirebaseApp} app The app used to fetch access tokens to sign API requests. + * @param app The app used to fetch access tokens to sign API requests. * * @constructor */ @@ -62,21 +62,21 @@ export class FirebaseInstanceIdRequestHandler { this.httpClient = new AuthorizedHttpClient(app); } - public deleteInstanceId(instanceId: string): Promise { - if (!validator.isNonEmptyString(instanceId)) { - return Promise.reject(new FirebaseInstanceIdError( - InstanceIdClientErrorCode.INVALID_INSTANCE_ID, - 'Instance ID must be a non-empty string.', + public deleteInstallation(fid: string): Promise { + if (!validator.isNonEmptyString(fid)) { + return Promise.reject(new FirebaseInstallationsError( + InstallationsClientErrorCode.INVALID_INSTALLATION_ID, + 'Installation ID must be a non-empty string.', )); } - return this.invokeRequestHandler(new ApiSettings(instanceId, 'DELETE')); + return this.invokeRequestHandler(new ApiSettings(fid, 'DELETE')); } /** * Invokes the request handler based on the API settings object passed. * - * @param {ApiSettings} apiSettings The API endpoint settings to apply to request and response. - * @return {Promise} A promise that resolves when the request is complete. + * @param apiSettings The API endpoint settings to apply to request and response. + * @return A promise that resolves when the request is complete. */ private invokeRequestHandler(apiSettings: ApiSettings): Promise { return this.getPathPrefix() @@ -98,8 +98,8 @@ export class FirebaseInstanceIdRequestHandler { response.data.error : response.text; const template: string = ERROR_CODES[response.status]; const message: string = template ? - `Instance ID "${apiSettings.getEndpoint()}": ${template}` : errorMessage; - throw new FirebaseInstanceIdError(InstanceIdClientErrorCode.API_ERROR, message); + `Installation ID "${apiSettings.getEndpoint()}": ${template}` : errorMessage; + throw new FirebaseInstallationsError(InstallationsClientErrorCode.API_ERROR, message); } // In case of timeouts and other network errors, the HttpClient returns a // FirebaseError wrapped in the response. Simply throw it here. @@ -116,9 +116,9 @@ export class FirebaseInstanceIdRequestHandler { .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { // Assert for an explicit projct ID (either via AppOptions or the cert itself). - throw new FirebaseInstanceIdError( - InstanceIdClientErrorCode.INVALID_PROJECT_ID, - 'Failed to determine project ID for InstanceId. Initialize the ' + throw new FirebaseInstallationsError( + InstallationsClientErrorCode.INVALID_PROJECT_ID, + 'Failed to determine project ID for Installations. Initialize the ' + 'SDK with service account credentials or set project ID as an app option. ' + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', ); diff --git a/src/installations/installations.ts b/src/installations/installations.ts new file mode 100644 index 0000000000..a55e5af295 --- /dev/null +++ b/src/installations/installations.ts @@ -0,0 +1,68 @@ +/*! + * 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 { FirebaseApp } from '../firebase-app'; +import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error'; +import { FirebaseInstallationsRequestHandler } from './installations-request-handler'; +import { installations } from './index'; +import * as validator from '../utils/validator'; + +import InstallationsInterface = installations.Installations; + +/** + * The `Installations` service for the current app. + */ +export class Installations implements InstallationsInterface { + + private app_: FirebaseApp; + private requestHandler: FirebaseInstallationsRequestHandler; + + /** + * @param app The app for this Installations service. + * @constructor + */ + constructor(app: FirebaseApp) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseInstallationsError( + InstallationsClientErrorCode.INVALID_ARGUMENT, + 'First argument passed to admin.installations() must be a valid Firebase app instance.', + ); + } + + this.app_ = app; + this.requestHandler = new FirebaseInstallationsRequestHandler(app); + } + + /** + * Deletes the specified installation ID and the associated data from Firebase. + * + * @param fid The Firebase installation ID to be deleted. + * + * @return A promise fulfilled when the installation ID is deleted. + */ + public deleteInstallation(fid: string): Promise { + return this.requestHandler.deleteInstallation(fid); + } + + /** + * Returns the app associated with this Installations instance. + * + * @return The app associated with this Installations instance. + */ + get app(): FirebaseApp { + return this.app_; + } +} diff --git a/src/instance-id/index.ts b/src/instance-id/index.ts index 09cbfe4022..a5ea77eb8b 100644 --- a/src/instance-id/index.ts +++ b/src/instance-id/index.ts @@ -38,6 +38,9 @@ import { app } from '../firebase-namespace-api'; * var otherInstanceId = admin.instanceId(otherApp); *``` * + * This API is deprecated. Developers are advised to use the `admin.installations()` + * API to delete their instance IDs and Firebase installation IDs. + * * @param app Optional app whose `InstanceId` service to * return. If not provided, the default `InstanceId` service will be * returned. @@ -45,24 +48,18 @@ import { app } from '../firebase-namespace-api'; * @return The default `InstanceId` service if * no app is provided or the `InstanceId` service associated with the * provided app. + * + * @deprecated */ export declare function instanceId(app?: app.App): instanceId.InstanceId; /* eslint-disable @typescript-eslint/no-namespace */ export namespace instanceId { /** - * Gets the {@link InstanceId `InstanceId`} service for the + * The {@link InstanceId `InstanceId`} service for the * current app. * - * @example - * ```javascript - * var instanceId = app.instanceId(); - * // The above is shorthand for: - * // var instanceId = admin.instanceId(app); - * ``` - * - * @return The `InstanceId` service for the - * current app. + * @deprecated */ export interface InstanceId { app: app.App; @@ -76,9 +73,14 @@ export namespace instanceId { * [Delete an Instance ID](/support/privacy/manage-iids#delete_an_instance_id) * for more information. * + * This API is deprecated. Developers are advised to use the `Installations.deleteInstallation()` + * API instead. + * * @param instanceId The instance ID to be deleted. * * @return A promise fulfilled when the instance ID is deleted. + * + * @deprecated */ deleteInstanceId(instanceId: string): Promise; } diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index 80afaeae48..80937ffc0c 100644 --- a/src/instance-id/instance-id.ts +++ b/src/instance-id/instance-id.ts @@ -15,8 +15,9 @@ */ import { FirebaseApp } from '../firebase-app'; -import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; -import { FirebaseInstanceIdRequestHandler } from './instance-id-request-internal'; +import { + FirebaseInstallationsError, FirebaseInstanceIdError, InstallationsClientErrorCode, InstanceIdClientErrorCode, +} from '../utils/error'; import { instanceId } from './index'; import * as validator from '../utils/validator'; @@ -39,10 +40,9 @@ import InstanceIdInterface = instanceId.InstanceId; export class InstanceId implements InstanceIdInterface { private app_: FirebaseApp; - private requestHandler: FirebaseInstanceIdRequestHandler; /** - * @param {FirebaseApp} app The app for this InstanceId service. + * @param app The app for this InstanceId service. * @constructor */ constructor(app: FirebaseApp) { @@ -54,7 +54,6 @@ export class InstanceId implements InstanceIdInterface { } this.app_ = app; - this.requestHandler = new FirebaseInstanceIdRequestHandler(app); } /** @@ -71,16 +70,25 @@ export class InstanceId implements InstanceIdInterface { * @return A promise fulfilled when the instance ID is deleted. */ public deleteInstanceId(instanceId: string): Promise { - return this.requestHandler.deleteInstanceId(instanceId) - .then(() => { - // Return nothing on success + return this.app.installations().deleteInstallation(instanceId) + .catch((err) => { + if (err instanceof FirebaseInstallationsError) { + let code = err.code.replace('installations/', ''); + if (code === InstallationsClientErrorCode.INVALID_INSTALLATION_ID.code) { + code = InstanceIdClientErrorCode.INVALID_INSTANCE_ID.code; + } + + throw new FirebaseInstanceIdError({ code, message: err.message }); + } + + throw err; }); } /** * Returns the app associated with this InstanceId instance. * - * @return {FirebaseApp} The app associated with this InstanceId instance. + * @return The app associated with this InstanceId instance. */ get app(): FirebaseApp { return this.app_; diff --git a/src/utils/error.ts b/src/utils/error.ts index caa781e8f3..fcec6d5fd3 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -224,6 +224,23 @@ export class FirebaseInstanceIdError extends FirebaseError { constructor(info: ErrorInfo, message?: string) { // Override default message if custom message provided. super({ code: 'instance-id/' + info.code, message: message || info.message }); + (this as any).__proto__ = FirebaseInstanceIdError.prototype; + } +} + +/** + * Firebase Installations service error code structure. This extends `FirebaseError`. + * + * @param info The error code info. + * @param message The error message. This will override the default + * message if provided. + * @constructor + */ +export class FirebaseInstallationsError extends FirebaseError { + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super({ code: 'installations/' + info.code, message: message || info.message }); + (this as any).__proto__ = FirebaseInstallationsError.prototype; } } @@ -808,7 +825,7 @@ export class MessagingClientErrorCode { }; } -export class InstanceIdClientErrorCode { +export class InstallationsClientErrorCode { public static INVALID_ARGUMENT = { code: 'invalid-argument', message: 'Invalid argument provided.', @@ -817,13 +834,20 @@ export class InstanceIdClientErrorCode { code: 'invalid-project-id', message: 'Invalid project ID provided.', }; - public static INVALID_INSTANCE_ID = { - code: 'invalid-instance-id', - message: 'Invalid instance ID provided.', + public static INVALID_INSTALLATION_ID = { + code: 'invalid-installation-id', + message: 'Invalid installation ID provided.', }; public static API_ERROR = { code: 'api-error', - message: 'Instance ID API call failed.', + message: 'Installation ID API call failed.', + }; +} + +export class InstanceIdClientErrorCode extends InstallationsClientErrorCode { + public static INVALID_INSTANCE_ID = { + code: 'invalid-instance-id', + message: 'Invalid instance ID provided.', }; } diff --git a/test/integration/installations.spec.ts b/test/integration/installations.spec.ts new file mode 100644 index 0000000000..982bd3a218 --- /dev/null +++ b/test/integration/installations.spec.ts @@ -0,0 +1,31 @@ +/*! + * 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 * as admin from '../../lib/index'; +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('admin.installations', () => { + it('deleteInstallation() fails when called with fictive-ID0 instance ID', () => { + // instance ids have to conform to /[cdef][A-Za-z0-9_-]{9}[AEIMQUYcgkosw048]/ + return admin.installations().deleteInstallation('fictive-ID0') + .should.eventually.be + .rejectedWith('Installation ID "fictive-ID0": Failed to find the installation ID.'); + }); +}); diff --git a/test/integration/instance-id.spec.ts b/test/integration/instance-id.spec.ts index a7d6eb6df2..d98c2802f6 100644 --- a/test/integration/instance-id.spec.ts +++ b/test/integration/instance-id.spec.ts @@ -26,6 +26,6 @@ describe('admin.instanceId', () => { // instance ids have to conform to /[cdef][A-Za-z0-9_-]{9}[AEIMQUYcgkosw048]/ return admin.instanceId().deleteInstanceId('fictive-ID0') .should.eventually.be - .rejectedWith('Instance ID "fictive-ID0": Failed to find the instance ID.'); + .rejectedWith('Installation ID "fictive-ID0": Failed to find the installation ID.'); }); }); diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index 0989e718d0..1929637f94 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -37,6 +37,7 @@ import { machineLearning } from '../../src/machine-learning/index'; import { storage } from '../../src/storage/index'; import { firestore } from '../../src/firestore/index'; import { database } from '../../src/database/index'; +import { installations } from '../../src/installations/index'; import { instanceId } from '../../src/instance-id/index'; import { projectManagement } from '../../src/project-management/index'; import { securityRules } from '../../src/security-rules/index'; @@ -50,6 +51,7 @@ import Messaging = messaging.Messaging; import MachineLearning = machineLearning.MachineLearning; import Storage = storage.Storage; import Firestore = firestore.Firestore; +import Installations = installations.Installations; import InstanceId = instanceId.InstanceId; import ProjectManagement = projectManagement.ProjectManagement; import SecurityRules = securityRules.SecurityRules; @@ -567,6 +569,32 @@ describe('FirebaseApp', () => { }); }); + describe('installations()', () => { + it('should throw if the app has already been deleted', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + return app.delete().then(() => { + expect(() => { + return app.installations(); + }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); + }); + }); + + it('should return the InstanceId client', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + const fis: Installations = app.installations(); + expect(fis).not.be.null; + }); + + it('should return a cached version of InstanceId on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + const service1: Installations = app.installations(); + const service2: Installations = app.installations(); + expect(service1).to.equal(service2); + }); + }); + describe('instanceId()', () => { it('should throw if the app has already been deleted', () => { const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index e2efb84f4e..45047df4a7 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -52,6 +52,7 @@ import { machineLearning } from '../../src/machine-learning/index'; import { storage } from '../../src/storage/index'; import { firestore } from '../../src/firestore/index'; import { database } from '../../src/database/index'; +import { installations } from '../../src/installations/index'; import { instanceId } from '../../src/instance-id/index'; import { projectManagement } from '../../src/project-management/index'; import { securityRules } from '../../src/security-rules/index'; @@ -60,6 +61,7 @@ import { appCheck } from '../../src/app-check/index'; import { AppCheck as AppCheckImpl } from '../../src/app-check/app-check'; import { Auth as AuthImpl } from '../../src/auth/auth'; +import { Installations as InstallationsImpl } from '../../src/installations/installations'; import { InstanceId as InstanceIdImpl } from '../../src/instance-id/instance-id'; import { MachineLearning as MachineLearningImpl } from '../../src/machine-learning/machine-learning'; import { Messaging as MessagingImpl } from '../../src/messaging/messaging'; @@ -73,6 +75,7 @@ import AppCheck = appCheck.AppCheck; import Auth = auth.Auth; import Database = database.Database; import Firestore = firestore.Firestore; +import Installations = installations.Installations; import InstanceId = instanceId.InstanceId; import MachineLearning = machineLearning.MachineLearning; import Messaging = messaging.Messaging; @@ -603,6 +606,46 @@ describe('FirebaseNamespace', () => { }); }); + describe('#installations()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.installations(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.installations(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should return a valid namespace when the default app is initialized', () => { + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); + const fis: Installations = firebaseNamespace.installations(); + expect(fis).to.not.be.null; + expect(fis.app).to.be.deep.equal(app); + }); + + it('should return a valid namespace when the named app is initialized', () => { + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const fis: Installations = firebaseNamespace.installations(app); + expect(fis).to.not.be.null; + expect(fis.app).to.be.deep.equal(app); + }); + + it('should return a reference to Installations type', () => { + expect(firebaseNamespace.installations.Installations).to.be.deep.equal(InstallationsImpl); + }); + + it('should return a cached version of Installations on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const service1: Installations = firebaseNamespace.installations(); + const service2: Installations = firebaseNamespace.installations(); + expect(service1).to.equal(service2); + }); + }); + describe('#instanceId()', () => { it('should throw when called before initializing an app', () => { expect(() => { diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index 48c87e24e4..1cccc1def5 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -60,9 +60,12 @@ import './storage/storage.spec'; // Firestore import './firestore/firestore.spec'; +// Installations +import './installations/installations.spec'; +import './installations/installations-request-handler.spec'; + // InstanceId import './instance-id/instance-id.spec'; -import './instance-id/instance-id-request.spec'; // ProjectManagement import './project-management/project-management.spec'; diff --git a/test/unit/instance-id/instance-id-request.spec.ts b/test/unit/installations/installations-request-handler.spec.ts similarity index 72% rename from test/unit/instance-id/instance-id-request.spec.ts rename to test/unit/installations/installations-request-handler.spec.ts index dd28f0f8ef..dcc32b464b 100644 --- a/test/unit/instance-id/instance-id-request.spec.ts +++ b/test/unit/installations/installations-request-handler.spec.ts @@ -28,7 +28,7 @@ import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/firebase-app'; import { HttpClient } from '../../../src/utils/api-request'; -import { FirebaseInstanceIdRequestHandler } from '../../../src/instance-id/instance-id-request-internal'; +import { FirebaseInstallationsRequestHandler } from '../../../src/installations/installations-request-handler'; chai.should(); chai.use(sinonChai); @@ -36,7 +36,7 @@ chai.use(chaiAsPromised); const expect = chai.expect; -describe('FirebaseInstanceIdRequestHandler', () => { +describe('FirebaseInstallationsRequestHandler', () => { const projectId = 'project_id'; const mockAccessToken: string = utils.generateRandomAccessToken(); let stubs: sinon.SinonStub[] = []; @@ -69,24 +69,24 @@ describe('FirebaseInstanceIdRequestHandler', () => { describe('Constructor', () => { it('should succeed with a FirebaseApp instance', () => { expect(() => { - return new FirebaseInstanceIdRequestHandler(mockApp); + return new FirebaseInstallationsRequestHandler(mockApp); }).not.to.throw(Error); }); }); - describe('deleteInstanceId', () => { + describe('deleteInstallation', () => { const httpMethod = 'DELETE'; const host = 'console.firebase.google.com'; - const path = `/v1/project/${projectId}/instanceId/test-iid`; + const path = `/v1/project/${projectId}/instanceId/test-fid`; const timeout = 10000; - it('should be fulfilled given a valid instance ID', () => { + it('should be fulfilled given a valid installation ID', () => { const stub = sinon.stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom('')); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); - return requestHandler.deleteInstanceId('test-iid') + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: httpMethod, @@ -102,14 +102,14 @@ describe('FirebaseInstanceIdRequestHandler', () => { .rejects(utils.errorFrom({}, 404)); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); - return requestHandler.deleteInstanceId('test-iid') + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') .then(() => { throw new Error('Unexpected success'); }) .catch((error) => { - expect(error.code).to.equal('instance-id/api-error'); - expect(error.message).to.equal('Instance ID "test-iid": Failed to find the instance ID.'); + expect(error.code).to.equal('installations/api-error'); + expect(error.message).to.equal('Installation ID "test-fid": Failed to find the installation ID.'); }); }); @@ -118,14 +118,14 @@ describe('FirebaseInstanceIdRequestHandler', () => { .rejects(utils.errorFrom({}, 409)); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); - return requestHandler.deleteInstanceId('test-iid') + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') .then(() => { throw new Error('Unexpected success'); }) .catch((error) => { - expect(error.code).to.equal('instance-id/api-error'); - expect(error.message).to.equal('Instance ID "test-iid": Already deleted.'); + expect(error.code).to.equal('installations/api-error'); + expect(error.message).to.equal('Installation ID "test-fid": Already deleted.'); }); }); @@ -135,13 +135,13 @@ describe('FirebaseInstanceIdRequestHandler', () => { .rejects(utils.errorFrom(expectedResult, 511)); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); - return requestHandler.deleteInstanceId('test-iid') + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') .then(() => { throw new Error('Unexpected success'); }) .catch((error) => { - expect(error.code).to.equal('instance-id/api-error'); + expect(error.code).to.equal('installations/api-error'); expect(error.message).to.equal('test error'); }); }); diff --git a/test/unit/installations/installations.spec.ts b/test/unit/installations/installations.spec.ts new file mode 100644 index 0000000000..e7816fa556 --- /dev/null +++ b/test/unit/installations/installations.spec.ts @@ -0,0 +1,190 @@ +/*! + * @license + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'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 utils from '../utils'; +import * as mocks from '../../resources/mocks'; + +import { Installations } from '../../../src/installations/installations'; +import { FirebaseInstallationsRequestHandler } from '../../../src/installations/installations-request-handler'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../../../src/utils/error'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Installations', () => { + let fis: Installations; + let mockApp: FirebaseApp; + let mockCredentialApp: FirebaseApp; + let getTokenStub: sinon.SinonStub; + + let nullAccessTokenClient: Installations; + let malformedAccessTokenClient: Installations; + let rejectedPromiseAccessTokenClient: Installations; + + let googleCloudProject: string | undefined; + let gcloudProject: string | undefined; + + const noProjectIdError = 'Failed to determine project ID for Installations. 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(); + getTokenStub = utils.stubGetAccessToken(undefined, mockApp); + mockCredentialApp = mocks.mockCredentialApp(); + fis = new Installations(mockApp); + + googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; + gcloudProject = process.env.GCLOUD_PROJECT; + + nullAccessTokenClient = new Installations(mocks.appReturningNullAccessToken()); + malformedAccessTokenClient = new Installations(mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenClient = new Installations(mocks.appRejectedWhileFetchingAccessToken()); + }); + + afterEach(() => { + getTokenStub.restore(); + process.env.GOOGLE_CLOUD_PROJECT = googleCloudProject; + process.env.GCLOUD_PROJECT = gcloudProject; + return mockApp.delete(); + }); + + + describe('Constructor', () => { + const invalidApps = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidApps.forEach((invalidApp) => { + it('should throw given invalid app: ' + JSON.stringify(invalidApp), () => { + expect(() => { + const iidAny: any = Installations; + return new iidAny(invalidApp); + }).to.throw('First argument passed to admin.installations() must be a valid Firebase app instance.'); + }); + }); + + it('should throw given no app', () => { + expect(() => { + const iidAny: any = Installations; + return new iidAny(); + }).to.throw('First argument passed to admin.installations() must be a valid Firebase app instance.'); + }); + + 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 installations = new Installations(mockCredentialApp); + return installations.deleteInstallation('iid') + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new Installations(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(fis.app).to.equal(mockApp); + }); + + it('is read-only', () => { + expect(() => { + (fis as any).app = mockApp; + }).to.throw('Cannot set property app of # which has only a getter'); + }); + }); + + describe('deleteInstallation()', () => { + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + const expectedError = new FirebaseInstallationsError(InstallationsClientErrorCode.API_ERROR); + const testInstallationId = 'test-iid'; + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no installation ID', () => { + return (fis as any).deleteInstallation() + .should.eventually.be.rejected.and.have.property('code', 'installations/invalid-installation-id'); + }); + + it('should be rejected given an invalid installation ID', () => { + return fis.deleteInstallation('') + .should.eventually.be.rejected.and.have.property('code', 'installations/invalid-installation-id'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenClient.deleteInstallation(testInstallationId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenClient.deleteInstallation(testInstallationId) + .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 rejectedPromiseAccessTokenClient.deleteInstallation(testInstallationId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve without errors on success', () => { + const stub = sinon.stub(FirebaseInstallationsRequestHandler.prototype, 'deleteInstallation') + .resolves(); + stubs.push(stub); + return fis.deleteInstallation(testInstallationId) + .then(() => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(testInstallationId); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub deleteInstallation to throw a backend error. + const stub = sinon.stub(FirebaseInstallationsRequestHandler.prototype, 'deleteInstallation') + .rejects(expectedError); + stubs.push(stub); + return fis.deleteInstallation(testInstallationId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(testInstallationId); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); +}); diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index b68f8b3ac1..a35d2a01e2 100644 --- a/test/unit/instance-id/instance-id.spec.ts +++ b/test/unit/instance-id/instance-id.spec.ts @@ -27,9 +27,12 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { InstanceId } from '../../../src/instance-id/instance-id'; -import { FirebaseInstanceIdRequestHandler } from '../../../src/instance-id/instance-id-request-internal'; +import { Installations } from '../../../src/installations/installations'; import { FirebaseApp } from '../../../src/firebase-app'; -import { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../../../src/utils/error'; +import { + FirebaseInstallationsError, FirebaseInstanceIdError, + InstallationsClientErrorCode, InstanceIdClientErrorCode, +} from '../../../src/utils/error'; chai.should(); chai.use(sinonChai); @@ -50,7 +53,7 @@ describe('InstanceId', () => { let googleCloudProject: string | undefined; let gcloudProject: string | undefined; - const noProjectIdError = 'Failed to determine project ID for InstanceId. Initialize the SDK ' + const noProjectIdError = 'Failed to determine project ID for Installations. Initialize the SDK ' + 'with service account credentials or set project ID as an app option. Alternatively set the ' + 'GOOGLE_CLOUD_PROJECT environment variable.'; @@ -127,7 +130,6 @@ describe('InstanceId', () => { // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; - const expectedError = new FirebaseInstanceIdError(InstanceIdClientErrorCode.API_ERROR); const testInstanceId = 'test-iid'; afterEach(() => { @@ -161,7 +163,7 @@ describe('InstanceId', () => { }); it('should resolve without errors on success', () => { - const stub = sinon.stub(FirebaseInstanceIdRequestHandler.prototype, 'deleteInstanceId') + const stub = sinon.stub(Installations.prototype, 'deleteInstallation') .resolves(); stubs.push(stub); return iid.deleteInstanceId(testInstanceId) @@ -171,10 +173,11 @@ describe('InstanceId', () => { }); }); - it('should throw an error when the backend returns an error', () => { + it('should throw a FirebaseInstanceIdError error when the backend returns an error', () => { // Stub deleteInstanceId to throw a backend error. - const stub = sinon.stub(FirebaseInstanceIdRequestHandler.prototype, 'deleteInstanceId') - .returns(Promise.reject(expectedError)); + const originalError = new FirebaseInstallationsError(InstallationsClientErrorCode.API_ERROR); + const stub = sinon.stub(Installations.prototype, 'deleteInstallation') + .rejects(originalError); stubs.push(stub); return iid.deleteInstanceId(testInstanceId) .then(() => { @@ -183,7 +186,9 @@ describe('InstanceId', () => { // Confirm underlying API called with expected parameters. expect(stub).to.have.been.calledOnce.and.calledWith(testInstanceId); // Confirm expected error returned. - expect(error).to.equal(expectedError); + const expectedError = new FirebaseInstanceIdError(InstanceIdClientErrorCode.API_ERROR); + expect(error).to.be.instanceOf(FirebaseInstanceIdError) + expect(error).to.deep.include(expectedError); }); }); }); From 6f73c8c803ed88e32c17c9c62a644074ed6ec342 Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Wed, 23 Jun 2021 14:29:58 -0700 Subject: [PATCH 144/160] fix: Updated TOC for new Auth type aliases (#1342) --- docgen/content-sources/node/toc.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index fa5808c5f7..636f59c746 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -38,12 +38,14 @@ toc: path: /docs/reference/admin/node/admin.auth.Auth-1 - title: "ActionCodeSettings" path: /docs/reference/admin/node/admin.auth.ActionCodeSettings - - title: "AuthProviderConfig" - path: /docs/reference/admin/node/admin.auth.AuthProviderConfig - title: "AuthProviderConfigFilter" path: /docs/reference/admin/node/admin.auth.AuthProviderConfigFilter - - title: "CreateMultiFactorInfoRequest" - path: /docs/reference/admin/node/admin.auth.CreateMultiFactorInfoRequest + - title: "BaseAuthProviderConfig" + path: /docs/reference/admin/node/admin.auth.BaseAuthProviderConfig + - title: "BaseCreateMultiFactorInfoRequest" + path: /docs/reference/admin/node/admin.auth.BaseCreateMultiFactorInfoRequest + - title: "BaseUpdateMultiFactorInfoRequest" + path: /docs/reference/admin/node/admin.auth.BaseUpdateMultiFactorInfoRequest - title: "CreatePhoneMultiFactorInfoRequest" path: /docs/reference/admin/node/admin.auth.CreatePhoneMultiFactorInfoRequest - title: "CreateRequest" @@ -82,8 +84,6 @@ toc: path: /docs/reference/admin/node/admin.auth.TenantAwareAuth - title: "TenantManager" path: /docs/reference/admin/node/admin.auth.TenantManager - - title: "UpdateMultiFactorInfoRequest" - path: /docs/reference/admin/node/admin.auth.UpdateMultiFactorInfoRequest - title: "UpdatePhoneMultiFactorInfoRequest" path: /docs/reference/admin/node/admin.auth.UpdatePhoneMultiFactorInfoRequest - title: "UpdateRequest" From 2ca2bcaef0e4ecee73426660007605ad2f87e01f Mon Sep 17 00:00:00 2001 From: Hiranya Jayathilaka Date: Thu, 24 Jun 2021 12:14:14 -0700 Subject: [PATCH 145/160] [chore] Release 9.10.0 (#1345) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 11b2c6ce63..ffa128f69a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.9.0", + "version": "9.10.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 2feece31422c62ba9f57751221c8e459abf931c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Jun 2021 12:46:23 -0700 Subject: [PATCH 146/160] build(deps-dev): bump @types/request-promise from 4.1.46 to 4.1.47 (#1338) Bumps [@types/request-promise](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/request-promise) from 4.1.46 to 4.1.47. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/request-promise) --- updated-dependencies: - dependency-name: "@types/request-promise" 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 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index d10b61f0f3..aa5c241ddf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.9.0", + "version": "9.10.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1229,9 +1229,9 @@ "dev": true }, "@types/bluebird": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.32.tgz", - "integrity": "sha512-dIOxFfI0C+jz89g6lQ+TqhGgPQ0MxSnh/E4xuC0blhFtyW269+mPG5QeLgbdwst/LvdP8o1y0o/Gz5EHXLec/g==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.35.tgz", + "integrity": "sha512-2WeeXK7BuQo7yPI4WGOBum90SzF/f8rqlvpaXx4rjeTmNssGRDHWf7fgDUH90xMB3sUOu716fUK5d+OVx0+ncQ==", "dev": true }, "@types/body-parser": { @@ -1415,9 +1415,9 @@ } }, "@types/request-promise": { - "version": "4.1.46", - "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.46.tgz", - "integrity": "sha512-3Thpj2Va5m0ji3spaCk8YKrjkZyZc6RqUVOphA0n/Xet66AW/AiOAs5vfXhQIL5NmkaO7Jnun7Nl9NEjJ2zBaw==", + "version": "4.1.47", + "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.47.tgz", + "integrity": "sha512-eRSZhAS8SMsrWOM8vbhxFGVZhTbWSJvaRKyufJTdIf4gscUouQvOBlfotPSPHbMR3S7kfkyKbhb1SWPmQdy3KQ==", "dev": true, "requires": { "@types/bluebird": "*", From dd942b0a1dce1952f8cfe7f0739e939d207106ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jul 2021 14:05:37 -0700 Subject: [PATCH 147/160] build(deps): bump @firebase/database from 0.10.4 to 0.10.5 (#1350) Bumps [@firebase/database](https://github.com/firebase/firebase-js-sdk/tree/HEAD/packages/database) from 0.10.4 to 0.10.5. - [Release notes](https://github.com/firebase/firebase-js-sdk/releases) - [Changelog](https://github.com/firebase/firebase-js-sdk/blob/master/packages/database/CHANGELOG.md) - [Commits](https://github.com/firebase/firebase-js-sdk/commits/@firebase/database@0.10.5/packages/database) --- updated-dependencies: - dependency-name: "@firebase/database" 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 | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa5c241ddf..8db66cfb9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -511,6 +511,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.2.tgz", "integrity": "sha512-QT+o6VaBCz/k8wmC/DErU9dQK2QeIoHtkBkryZVTSRkrvulglEWNIpbPp86UbuqZZd1wwzoh6m7BL6JbdEp9SQ==", + "dev": true, "requires": { "@firebase/util": "1.1.0", "tslib": "^2.1.0" @@ -519,17 +520,18 @@ "tslib": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "dev": true } } }, "@firebase/database": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.10.4.tgz", - "integrity": "sha512-Mi6fJGzv9JH+GoYhgzSQAxsUhanW4jU6lqe/9kTyxNxHd+asphoJXJcKDs97uxRaowmSzu5LSAkGlWe63vJ7wA==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.10.5.tgz", + "integrity": "sha512-/KAFZGSvvL3J4EytZsl5kgqhZwEV+ZTz6mCS3VPigkkECzT1E/JRm9h8DY5/VWmoyfqc5O2F3kqrrLf7AovoHg==", "requires": { "@firebase/auth-interop-types": "0.1.6", - "@firebase/component": "0.5.2", + "@firebase/component": "0.5.3", "@firebase/database-types": "0.7.2", "@firebase/logger": "0.2.6", "@firebase/util": "1.1.0", @@ -537,6 +539,15 @@ "tslib": "^2.1.0" }, "dependencies": { + "@firebase/component": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.3.tgz", + "integrity": "sha512-/TzwmlK35Mnr31zA9D4X0Obln7waAtV7nDLuNVtWhlXl0sSYRxnGES4dOhSXi0yWRneaNr+OiRBZ2gsc9PWWRg==", + "requires": { + "@firebase/util": "1.1.0", + "tslib": "^2.1.0" + } + }, "tslib": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", From bf959e31ef0e04f7039db1c8f41c2fb70605757f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jul 2021 14:24:26 -0700 Subject: [PATCH 148/160] build(deps-dev): bump @types/nock from 9.3.1 to 11.1.0 (#1351) Bumps [@types/nock](https://github.com/nock/nock) from 9.3.1 to 11.1.0. - [Release notes](https://github.com/nock/nock/releases) - [Changelog](https://github.com/nock/nock/blob/main/CHANGELOG.md) - [Commits](https://github.com/nock/nock/compare/v9.3.1...v11.1.0) --- updated-dependencies: - dependency-name: "@types/nock" 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 | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8db66cfb9c..4bac8d278b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1390,12 +1390,12 @@ "dev": true }, "@types/nock": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.3.1.tgz", - "integrity": "sha512-eOVHXS5RnWOjTVhu3deCM/ruy9E6JCgeix2g7wpFiekQh3AaEAK1cz43tZDukKmtSmQnwvSySq7ubijCA32I7Q==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-11.1.0.tgz", + "integrity": "sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==", "dev": true, "requires": { - "@types/node": "*" + "nock": "*" } }, "@types/node": { diff --git a/package.json b/package.json index ffa128f69a..2b39dd8949 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@types/lodash": "^4.14.104", "@types/minimist": "^1.2.0", "@types/mocha": "^8.2.2", - "@types/nock": "^9.1.0", + "@types/nock": "^11.1.0", "@types/request": "^2.47.0", "@types/request-promise": "^4.1.41", "@types/sinon": "^9.0.0", From e9cd6bfbccb54ad07492e2a5c191162a85fe20be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Jul 2021 14:36:34 -0700 Subject: [PATCH 149/160] build(deps-dev): bump @types/sinon from 9.0.4 to 10.0.2 (#1326) Bumps [@types/sinon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sinon) from 9.0.4 to 10.0.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sinon) --- updated-dependencies: - dependency-name: "@types/sinon" 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 | 25 +++++++++++++++---------- package.json | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4bac8d278b..87b8450643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1445,12 +1445,23 @@ } }, "@types/sinon": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.4.tgz", - "integrity": "sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.2.tgz", + "integrity": "sha512-BHn8Bpkapj8Wdfxvh2jWIUoaYB/9/XhsL0oOvBfRagJtKlSl9NWPcFOz2lRukI9szwGxFtYZCTejJSqsGDbdmw==", "dev": true, "requires": { - "@types/sinonjs__fake-timers": "*" + "@sinonjs/fake-timers": "^7.1.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "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" + } + } } }, "@types/sinon-chai": { @@ -1463,12 +1474,6 @@ "@types/sinon": "*" } }, - "@types/sinonjs__fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz", - "integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==", - "dev": true - }, "@types/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", diff --git a/package.json b/package.json index 2b39dd8949..2d43a5b8be 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@types/nock": "^11.1.0", "@types/request": "^2.47.0", "@types/request-promise": "^4.1.41", - "@types/sinon": "^9.0.0", + "@types/sinon": "^10.0.2", "@types/sinon-chai": "^3.0.0", "@typescript-eslint/eslint-plugin": "^2.20.0", "@typescript-eslint/parser": "^2.20.0", From 068ea8031292bc608ad6fdfe2e11c77de88aa690 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jul 2021 11:35:53 -0700 Subject: [PATCH 150/160] build(deps): bump @firebase/database from 0.10.5 to 0.10.6 (#1356) Bumps [@firebase/database](https://github.com/firebase/firebase-js-sdk/tree/HEAD/packages/database) from 0.10.5 to 0.10.6. - [Release notes](https://github.com/firebase/firebase-js-sdk/releases) - [Changelog](https://github.com/firebase/firebase-js-sdk/blob/master/packages/database/CHANGELOG.md) - [Commits](https://github.com/firebase/firebase-js-sdk/commits/@firebase/database@0.10.6/packages/database) --- updated-dependencies: - dependency-name: "@firebase/database" 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 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87b8450643..07077f845f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -526,12 +526,12 @@ } }, "@firebase/database": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.10.5.tgz", - "integrity": "sha512-/KAFZGSvvL3J4EytZsl5kgqhZwEV+ZTz6mCS3VPigkkECzT1E/JRm9h8DY5/VWmoyfqc5O2F3kqrrLf7AovoHg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.10.6.tgz", + "integrity": "sha512-AGxRnKaJQd4Pq7sblrWI39XM5N2u/pZOeopMxVRja38Cubxp6P5T7lzpp0xNSOQ/RszAoHskGIlCfIz+teaXSQ==", "requires": { "@firebase/auth-interop-types": "0.1.6", - "@firebase/component": "0.5.3", + "@firebase/component": "0.5.4", "@firebase/database-types": "0.7.2", "@firebase/logger": "0.2.6", "@firebase/util": "1.1.0", @@ -540,9 +540,9 @@ }, "dependencies": { "@firebase/component": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.3.tgz", - "integrity": "sha512-/TzwmlK35Mnr31zA9D4X0Obln7waAtV7nDLuNVtWhlXl0sSYRxnGES4dOhSXi0yWRneaNr+OiRBZ2gsc9PWWRg==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.4.tgz", + "integrity": "sha512-KoLDPTsvxWr6FT9kn/snffJItaWXZLHLJlZVKiiw+flKE6MVA8Eec+ctvM2zcsMZzC2Z47gFnVqywfBlOevmpQ==", "requires": { "@firebase/util": "1.1.0", "tslib": "^2.1.0" From b8e837b1ca2ab88e4703a3193f2cce769fd56778 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Jul 2021 15:39:21 -0700 Subject: [PATCH 151/160] build(deps): bump jwks-rsa from 2.0.2 to 2.0.3 (#1361) Bumps [jwks-rsa](https://github.com/auth0/node-jwks-rsa) from 2.0.2 to 2.0.3. - [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.2...v2.0.3) --- updated-dependencies: - dependency-name: jwks-rsa 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 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07077f845f..609148a606 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1296,9 +1296,9 @@ "dev": true }, "@types/express": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.11.tgz", - "integrity": "sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==", + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", + "integrity": "sha512-pTYas6FrP15B1Oa0bkN5tQMNqOcVXa9j4FTFtO8DWI9kppKib+6NJtfTOOLcwxuuYvcX2+dVG6et1SxW/Kc17Q==", "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.18", @@ -1316,9 +1316,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz", - "integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==", + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.22.tgz", + "integrity": "sha512-WdqmrUsRS4ootGha6tVwk/IVHM1iorU8tGehftQD2NWiPniw/sm7xdJOIlXLwqdInL9wBw/p7oO8vaYEF3NDmA==", "requires": { "@types/node": "*", "@types/qs": "*", @@ -5760,13 +5760,13 @@ } }, "jwks-rsa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.2.tgz", - "integrity": "sha512-oRnlZvmP21LxqEgEFiPycLn3jyw/QuynyaERe7GMxR4TlTg7BRGBgEyEN+rRN4xGHMekXur1RY/MSt8UJBiSgA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.3.tgz", + "integrity": "sha512-/rkjXRWAp0cS00tunsHResw68P5iTQru8+jHufLNv3JHc4nObFEndfEUSuPugh09N+V9XYxKUqi7QrkmCHSSSg==", "requires": { "@types/express-jwt": "0.0.42", "debug": "^4.1.0", - "jose": "^2.0.2", + "jose": "^2.0.5", "limiter": "^1.1.5", "lru-memoizer": "^2.1.2" } From c87f6409908aedf7d87245f3e3a9e709210115d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Jul 2021 16:24:22 -0700 Subject: [PATCH 152/160] build(deps-dev): bump yargs from 16.1.0 to 17.0.1 (#1357) Bumps [yargs](https://github.com/yargs/yargs) from 16.1.0 to 17.0.1. - [Release notes](https://github.com/yargs/yargs/releases) - [Changelog](https://github.com/yargs/yargs/blob/master/CHANGELOG.md) - [Commits](https://github.com/yargs/yargs/compare/v16.1.0...v17.0.1) --- updated-dependencies: - dependency-name: yargs 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 | 50 ++++++++++++----------------------------------- package.json | 2 +- 2 files changed, 14 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index 609148a606..f4b09cca9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9915,9 +9915,9 @@ "optional": true }, "yargs": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.1.0.tgz", - "integrity": "sha512-upWFJOmDdHN0syLuESuvXDmrRcWd1QafJolHskzaw79uZa7/x53gxQKiR07W59GWY1tFhhU/Th9DrtSfpS782g==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz", + "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==", "dev": true, "requires": { "cliui": "^7.0.2", @@ -9925,7 +9925,7 @@ "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", - "y18n": "^5.0.2", + "y18n": "^5.0.5", "yargs-parser": "^20.2.2" }, "dependencies": { @@ -9935,19 +9935,10 @@ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, "cliui": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.3.tgz", - "integrity": "sha512-Gj3QHTkVMPKqwP3f7B4KPkBZRMR9r4rfi5bXFpg1a+Svvj8l7q5CnkBkVQzfxT5DFSsGk2+PascOgL0JYkL2kw==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { "string-width": "^4.2.0", @@ -9955,21 +9946,6 @@ "wrap-ansi": "^7.0.0" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -9983,9 +9959,9 @@ "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -10020,9 +9996,9 @@ "dev": true }, "yargs-parser": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.3.tgz", - "integrity": "sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true } } diff --git a/package.json b/package.json index 2d43a5b8be..73e58f6d9b 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,6 @@ "ts-node": "^9.0.0", "typedoc": "^0.19.2", "typescript": "^3.7.3", - "yargs": "^16.0.0" + "yargs": "^17.0.1" } } From 0b45481fd14d7920a7d7b0e85459c503458f2b1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Jul 2021 16:30:02 -0700 Subject: [PATCH 153/160] build(deps-dev): bump @types/chai from 4.2.11 to 4.2.21 (#1365) Bumps [@types/chai](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chai) from 4.2.11 to 4.2.21. - [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 f4b09cca9c..b9568838f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1261,9 +1261,9 @@ "dev": true }, "@types/chai": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.11.tgz", - "integrity": "sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==", + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.21.tgz", + "integrity": "sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==", "dev": true }, "@types/chai-as-promised": { From bfaec79efd521a18fbfd01a96c9fb02e6ca4eb1e Mon Sep 17 00:00:00 2001 From: Daniel Hritzkiv Date: Mon, 12 Jul 2021 18:37:11 -0400 Subject: [PATCH 154/160] Update index.ts (#1367) Fix typo in comments: `sendMulticase` -> `sendMulticast` --- src/messaging/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/messaging/index.ts b/src/messaging/index.ts index 6c074cea7c..fc0a38338d 100644 --- a/src/messaging/index.ts +++ b/src/messaging/index.ts @@ -77,7 +77,7 @@ export namespace messaging { export type Message = TokenMessage | TopicMessage | ConditionMessage; /** - * Payload for the admin.messaing.sendMulticase() method. The payload contains all the fields + * Payload for the admin.messaing.sendMulticast() method. The payload contains all the fields * in the BaseMessage type, and a list of tokens. */ export interface MulticastMessage extends BaseMessage { From 760cd6a6a976fae6abfc213210b7b1950d69cdce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jul 2021 16:35:01 -0700 Subject: [PATCH 155/160] build(deps): bump @google-cloud/firestore from 4.12.2 to 4.13.1 (#1369) Bumps [@google-cloud/firestore](https://github.com/googleapis/nodejs-firestore) from 4.12.2 to 4.13.1. - [Release notes](https://github.com/googleapis/nodejs-firestore/releases) - [Changelog](https://github.com/googleapis/nodejs-firestore/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/nodejs-firestore/compare/v4.12.2...v4.13.1) --- updated-dependencies: - dependency-name: "@google-cloud/firestore" 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 | 58 +++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9568838f7..d9016d918c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -626,14 +626,14 @@ } }, "@google-cloud/firestore": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.12.2.tgz", - "integrity": "sha512-5rurTAJXQ0SANEf8K9eA2JAB5zAh+pu4tGRnkZx5gBWQLZXdBFdtepS+irvKuSXw1KbeAQOuRANSc/nguys6SQ==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.13.1.tgz", + "integrity": "sha512-LtxboFZQ3MGwy1do8a0ykMJocM+TFgOpZoAihMwW498UDd641DJgJu0Kw0CD0bPpEaYUfhbeAUBq2ZO63DOz7g==", "optional": true, "requires": { "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", - "google-gax": "^2.12.0", + "google-gax": "^2.17.0", "protobufjs": "^6.8.6" } }, @@ -688,18 +688,18 @@ } }, "@grpc/grpc-js": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.2.tgz", - "integrity": "sha512-UXepkOKCATJrhHGsxt+CGfpZy9zUn1q9mop5kfcXq1fBkTePxVNPOdnISlCbJFlCtld+pSLGyZCzr9/zVprFKA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.4.tgz", + "integrity": "sha512-AxtZcm0mArQhY9z8T3TynCYVEaSKxNCa9mVhVwBCUnsuUEe8Zn94bPYYKVQSLt+hJJ1y0ukr3mUvtWfcATL/IQ==", "optional": true, "requires": { "@types/node": ">=12.12.47" } }, "@grpc/proto-loader": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.2.tgz", - "integrity": "sha512-q2Qle60Ht2OQBCp9S5hv1JbI4uBBq6/mqSevFNK3ZEgRDBCAkWqZPUhD/K9gXOHrHKluliHiVq2L9sw1mVyAIg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.4.tgz", + "integrity": "sha512-7xvDvW/vJEcmLUltCUGOgWRPM8Oofv0eCFSVMuKqaqWJaXSzmB+m9hiyqe34QofAl4WAzIKUZZlinIF9FOHyTQ==", "optional": true, "requires": { "@types/long": "^4.0.1", @@ -791,9 +791,9 @@ } }, "yargs-parser": { - "version": "20.2.7", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", - "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "optional": true } } @@ -4360,9 +4360,9 @@ } }, "google-gax": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.14.1.tgz", - "integrity": "sha512-I5RDEN7MEptrCxeHX3ht7nKFGfyjgYX4hQKI9eVMBohMzVbFSwWUndo0CcKXu8es7NhB4gt2XYLm1AHkXhtHpA==", + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.17.1.tgz", + "integrity": "sha512-CoR7OYuEzIDt3mp7cLYL+oGPmYdImS1WEwIvjF0zk0LhEBBmvRjWHTpBwazLGsT8hz+6zPh/4fjIjNaUxzIvzg==", "optional": true, "requires": { "@grpc/grpc-js": "~1.3.0", @@ -4371,7 +4371,7 @@ "abort-controller": "^3.0.0", "duplexify": "^4.0.0", "fast-text-encoding": "^1.0.3", - "google-auth-library": "^7.0.2", + "google-auth-library": "^7.3.0", "is-stream-ended": "^0.1.4", "node-fetch": "^2.6.1", "object-hash": "^2.1.1", @@ -4405,9 +4405,9 @@ } }, "gcp-metadata": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", - "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.0.tgz", + "integrity": "sha512-L9XQUpvKJCM76YRSmcxrR4mFPzPGsgZUH+GgHMxAET8qc6+BhRJq63RLhWakgEO2KKVgeSDVfyiNjkGSADwNTA==", "optional": true, "requires": { "gaxios": "^4.0.0", @@ -4415,9 +4415,9 @@ } }, "google-auth-library": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.1.1.tgz", - "integrity": "sha512-+Q1linq/To3DYLyPz4UTEkQ0v5EOXadMM/S+taLV3W9611hq9zqg8kgGApqbTQnggtwdO9yU1y2YT7+83wdTRg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.3.0.tgz", + "integrity": "sha512-MPeeMlnsYnoiiVFMwX3hgaS684aiXrSqKoDP+xL4Ejg4Z0qLvIeg4XsaChemyFI8ZUO7ApwDAzNtgmhWSDNh5w==", "optional": true, "requires": { "arrify": "^2.0.0", @@ -4432,18 +4432,18 @@ } }, "google-p12-pem": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", - "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.0.tgz", + "integrity": "sha512-JUtEHXL4DY/N+xhlm7TC3qL797RPAtk0ZGXNs3/gWyiDHYoA/8Rjes0pztkda+sZv4ej1EoO2KhWgW5V9KTrSQ==", "optional": true, "requires": { "node-forge": "^0.10.0" } }, "gtoken": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.2.1.tgz", - "integrity": "sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.0.tgz", + "integrity": "sha512-mCcISYiaRZrJpfqOs0QWa6lfEM/C1V9ASkzFmuz43XBb5s1Vynh+CZy1ECeeJXVGx2PRByjYzb4Y4/zr1byr0w==", "optional": true, "requires": { "gaxios": "^4.0.0", From 4e816f44a3f3a67fcf912b6013c5beccb2210f8b Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 14 Jul 2021 11:33:35 -0400 Subject: [PATCH 156/160] feat(fac): Add custom TTL options for App Check (#1363) * Add custom ttl options for App Check * PR fixes * Add integration tests * PR fixes --- etc/firebase-admin.api.md | 5 +- src/app-check/app-check.ts | 10 +- src/app-check/index.ts | 16 ++- src/app-check/token-generator.ts | 46 ++++++++- src/messaging/messaging-internal.ts | 27 +---- src/utils/index.ts | 26 +++++ test/integration/app-check.spec.ts | 14 +++ test/unit/app-check/app-check.spec.ts | 9 ++ test/unit/app-check/token-generator.spec.ts | 104 +++++++++++++++++++- test/unit/utils/index.spec.ts | 15 ++- 10 files changed, 232 insertions(+), 40 deletions(-) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index 6017b0c72a..427bc43099 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -53,13 +53,16 @@ export namespace appCheck { export interface AppCheck { // (undocumented) app: app.App; - createToken(appId: string): Promise; + createToken(appId: string, options?: AppCheckTokenOptions): Promise; verifyToken(appCheckToken: string): Promise; } export interface AppCheckToken { token: string; ttlMillis: number; } + export interface AppCheckTokenOptions { + ttlMillis?: number; + } export interface DecodedAppCheckToken { // (undocumented) [key: string]: any; diff --git a/src/app-check/app-check.ts b/src/app-check/app-check.ts index 42d8391043..175d023419 100644 --- a/src/app-check/app-check.ts +++ b/src/app-check/app-check.ts @@ -26,6 +26,7 @@ import { cryptoSignerFromApp } from '../utils/crypto-signer'; import AppCheckInterface = appCheck.AppCheck; import AppCheckToken = appCheck.AppCheckToken; +import AppCheckTokenOptions = appCheck.AppCheckTokenOptions; import VerifyAppCheckTokenResponse = appCheck.VerifyAppCheckTokenResponse; /** @@ -56,18 +57,19 @@ export class AppCheck implements AppCheckInterface { * back to a client. * * @param appId The app ID to use as the JWT app_id. + * @param options Optional options object when creating a new App Check Token. * - * @return A promise that fulfills with a `AppCheckToken`. + * @returns A promise that fulfills with a `AppCheckToken`. */ - public createToken(appId: string): Promise { - return this.tokenGenerator.createCustomToken(appId) + public createToken(appId: string, options?: AppCheckTokenOptions): Promise { + return this.tokenGenerator.createCustomToken(appId, options) .then((customToken) => { return this.client.exchangeToken(customToken, appId); }); } /** - * Veifies an App Check token. + * Verifies an App Check token. * * @param appCheckToken The App Check token to verify. * diff --git a/src/app-check/index.ts b/src/app-check/index.ts index 6552d9208d..295f49e4be 100644 --- a/src/app-check/index.ts +++ b/src/app-check/index.ts @@ -61,10 +61,11 @@ export namespace appCheck { * back to a client. * * @param appId The App ID of the Firebase App the token belongs to. + * @param options Optional options object when creating a new App Check Token. * - * @return A promise that fulfills with a `AppCheckToken`. + * @returns A promise that fulfills with a `AppCheckToken`. */ - createToken(appId: string): Promise; + createToken(appId: string, options?: AppCheckTokenOptions): Promise; /** * Verifies a Firebase App Check token (JWT). If the token is valid, the promise is @@ -95,6 +96,17 @@ export namespace appCheck { ttlMillis: number; } + /** + * Interface representing App Check token options. + */ + export interface AppCheckTokenOptions { + /** + * The length of time, in milliseconds, for which the App Check token will + * be valid. This value must be between 30 minutes and 7 days, inclusive. + */ + ttlMillis?: number; + } + /** * Interface representing a decoded Firebase App Check token, returned from the * {@link appCheck.AppCheck.verifyToken `verifyToken()`} method. diff --git a/src/app-check/token-generator.ts b/src/app-check/token-generator.ts index 1b557438bb..6c5b9eeda6 100644 --- a/src/app-check/token-generator.ts +++ b/src/app-check/token-generator.ts @@ -15,8 +15,10 @@ * limitations under the License. */ +import { appCheck } from './index'; + import * as validator from '../utils/validator'; -import { toWebSafeBase64 } from '../utils'; +import { toWebSafeBase64, transformMillisecondsToSecondsString } from '../utils'; import { CryptoSigner, CryptoSignerError, CryptoSignerErrorCode } from '../utils/crypto-signer'; import { @@ -26,7 +28,11 @@ import { } from './app-check-api-client-internal'; import { HttpError } from '../utils/api-request'; +import AppCheckTokenOptions = appCheck.AppCheckTokenOptions; + const ONE_HOUR_IN_SECONDS = 60 * 60; +const ONE_MINUTE_IN_MILLIS = 60 * 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'; @@ -63,12 +69,16 @@ export class AppCheckTokenGenerator { * @return A Promise fulfilled with a custom token signed with a service account key * that can be exchanged to an App Check token. */ - public createCustomToken(appId: string): Promise { + public createCustomToken(appId: string, options?: AppCheckTokenOptions): Promise { if (!validator.isNonEmptyString(appId)) { throw new FirebaseAppCheckError( 'invalid-argument', '`appId` must be a non-empty string.'); } + let customOptions = {}; + if (typeof options !== 'undefined') { + customOptions = this.validateTokenOptions(options); + } return this.signer.getAccountId().then((account) => { const header = { alg: this.signer.algorithm, @@ -83,6 +93,7 @@ export class AppCheckTokenGenerator { aud: FIREBASE_APP_CHECK_AUDIENCE, exp: iat + ONE_HOUR_IN_SECONDS, iat, + ...customOptions, }; const token = `${this.encodeSegment(header)}.${this.encodeSegment(body)}`; return this.signer.sign(Buffer.from(token)) @@ -98,6 +109,35 @@ export class AppCheckTokenGenerator { const buffer: Buffer = (segment instanceof Buffer) ? segment : Buffer.from(JSON.stringify(segment)); return toWebSafeBase64(buffer).replace(/=+$/, ''); } + + /** + * Checks if a given `AppCheckTokenOptions` object is valid. If successful, returns an object with + * custom properties. + * + * @param options An options object to be validated. + * @returns A custom object with ttl converted to protobuf Duration string format. + */ + private validateTokenOptions(options: AppCheckTokenOptions): {[key: string]: any} { + if (!validator.isNonNullObject(options)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'AppCheckTokenOptions must be a non-null object.'); + } + if (typeof options.ttlMillis !== 'undefined') { + if (!validator.isNumber(options.ttlMillis)) { + throw new FirebaseAppCheckError('invalid-argument', + 'ttlMillis must be a duration in milliseconds.'); + } + // ttlMillis must be between 30 minutes and 7 days (inclusive) + if (options.ttlMillis < (ONE_MINUTE_IN_MILLIS * 30) || options.ttlMillis > (ONE_DAY_IN_MILLIS * 7)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).'); + } + return { ttl: transformMillisecondsToSecondsString(options.ttlMillis) }; + } + return {}; + } } /** @@ -123,7 +163,7 @@ export function appCheckErrorFromCryptoSignerError(err: Error): Error { code = APP_CHECK_ERROR_CODE_MAPPING[status]; } return new FirebaseAppCheckError(code, - `Error returned from server while siging a custom token: ${description}` + `Error returned from server while signing a custom token: ${description}` ); } return new FirebaseAppCheckError('internal-error', diff --git a/src/messaging/messaging-internal.ts b/src/messaging/messaging-internal.ts index 227f894eea..d84328e722 100644 --- a/src/messaging/messaging-internal.ts +++ b/src/messaging/messaging-internal.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { renameProperties } from '../utils/index'; +import { renameProperties, transformMillisecondsToSecondsString } from '../utils/index'; import { MessagingClientErrorCode, FirebaseMessagingError, } from '../utils/error'; import { messaging } from './index'; import * as validator from '../utils/validator'; @@ -589,28 +589,3 @@ function validateAndroidFcmOptions(fcmOptions: AndroidFcmOptions | undefined): v MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } } - -/** - * Transforms milliseconds to the format expected by FCM service. - * Returns the duration in seconds with up to nine fractional - * digits, terminated by 's'. Example: "3.5s". - * - * @param {number} milliseconds The duration in milliseconds. - * @return {string} The resulting formatted string in seconds with up to nine fractional - * digits, terminated by 's'. - */ -function transformMillisecondsToSecondsString(milliseconds: number): string { - let duration: string; - const seconds = Math.floor(milliseconds / 1000); - const nanos = (milliseconds - seconds * 1000) * 1000000; - if (nanos > 0) { - let nanoString = nanos.toString(); - while (nanoString.length < 9) { - nanoString = '0' + nanoString; - } - duration = `${seconds}.${nanoString}s`; - } else { - duration = `${seconds}s`; - } - return duration; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index b8cfa2faf0..d047397667 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -190,3 +190,29 @@ export function generateUpdateMask( } return updateMask; } + +/** + * Transforms milliseconds to a protobuf Duration type string. + * Returns the duration in seconds with up to nine fractional + * digits, terminated by 's'. Example: "3 seconds 0 nano seconds as 3s, + * 3 seconds 1 nano seconds as 3.000000001s". + * + * @param milliseconds The duration in milliseconds. + * @returns The resulting formatted string in seconds with up to nine fractional + * digits, terminated by 's'. + */ +export function transformMillisecondsToSecondsString(milliseconds: number): string { + let duration: string; + const seconds = Math.floor(milliseconds / 1000); + const nanos = Math.floor((milliseconds - seconds * 1000) * 1000000); + if (nanos > 0) { + let nanoString = nanos.toString(); + while (nanoString.length < 9) { + nanoString = '0' + nanoString; + } + duration = `${seconds}.${nanoString}s`; + } else { + duration = `${seconds}s`; + } + return duration; +} diff --git a/test/integration/app-check.spec.ts b/test/integration/app-check.spec.ts index 32386f32bc..82ad153498 100644 --- a/test/integration/app-check.spec.ts +++ b/test/integration/app-check.spec.ts @@ -53,6 +53,20 @@ describe('admin.appCheck', () => { expect(token).to.have.keys(['token', 'ttlMillis']); expect(token.token).to.be.a('string').and.to.not.be.empty; expect(token.ttlMillis).to.be.a('number'); + expect(token.ttlMillis).to.equals(3600000); + }); + }); + + it('should succeed with a valid token and a custom ttl', function() { + if (!appId) { + this.skip(); + } + return admin.appCheck().createToken(appId as string, { ttlMillis: 1800000 }) + .then((token) => { + expect(token).to.have.keys(['token', 'ttlMillis']); + expect(token.token).to.be.a('string').and.to.not.be.empty; + expect(token.ttlMillis).to.be.a('number'); + expect(token.ttlMillis).to.equals(1800000); }); }); diff --git a/test/unit/app-check/app-check.spec.ts b/test/unit/app-check/app-check.spec.ts index c30970bc13..05c598f301 100644 --- a/test/unit/app-check/app-check.spec.ts +++ b/test/unit/app-check/app-check.spec.ts @@ -147,6 +147,15 @@ describe('AppCheck', () => { .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); }); + it('should propagate API errors with custom options', () => { + const stub = sinon + .stub(AppCheckApiClient.prototype, 'exchangeToken') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return appCheck.createToken(APP_ID, { ttlMillis: 1800000 }) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + it('should resolve with AppCheckToken on success', () => { const response = { token: 'token', ttlMillis: 3000 }; const stub = sinon diff --git a/test/unit/app-check/token-generator.spec.ts b/test/unit/app-check/token-generator.spec.ts index 66bb7cffba..2cf10b8bc5 100644 --- a/test/unit/app-check/token-generator.spec.ts +++ b/test/unit/app-check/token-generator.spec.ts @@ -122,11 +122,58 @@ describe('AppCheckTokenGenerator', () => { }).to.throw(FirebaseAppCheckError).with.property('code', 'app-check/invalid-argument'); }); - it('should be fulfilled with a Firebase Custom JWT', () => { + const invalidOptions = [null, NaN, 0, 1, true, false, [], _.noop]; + invalidOptions.forEach((invalidOption) => { + it('should throw given an invalid options: ' + JSON.stringify(invalidOption), () => { + expect(() => { + tokenGenerator.createCustomToken(APP_ID, invalidOption as any); + }).to.throw(FirebaseAppCheckError).with.property('message', 'AppCheckTokenOptions must be a non-null object.'); + }); + }); + + const invalidTtls = [null, NaN, '0', 'abc', '', true, false, [], {}, { a: 1 }, _.noop]; + invalidTtls.forEach((invalidTtl) => { + it('should throw given an options object with invalid ttl: ' + JSON.stringify(invalidTtl), () => { + expect(() => { + tokenGenerator.createCustomToken(APP_ID, { ttlMillis: invalidTtl as any }); + }).to.throw(FirebaseAppCheckError).with.property('message', + 'ttlMillis must be a duration in milliseconds.'); + }); + }); + + const THIRTY_MIN_IN_MS = 1800000; + const SEVEN_DAYS_IN_MS = 604800000; + [-100, -1, 0, 10, THIRTY_MIN_IN_MS - 1, SEVEN_DAYS_IN_MS + 1, SEVEN_DAYS_IN_MS * 2].forEach((ttlMillis) => { + it('should throw given options with ttl < 30 minutes or ttl > 7 days:' + JSON.stringify(ttlMillis), () => { + expect(() => { + tokenGenerator.createCustomToken(APP_ID, { ttlMillis }); + }).to.throw(FirebaseAppCheckError).with.property( + 'message', 'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).'); + }); + }); + + it('should be fulfilled with a Firebase Custom JWT with only an APP ID', () => { return tokenGenerator.createCustomToken(APP_ID) .should.eventually.be.a('string').and.not.be.empty; }); + [ + [THIRTY_MIN_IN_MS, '1800s'], + [THIRTY_MIN_IN_MS + 1, '1800.001000000s'], + [SEVEN_DAYS_IN_MS / 2, '302400s'], + [SEVEN_DAYS_IN_MS - 1, '604799.999000000s'], + [SEVEN_DAYS_IN_MS, '604800s'] + ].forEach((ttl) => { + it('should be fulfilled with a Firebase Custom JWT with a valid custom ttl' + JSON.stringify(ttl[0]), () => { + return tokenGenerator.createCustomToken(APP_ID, { ttlMillis: ttl[0] as number }) + .then((token) => { + const decoded = jwt.decode(token) as { [key: string]: any }; + + expect(decoded['ttl']).to.equal(ttl[1]); + }); + }); + }); + it('should be fulfilled with a JWT with the correct decoded payload', () => { clock = sinon.useFakeTimers(1000); @@ -147,6 +194,57 @@ describe('AppCheckTokenGenerator', () => { }); }); + [{}, { ttlMillis: undefined }, { a: 123 }].forEach((options) => { + it('should be fulfilled with no ttl in the decoded payload when ttl is not provided in options', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(APP_ID, options) + .then((token) => { + const decoded = jwt.decode(token); + const expected: { [key: string]: any } = { + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: APP_ID, + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_APP_CHECK_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + }; + + expect(decoded).to.deep.equal(expected); + }); + }); + }); + + [ + [1800000.000001, '1800.000000001s'], + [1800000.001, '1800.000000999s'], + [172800000, '172800s'], + [604799999, '604799.999000000s'], + [604800000, '604800s'] + ].forEach((ttl) => { + it('should be fulfilled with a JWT with custom ttl in decoded payload', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(APP_ID, { ttlMillis: ttl[0] as number }) + .then((token) => { + const decoded = jwt.decode(token); + const expected: { [key: string]: any } = { + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: APP_ID, + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_APP_CHECK_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + ttl: ttl[1], + }; + + expect(decoded).to.deep.equal(expected); + }); + }); + }); + it('should be fulfilled with a JWT with the correct header', () => { clock = sinon.useFakeTimers(1000); @@ -225,7 +323,7 @@ describe('AppCheckTokenGenerator', () => { expect(appCheckError).to.be.an.instanceof(FirebaseAppCheckError); expect(appCheckError).to.have.property('code', 'app-check/unknown-error'); expect(appCheckError).to.have.property('message', - 'Error returned from server while siging a custom token: server error.'); + 'Error returned from server while signing a custom token: server error.'); }); it('should convert CryptoSignerError HttpError with no error.message to FirebaseAppCheckError', () => { @@ -240,7 +338,7 @@ describe('AppCheckTokenGenerator', () => { expect(appCheckError).to.be.an.instanceof(FirebaseAppCheckError); expect(appCheckError).to.have.property('code', 'app-check/unknown-error'); expect(appCheckError).to.have.property('message', - 'Error returned from server while siging a custom token: '+ + 'Error returned from server while signing a custom token: '+ '{"status":500,"headers":{},"data":{"error":{}},"text":"{\\"error\\":{}}"}'); }); diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index e63993fb83..9729dc7817 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, + toWebSafeBase64, formatString, generateUpdateMask, transformMillisecondsToSecondsString, } from '../../../src/utils/index'; import { isNonEmptyString } from '../../../src/utils/validator'; import { FirebaseApp } from '../../../src/firebase-app'; @@ -383,3 +383,16 @@ describe('generateUpdateMask()', () => { .to.deep.equal(['b', 'c', 'd', 'e', 'f', 'k', 'l', 'n']); }); }); + + +describe('transformMillisecondsToSecondsString()', () => { + [ + [3000.000001, '3s'], [3000.001, '3.000001000s'], + [3000, '3s'], [3500, '3.500000000s'] + ].forEach((duration) => { + it('should transform to protobuf duration string when provided milliseconds:' + JSON.stringify(duration[0]), + () => { + expect(transformMillisecondsToSecondsString(duration[0] as number)).to.equal(duration[1]); + }); + }); +}); From c2b126b3b18e4854646a7c85de770c353c3b7414 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 14 Jul 2021 13:55:12 -0400 Subject: [PATCH 157/160] Reduce App Check custom token exp to 5 mins (#1372) --- src/app-check/token-generator.ts | 6 +++--- test/unit/app-check/token-generator.spec.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app-check/token-generator.ts b/src/app-check/token-generator.ts index 6c5b9eeda6..86745b793c 100644 --- a/src/app-check/token-generator.ts +++ b/src/app-check/token-generator.ts @@ -30,8 +30,8 @@ import { HttpError } from '../utils/api-request'; import AppCheckTokenOptions = appCheck.AppCheckTokenOptions; -const ONE_HOUR_IN_SECONDS = 60 * 60; -const ONE_MINUTE_IN_MILLIS = 60 * 1000; +const ONE_MINUTE_IN_SECONDS = 60; +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 @@ -91,7 +91,7 @@ export class AppCheckTokenGenerator { // eslint-disable-next-line @typescript-eslint/camelcase app_id: appId, aud: FIREBASE_APP_CHECK_AUDIENCE, - exp: iat + ONE_HOUR_IN_SECONDS, + exp: iat + (ONE_MINUTE_IN_SECONDS * 5), iat, ...customOptions, }; diff --git a/test/unit/app-check/token-generator.spec.ts b/test/unit/app-check/token-generator.spec.ts index 2cf10b8bc5..2c612c1b64 100644 --- a/test/unit/app-check/token-generator.spec.ts +++ b/test/unit/app-check/token-generator.spec.ts @@ -43,7 +43,7 @@ chai.use(chaiAsPromised); const expect = chai.expect; const ALGORITHM = 'RS256'; -const ONE_HOUR_IN_SECONDS = 60 * 60; +const FIVE_MIN_IN_SECONDS = 60 * 5; const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1beta.TokenExchangeService'; /** @@ -184,7 +184,7 @@ describe('AppCheckTokenGenerator', () => { // eslint-disable-next-line @typescript-eslint/camelcase app_id: APP_ID, iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, + exp: FIVE_MIN_IN_SECONDS + 1, aud: FIREBASE_APP_CHECK_AUDIENCE, iss: mocks.certificateObject.client_email, sub: mocks.certificateObject.client_email, @@ -205,7 +205,7 @@ describe('AppCheckTokenGenerator', () => { // eslint-disable-next-line @typescript-eslint/camelcase app_id: APP_ID, iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, + exp: FIVE_MIN_IN_SECONDS + 1, aud: FIREBASE_APP_CHECK_AUDIENCE, iss: mocks.certificateObject.client_email, sub: mocks.certificateObject.client_email, @@ -233,7 +233,7 @@ describe('AppCheckTokenGenerator', () => { // eslint-disable-next-line @typescript-eslint/camelcase app_id: APP_ID, iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, + exp: FIVE_MIN_IN_SECONDS + 1, aud: FIREBASE_APP_CHECK_AUDIENCE, iss: mocks.certificateObject.client_email, sub: mocks.certificateObject.client_email, @@ -275,7 +275,7 @@ describe('AppCheckTokenGenerator', () => { }); }); - it('should be fulfilled with a JWT which expires after one hour', () => { + it('should be fulfilled with a JWT which expires after five minutes', () => { clock = sinon.useFakeTimers(1000); let token: string; @@ -283,7 +283,7 @@ describe('AppCheckTokenGenerator', () => { .then((result) => { token = result; - clock!.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + clock!.tick((FIVE_MIN_IN_SECONDS * 1000) - 1); // Token should still be valid return verifyToken(token, mocks.keyPairs[0].public); From ba07e12578c5f63562594c1fce632418376f2ef4 Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 15 Jul 2021 13:58:22 -0400 Subject: [PATCH 158/160] Add AppCheckTokenOptions type to ToC (#1375) Add `AppCheckTokenOptions` to `ToC` --- docgen/content-sources/node/toc.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index 636f59c746..c563a522ba 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -26,6 +26,8 @@ toc: path: /docs/reference/admin/node/admin.appCheck.AppCheck-1 - title: "AppCheckToken" path: /docs/reference/admin/node/admin.appCheck.AppCheckToken + - title: "AppCheckTokenOptions" + path: /docs/reference/admin/node/admin.appCheck.AppCheckTokenOptions - title: "DecodedAppCheckToken" path: /docs/reference/admin/node/admin.appCheck.DecodedAppCheckToken - title: "VerifyAppCheckTokenResponse" From 21869eef732679c6f6535acf810d191d7bcaa75e Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 15 Jul 2021 15:35:01 -0400 Subject: [PATCH 159/160] Fix typo and formatting in docs (#1378) - Fix typo and add back-ticks --- src/messaging/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/messaging/index.ts b/src/messaging/index.ts index fc0a38338d..0ed93e01e9 100644 --- a/src/messaging/index.ts +++ b/src/messaging/index.ts @@ -71,14 +71,14 @@ export namespace messaging { } /** - * Payload for the admin.messaging.send() operation. The payload contains all the fields - * in the BaseMessage type, and exactly one of token, topic or condition. + * Payload for the `admin.messaging.send()` operation. The payload contains all the fields + * in the `BaseMessage` type, and exactly one of token, topic or condition. */ export type Message = TokenMessage | TopicMessage | ConditionMessage; /** - * Payload for the admin.messaing.sendMulticast() method. The payload contains all the fields - * in the BaseMessage type, and a list of tokens. + * Payload for the `admin.messaging.sendMulticast()` method. The payload contains all the fields + * in the `BaseMessage` type, and a list of tokens. */ export interface MulticastMessage extends BaseMessage { tokens: string[]; From 20dc4626104ff728feda467b769203330aa55f7a Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Thu, 15 Jul 2021 17:18:51 -0400 Subject: [PATCH 160/160] [chore] Release 9.11.0 (#1376) - Release 9.11.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73e58f6d9b..01ee014918 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "9.10.0", + "version": "9.11.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0",